Cursors and Editing Properties¶
It is easy to edit the properties of a molecule. The best way to
do this is by creating a sire.mol.Cursor
for the molecule.
>>> cursor = mol.cursor()
>>> print(cursor)
Cursor(molecule, ACE:2)
The Cursor
represents a property editor that can
be scanned across the molecule. First, we will scan to the atom
called CA
using the atom
function.
>>> cursor = cursor.atom("CA")
>>> print(cursor)
Cursor(atom, CA:9)
The cursor provides a dictionary-style interface to all of the atom’s properties.
>>> print(cursor.keys())
['gb_screening', 'mass', 'ambertype', 'coordinates',
'treechain', 'atomtype', 'velocity', 'LJ', 'gb_radii', 'charge', 'element']
>>> print(cursor.items())
[('gb_radii', 1.7 Å),
('coordinates', ( 16.5371 Å, 5.02707 Å, 15.812 Å )),
('charge', 0.0337 |e|),
('mass', 12.01 g mol-1),
('LJ', LJ( sigma = 3.39967 Å, epsilon = 0.1094 kcal mol-1 )),
('velocity', ( 0.00308134 Å ps-1, -0.0190426 Å ps-1, 0.00618047 Å ps-1 )),
('atomtype', 'CX'),
('gb_screening', 0.72),
('treechain', 'M '),
('element', Carbon (C, 6)),
('ambertype', 'CX')]
>>> print(cursor["coordinates"])
( 16.5371 Å, 5.02707 Å, 15.812 Å )
Assiging to the dictionary will update the corresponding property.
>>> cursor["coordinates"] = (1.0, 2.0, 3.0)
>>> print(cursor["coordinates"])
( 1 Å, 2 Å, 3 Å )
Assigning to a non-existent key will create a new property.
>>> cursor["color"] = "charcoal"
>>> print(cursor["color"])
charcoal
An alternative to using the dictionary-type functions is to use
the set
and get
functions, e.g.
>>> cursor.set("color", "charcoal")
>>> print(cursor.get("color"))
charcoal
The cursor is editing a copy of the molecule. To commit and save the
changes, you need to use the commit()
function.
>>> mol = cursor.molecule().commit()
>>> print(mol["CA"].property("color"))
charcoal
>>> print(mol["CA"].coordinates())
( 1 Å, 2 Å, 3 Å )
Note
The mol
object was itself a copy from the original in the
mols
container loaded from the file. To update the original
in mols
, you need to call the update
function, e.g.
mols.update(mol)
. We will cover editing and updating
molecules in more detail in a later chapter.
Cursors are useful as they make it easy to iterate over and edit
the properties of several atoms. This is because the atoms()
function
returns a list of cursors, one per atom.
>>> cursor = mol.cursor()
>>> print(cursor.atoms())
Cursors( size=22
1: Cursor(atom, HH31:1)
2: Cursor(atom, CH3:2)
3: Cursor(atom, HH32:3)
4: Cursor(atom, HH33:4)
5: Cursor(atom, C:5)
...
18: Cursor(atom, H:18)
19: Cursor(atom, CH3:19)
20: Cursor(atom, HH31:20)
21: Cursor(atom, HH32:21)
22: Cursor(atom, HH33:22)
)
>>> for atom in cursor.atoms():
... atom["color"] = atom["element"].color_name()
>>> mol = cursor.commit()
>>> print(mol.property("color"))
SireMol::AtomStringProperty( size=22
0: white
1: charcoal
2: white
3: white
4: charcoal
...
17: white
18: charcoal
19: white
20: white
21: white
)
Note
Note how we have used the sire.mol.Element.color_name()
function of sire.mol.Element()
to get the color typically
used to represent each atom in a molecular viewer.
This process of creating a cursor, then applying a change to every single
atom in the cursor, then commiting the changes back the molecule, is very common.
It is so common, that sire provides the apply
function to enable
you to write this as a single line of code;
>>> mol = mol.cursor().atoms().apply(
... lambda atom: atom.set("color", atom["element"].color_name())).commit()
>>> print(mol.property("color"))
SireMol::AtomStringProperty( size=22
0: white
1: charcoal
2: white
3: white
4: charcoal
...
17: white
18: charcoal
19: white
20: white
21: white
)
Note
Note how we have to use the atom.set("color", ...)
rather than
atom["color"] = ...
in the lambda expression. This is because
assignments (using =
) are not supported in a Python lambda expression.
Searching by property¶
You have already seen how to search for the more
standard properties, such as element
, mass
and charge
.
You can also search for custom properties, such as the color
property
we added above, using atom property
.
>>> print(mol["atom property color == charcoal"])
Selector<SireMol::Atom>( size=6
0: Atom( CH3:2 [ 18.98, 3.45, 13.39] )
1: Atom( C:5 [ 18.48, 4.55, 14.35] )
2: Atom( CA:9 [ 16.54, 5.03, 15.81] )
3: Atom( CB:11 [ 16.05, 6.39, 15.26] )
4: Atom( C:15 [ 15.37, 4.19, 16.43] )
5: Atom( CH3:19 [ 13.83, 3.94, 18.35] )
)
This supports any properties that are strings, numbers or boolean types.
All of the standard comparison operators (e.g. ==
, >=
, !=
etc.)
are supported.
For example, we could add a radius
property based on each element’s
covalent radius…
>>> cursor = mol.cursor()
>>> for atom in cursor.atoms():
... atom["radius"] = atom["element"].covalent_radius().value()
>>> mol = cursor.commit()
>>> print(mol.property("radius"))
SireMol::AtomFloatProperty( size=22
0: 0.23
1: 0.68
2: 0.23
3: 0.23
4: 0.68
...
17: 0.23
18: 0.68
19: 0.23
20: 0.23
21: 0.23
)
Note
Note how we have used the .value()
function on the radius
to get the raw value of the radius, without the units.
We need to do this because we want to be able to search using
the radius. Searching can only be performed with simple (numeric
or boolean) properties.
or, using apply
, this could be written as
>>> mol = mol.cursor().atoms().apply(
... lambda atom: atom.set("radius",
... atom["element"].covalent_radius().value()
... )).commit()
Note
You can use either .cursor().atoms()
or .atoms().cursor()
- the
order does not change the result.
We can now search for all atoms that have a radius that is less than 0.5
.
>>> print(mol["atom property radius < 0.5"])
Selector<SireMol::Atom>( size=12
0: Atom( HH31:1 [ 18.45, 3.49, 12.44] )
1: Atom( HH32:3 [ 20.05, 3.63, 13.29] )
2: Atom( HH33:4 [ 18.80, 2.43, 13.73] )
3: Atom( H:8 [ 16.68, 3.62, 14.22] )
4: Atom( HA:10 [ 17.29, 5.15, 16.59] )
...
7: Atom( HB3:14 [ 15.24, 6.18, 14.55] )
8: Atom( H:18 [ 15.34, 5.45, 17.96] )
9: Atom( HH31:20 [ 14.35, 3.41, 19.15] )
10: Atom( HH32:21 [ 13.19, 4.59, 18.94] )
11: Atom( HH33:22 [ 13.21, 3.33, 17.69] )
)
Boolean properties are particularly useful, as these can be used to mark atoms as matching particular criteria.
For example, we could set a property that is True
for oxygen atoms using either
>>> cursor = mol.cursor()
>>> for atom in cursor.atoms("element O"):
... atom["special"] = True
>>> mol = cursor.commit()
or
>>> mol = mol.cursor().atoms("element O").apply(
... lambda atom: atom.set("special", True)).commit()
and can then use this property to search for those atoms.
>>> print(mol["atom property special == True"])
Selector<SireMol::Atom>( size=2
0: Atom( O:6 [ 19.19, 5.44, 14.76] )
1: Atom( O:16 [ 14.94, 3.17, 15.88] )
)
This search can be simplified to
>>> print(mol["atom property special"])
Selector<SireMol::Atom>( size=2
0: Atom( O:6 [ 19.19, 5.44, 14.76] )
1: Atom( O:16 [ 14.94, 3.17, 15.88] )
)
This is because an atom property search will return all of the atoms that have a non-zero, non-empty or non-false value for the specified property.
Deleting properties¶
You can remove properties from the cursor in the same way that you
remove properties from a normal Python dictionary. You just del
the key for the property you want to remove, or call the
delete
function of the Cursor, passing in the key.
For example, we can delete the radius
property we created earlier
using
>>> cursor = mol.cursor()
>>> del cursor["radius"]
>>> mol = cursor.commit()
>>> print(mol.property("radius"))
KeyError: 'SireBase::missing_property: There is no property with
name "radius". Available properties are [ velocity, element, gb_radius_set,
bond, forcefield, gb_radii, color, angle, improper, gb_screening, intrascale,
LJ, coordinates, dihedral, treechain, connectivity, special, charge,
ambertype, parameters, atomtype, mass ].
(call sire.error.get_last_error_details() for more info)'
Or, alternatively, using the delete
function,
>>> cursor = mol.cursor()
>>> cursor.delete("radius")
>>> mol = cursor.commit()
or, as one line,
>>> mol = mol.cursor().delete("radius").commit()
We can also remove the properties from individual atoms. Here, we will
remove the special
property from the oxygen atoms
>>> cursor = mol.cursor()
>>> for atom in cursor.atoms("element O"):
... del atom["special"]
>>> mol = cursor.commit()
or, alternatively, using the delete
function,
>>> mol = mol.cursor().atoms("element O").delete("special").commit()
Deleting a property from an atom will reset it to a default-constructed
value. This is False
(or 0
) for boolean properties.
>>> print(mol["element O"].property("special"))
[0, 0]
While this is what you want for boolean properties, this may give
unexpected results for more complex properties. For example, deleting
the coordinates
property from an atom will set its coordinates to
(0, 0, 0)
…
>>> mol = mol.cursor().atoms("element O").delete("coordinates").commit()
>>> print(mol["element O"].property("coordinates"))
[( 0, 0, 0 ), ( 0, 0, 0 )]