from typing import List as _List
__all__ = ["Cursor", "Cursors", "CursorsM"]
class _CursorData:
"""This is the shared data class that holds all of the data
for a Cursor. This is held by all Cursors that are derived
from the same object, meaning that multiple Cursors
can share the same molecule editor. Note that the
Cursor is not thread-safe (unlike the underlying
system used by Sire)
"""
def __init__(self, molecule=None, map=None):
if molecule is None:
self.molecule = None
self.map = None
self.connectivity = None
self.connectivity_property = None
return
else:
self.molecule = molecule.molecule().edit()
from ..base import create_map
self.map = create_map(map)
self.connectivity_property = self.map["connectivity"].source()
if self.connectivity_property is None:
# we cannot support value-based connectivity properties
self.map.set("connectivity", "connectivity")
self.connectivity_property = self.map["connectivity"].source()
try:
self.connectivity = self.molecule.property(
self.connectivity_property
).edit()
except Exception:
# the molecule doesn't have a connectivity. Create one for it
if self.molecule.has_property(self.map["coordinates"].source()):
from ..legacy.Mol import CovalentBondHunter
hunter = CovalentBondHunter()
try:
connectivity = hunter(self.molecule, self.map)
self.molecule.set_property(self.connectivity_property, connectivity)
self.connectivity = connectivity.edit()
except Exception as e:
from ..utils import Console
Console.warning(
"Cannot auto-generate a connectivity for "
f"{self.molecule}. The error is:\n\n{e}"
)
def number(self):
"""Return the molnum number of the molecule being edited
by this cursor
"""
return self.molecule.number()
def merge(self, map):
"""Return a property map that is the combination
of self.map and the passed map. The properties
set in the passed map have precedence.
"""
if map is None:
return self.map
else:
from ..base import create_map
return self.map.merge(create_map(map))
def remove_internal_property(self, internal, key):
self.connectivity.remove_property(internal, key)
self.molecule.set_property(
self.connectivity_property, self.connectivity.commit()
)
def set_internal_property(self, internal, key, value):
if value is None:
self.remove_internal_property(internal, key)
else:
self.connectivity.set_property(internal, key, value)
self.molecule.set_property(
self.connectivity_property, self.connectivity.commit()
)
def set_internal_properties(self, internal, values):
for key in values.keys():
value = values[key]
if value is None:
self.connectivity.remove_property(internal, key)
else:
self.connectivity.set_property(internal, key, value)
self.molecule.set_property(
self.connectivity_property, self.connectivity.commit()
)
def update(self, view):
try:
return self.molecule[view.index()]
except Exception:
return self.molecule
_default_process_map = None
_weightfuncs = None
def _process_move_options(view, anchor, weighting, map):
"""Internal function used to process the passed move options
and return a property map with those options set.
The default is to have no anchors and to have
the AbsFromNumber weighting (this is defined
in weightfunction.h in the C++ layer).
Here we convert the weighting (as a string option)
into a WeightFunction, plus we convert the anchor
(as a view and then selection into that view) into
an AtomSelection object
"""
global _default_process_map
if _default_process_map is None:
from ..base import PropertyMap
from ..legacy.Mol import AbsFromMass
_default_process_map = PropertyMap()
_default_process_map.set("weight function", AbsFromMass())
if map is None:
map = _default_process_map.clone()
else:
map = _default_process_map.merge(map)
if anchor is not None:
try:
selection = view[anchor].selection()
map.set("anchors", selection)
except Exception as e:
from ..utils import Console
Console.warning(f"Unable to find anchors '{anchor}'.\n\n{e}")
if weighting is not None:
global _weightfuncs
if _weightfuncs is None:
from ..legacy.Mol import (
AbsFromMass,
RelFromMass,
AbsFromNumber,
RelFromNumber,
)
_weightfuncs = {
"relative_mass": RelFromMass,
"absolute_mass": AbsFromMass,
"relative_number": RelFromNumber,
"absolute_number": AbsFromNumber,
}
try:
weighting = _weightfuncs[weighting]()
except Exception:
raise ValueError(
f"Unsupported weighting: {weighting}. Supported values "
f"are {_weightfuncs.keys()}."
)
map.set("weight function", weighting)
return map
[docs]
class Cursor:
"""This class provides a cursor that can be used to navigate through
and edit the properties of Molecules. This makes the whole
getting and setting of properties more pythonic in writing
style, while also saving some typing.
"""
def __init__(self, molecule=None, internal=None, map=None):
"""Construct the Cursor to explore and edit the
properties of the passed MoleculeView.
Note that you normally don't call this yourself.
Instead, you would create a Cursor by calling
the `.cursor()` function on the molecule view
itself.
Examples:
>>> cursor = mol.cursor()
>>> cursor["cat"] = "meow"
>>> mol = cursor.commit()
"""
self._d = _CursorData(molecule=molecule, map=map)
self._view = self._d.update(molecule)
if (molecule is not None) and (internal is None):
w = molecule.what()
if (
w.endswith("Bond")
or w.endswith("Angle")
or w.endswith("Dihedral")
or w.endswith("Dihedral")
):
internal = molecule.id()
self._internal = internal
self._add_extra_functions()
def _update(self):
self._view = self._d.update(self._view)
def __str__(self):
if self._d.molecule is None:
return "Cursor::null"
elif self.is_internal():
# This is an Internal Cursor
a0 = self._d.molecule[self._internal.atom0()]
a1 = self._d.molecule[self._internal.atom1()]
if self.is_bond():
return (
f"Cursor(bond, "
f"{a0.name().value()}:{a0.number().value()} => "
f"{a1.name().value()}:{a1.number().value()})"
)
elif self.is_angle():
a2 = self._d.molecule[self._internal.atom2()]
return (
f"Cursor(angle, "
f"{a0.name().value()}:{a0.number().value()} <= "
f"{a1.name().value()}:{a1.number().value()} => "
f"{a2.name().value()}:{a2.number().value()})"
)
else:
a2 = self._d.molecule[self._internal.atom2()]
a3 = self._d.molecule[self._internal.atom3()]
if self.is_dihedral():
return (
f"Cursor(dihedral, "
f"{a0.name().value()}:{a0.number().value()} <= "
f"{a1.name().value()}:{a1.number().value()} == "
f"{a2.name().value()}:{a2.number().value()} => "
f"{a3.name().value()}:{a3.number().value()})"
)
else:
return (
f"Cursor(improper, "
f"{a0.name().value()}:{a0.number().value()} => "
f"{a1.name().value()}:{a1.number().value()} == "
f"{a2.name().value()}:{a2.number().value()} <= "
f"{a3.name().value()}:{a3.number().value()})"
)
else:
# This is a view cursor
try:
return f"Cursor({self.type()}, {self.name}:{self.number})"
except Exception:
return f"Cursor({self.type()}, {self.name})"
def __repr__(self):
return self.__str__()
def __delitem__(self, key):
"""Delete the property with specified key"""
self._update()
if self.is_internal():
self._d.remove_internal_property(self._internal, key)
else:
if self.is_molecule():
# remove the property entirely
self._d.molecule.remove_property(key)
else:
# replace the property with a default-constructed value
self.__setitem__(key, None)
self._update()
def __contains__(self, key):
"""Return whether a property with the specified key is
contained in this view.
"""
self._update()
if self.is_internal():
return self._d.connectivity.has_property(self._internal, key)
else:
return self._view.has_property(key)
def __len__(self):
return len(self.view())
def __call__(self, key):
"""Return a cursor that represents the sub-view of this
cursor, indexed by key. For example,
cursor("element C") would return a cursor for all
of the carbon atoms in this view.
"""
view = self.view()[key]
if view.is_selector():
return self._from_views(view)
else:
return self._from_view(view)
def __getitem__(self, key):
"""Return the property that matches the passed key, OR the
sub-view that matches the key. This will only look for
the sub-view if there is no matching property. Use
the __call__ function if you only want to search for
sub-views.
"""
if type(key) is not str:
return self.__call__(key)
try:
return self.get(key)
except Exception as e:
property_error = e
# We can't find the property so try the sub-view
try:
return self.__call__(key)
except Exception:
pass
# We can't find the sub-view, but since this searches the
# property first, we will raise the property exception
raise property_error
def __setitem__(self, key, value):
"""Set the property with key 'key' to the passed 'value'"""
self._update()
if self.is_internal():
self._d.set_internal_property(self._internal, key, value)
else:
if value is None:
# we need to create a default-constructed value
try:
v = self._view.property(key)
value = v.__class__()
except KeyError:
# No existing property - assume this is Boolean False
value = False
self._view.set_property(key, value)
self._d.molecule = self._view.molecule()
self._update()
def _add_bond_functions(self):
"""Internal function used to add member functions that are
specific only to cursors that operate on bonds
"""
def get_length(map=None):
"""Return the length of the bond being edited by this cursor"""
map = self._d.merge(map)
return self.view().length(map=map)
def set_length(value, anchor=None, weighting=None, auto_align=True, map=None):
"""Set the length of the bond being edited by this cursor to
'value'. This should be either a length unit, or a float
(in which case it is converted into a value with default
length units)
value: float or length
The length to which to set this bond.
anchor: string or ID
The search or ID to identify atoms in the view that
are anchored, and which cannot be moved when the
internal is set.
weighting: string
The weighting function used to distribute the move
across the atoms in the view. This is either;
'absolute_number', 'relative_number',
'absolute_mass' or 'relative_mass'. It defaults
to 'absolute_number'
auto_align: bool
Whether or not to align the molecule against itself
after the move
map: sire.base.PropertyMap or dict
Map of property keys that are passed through to
control which properties are used for the move,
plus fine-grained control of the weight function
or anchors.
"""
from ..units import length
value = length(value)
view = self._d.molecule.commit()
map = _process_move_options(
view=view, anchor=anchor, weighting=weighting, map=map
)
map = self._d.merge(map)
moved = view.move().set(self._internal, value, map)
if auto_align and (anchor is None):
moved.align(view, map)
self._d.molecule = moved.commit().molecule().edit()
self._update()
return self
def change_length(
delta, anchor=None, weighting=None, auto_align=True, map=None
):
"""Change the length of the bond being edited by this cursor by
'delta'. This should be either a length unit, or a float
(in which case it is converted into a value with default
length units)
delta: float or length
The length by which to change this bond.
anchor: string or ID
The search or ID to identify atoms in the view that
are anchored, and which cannot be moved when the
internal is set.
weighting: string
The weighting function used to distribute the move
across the atoms in the view. This is either;
'absolute_number', 'relative_number',
'absolute_mass' or 'relative_mass'. It defaults
to 'absolute_number'
auto_align: bool
Whether or not to align the molecule against itself
after the move
map: sire.base.PropertyMap or dict
Map of property keys that are passed through to
control which properties are used for the move,
plus fine-grained control of the weight function
or anchors.
"""
from ..units import length
delta = length(delta)
view = self._d.molecule.commit()
map = _process_move_options(
view=view, anchor=anchor, weighting=weighting, map=map
)
map = self._d.merge(map)
moved = view.move().change(self._internal, delta, map)
if auto_align and (anchor is None):
moved.align(view, map)
self._d.molecule = moved.commit().molecule().edit()
self._update()
return self
def get_potential(map=None):
"""Return the energy expression for this bond"""
map = self._d.merge(map)
return self._d.molecule.property(map[self.type()]).get(self.id())
def set_potential(value, map=None):
"""Set the energy expression for this bond to 'value'"""
from ..cas import Expression
map = self._d.merge(map)
internals = self._d.molecule.property(map[self.type()])
internals.set(self.id(), Expression(value))
self._d.molecule.set_property(map[self.type()], internals)
self._update()
return self
self.length = get_length
self.set_length = set_length
self.change_length = change_length
self.measure = get_length
self.set_measure = set_length
self.change_measure = change_length
self.get_potential = get_potential
self.set_potential = set_potential
def _add_angle_functions(self):
"""Internal function used to add member functions that are
specific only to cursors that operate on angles, dihedrals
or impropers
"""
def get_size(map=None):
"""Return the size of the internal being edited by this cursor"""
map = self._d.merge(map)
return self.view().size(map=map)
def set_size(
value,
anchor=None,
weighting=None,
auto_align=True,
move_all=True,
map=None,
):
"""Set the size of the internal being edited by this cursor to
'delta'. This should be either an angle unit, or a float
(in which case it is converted into a value with default
angle units)
value: float or angle
The angle to which to set this internal.
anchor: string or ID
The search or ID to identify atoms in the view that
are anchored, and which cannot be moved when the
internal is set.
weighting: string
The weighting function used to distribute the move
across the atoms in the view. This is either;
'absolute_number', 'relative_number',
'absolute_mass' or 'relative_mass'. It defaults
to 'absolute_number'
auto_align: bool
Whether or not to align the molecule against itself
after the move
move_all: bool
Option only used for dihedrals. Whether or not to
move all atoms around the dihedral or just the
specified dihedral
map: sire.base.PropertyMap or dict
Map of property keys that are passed through to
control which properties are used for the move,
plus fine-grained control of the weight function
or anchors.
"""
from ..units import angle
value = angle(value)
view = self._d.molecule.commit()
map = _process_move_options(
view=view, anchor=anchor, weighting=weighting, map=map
)
map = self._d.merge(map)
if move_all and self.is_dihedral():
moved = view.move().set_all(self._internal, value, map)
else:
moved = view.move().set(self._internal, value, map)
if auto_align and (anchor is None):
moved.align(view, map)
self._d.molecule = moved.commit().molecule().edit()
self._update()
return self
def change_size(
delta,
anchor=None,
weighting=None,
auto_align=True,
move_all=True,
map=None,
):
"""Change the size of the internal being edited by this cursor by
'delta'. This should be either an angle unit, or a float
(in which case it is converted into a value with default
angle units)
delta: float or angle
The angle by which this internal is changed.
anchor: string or ID
The search or ID to identify atoms in the view that
are anchored, and which cannot be moved when the
internal is set.
weighting: string
The weighting function used to distribute the move
across the atoms in the view. This is either;
'absolute_number', 'relative_number',
'absolute_mass' or 'relative_mass'. It defaults
to 'absolute_number'
auto_align: bool
Whether or not to align the molecule against itself
after the move
move_all: bool
Option only used for dihedrals. Whether or not to
move all atoms around the dihedral or just the
specified dihedral
map: sire.base.PropertyMap or dict
Map of property keys that are passed through to
control which properties are used for the move,
plus fine-grained control of the weight function
or anchors.
"""
from ..units import angle
delta = angle(delta)
view = self._d.molecule.commit()
map = _process_move_options(
view=view, anchor=anchor, weighting=weighting, map=map
)
map = self._d.merge(map)
if move_all and self.is_dihedral():
from . import BondID
center_bond = BondID(self._internal.atom1(), self._internal.atom2())
moved = view.move().change(center_bond, delta, map)
else:
moved = view.move().change(self._internal, delta, map)
if auto_align and (anchor is None):
moved.align(view, map)
self._d.molecule = moved.commit().molecule().edit()
self._update()
def get_potential(map=None):
"""Return the energy expression for this internal"""
map = self._d.merge(map)
return self._d.molecule.property(map[self.type()]).get(self.id())
def set_potential(value, map=None):
"""Set the energy expression for this internal to 'value'"""
from ..cas import Expression
map = self._d.merge(map)
print(map)
internals = self._d.molecule.property(map[self.type()])
internals.set(self.id(), Expression(value))
self._d.molecule.set_property(map[self.type()], internals)
self._update()
return self
self.size = get_size
self.set_size = set_size
self.change_size = change_size
self.measure = get_size
self.set_measure = set_size
self.change_measure = change_size
self.get_potential = get_potential
self.set_potential = set_potential
def _add_extra_functions(self):
"""Internal function that adds additional functions to this
cursor depending on what type of object is being edited.
"""
if self.is_bond():
self._add_bond_functions()
elif self.is_internal():
self._add_angle_functions()
[docs]
def is_same_editor(self, other):
"""Return whether this Cursor is using the same editor to edit
the molecule as 'other'. This returns true if the underlying
editor for both cursors is the same, i.e. changes made by
one cursor would be seen and be editable by the other cursor.
"""
try:
# Other is a Cursor
return self._d is other._d
except AttributeError:
pass
try:
# Other is a Cursors
return self._d is other._parent._d
except AttributeError:
# Other is something else (a CursorsM?)
return False
[docs]
def bonds(self, *args, **kwargs):
"""Return cursors for all of the bonds in this
view or, if 'id' is supplied, the bonds in this
view that match 'id'
"""
self._update()
cursors = []
bonds = self.view().bonds(*args, **kwargs)
for bond in bonds:
c = Cursor()
c._d = self._d
c._view = self._d.molecule
c._internal = bond.id()
c._add_extra_functions()
cursors.append(c)
return Cursors(self, cursors, bonds)
[docs]
def angles(self, *args, **kwargs):
"""Return cursors for all of the angles in this
view or, if 'id' is supplied, the angles in this
view that match 'id'
"""
self._update()
cursors = []
angles = self.view().angles(*args, **kwargs)
for angle in angles:
c = Cursor()
c._d = self._d
c._view = self._d.molecule
c._internal = angle.id()
c._add_extra_functions()
cursors.append(c)
return Cursors(self, cursors, angles)
[docs]
def dihedrals(self, *args, **kwargs):
"""Return cursors for all of the dihedrals in this
view or, if 'id' is supplied, the dihedrals in this
view that match 'id'
"""
self._update()
cursors = []
dihedrals = self.view().dihedrals(*args, **kwargs)
for dihedral in dihedrals:
c = Cursor()
c._d = self._d
c._view = self._d.molecule
c._internal = dihedral.id()
c._add_extra_functions()
cursors.append(c)
return Cursors(self, cursors, dihedrals)
[docs]
def impropers(self, *args, **kwargs):
"""Return cursors for all of the impropers in this
view or, if 'id' is supplied, the impropers in this
view that match 'id'
"""
self._update()
cursors = []
impropers = self.view().impropers(*args, **kwargs)
for improper in impropers:
c = Cursor()
c._d = self._d
c._view = self._d.molecule
c._internal = improper.id()
c._add_extra_functions()
cursors.append(c)
return Cursors(self, cursors, impropers)
def _from_view(self, view):
"""Hidden function that allows a single view of the
current molecule to be converted to a Cursor
"""
if not self._d.molecule.is_same_molecule(view):
raise ValueError(
f"You cannot create from this view ({view}) as it is from "
f"a different molecule to {self._d.molecule}."
)
c = Cursor()
c._d = self._d
from ..mm import Bond, Angle, Dihedral, Improper
from . import Molecule
if type(view) is Molecule:
c._view = self._d.molecule
elif type(view) in [Bond, Angle, Dihedral, Improper]:
c._view = self._d.molecule
c._internal = view.id()
else:
c._view = self._d.molecule[view.index()]
c._add_extra_functions()
return c
def _from_views(self, views):
"""Hidden function that allows a set of views, e.g. from
a Selector_Atom_, SelectorBond etc, to be converted
to a set of Cursors
"""
cursors = []
from ..mm import Bond, Angle, Dihedral, Improper
for view in views:
c = Cursor()
c._d = self._d
if type(view) in [Bond, Angle, Dihedral, Improper]:
c._view = self._d.molecule
c._internal = view.id()
else:
# likely an atom, residue, chain or segment
c._view = self._d.molecule[view.index()]
c._add_extra_functions()
cursors.append(c)
return Cursors(self, cursors, views)
[docs]
def atoms(self, id=None):
"""Return cursors for all of atoms in this view,
of, if 'id' is supplied, the atoms in this view
that match 'id'
"""
cursors = []
view = self.commit()
if id is None:
atoms = view.atoms()
else:
atoms = view.atoms(id)
for atom in atoms:
c = Cursor()
c._d = self._d
c._view = self._d.molecule.atom(atom.index())
c._add_extra_functions()
cursors.append(c)
return Cursors(self, cursors, atoms)
[docs]
def residues(self, id=None):
"""Return cursors for all of residues in this view,
of, if 'id' is supplied, the residues in this view
that match 'id'
"""
cursors = []
view = self.commit()
if id is None:
residues = view.residues()
else:
residues = view.residues(id)
for residue in residues:
c = Cursor()
c._d = self._d
c._view = self._d.molecule.residue(residue.index())
c._add_extra_functions()
cursors.append(c)
return Cursors(self, cursors, residues)
[docs]
def chains(self, id=None):
"""Return cursors for all of chains in this view,
of, if 'id' is supplied, the chains in this view
that match 'id'
"""
cursors = []
view = self.commit()
if id is None:
chains = view.chains()
else:
chains = view.chains(id)
for chain in chains:
c = Cursor()
c._d = self._d
c._view = self._d.molecule.chain(chain.index())
c._add_extra_functions()
cursors.append(c)
return Cursors(self, cursors, chains)
[docs]
def segments(self, id=None):
"""Return cursors for all of segments in this view,
of, if 'id' is supplied, the segments in this view
that match 'id'
"""
cursors = []
view = self.commit()
if id is None:
segments = view.segments()
else:
segments = view.segments(id)
for segment in segments:
c = Cursor()
c._d = self._d
c._view = self._d.molecule.segment(segment.index())
c._add_extra_functions()
cursors.append(c)
return Cursors(self, cursors, segments)
[docs]
def atom(self, i):
"""Return the atom in the molecule that matches the passed ID"""
self._update()
c = Cursor()
c._d = self._d
c._view = self._view.atom(i)
c._add_extra_functions()
c._update()
return c
[docs]
def residue(self, i=None):
"""Return the residue in the molecule that matches the passed ID.
If 'i' is None, then this returns the residue that contains
this atom (if this is an atom)
"""
if i is None:
try:
c = Cursor()
c._d = self._d
c._view = self._view.residue()
c._add_extra_functions()
c._update()
return c
except Exception:
raise TypeError(
f"There is no residue that contains {self.type()}:" f"{self.id()}"
)
self._update()
c = Cursor()
c._d = self._d
c._view = self._view.residue(i)
c._add_extra_functions()
c._update()
return c
[docs]
def chain(self, i=None):
"""Return the chain in the molecule that matches the passed ID.
If 'i' is None, then this returns the chain that contains
this atom (if this is an atom)"""
if i is None:
try:
c = Cursor()
c._d = self._d
c._view = self._view.chain()
c._add_extra_functions()
c._update()
return c
except Exception:
raise TypeError(
f"There is no chain that contains {self.type()}:" f"{self.id()}"
)
self._update()
c = Cursor()
c._d = self._d
c._view = self._view.chain(i)
c._add_extra_functions()
c._update()
return c
[docs]
def segment(self, i=None):
"""Return the segment in the molecule that matches the passed ID.
If 'i' is None, then this returns the segment that contains
this atom (if this is an atom)"""
if i is None:
try:
c = Cursor()
c._d = self._d
c._view = self._view.segment()
c._add_extra_functions()
c._update()
return c
except Exception:
raise TypeError(
f"There is no segment that contains {self.type()}:" f"{self.id()}"
)
self._update()
c = Cursor()
c._d = self._d
c._view = self._view.segment(i)
c._add_extra_functions()
c._update()
return c
[docs]
def molecule(self):
"""Return the molecule"""
self._update()
c = Cursor()
c._d = self._d
c._view = self._d.molecule
c._add_extra_functions()
return c
[docs]
def bond(self, *args, **kwargs):
"""Return the Cursor for the specified bond."""
bond = self.view().bond(*args, **kwargs)
c = Cursor()
c._d = self._d
c._view = self._d.molecule
c._internal = bond.id()
c._add_extra_functions()
if "map" in kwargs:
raise ValueError(
"The 'map' parameter is not allowed when creating a "
"Cursor for a bond"
)
return c
[docs]
def angle(self, *args, **kwargs):
"""Return the Cursor for the specified angle."""
angle = self.view().angle(*args, **kwargs)
c = Cursor()
c._d = self._d
c._view = self._d.molecule
c._internal = angle.id()
c._add_extra_functions()
if "map" in kwargs:
raise ValueError(
"The 'map' parameter is not allowed when creating a "
"Cursor for an angle"
)
return c
[docs]
def dihedral(self, *args, **kwargs):
"""Return the Cursor for the specified dihedral."""
dihedral = self.view().dihedral(*args, **kwargs)
c = Cursor()
c._d = self._d
c._view = self._d.molecule
c._internal = dihedral.id()
c._add_extra_functions()
if "map" in kwargs:
raise ValueError(
"The 'map' parameter is not allowed when creating a "
"Cursor for a dihedral"
)
return c
[docs]
def improper(self, *args, **kwargs):
"""Return the Cursor for the specified improper."""
improper = self.view().improper(*args, **kwargs)
c = Cursor()
c._d = self._d
c._view = self._d.molecule
c._internal = improper.id()
c._add_extra_functions()
if "map" in kwargs:
raise ValueError(
"The 'map' parameter is not allowed when creating a "
"Cursor for an improper"
)
return c
[docs]
def parent(self):
"""Return the cursor of the parent object (e.g. parent residue
of the atom, parent chain of the residue, parent molecule
of the bond etc. This will return the Cursor for the whole
molecule if there isn't a suitable parent
"""
t = self.type()
c = Cursor()
c._d = self._d
c._view = c._d.molecule
# The parent of an Atom is a Residue (if it is in one), and
# the parent of a Residue is a Chain (if it is in one).
# If this fails, then the parent is the Molecule
try:
if t == "atom":
c._view = self._view.residue()
elif t == "residue":
c._view = self._view.chain()
except Exception:
pass
c._add_extra_functions()
c._update()
return c
[docs]
def get(self, key):
"""Return the property associated with key 'key'"""
self._update()
if self.is_internal():
return self._d.connectivity.property(self._internal, key)
else:
return self._view.property(key)
[docs]
def set(self, key, value):
"""Set the property associated with key 'key' to the
passed value
"""
self.__setitem__(key, value)
return self
[docs]
def delete(self, key):
"""Remove the property associated with the key 'key'"""
self.__delitem__(key)
return self
[docs]
def get_name(self):
"""Return the name of the current view. Note that this
returns the name as a simple string (it is not a
AtomName, ResName etc)"""
self._update()
if self.is_internal():
raise TypeError(
"An internal (bond/angle/dihedral/improper) does not have " "a name!"
)
return self._view.name().value()
[docs]
def set_name(self, name):
"""Set the name of the object in the current view"""
self._update()
if self.is_internal():
raise TypeError(
"An internal (bond/angle/dihedral/improper) does not have " "a name!"
)
# get the type right...
orig_name = self._view.name()
try:
self._view.rename(orig_name.__class__(name))
except Exception:
# This is a molecule, so can set name using a string
self._view.rename(name)
self._d.molecule = self._view.molecule()
return self
[docs]
def get_number(self):
"""Return the number of the current view. This returns the
number as a simple number (it is not a AtomNum, ResNum etc)"""
self._update()
if self.is_internal():
raise TypeError(
"An internal (bond/angle/dihedral/improper) does not have " "a number!"
)
try:
return self._view.number().value()
except Exception:
raise TypeError(f"A {self._view.what()} does not have a number!")
[docs]
def set_number(self, number):
"""Set the number of the object in the current view"""
self._update()
if self.is_internal():
raise TypeError(
"An internal (bond/angle/dihedral/improper) does not have " "a number!"
)
try:
orig_number = self._view.number()
self._view.renumber(orig_number.__class__(number))
self._d.molecule = self._view.molecule()
except AttributeError:
raise TypeError(f"A {self._view.what()} does not have a number!")
return self
[docs]
def get_index(self):
"""
Return the index of the current view. This returns it as
as simple number (i.e. not as an AtomIdx, ResIdx etc)
"""
self._update()
if self.is_internal():
raise TypeError(
"An internal (bond/angle/dihedral/improper) does not have " "an index!"
)
return self._view.index().value()
[docs]
def renumber(self):
"""
Renumber the underlying molecule holding this view. This makes the
molecule distinct from its other copies (molecules are uniquely
idenfied by their number). You don't have control over the number
- this is generated randomly and is guaranteed to not be a number
of a molecule that already exists
"""
self._update()
self._d.molecule.renumber()
self._view.update(self._d.molecule)
name = property(get_name, set_name)
number = property(get_number, set_number)
index = property(get_index)
[docs]
def id(self):
"""Return the ID of this view (e.g. AtomIdx, MolNum, BondID)"""
self._update()
if self.is_internal():
return self._internal
try:
return self._view.index()
except Exception:
# objects without an index (e.g. molecule)
# use a number for their ID
return self._view.number()
[docs]
def type(self):
"""Return the type of this Cursor (e.g. 'atom', 'bond',
'residue', 'chain', 'segment' or 'molecule')
"""
if self.is_internal():
if self.is_bond():
return "bond"
elif self.is_angle():
return "angle"
elif self.is_dihedral():
return "dihedral"
elif self.is_improper():
return "improper"
w = self._view.what()
if w.find("Atom") != -1:
return "atom"
elif w.find("Res") != -1:
return "residue"
elif w.find("Chain") != -1:
return "chain"
elif w.find("Seg") != -1:
return "segment"
elif w.find("Mol") != -1:
return "molecule"
else:
raise TypeError(f"Cannot identify cursor type {w}")
[docs]
def is_molecule(self):
"""Return whether this is pointing to a Molecule"""
return self.type() == "molecule"
[docs]
def is_internal(self):
"""Return whether or not this is a view of an internal
(i.e. bond, angle, dihedral or improper)
"""
return self._internal is not None
[docs]
def is_bond(self):
"""Return whether this is pointing to a Bond"""
from . import BondID
return BondID in type(self._internal).mro()
[docs]
def is_angle(self):
"""Return whether this is pointing to an Angle"""
from . import AngleID
return AngleID in type(self._internal).mro()
[docs]
def is_dihedral(self):
"""Return whether this is pointing to a Dihedral"""
from . import DihedralID
return DihedralID in type(self._internal).mro()
[docs]
def is_improper(self):
"""Return whether this is pointing to an Improper"""
from . import ImproperID
return ImproperID in type(self._internal).mro()
[docs]
def is_atom(self):
"""Return whether this is pointing to an Atom"""
return self.type() == "atom"
[docs]
def is_residue(self):
"""Return whether this is pointing to a Residue"""
return self.type() == "residue"
[docs]
def is_chain(self):
"""Return whether this is pointing to a Chain"""
return self.type() == "chain"
[docs]
def is_segment(self):
"""Return whether this is pointing to a Segment"""
return self.type() == "segment"
[docs]
def evaluate(self, *args, **kwargs):
"""Return a :class:`sire.mol.Evaluator` for the view
in this cursor
"""
self._update()
if self.is_internal():
if self.is_bond():
from ..mm import Bond
return Bond(self._d.molecule, self._internal)
elif self.is_angle():
from ..mm import Angle
return Angle(self._d.molecule, self._internal)
elif self.is_dihedral():
from ..mm import Dihedral
return Dihedral(self._d.molecule, self._internal)
elif self.is_improper():
from ..mm import Improper
return Improper(self._d.molecule, self._internal)
return self._view.evaluate(*args, **kwargs)
[docs]
def view(self):
"""Return the view underpinning this cursor. This is actually
the same as committing the changes
"""
return self.commit()
[docs]
def commit(self):
"""Commit all of the changes and return the newly
edited molecule (or MoleculeView)
"""
self._update()
mol = self._d.molecule.commit()
if self.is_internal():
if self.is_bond():
from ..mm import Bond
return Bond(self._d.molecule, self._internal)
elif self.is_angle():
from ..mm import Angle
return Angle(self._d.molecule, self._internal)
elif self.is_dihedral():
from ..mm import Dihedral
return Dihedral(self._d.molecule, self._internal)
elif self.is_improper():
from ..mm import Improper
return Improper(self._d.molecule, self._internal)
try:
return mol[self._view.index()]
except Exception:
return mol
[docs]
def apply(self, func, *args, **kwargs):
"""Apply the passed function (with optional position and keyword
arguments) to this Cursor. As the function is intended to use
the Cursor to edit molecules, only this Cursor will be returned.
This lets you run `.apply(...).commit()` as a single line.
The function can be either;
1. a string containing the name of the function to call, or
2. an actual function (either a normal function or a lambda expression)
You can optionally pass in positional and keyword arguments
here that will be passed to the function.
Args:
func (str or function): The function to be called, or the name
of the function to be called.
Returns:
Cursor: This cursor
"""
if str(func) == func:
# we calling a named function
func = getattr(self, func)
func(self, *args, **kwargs)
return self
def keys(self):
self._update()
if self.is_internal():
return self._d.connectivity.property_keys(self._internal)
else:
return self._view.property_keys()
def values(self):
self._update()
try:
if self.is_internal():
return self._d.connectivity.property_values(self._internal)
else:
return self._view.property_values()
except Exception:
vals = []
for key in self.keys():
vals.append(self.__getitem__(key))
return vals
def items(self):
self._update()
if self.is_internal():
keys = self._d.connectivity.property_keys(self._internal)
items = []
for key in keys:
items.append((key, self._d.connectivity.property(self._internal, key)))
else:
keys = self._view.property_keys()
items = []
for key in keys:
items.append((key, self._view.property(key)))
return items
[docs]
def properties(self):
"""Return the sire.base.Properties object for the properties
of the current view
"""
from ..legacy.Base import Properties
p = Properties()
if self.is_internal():
return self._d.connectivity.properties(self._internal)
else:
for key in self.keys():
p[key] = self.__getitem__(key)
return p
[docs]
def num_frames(self):
"""Return the number of trajectory frames contained by the molecule"""
return self._d.molecule.num_frames()
[docs]
def load_frame(self, *args, **kwargs):
"""Call the `load_frame` function on the contained view, passing
the arguments directly. This is equivalent to calling
`load_frame` directly on the contained view.
"""
self._d.molecule.load_frame(*args, **kwargs)
self._update()
return self
[docs]
def save_frame(self, *args, **kwargs):
"""Call the `save_frame` function on the contained view, passing
the arguments directly. This is equivalent to calling
`save_frame` directly on the contained view.
"""
self._d.molecule.save_frame(*args, **kwargs)
self._update()
return self
[docs]
def delete_frame(self, *args, **kwargs):
"""Call the `delete_frame` function on the contained view, passing
the arguments directly. This is equivalent to calling
`delete_frame` directly on the contained view.
"""
self._d.molecule.delete_frame(*args, **kwargs)
self._update()
return self
[docs]
def make_whole(self, center=None, map=None):
"""
Make all of the atoms operated on by this cursor whole
(they won't be broken across a periodic box boundary)
Use 'map' to specify the property map used to find the
coordinates and space properties. Pass in 'center'
to specify the center of the periodic box into
which they should be wrapped.
"""
map = self._d.merge(map)
view = self.commit()
if center is None:
view = view.move().make_whole(map=map).commit()
else:
from ..maths import Vector
view = view.move().make_whole(center=Vector(center), map=map).commit()
self._d.molecule = view.molecule().edit()
self._update()
return self
[docs]
def translate(self, *args, map=None):
"""
Translate all of the atoms operated on by this cursor
by the passed arguments (these are converted automatically
to a sr.maths.Vector). Use 'map' to specify the property
map to use to find the coordinates property
"""
from ..maths import Vector
delta = Vector(*args)
map = self._d.merge(map)
view = self.commit()
view = view.move().translate(delta, map=map).commit()
self._d.molecule = view.molecule().edit()
self._update()
return self
[docs]
def rotate(
self,
angle=None,
axis=None,
center=None,
quaternion=None,
matrix=None,
rotate_velocities: bool = None,
map=None,
):
"""Rotate all of the atoms operated on by this cursor
by the passed arguments. Use 'map' to specify the
property map to use to find the coordinates property.
There are many ways to specify the rotation, hence
the number of named arguments:
angle: (float or angle)
The angle to rotate by - this is interpreted as
degrees if you pass in a float. Otherwise use
sire.units.degrees or sire.units.radians to specify
the angle unit. This is superseded by the
quaternion or matrix arguments.
axis: sire.maths.Vector (or anything that can convert to a Vector)
The vector about which to rotate. If this is not
specified, and no other rotation specification is
used, then the rotation is about the z axis.
This is superseded by the quaternion or
matrix arguments.
center: sire.maths.Vector (or anything that can convert to a Vector)
The center for the rotation. If this isn't passed then
the center of mass of the atoms operated on by this
cursor is used.
quaternion: sire.maths.Quaternion
The Quaternion description of the rotation. Note that,
if you pass this, then the angle, axis and matrix
arguments will be ignored.
matrix: sire.maths.Matrix
The 3x3 rotation matrix that describes the rotation.
Note that, if you pass this, then the angle and axis
arguments will be ignored. This is superseded by
the quaternion argument.
rotate_velocities: bool
Whether or not to rotate the velocities as well as
the coordinates (default True)
map: None, dict or sire.base.PropertyMap
The property map used to find the coordinates property
"""
from ..maths import create_quaternion, Vector
quaternion = create_quaternion(
angle=angle, axis=axis, matrix=matrix, quaternion=quaternion
)
view = self.commit()
map = self._d.merge(map)
if map is None:
from ..base import create_map
map = create_map({})
if rotate_velocities is None:
if not map.specified("rotate_velocities"):
map.set("rotate_velocities", True)
else:
map.set("rotate_velocities", bool(rotate_velocities))
if center is None:
center = view.evaluate().center_of_mass(map=map)
else:
center = Vector(center)
view = view.move().rotate(quaternion, center, map=map).commit()
self._d.molecule = view.molecule().edit()
self._update()
return self
[docs]
class Cursors:
"""This class holds a list of Cursors. It provides some convenience
functions that make working with lists of Cursor objects easier.
This includes being able to commit back to the Cursor that
created the list, plus being able to apply a function to
each Cursor in the list
"""
def __init__(self, parent: Cursor, cursors: _List[Cursor], view):
if type(parent) is not Cursor:
raise TypeError(f"{parent} must be a Cursor object!")
for c in cursors:
if type(c) is not Cursor:
raise TypeError(f"{c} must be a Cursor object!")
if c._d is not parent._d:
raise ValueError("The list of cursors must be created from the parent!")
self._parent = parent
self._cursors = cursors
self._view = view
self._add_extra_functions()
def __call__(self, i):
"""Return the sub-view(s) of this cursor that match the index 'i'.
Note that this will not look at the properties of the cursors.
"""
try:
# do the simple thing if we are asking for the ith cursor
idx = int(i)
return self._cursors[idx]
except Exception:
pass
# otherwise we need to search the parent and
# get the cursors as needed
view = self._view[i]
if view.is_selector():
return self._parent._from_views(view)
else:
return self._parent._from_view(view)
def __getitem__(self, key):
"""Either find the property that matches the passed 'key' or
look for the sub-view(s) of the cursor that match the passed key.
This will only look for sub-view(s) if there are no matching
properties. Use the __call__ operator to skip the property search.
"""
if type(key) is not str:
return self.__call__(key)
try:
return self.get(key)
except Exception as e:
property_error = e
# We couldn't find the property, so instead
# try to find the matching sub-view
try:
return self.__call__(key)
except Exception:
pass
# This interface is primarily for properties,
# so raise the missing property error
raise property_error
def __setitem__(self, key, value):
if type(value) is list or type(value) is tuple:
if len(value) == len(self._cursors):
for i, v in enumerate(value):
self._cursors[i].set(key, v)
return
for cursor in self._cursors:
cursor.set(key, value)
def __delitem__(self, key):
for cursor in self._cursors:
cursor.delete(key)
def __contains__(self, key):
for cursor in self._cursors:
if key in cursor:
return True
return False
def _update(self):
"""Internal function used to ensure that all
child cursors are up to date"""
for cursor in self._cursors:
cursor._update()
def __len__(self):
return len(self._cursors)
def __str__(self):
if len(self) == 0:
return "Cursors::null"
else:
lines = []
n = len(self._cursors)
if n <= 10:
for i in range(0, n):
lines.append(f"{i+1}: {self._cursors[i]}")
else:
for i in range(0, 5):
lines.append(f"{i+1}: {self._cursors[i]}")
lines.append("...")
for i in range(n - 5, n):
lines.append(f"{i+1}: {self._cursors[i]}")
lines = "\n".join(lines)
return f"Cursors( size={n}\n{lines}\n)"
def __repr__(self):
return self.__str__()
def _add_bond_functions(self):
"""Internal function that adds functions to this cursor
that are useful when editing bonds
"""
def get_lengths(map=None):
"""Return the lengths of all bonds being edited by these
cursors
"""
map = self._parent._d.merge(map)
lengths = []
for cursor in self._cursors:
lengths.append(cursor.length(map=map))
return lengths
def set_lengths(values, anchor=None, weighting=None, auto_align=True, map=None):
"""
Set the lengths of the bonds being edited by this cursor to the
specified
values. Note that there should be the same number of values
as there are cursors, and they should all be floats or
lengths.
values: list[float] or list[length]
The lengths to which to set these bonds.
anchor: string or ID
The search or ID to identify atoms in the view that
are anchored, and which cannot be moved when the
internal is set.
weighting: string
The weighting function used to distribute the move
across the atoms in the view. This is either;
'absolute_number', 'relative_number',
'absolute_mass' or 'relative_mass'. It defaults
to 'absolute_number'
auto_align: bool
Whether or not to align the molecule against itself
after the move
map: sire.base.PropertyMap or dict
Map of property keys that are passed through to
control which properties are used for the move,
plus fine-grained control of the weight function
or anchors.
"""
if not hasattr(values, "__len__"):
return self.set_length(
value=values,
anchor=anchor,
weighting=weighting,
auto_align=auto_align,
map=map,
)
elif len(values) != len(self._cursors):
raise ValueError(
f"The number of length values ({len(values)}) does "
f"not equal the number of cursors ({len(self._cursors)})."
)
from ..units import length
values = [length(value) for value in values]
molecule = self._parent._d.molecule.commit()
map = _process_move_options(
view=molecule, anchor=anchor, weighting=weighting, map=map
)
map = self._parent._d.merge(map)
moved = molecule.move()
for value, cursor in zip(values, self._cursors):
moved.set(cursor._internal, value, map)
if auto_align and (anchor is None):
moved.align(molecule, map)
self._parent._d.molecule = moved.commit().molecule().edit()
self._update()
return self
def set_length(value, anchor=None, weighting=None, auto_align=True, map=None):
"""Set all bonds edited by this cursor to the supplied length.
value: float or length
The length to which to set these bonds.
anchor: string or ID
The search or ID to identify atoms in the view that
are anchored, and which cannot be moved when the
internal is set.
weighting: string
The weighting function used to distribute the move
across the atoms in the view. This is either;
'absolute_number', 'relative_number',
'absolute_mass' or 'relative_mass'. It defaults
to 'absolute_number'
auto_align: bool
Whether or not to align the molecule against itself
after the move
map: sire.base.PropertyMap or dict
Map of property keys that are passed through to
control which properties are used for the move,
plus fine-grained control of the weight function
or anchors.
"""
values = [value for _ in range(0, len(self._cursors))]
set_lengths(
values,
anchor=anchor,
weighting=weighting,
auto_align=auto_align,
map=map,
)
return self
def change_lengths(
deltas, anchor=None, weighting=None, auto_align=True, map=None
):
"""Change the bonds being edited by this cursor by the specified
values. Note that there should be the same number of values
as there are cursors, and they should all be floats or
lengths.
deltas: list[float] or list[length]
The lengths by which to change these bonds.
anchor: string or ID
The search or ID to identify atoms in the view that
are anchored, and which cannot be moved when the
internal is set.
weighting: string
The weighting function used to distribute the move
across the atoms in the view. This is either;
'absolute_number', 'relative_number',
'absolute_mass' or 'relative_mass'. It defaults
to 'absolute_number'
auto_align: bool
Whether or not to align the molecule against itself
after the move
map: sire.base.PropertyMap or dict
Map of property keys that are passed through to
control which properties are used for the move,
plus fine-grained control of the weight function
or anchors.
"""
if not hasattr(deltas, "__len__"):
return self.change_length(
delta=deltas,
anchor=anchor,
weighting=weighting,
auto_align=auto_align,
map=map,
)
elif len(deltas) != len(self._cursors):
raise ValueError(
f"The number of length values ({len(deltas)}) does "
f"not equal the number of cursors ({len(self._cursors)})."
)
from ..units import length
deltas = [length(delta) for delta in deltas]
molecule = self._parent._d.molecule.commit()
map = _process_move_options(
view=molecule, anchor=anchor, weighting=weighting, map=map
)
map = self._parent._d.merge(map)
moved = molecule.move()
for delta, cursor in zip(deltas, self._cursors):
moved.change(cursor._internal, delta, map)
if auto_align and (anchor is None):
moved.align(molecule, map)
self._parent._d.molecule = moved.commit().molecule().edit()
self._update()
return self
def change_length(
delta, anchor=None, weighting=None, auto_align=True, map=None
):
"""Change all bonds edited by this cursor by the supplied length.
delta: float or length
The length by which to change these bonds.
anchor: string or ID
The search or ID to identify atoms in the view that
are anchored, and which cannot be moved when the
internal is set.
weighting: string
The weighting function used to distribute the move
across the atoms in the view. This is either;
'absolute_number', 'relative_number',
'absolute_mass' or 'relative_mass'. It defaults
to 'absolute_number'
auto_align: bool
Whether or not to align the molecule against itself
after the move
map: sire.base.PropertyMap or dict
Map of property keys that are passed through to
control which properties are used for the move,
plus fine-grained control of the weight function
or anchors."""
deltas = [delta for _ in range(0, len(self._cursors))]
change_lengths(
deltas,
anchor=anchor,
weighting=weighting,
auto_align=auto_align,
map=map,
)
self.lengths = get_lengths
self.set_length = set_length
self.set_lengths = set_lengths
self.change_length = change_length
self.change_lengths = change_lengths
self.measures = get_lengths
self.set_measure = set_length
self.set_measures = set_lengths
self.change_measure = set_length
self.change_measures = set_lengths
def _add_angle_functions(self):
"""Internal function that adds functions to this cursor
that are useful when editing angles, dihedrals or impropers
"""
def get_sizes(map=None):
"""Return the sizes of all internals being edited by these
cursors
"""
map = self._parent._d.merge(map)
sizes = []
for cursor in self._cursors:
sizes.append(cursor.size(map=map))
return sizes
def set_sizes(
values,
anchor=None,
weighting=None,
auto_align=True,
move_all=True,
map=None,
):
"""Set the internals being edited by this cursor to the specified
values. Note that there should be the same number of values
as there are cursors, and they should all be floats or
angles.
values: list[float] or list[angle]
The angles to which to set these internals.
anchor: string or ID
The search or ID to identify atoms in the view that
are anchored, and which cannot be moved when the
internal is set.
weighting: string
The weighting function used to distribute the move
across the atoms in the view. This is either;
'absolute_number', 'relative_number',
'absolute_mass' or 'relative_mass'. It defaults
to 'absolute_number'
auto_align: bool
Whether or not to align the molecule against itself
after the move
move_all: bool
Option only used for dihedrals. Whether or not to
move all atoms around the dihedral or just the
specified dihedral
map: sire.base.PropertyMap or dict
Map of property keys that are passed through to
control which properties are used for the move,
plus fine-grained control of the weight function
or anchors.
"""
if not hasattr(values, "__len__"):
return self.set_size(
value=values,
anchor=anchor,
weighting=weighting,
auto_align=auto_align,
move_all=move_all,
map=map,
)
elif len(values) != len(self._cursors):
raise ValueError(
f"The number of angle values ({len(values)}) does "
f"not equal the number of cursors ({len(self._cursors)})."
)
from ..units import angle
values = [angle(value) for value in values]
view = self._parent._d.molecule.commit()
map = _process_move_options(
view=view, anchor=anchor, weighting=weighting, map=map
)
map = self._parent._d.merge(map)
moved = view.move()
if move_all and self._cursors[0].is_dihedral():
for value, cursor in zip(values, self._cursors):
moved.set_all(cursor._internal, value, map)
else:
for value, cursor in zip(values, self._cursors):
moved.set(cursor._internal, value, map)
if auto_align and (anchor is None):
moved.align(view, map)
self._parent._d.molecule = moved.commit().molecule().edit()
self._update()
return self
def set_size(
value,
anchor=None,
weighting=None,
auto_align=True,
move_all=True,
map=None,
):
"""Set all internals edited by this cursor to the supplied angle.
value: float or angle
The angle to which to set these internals.
anchor: string or ID
The search or ID to identify atoms in the view that
are anchored, and which cannot be moved when the
internal is set.
weighting: string
The weighting function used to distribute the move
across the atoms in the view. This is either;
'absolute_number', 'relative_number',
'absolute_mass' or 'relative_mass'. It defaults
to 'absolute_number'
auto_align: bool
Whether or not to align the molecule against itself
after the move
move_all: bool
Option only used for dihedrals. Whether or not to
move all atoms around the dihedral or just the
specified dihedral
map: sire.base.PropertyMap or dict
Map of property keys that are passed through to
control which properties are used for the move,
plus fine-grained control of the weight function
or anchors.
"""
values = [value for _ in range(0, len(self._cursors))]
set_sizes(
values,
anchor=anchor,
weighting=weighting,
auto_align=auto_align,
move_all=move_all,
map=map,
)
return self
def change_sizes(
deltas,
anchor=None,
weighting=None,
auto_align=True,
move_all=True,
map=None,
):
"""
Change the internals being edited by this cursor by the specified
values. Note that there should be the same number of values
as there are cursors, and they should all be floats or
angles.
deltas: list[float] or list[angle]
The angles by which to change these internals.
anchor: string or ID
The search or ID to identify atoms in the view that
are anchored, and which cannot be moved when the
internal is set.
weighting: string
The weighting function used to distribute the move
across the atoms in the view. This is either;
'absolute_number', 'relative_number',
'absolute_mass' or 'relative_mass'. It defaults
to 'absolute_number'
auto_align: bool
Whether or not to align the molecule against itself
after the move
move_all: bool
Option only used for dihedrals. Whether or not to
move all atoms around the dihedral or just the
specified dihedral
map: sire.base.PropertyMap or dict
Map of property keys that are passed through to
control which properties are used for the move,
plus fine-grained control of the weight function
or anchors.
"""
if not hasattr(deltas, "__len__"):
return self.change_size(
delta=deltas,
anchor=anchor,
weighting=weighting,
auto_align=auto_align,
move_all=move_all,
map=map,
)
elif len(deltas) != len(self._cursors):
raise ValueError(
f"The number of angle values ({len(deltas)}) does "
f"not equal the number of cursors ({len(self._cursors)})."
)
from ..units import angle
deltas = [angle(delta) for delta in deltas]
view = self._parent._d.molecule.commit()
map = _process_move_options(
view=view, anchor=anchor, weighting=weighting, map=map
)
map = self._parent._d.merge(map)
moved = view.move()
if move_all and self._cursors[0].is_dihedral():
from . import BondID
for delta, cursor in zip(deltas, self._cursors):
bond = BondID(cursor._internal.atom0(), cursor._internal.atom1())
moved.change(bond, delta, map)
else:
for delta, cursor in zip(deltas, self._cursors):
moved.change(cursor._internal, delta, map)
if auto_align and (anchor is None):
moved.align(view, map)
self._parent._d.molecule = moved.commit().molecule().edit()
self._update()
return self
def change_size(
delta,
anchor=None,
weighting=None,
auto_align=True,
move_all=True,
map=None,
):
"""
Change all internals edited by this cursor by the supplied angle.
delta: float or angle
The angle by which to change these internals.
anchor: string or ID
The search or ID to identify atoms in the view that
are anchored, and which cannot be moved when the
internal is set.
weighting: string
The weighting function used to distribute the move
across the atoms in the view. This is either;
'absolute_number', 'relative_number',
'absolute_mass' or 'relative_mass'. It defaults
to 'absolute_number'
auto_align: bool
Whether or not to align the molecule against itself
after the move
move_all: bool
Option only used for dihedrals. Whether or not to
move all atoms around the dihedral or just the
specified dihedral
map: sire.base.PropertyMap or dict
Map of property keys that are passed through to
control which properties are used for the move,
plus fine-grained control of the weight function
or anchors.
"""
deltas = [delta for _ in range(0, len(self._cursors))]
change_sizes(
deltas,
anchor=anchor,
weighting=weighting,
auto_align=auto_align,
move_all=move_all,
map=map,
)
return self
self.sizes = get_sizes
self.set_size = set_size
self.set_sizes = set_sizes
self.change_size = change_size
self.change_sizes = change_sizes
self.measures = get_sizes
self.set_measure = set_size
self.set_measures = set_sizes
self.change_measure = set_size
self.change_measures = set_sizes
def _add_extra_functions(self):
"""Internal function used to add extra member functions to this
cursor depending on the type of object being edited
"""
if hasattr(self._cursors[0], "set_length"):
self._add_bond_functions()
elif hasattr(self._cursors[0], "set_size"):
self._add_angle_functions()
[docs]
def is_same_editor(self, other):
"""Return whether this is using the same editor to edit
the molecule as 'other'. This returns true if the underlying
editor for both cursors is the same, i.e. changes made by
one cursor would be seen and be editable by the other cursor.
"""
return self._parent.is_same_editor(other)
[docs]
def get(self, key):
"""Return the property associated with key 'key'"""
values = []
for cursor in self._cursors:
values.append(cursor.get(key))
return values
[docs]
def set(self, key, value):
"""Set the property associated with key 'key' to the
passed value
"""
self.__setitem__(key, value)
return self
[docs]
def delete(self, key):
"""Remove the property associated with the key 'key'"""
self.__delitem__(key)
return self
[docs]
def view(self):
"""Return the view underpinning this cursor. This is
the same as calling '.commit()'
"""
ret = self._view.clone()
from ..legacy.Mol import Molecules
ret.update(Molecules(self.commit()))
return ret
[docs]
def commit(self):
"""Commit all of the changes and return the newly
edited molecule (or MoleculeView). This commits
on the parent Cursor that was used to create
this list, e.g.
>>> mol.cursor().atoms().commit()
will commit and return the updated molecule (mol).
This is equivalent to `self.parent().commit()`
"""
return self._parent.commit()
[docs]
def atoms(self, id=None):
"""Return cursors for all of atoms in this view,
of, if 'id' is supplied, the atoms in this view
that match 'id'
"""
if id is None:
return self._parent._from_views(self._view.atoms())
else:
return self._parent._from_views(self._view.atoms(id))
[docs]
def residues(self, id=None):
"""Return cursors for all of residues in this view,
of, if 'id' is supplied, the residues in this view
that match 'id'
"""
if id is None:
return self._parent._from_views(self._view.residues())
else:
return self._parent._from_views(self._view.residues(id))
[docs]
def chains(self, id=None):
"""Return cursors for all of chains in this view,
of, if 'id' is supplied, the chains in this view
that match 'id'
"""
if id is None:
return self._parent._from_views(self._view.chains())
else:
return self._parent._from_views(self._view.chains(id))
[docs]
def segments(self, id=None):
"""Return cursors for all of segments in this view,
of, if 'id' is supplied, the segments in this view
that match 'id'
"""
if id is None:
return self._parent._from_views(self._view.segments())
else:
return self._parent._from_views(self._view.segments(id))
[docs]
def bonds(self, *args, **kwargs):
"""Return cursors for all of the bonds in this
view or, if 'id' is supplied, the bonds in this
view that match 'id'
"""
return self._parent._from_views(self._view.bonds(*args, **kwargs))
[docs]
def angles(self, *args, **kwargs):
"""Return cursors for all of the angles in this
view or, if 'id' is supplied, the angles in this
view that match 'id'
"""
return self._parent._from_views(self._view.angles(*args, **kwargs))
[docs]
def dihedrals(self, *args, **kwargs):
"""Return cursors for all of the dihedrals in this
view or, if 'id' is supplied, the dihedrals in this
view that match 'id'
"""
return self._parent._from_views(self._view.dihedrals(*args, **kwargs))
[docs]
def impropers(self, *args, **kwargs):
"""Return cursors for all of the impropers in this
view or, if 'id' is supplied, the impropers in this
view that match 'id'
"""
return self._parent._from_views(self._view.impropers(*args, **kwargs))
[docs]
def atom(self, i):
"""Return the atom in the molecule that matches the passed ID"""
return self._parent._from_view(self._view.atom(i))
[docs]
def residue(self, i=None):
"""Return the residue in the molecule that matches the passed ID.
If 'i' is None, then this returns the residue that contains
this atom (if this is an atom)
"""
if i is None:
return self._parent._from_view(self._view.residue())
else:
return self._parent._from_view(self._view.residue(i))
[docs]
def chain(self, i=None):
"""Return the chain in the molecule that matches the passed ID.
If 'i' is None, then this returns the chain that contains
this atom (if this is an atom)"""
if i is None:
return self._parent._from_view(self._view.chain())
else:
return self._parent._from_view(self._view.chain(i))
[docs]
def segment(self, i=None):
"""Return the segment in the molecule that matches the passed ID.
If 'i' is None, then this returns the segment that contains
this atom (if this is an atom)"""
if i is None:
return self._parent._from_view(self._view.segment())
else:
return self._parent._from_view(self._view.segment(i))
[docs]
def molecule(self):
"""Return the molecule"""
return self._parent.molecule()
[docs]
def bond(self, *args, **kwargs):
"""Return the Cursor for the specified bond."""
return self._parent._from_view(self._view.bond(*args, **kwargs))
[docs]
def angle(self, *args, **kwargs):
"""Return the Cursor for the specified angle."""
return self._parent._from_view(self._view.angle(*args, **kwargs))
[docs]
def dihedral(self, *args, **kwargs):
"""Return the Cursor for the specified dihedral."""
return self._parent._from_view(self._view.dihedral(*args, **kwargs))
[docs]
def improper(self, *args, **kwargs):
"""Return the Cursor for the specified improper."""
return self._parent._from_view(self._view.improper(*args, **kwargs))
[docs]
def invert(self):
"""Return the inverse view of this cursor (i.e. all views that
are not selected - same as view.invert())"""
return self._parent._from_views(self._view.invert())
[docs]
def parent(self):
"""Return the parent cursor"""
return self._parent
[docs]
def apply(self, func, *args, **kwargs):
"""
Apply the passed function (with optional position and keyword
arguments) to all of the cursors in this list of Cursors
(i.e. everything except the parent). As the function is intended to use
the Cursor to edit molecules, only this Cursors object will be
returned.
This lets you run `.apply(...).commit()` as a single line.
The function can be either;
1. a string containing the name of the function to call, or
2. an actual function (either a normal function or a lambda expression)
You can optionally pass in positional and keyword arguments
here that will be passed to the function.
Args:
func (str or function): The function to be called, or the name
of the function to be called.
Returns:
Cursors: This list of cursors
"""
for cursor in self._cursors:
cursor.apply(func, *args, **kwargs)
return self
[docs]
def num_frames(self):
"""
Return the number of frames in the trajectory held by this molecule
"""
return self._parent.num_frames()
[docs]
def load_frame(self, *args, **kwargs):
"""Call `load_frame` with these arguments on all contained cursors"""
self._parent.load_frame(*args, **kwargs)
self._update()
return self
[docs]
def save_frame(self, *args, **kwargs):
"""Call 'save_frame' with these arguments on all contained cursors"""
self._parent.save_frame(*args, **kwargs)
self._update()
return self
[docs]
def delete_frame(self, *args, **kwargs):
"""Call 'delete_frame' with these arguments on all contained cursors"""
self._parent.delete_frame(*args, **kwargs)
self._update()
return self
[docs]
def make_whole(self, center=None, map=None):
"""
Make all of the atoms operated on by this cursor whole
(they won't be broken across a periodic box boundary)
Use 'map' to specify the property map used to find the
coordinates and space properties. Pass in 'center'
to specify the center of the periodic box into
which they should be wrapped.
"""
for cursor in self._cursors:
cursor.make_whole(center=center, map=map)
return self
[docs]
def translate(self, *args, map=None):
"""
Translate all of the atoms operated on by these cursors
by the passed arguments (these are converted automatically
to a sr.maths.Vector). Use 'map' to specify the property
map to use to find the coordinates property
"""
for cursor in self._cursors:
cursor.translate(*args, map=map)
return self
[docs]
def rotate(
self,
angle=None,
axis=None,
center=None,
quaternion=None,
matrix=None,
rotate_velocities: bool = None,
map=None,
):
"""Rotate all of the atoms operated on by this cursor
by the passed arguments. Use 'map' to specify the
property map to use to find the coordinates property.
There are many ways to specify the rotation, hence
the number of named arguments:
angle: (float or angle)
The angle to rotate by - this is interpreted as
degrees if you pass in a float. Otherwise use
sire.units.degrees or sire.units.radians to specify
the angle unit. This is superseded by the
quaternion or matrix arguments.
axis: sire.maths.Vector (or anything that can convert to a Vector)
The vector about which to rotate. If this is not
specified, and no other rotation specification is
used, then the rotation is about the z axis.
This is superseded by the quaternion or
matrix arguments.
center: sire.maths.Vector (or anything that can convert to a Vector)
The center for the rotation. If this isn't passed then
the center of mass of the atoms operated on by this
cursor is used.
quaternion: sire.maths.Quaternion
The Quaternion description of the rotation. Note that,
if you pass this, then the angle, axis and matrix
arguments will be ignored.
matrix: sire.maths.Matrix
The 3x3 rotation matrix that describes the rotation.
Note that, if you pass this, then the angle and axis
arguments will be ignored. This is superseded by
the quaternion argument.
rotate_velocities: bool
Whether or not to rotate the velocities as well as
the coordinates (default True)
map: None, dict or sire.base.PropertyMap
The property map used to find the coordinates property
"""
from ..maths import create_quaternion
quaternion = create_quaternion(
angle=angle, axis=axis, matrix=matrix, quaternion=quaternion
)
for cursor in self._cursors:
cursor.rotate(
quaternion=quaternion,
center=center,
rotate_velocities=rotate_velocities,
map=map,
)
return self
[docs]
class CursorsM:
"""This class holds a list of Cursor/Cursors that operate across
multiple molecules. This allows you to perform editing
operations across many molecules at the same time.
"""
def __init__(self, parent=None, map=None):
self._parent = None
self._cursors = []
self._molcursors = {}
if parent is not None:
self._parent = parent.clone()
for child in parent:
child_mol = child.molecule()
molnum = child_mol.number()
if molnum not in self._molcursors:
self._molcursors[molnum] = child_mol.cursor(map=map)
self._cursors.append(self._molcursors[molnum]._from_view(child))
self._add_extra_functions()
def _add_bond_functions(self):
"""Internal function used to add functions for editing bonds"""
def get_lengths(map=None):
"""Return the lengths of all the bonds edited by this cursor."""
map = self._cursors[0]._d.merge(map)
lengths = []
for cursor in self._cursors:
lengths.append(cursor.length(map=map))
return lengths
def set_lengths(values, anchor=None, weighting=None, auto_align=True, map=None):
"""
Set the lengths of the bonds being edited by this cursor to the
specified values. Note that there should be the same number of
values as there are cursors, and they should all be floats or
lengths.
values: list[float] or list[length]
The lengths to which to set these bonds.
anchor: string or ID
The search or ID to identify atoms in the view that
are anchored, and which cannot be moved when the
internal is set.
weighting: string
The weighting function used to distribute the move
across the atoms in the view. This is either;
'absolute_number', 'relative_number',
'absolute_mass' or 'relative_mass'. It defaults
to 'absolute_number'
auto_align: bool
Whether or not to align the molecule against itself
after the move
map: sire.base.PropertyMap or dict
Map of property keys that are passed through to
control which properties are used for the move,
plus fine-grained control of the weight function
or anchors.
"""
if not hasattr(values, "__len__"):
return self.set_length(
value=values,
anchor=anchor,
weighting=weighting,
auto_align=auto_align,
map=map,
)
elif len(values) != len(self._cursors):
raise ValueError(
f"The number of length values ({len(values)}) does "
f"not equal the number of cursors ({len(self._cursors)})."
)
from ..units import length
values = [length(value) for value in values]
movers = {}
molecules = {}
maps = {}
for value, cursor in zip(values, self._cursors):
molnum = cursor._d.number()
if molnum not in movers:
molecule = self._molcursors[molnum]._d.molecule.commit()
molecules[molnum] = molecule
movers[molnum] = molecule.move()
maps[molnum] = self._cursors[0]._d.merge(
_process_move_options(
view=molecule,
anchor=anchor,
weighting=weighting,
map=map,
)
)
movers[molnum].set(cursor._internal, value, maps[molnum])
for molnum, mover in movers.items():
if auto_align and (anchor is None):
mover.align(molecules[molnum], maps[molnum])
self._molcursors[molnum]._d.molecule = mover.commit().edit()
self._molcursors[molnum]._update()
self._update()
return self
def set_length(value, anchor=None, weighting=None, auto_align=True, map=None):
"""Set the lengths of all of the bonds edited by this
cursor to the passed value.
value: float or length
The length to which to set these bonds.
anchor: string or ID
The search or ID to identify atoms in the view that
are anchored, and which cannot be moved when the
internal is set.
weighting: string
The weighting function used to distribute the move
across the atoms in the view. This is either;
'absolute_number', 'relative_number',
'absolute_mass' or 'relative_mass'. It defaults
to 'absolute_number'
auto_align: bool
Whether or not to align the molecule against itself
after the move
map: sire.base.PropertyMap or dict
Map of property keys that are passed through to
control which properties are used for the move,
plus fine-grained control of the weight function
or anchors.
"""
values = [value for _ in self._cursors]
set_lengths(
values,
anchor=anchor,
weighting=weighting,
auto_align=auto_align,
map=map,
)
return self
def change_lengths(
deltas, anchor=None, weighting=None, auto_align=True, map=None
):
"""
Change the lengths of the bonds being edited by this cursor by
the specified deltas. Note that there should be the same number of
values as there are cursors, and they should all be floats or
lengths.
deltas: list[float] or list[length]
The lengths by which to change these bonds.
anchor: string or ID
The search or ID to identify atoms in the view that
are anchored, and which cannot be moved when the
internal is set.
weighting: string
The weighting function used to distribute the move
across the atoms in the view. This is either;
'absolute_number', 'relative_number',
'absolute_mass' or 'relative_mass'. It defaults
to 'absolute_number'
auto_align: bool
Whether or not to align the molecule against itself
after the move
map: sire.base.PropertyMap or dict
Map of property keys that are passed through to
control which properties are used for the move,
plus fine-grained control of the weight function
or anchors.
"""
if not hasattr(deltas, "__len__"):
return self.change_length(
delta=deltas,
anchor=anchor,
weighting=weighting,
auto_align=auto_align,
map=map,
)
elif len(deltas) != len(self._cursors):
raise ValueError(
f"The number of length values ({len(deltas)}) does "
f"not equal the number of cursors ({len(self._cursors)})."
)
from ..units import length
deltas = [length(delta) for delta in deltas]
movers = {}
molecules = {}
maps = {}
for delta, cursor in zip(deltas, self._cursors):
molnum = cursor._d.number()
if molnum not in movers:
molecule = self._molcursors[molnum]._d.molecule.commit()
molecules[molnum] = molecule
movers[molnum] = molecule.move()
maps[molnum] = self._cursors[0]._d.merge(
_process_move_options(
view=molecule,
anchor=anchor,
weighting=weighting,
map=map,
)
)
movers[molnum].change(cursor._internal, delta, maps[molnum])
for molnum, mover in movers.items():
if auto_align and (anchor is None):
mover.align(molecules[molnum], maps[molnum])
self._molcursors[molnum]._d.molecule = mover.commit().edit()
self._molcursors[molnum]._update()
self._update()
return self
def change_length(
delta, anchor=None, weighting=None, auto_align=True, map=None
):
"""Change the lengths of all of the bonds edited by this
cursor by the passed value.
delta: float or length
The length by which to change these bonds.
anchor: string or ID
The search or ID to identify atoms in the view that
are anchored, and which cannot be moved when the
internal is set.
weighting: string
The weighting function used to distribute the move
across the atoms in the view. This is either;
'absolute_number', 'relative_number',
'absolute_mass' or 'relative_mass'. It defaults
to 'absolute_number'
auto_align: bool
Whether or not to align the molecule against itself
after the move
map: sire.base.PropertyMap or dict
Map of property keys that are passed through to
control which properties are used for the move,
plus fine-grained control of the weight function
or anchors.
"""
deltas = [delta for _ in self._cursors]
change_lengths(
deltas,
anchor=anchor,
weighting=weighting,
auto_align=auto_align,
map=map,
)
return self
self.lengths = get_lengths
self.set_length = set_length
self.set_lengths = set_lengths
self.change_length = change_length
self.change_lengths = change_lengths
self.measures = get_lengths
self.set_measure = set_length
self.set_measures = set_lengths
self.change_measure = change_length
self.change_measures = change_lengths
def _add_angle_functions(self):
"""Internal function used to add functions for editing
angles, dihedrals or impropers
"""
def get_sizes(map=None):
"""Return the sizes of all the internals edited by this cursor."""
map = self._cursors[0]._d.merge(map)
sizes = []
for cursor in self._cursors:
sizes.append(cursor.size(map=map))
return sizes
def set_sizes(
values,
anchor=None,
weighting=None,
auto_align=True,
move_all=True,
map=None,
):
"""
Set the sizes of the internals being edited by this cursor to the
specified values. Note that there should be the same number of
values as there are cursors, and they should all be floats or
angles.
values: list[float] or list[angle]
The angles to which to set these internals.
anchor: string or ID
The search or ID to identify atoms in the view that
are anchored, and which cannot be moved when the
internal is set.
weighting: string
The weighting function used to distribute the move
across the atoms in the view. This is either;
'absolute_number', 'relative_number',
'absolute_mass' or 'relative_mass'. It defaults
to 'absolute_number'
auto_align: bool
Whether or not to align the molecule against itself
after the move
move_all: bool
Option only used for dihedrals. Whether or not to
move all atoms around the dihedral or just the
specified dihedral
map: sire.base.PropertyMap or dict
Map of property keys that are passed through to
control which properties are used for the move,
plus fine-grained control of the weight function
or anchors.
"""
if not hasattr(values, "__len__"):
return self.set_size(
value=values,
anchor=anchor,
weighting=weighting,
auto_align=auto_align,
move_all=move_all,
map=map,
)
elif len(values) != len(self._cursors):
raise ValueError(
f"The number of angle values ({len(values)}) does "
f"not equal the number of cursors ({len(self._cursors)})."
)
from ..units import angle
values = [angle(value) for value in values]
molecules = {}
movers = {}
maps = {}
map = self._cursors[0]._d.merge(map)
for value, cursor in zip(values, self._cursors):
molnum = cursor._d.number()
if molnum not in movers:
molecule = self._molcursors[molnum]._d.molecule.commit()
molecules[molnum] = molecule
movers[molnum] = molecule.move()
maps[molnum] = self._cursors[0]._d.merge(
_process_move_options(
view=molecule,
anchor=anchor,
weighting=weighting,
map=map,
)
)
movers[molnum].set(cursor._internal, value, maps[molnum])
for molnum, mover in movers.items():
if auto_align and (anchor is None):
mover.align(molecules[molnum], map[molnum])
self._molcursors[molnum]._d.molecule = mover.commit().edit()
self._molcursors[molnum]._update()
self._update()
return self
def set_size(
value,
anchor=None,
weighting=None,
auto_align=True,
move_all=True,
map=None,
):
"""Set the sizes of all of the internals edited by this
cursor to the passed value.
value: float or angle
The angle to which to set these internals.
anchor: string or ID
The search or ID to identify atoms in the view that
are anchored, and which cannot be moved when the
internal is set.
weighting: string
The weighting function used to distribute the move
across the atoms in the view. This is either;
'absolute_number', 'relative_number',
'absolute_mass' or 'relative_mass'. It defaults
to 'absolute_number'
auto_align: bool
Whether or not to align the molecule against itself
after the move
move_all: bool
Option only used for dihedrals. Whether or not to
move all atoms around the dihedral or just the
specified dihedral
map: sire.base.PropertyMap or dict
Map of property keys that are passed through to
control which properties are used for the move,
plus fine-grained control of the weight function
or anchors.
"""
values = [value for _ in self._cursors]
set_sizes(
values,
anchor=anchor,
weighting=weighting,
auto_align=auto_align,
move_all=move_all,
map=map,
)
return self
def change_sizes(
deltas,
anchor=None,
weighting=None,
auto_align=True,
move_all=True,
map=None,
):
"""
Change the sizes of the internals being edited by this cursor by
the specified values. Note that there should be the same number of
values as there are cursors, and they should all be floats or
angles.
deltas: list[float] or list[angle]
The angles by which to change these internals.
anchor: string or ID
The search or ID to identify atoms in the view that
are anchored, and which cannot be moved when the
internal is set.
weighting: string
The weighting function used to distribute the move
across the atoms in the view. This is either;
'absolute_number', 'relative_number',
'absolute_mass' or 'relative_mass'. It defaults
to 'absolute_number'
auto_align: bool
Whether or not to align the molecule against itself
after the move
move_all: bool
Option only used for dihedrals. Whether or not to
move all atoms around the dihedral or just the
specified dihedral
map: sire.base.PropertyMap or dict
Map of property keys that are passed through to
control which properties are used for the move,
plus fine-grained control of the weight function
or anchors.
"""
if not hasattr(deltas, "__len__"):
return self.change_size(
delta=deltas,
anchor=anchor,
weighting=weighting,
auto_align=auto_align,
move_all=move_all,
map=map,
)
elif len(deltas) != len(self._cursors):
raise ValueError(
f"The number of angle values ({len(deltas)}) does "
f"not equal the number of cursors ({len(self._cursors)})."
)
from ..units import angle
deltas = [angle(delta) for delta in deltas]
molecules = {}
movers = {}
maps = {}
if move_all and self._cursors[0].is_dihedral():
from . import BondID
for delta, cursor in zip(deltas, self._cursors):
molnum = cursor._d.number()
if molnum not in movers:
molecule = self._molcursors[molnum]._d.molecule.commit()
molecules[molnum] = molecule
movers[molnum] = molecule.move()
maps[molnum] = self._cursors[0]._d.merge(
_process_move_options(
view=molecule,
anchor=anchor,
weighting=weighting,
map=map,
)
)
bond = BondID(cursor._internal.atom0(), cursor._internal.atom1())
movers[molnum].change(bond, delta, maps[molnum])
else:
for delta, cursor in zip(deltas, self._cursors):
molnum = cursor._d.number()
if molnum not in movers:
molecule = self._molcursors[molnum]._d.molecule.commit()
molecules[molnum] = molecule
movers[molnum] = molecule.move()
maps[molnum] = self._cursors[0]._d.merge(
_process_move_options(
view=molecule,
anchor=anchor,
weighting=weighting,
map=map,
)
)
movers[molnum].change(cursor._internal, delta, maps[molnum])
for molnum, mover in movers.items():
if auto_align and (anchor is None):
mover.align(molecules[molnum], maps[molnum])
self._molcursors[molnum]._d.molecule = mover.commit().edit()
self._molcursors[molnum]._update()
self._update()
return self
def change_size(
value,
anchor=None,
weighting=None,
auto_align=True,
move_all=True,
map=None,
):
"""Change the sizes of all of the internals edited by this
cursor by the passed value.
delta: float or angle
The angle by which to change these internals.
anchor: string or ID
The search or ID to identify atoms in the view that
are anchored, and which cannot be moved when the
internal is set.
weighting: string
The weighting function used to distribute the move
across the atoms in the view. This is either;
'absolute_number', 'relative_number',
'absolute_mass' or 'relative_mass'. It defaults
to 'absolute_number'
auto_align: bool
Whether or not to align the molecule against itself
after the move
move_all: bool
Option only used for dihedrals. Whether or not to
move all atoms around the dihedral or just the
specified dihedral
map: sire.base.PropertyMap or dict
Map of property keys that are passed through to
control which properties are used for the move,
plus fine-grained control of the weight function
or anchors.
"""
values = [value for _ in self._cursors]
change_sizes(
values,
anchor=anchor,
weighting=weighting,
auto_align=auto_align,
move_all=move_all,
map=map,
)
return self
self.sizes = get_sizes
self.set_size = set_size
self.set_sizes = set_sizes
self.change_size = change_size
self.change_sizes = change_sizes
self.measures = get_sizes
self.set_measure = set_size
self.set_measures = set_sizes
self.change_measure = change_size
self.change_measures = change_sizes
def _add_extra_functions(self):
"""Internal function used to add extra member functions depending
on the type of object being edited
"""
if hasattr(self._cursors[0], "set_length"):
self._add_bond_functions()
elif hasattr(self._cursors[0], "set_size"):
self._add_angle_functions()
def _from_view(self, view):
"""Internal function that constructs from a single view"""
molnum = view.molecule().number()
molcursor = self._molcursors[molnum]
c = Cursor()
c._d = molcursor._d
from ..mm import Bond, Angle, Dihedral, Improper
from . import Molecule
if type(view) is Molecule:
c._view = molcursor._d.molecule
elif type(view) in [Bond, Angle, Dihedral, Improper]:
c._view = molcursor._d.molecule
c._internal = view.id()
else:
c._view = molcursor._d.molecule[view.index()]
c._add_extra_functions()
c._update()
return c
def _from_views(self, views):
"""Internal function to construct from a set of views"""
ret = CursorsM()
ret._parent = views.clone()
ret._molcursors = self._molcursors
for view in views:
molnum = view.molecule().number()
ret._cursors.append(ret._molcursors[molnum]._from_view(view))
ret._add_extra_functions()
return ret
def __call__(self, i):
try:
# try the simplest case - the ith cursor
idx = int(i)
return self._cursors[idx]
except Exception:
pass
return self._from_views(self._parent[i])
def __getitem__(self, key):
if type(key) is not str:
return self.__call__(key)
try:
return self.get(key)
except Exception as e:
property_error = e
try:
return self.__call__(key)
except Exception:
pass
raise property_error
def __setitem__(self, key, value):
if type(value) is list or type(value) is tuple:
if len(value) == len(self._cursors):
for i, v in enumerate(value):
self._cursors[i].set(key, v)
return
for cursor in self._cursors:
cursor.set(key, value)
def __delitem__(self, key):
for cursor in self._cursors:
cursor.delete(key)
def __contains__(self, key):
for cursor in self._cursors:
if key in cursor:
return True
return False
def _update(self):
"""Ensure that all cursors are up to date"""
for cursor in self._cursors:
cursor._update()
def __len__(self):
return len(self._cursors)
def __str__(self):
if len(self) == 0:
return "CursorsM::null"
else:
lines = []
n = len(self._cursors)
if n <= 10:
for i in range(0, n):
lines.append(f"{i+1}: {self._cursors[i]}")
else:
for i in range(0, 5):
lines.append(f"{i+1}: {self._cursors[i]}")
lines.append("...")
for i in range(n - 5, n):
lines.append(f"{i+1}: {self._cursors[i]}")
lines = "\n".join(lines)
return f"CursorsM( size={n}\n{lines}\n)"
def __repr__(self):
return self.__str__()
[docs]
def get(self, key):
"""Return the property associated with key 'key'"""
values = []
for cursor in self._cursors:
values.append(cursor.get(key))
return values
[docs]
def set(self, key, value):
"""Set the property associated with key 'key' to the
passed value
"""
self.__setitem__(key, value)
return self
[docs]
def delete(self, key):
"""Remove the property associated with the key 'key'"""
self.__delitem__(key)
return self
[docs]
def view(self):
"""Return the view that underlies this Cursor. Note that
this may not be updated to reflect the edits made.
Use .commit() to get the up-to-date version of this view.
"""
return self._parent.clone()
[docs]
def commit(self):
"""Commit all of the changes and return the newly
edited multi-molecule view.
"""
from ..legacy.Mol import Molecules
updated = Molecules()
updated.reserve(len(self._cursors))
for cursor in self._cursors:
updated.add(cursor.commit())
self._parent.update(updated)
return self._parent
[docs]
def atoms(self, id=None):
"""Return cursors for all of atoms in this view,
or, if 'id' is supplied, the atoms in this view
that match 'id'
"""
if id is None:
return self._from_views(self._parent.atoms())
else:
return self._from_views(self._parent.atoms(id))
[docs]
def residues(self, id=None):
"""Return cursors for all of residues in this view,
or, if 'id' is supplied, the residues in this view
that match 'id'
"""
if id is None:
return self._from_views(self._parent.residues())
else:
return self._from_views(self._parent.residues(id))
[docs]
def chains(self, id=None):
"""Return cursors for all of chains in this view,
or, if 'id' is supplied, the chains in this view
that match 'id'
"""
if id is None:
return self._from_views(self._parent.chains())
else:
return self._from_views(self._parent.chains(id))
[docs]
def segments(self, id=None):
"""Return cursors for all of segments in this view,
or, if 'id' is supplied, the segments in this view
that match 'id'
"""
if id is None:
return self._from_views(self._parent.segments())
else:
return self._from_views(self._parent.segments(id))
[docs]
def molecules(self, id=None):
"""Return cursors for all of the molecules in this view,
or, if 'id' is supplied, the molecules in this view
that match 'id'
"""
if id is None:
return self._from_views(self._parent.molecules())
else:
return self._from_views(self._parent.molecules(id))
[docs]
def bonds(self, *args, **kwargs):
"""Return cursors for all of the bonds in this
view or, if 'id' is supplied, the bonds in this
view that match 'id'
"""
return self._from_views(self._parent.bonds(*args, **kwargs))
[docs]
def angles(self, *args, **kwargs):
"""Return cursors for all of the angles in this
view or, if 'id' is supplied, the angles in this
view that match 'id'
"""
return self._from_views(self._parent.angles(*args, **kwargs))
[docs]
def dihedrals(self, *args, **kwargs):
"""Return cursors for all of the dihedrals in this
view or, if 'id' is supplied, the dihedrals in this
view that match 'id'
"""
return self._from_views(self._parent.dihedrals(*args, **kwargs))
[docs]
def impropers(self, *args, **kwargs):
"""Return cursors for all of the impropers in this
view or, if 'id' is supplied, the impropers in this
view that match 'id'
"""
return self._from_views(self._parent.impropers(*args, **kwargs))
[docs]
def atom(self, i):
"""Return the atom in the molecule that matches the passed ID"""
return self._from_view(self._parent.atom(i))
[docs]
def residue(self, i=None):
"""Return the residue in the molecule that matches the passed ID.
If 'i' is None, then this returns the residue that contains
this atom (if this is an atom)
"""
if i is None:
return self._from_view(self._parent.residue())
else:
return self._from_view(self._parent.residue(i))
[docs]
def chain(self, i=None):
"""Return the chain in the molecule that matches the passed ID.
If 'i' is None, then this returns the chain that contains
this atom (if this is an atom)"""
if i is None:
return self._from_view(self._parent.chain())
else:
return self._from_view(self._parent.chain(i))
[docs]
def segment(self, i=None):
"""Return the segment in the molecule that matches the passed ID.
If 'i' is None, then this returns the segment that contains
this atom (if this is an atom)"""
if i is None:
return self._from_view(self._parent.segment())
else:
return self._from_view(self._parent.segment(i))
[docs]
def molecule(self, i=None):
"""Return the molecule"""
if i is None:
mols = self._parent.molecules()
if len(mols) != 1:
raise ValueError(
"There is more than one molecule in this view "
f"({len(mols)}). "
"You need to specify which one you want."
)
return self._from_view(mols[0])
else:
return self._from_view(self._parent.molecule(i))
[docs]
def bond(self, *args, **kwargs):
"""Return the Cursor for the specified bond."""
return self._from_view(self._parent.bond(*args, **kwargs))
[docs]
def angle(self, *args, **kwargs):
"""Return the Cursor for the specified angle."""
return self._from_view(self._parent.angle(*args, **kwargs))
[docs]
def dihedral(self, *args, **kwargs):
"""Return the Cursor for the specified dihedral."""
return self._from_view(self._parent.dihedral(*args, **kwargs))
[docs]
def improper(self, *args, **kwargs):
"""Return the Cursor for the specified improper."""
return self._from_view(self._parent.improper(*args, **kwargs))
[docs]
def invert(self):
"""Return the inverse view of this cursor (i.e. all views that
are not selected - same as view.invert())"""
return self._from_views(self._parent.invert())
[docs]
def apply(self, func, *args, **kwargs):
"""
Apply the passed function (with optional position and keyword
arguments) to all of the cursors in this list of Cursors.
As the function is intended to use
the Cursor to edit molecules, only this Cursors object will be
returned.
This lets you run `.apply(...).commit()` as a single line.
The function can be either;
1. a string containing the name of the function to call, or
2. an actual function (either a normal function or a lambda expression)
You can optionally pass in positional and keyword arguments
here that will be passed to the function.
Args:
func (str or function): The function to be called, or the name
of the function to be called.
Returns:
Cursors: This list of cursors
"""
for cursor in self._cursors:
cursor.apply(func, *args, **kwargs)
return self
[docs]
def num_frames(self):
"""Return the number of frames in the trajectories held by
this cursor
"""
num = None
for cursor in self._molcursors.values():
if num is None:
num = cursor.num_frames()
else:
n = cursor.num_frames()
if n < num:
num = n
return num
[docs]
def load_frame(self, *args, **kwargs):
"""Call the 'load_frame' function with these arguments on all
contained cursors"""
for cursor in self._molcursors.values():
cursor.load_frame(*args, **kwargs)
self._update()
return self
[docs]
def save_frame(self, *args, **kwargs):
"""Call the 'save_frame' function with these arguments on all
contained cursors"""
for cursor in self._molcursors.values():
cursor.save_frame(*args, **kwargs)
self._update()
return self
[docs]
def delete_frame(self, *args, **kwargs):
"""Call the 'delete_frame' function with these arguments on all
contained cursors"""
for cursor in self._molcursors.values():
cursor.delete_frame(*args, **kwargs)
self._update()
return self
[docs]
def make_whole(self, center=None, map=None):
"""
Make all of the atoms operated on by this cursor whole
(they won't be broken across a periodic box boundary)
Use 'map' to specify the property map used to find the
coordinates and space properties. Pass in 'center'
to specify the center of the periodic box into
which they should be wrapped.
"""
for cursor in self._cursors:
cursor.make_whole(center=center, map=map)
return self
[docs]
def translate(self, *args, map=None):
"""
Translate all of the atoms operated on by these cursors
by the passed arguments (these are converted automatically
to a sr.maths.Vector). Use 'map' to specify the property
map to use to find the coordinates property
"""
for cursor in self._cursors:
cursor.translate(*args, map=map)
return self
[docs]
def rotate(
self,
angle=None,
axis=None,
center=None,
quaternion=None,
matrix=None,
rotate_velocities: bool = None,
map=None,
):
"""Rotate all of the atoms operated on by this cursor
by the passed arguments. Use 'map' to specify the
property map to use to find the coordinates property.
There are many ways to specify the rotation, hence
the number of named arguments:
angle: (float or angle)
The angle to rotate by - this is interpreted as
degrees if you pass in a float. Otherwise use
sire.units.degrees or sire.units.radians to specify
the angle unit. This is superseded by the
quaternion or matrix arguments.
axis: sire.maths.Vector (or anything that can convert to a Vector)
The vector about which to rotate. If this is not
specified, and no other rotation specification is
used, then the rotation is about the z axis.
This is superseded by the quaternion or
matrix arguments.
center: sire.maths.Vector (or anything that can convert to a Vector)
The center for the rotation. If this isn't passed then
the center of mass of the atoms operated on by this
cursor is used.
quaternion: sire.maths.Quaternion
The Quaternion description of the rotation. Note that,
if you pass this, then the angle, axis and matrix
arguments will be ignored.
matrix: sire.maths.Matrix
The 3x3 rotation matrix that describes the rotation.
Note that, if you pass this, then the angle and axis
arguments will be ignored. This is superseded by
the quaternion argument.
rotate_velocities: bool
Whether or not to rotate the velocities as well as
the coordinates (default True)
map: None, dict or sire.base.PropertyMap
The property map used to find the coordinates property
"""
from ..maths import create_quaternion
quaternion = create_quaternion(
angle=angle, axis=axis, matrix=matrix, quaternion=quaternion
)
for cursor in self._cursors:
cursor.rotate(
quaternion=quaternion,
center=center,
rotate_velocities=rotate_velocities,
map=map,
)
return self