Source code for schrodinger.structure._structure

"""
A module containing the central interface for reading and editing
chemical structures.

`Structure` is a pythonic, object oriented wrapper for the mmct
library that provides access to atoms, bonds, and their properties.
It provides common methods for inspecting and editing coordinates,
e.g. `Structure.measure`, `Structure.adjust`, and `Structure.merge`.  The
`schrodinger.structutils` package provides additional functions and classes
that operate on `Structure` objects.

`PropertyName` provides translation between m2io datanames and user
friendly names.

Copyright Schrodinger, LLC. All rights reserved.
"""

import enum
import warnings
from collections.abc import MutableMapping
from contextlib import contextmanager

import schrodinger.application.canvas.utils as canvasutils
import schrodinger.infra.structure as cppstructure
from schrodinger.infra import canvas
from schrodinger.infra import fast3d
from schrodinger.infra import mm
from schrodinger.infra import mmcheck
from schrodinger.infra.mmbitset import Bitset
from schrodinger.infra.util import CreateWhenNeeded
from schrodinger.structutils import color
from schrodinger.utils.fileutils import MAESTRO
from schrodinger.utils.fileutils import SD

# Placeholders for lazy (circular) module imports.
analyze = None

# Values that can be used for _StructureAtom.secondary_structure attribute
SS_NONE = mm.MMCT_SS_NONE
SS_LOOP = mm.MMCT_SS_LOOP
SS_HELIX = mm.MMCT_SS_HELIX
SS_STRAND = mm.MMCT_SS_STRAND
SS_TURN = mm.MMCT_SS_TURN

# Values for _StructureBond.setStyle() method:
BOND_NOSTYLE = mm.MMCT_BOND_NOSTYLE
BOND_WIRE = mm.MMCT_BOND_WIRE
BOND_TUBE = mm.MMCT_BOND_TUBE
BOND_BALLNSTICK = mm.MMCT_BOND_BALLNSTICK

# Values for _StructureAtom.style attribute:
ATOM_NOSTYLE = mm.MMCT_ATOM_NOSTYLE
ATOM_CIRCLE = mm.MMCT_ATOM_CIRCLE
ATOM_CPK = mm.MMCT_ATOM_CPK
ATOM_BALLNSTICK = mm.MMCT_ATOM_BALLNSTICK

NO_SPECIFIC_ISOTOPE = mm.MMCT_NO_SPECIFIC_ISOTOPE

# A data structure used to maintain a relationship between the PDB one-letter
# codes and the three-letter residue names:
# Keys are 3-letter codes, values are 1-letter codes
RESIDUE_MAP_3_TO_1_LETTER = mm.mmpdb_get_three_to_one_letter_residue_map()
# TODO: give deprotonated cysteine (CYM) a one-letter-code in the map. Remove
#  when mmpdb_get_three_to_one_letter_residue_map is updated to include CYM
#  (SHARED-8879)
RESIDUE_MAP_3_TO_1_LETTER['CYM'] = 'C'

# Mapping of residue names from 1-letter to the 3-letter name for 20 standard
# amino acids onle. Use with care, as converting 1-letter codes to 3-letter
# codes will result in wrong residue names for non-standard residues. Use this
# map only when just the 1-letter code is available. NOTE: sequence objects
# often have the 3-letter names for each residue stored internally, which can
# be used instead.
RESIDUE_MAP_1_TO_3_LETTER = {
    "A": "ALA",
    "R": "ARG",
    "N": "ASN",
    "D": "ASP",
    "C": "CYS",
    "Q": "GLN",
    "E": "GLU",
    "G": "GLY",
    "H": "HIS",
    "I": "ILE",
    "L": "LEU",
    "K": "LYS",
    "M": "MET",
    "F": "PHE",
    "P": "PRO",
    "S": "SER",
    "T": "THR",
    "W": "TRP",
    "Y": "TYR",
    "V": "VAL"
}

# Map built-in long atom property names to short Structure.Atom properties:
ATOM_PROP_LINK = {
    'i_m_mmod_type': 'atom_type',
    'r_m_x_coord': 'x',
    'r_m_y_coord': 'y',
    'r_m_z_coord': 'z',
    'i_m_residue_number': 'resnum',
    's_m_insertion_code': 'inscode',
    's_m_chain_name': 'chain',
    'i_m_color': '_color_index',
    'r_m_charge1': 'partial_charge',
    'r_m_charge2': 'solvation_charge',
    's_m_pdb_residue_name': 'pdbres',
    's_m_pdb_atom_name': 'pdbname',
    's_m_grow_name': 'growname',  # deletable, see PYAPP-6051
    'i_m_atomic_number': 'atomic_number',
    'i_m_formal_charge': 'formal_charge',
    'i_m_representation': 'style',  # deletable, see PYAPP-6051
    'i_m_visibility': 'visible',
    's_m_atom_name': 'name',
    'i_m_secondary_structure': 'secondary_structure',  # deletable, see PYAPP-6051
    mm.M2IO_DATA_ATOM_ISOTOPE_PROPERTY: 'isotope',
}

# ATOM_PROP_LINKs that can be reset by setting the property in the tuple
# to the indicated default value (see PYAPP-6051).
RESETABLE_BUILTIN_ATOM_PROPS = {
    's_m_grow_name': ('growname', ""),
    's_m_label_format': ('label_format', ""),
    's_m_label_user_text': ('label_user_text', ""),
    'i_m_representation': ('style', 0),
    'i_m_secondary_structure': ('secondary_structure', -1)
}

ct_property_getters = {
    "s": mm.mmct_ct_property_get_string,
    "i": mm.mmct_ct_property_get_int,
    "r": mm.mmct_ct_property_get_real,
    "b": mm.mmct_ct_property_get_bool,
}

# A dictionary of heavy atom counts for standard residues:
_res_sizes = {
    'ACE ': 3,
    'ALA ': 5,
    'ARG ': 11,
    'ASH ': 8,
    'ASN ': 8,
    'ASP ': 8,
    'CME ': 10,
    'CSD ': 8,
    'CYS ': 6,
    'CYX ': 6,
    'GLH ': 9,
    'GLN ': 9,
    'GLU ': 9,
    'GLY ': 4,
    'HIS ': 10,
    'HIP ': 10,
    'HID ': 10,
    'HIE ': 10,
    'ILE ': 8,
    'LEU ': 8,
    'LYS ': 9,
    'LYN ': 9,
    'MET ': 8,
    'MSE ': 8,
    'NMA ': 2,
    'PHE ': 11,
    'PRO ': 7,
    'PTR ': 11,
    'SEP ': 10,
    'SER ': 6,
    'THR ': 7,
    'TPO ': 11,
    'TRP ': 14,
    'TYR ': 12,
    'TYS ': 16,
    'VAL ': 7,
    # RNA (Ev:130712)
    '  A ': 22,
    '  C ': 20,
    '  G ': 23,
    '  U ': 20,
    # DNA (Ev:130712)
    ' DA ': 21,
    ' DC ': 19,
    ' DG ': 22,
    ' DT ': 20,
}

PHI = 'Phi'
PSI = 'Psi'
OMEGA = 'Omega'
CHI1 = 'Chi1'
CHI2 = 'Chi2'
CHI3 = 'Chi3'
CHI4 = 'Chi4'
CHI5 = 'Chi5'
CHI_DIHEDRAL_NAMES = [CHI1, CHI2, CHI3, CHI4, CHI5]


[docs]@contextmanager def update_once(): """ A context manager to enable manual update mode to update the structure by calling update only once before exiting , and then restores the original manual update state. """ update_enabled = mm.mmct_is_enabled(mm.MMCT_MANUAL_UPDATE) mm.mmct_enable(mm.MMCT_MANUAL_UPDATE) try: yield mm.mmct_update() finally: if (update_enabled): mm.mmct_enable(mm.MMCT_MANUAL_UPDATE) else: mm.mmct_disable(mm.MMCT_MANUAL_UPDATE)
def _get_atom_names_for_chi_angle(res_type, angle_name): """ Return atom names for the specified Chi angle in the given residue type. :param res_type: Residue type (e.g. "ALA ") :type res_type: str :param angle_name: Chi angle name. One of: Chi1, Chi2, Chi3, Chi4, Chi5. See CHI_DIHEDRAL_NAMES. :type angle_name: str :raises ValueError if the given angle is invalid, or is not defined for the given residue. """ if angle_name not in CHI_DIHEDRAL_NAMES: raise ValueError("Unsupported angle name: %s" % angle_name) # Chi angle (1-5) chi_index = CHI_DIHEDRAL_NAMES.index(angle_name) mm.mmrotamer_initialize(mm.error_handler) try: num_chi = mm.mmrotamer_get_num_chi_angles(res_type) except mm.MmException as e: if e.rc == mm.MMROTAMER_RES_NOT_FOUND: raise ValueError( 'There is no rotamer information for residue "%s"' % res_type) if chi_index >= num_chi: raise ValueError('No angle "%s" in the database for residue "%s"' % (angle_name, res_type)) at_names = mm.mmrotamer_get_chi_def(res_type, chi_index) mm.mmrotamer_terminate() return at_names
[docs]@enum.unique class BondType(enum.Enum): """ These represent varying bond types, which are independent from bond orders. """ Zero = mm.pymmlibs._MMCT_ZERO_BOND Dative = mm.pymmlibs._MMCT_DATIVE_BOND Single = mm.pymmlibs._MMCT_SINGLE_BOND Double = mm.pymmlibs._MMCT_DOUBLE_BOND Triple = mm.pymmlibs._MMCT_TRIPLE_BOND
[docs]class UndefinedStereochemistry(Exception): """ Raised by SmilesStructure.getStructure() when atoms with undefined chirality are present. """
[docs]class AtomsInRingError(ValueError): """ Raised by Structure.adjust() when atoms are in a ring, so adjustment can't be made without distorting the structure. """
# Module level function _addBond. Called by Structure and Atom add bond def _addBond(st, atom1, atom2, bond_type): """ Add a bond of the specified type between the two atoms, atom1 and atom2. The atoms can be atom objects or integer index from 1 to the number of atoms in the structure. If the two atoms are already bonded then just the bond type is changed. :param bond_type: the preferred type is a schrodinger.Structure.BondType value. A legacy option of passing an integer for bond order is supported. :type bond_type: schrodinger.Structure.BondType """ BOND_ORDER_TO_TYPE = { mm.MMCT_ZERO: BondType.Zero, mm.MMCT_SINGLE: BondType.Single, mm.MMCT_DOUBLE: BondType.Double, mm.MMCT_TRIPLE: BondType.Triple } if not isinstance(bond_type, BondType): # Assume type is an integer bond order bond_order = bond_type try: bond_type = BOND_ORDER_TO_TYPE[bond_order] except KeyError: raise KeyError(f'"{bond_order}" is not a valid bond order.') # FIXME: Remove deprecated logic if st.areBound(atom1, atom2): msg = ("usage deprecation: addBond should not be called with an " "existing bond.\nPlease update calling code to set bond types " "via _StructureBond.type") warnings.warn(msg, DeprecationWarning, stacklevel=3) mm.mmct_bond_set_type(st.handle, atom1, atom2, bond_type) return st._cpp_structure.addBond(atom1, atom2, bond_type) return st.getBond(atom1, atom2) class _NotSortable(object): """Mix-in for classes that shouldn't be sortable or orderable.""" def __lt__(self, that): raise TypeError( "unsupported operand type(s): '{self.__class__.__name__}' does not support comparisons" ) __gt__ = __lt__ __ge__ = __lt__ __le__ = __lt__ # Classes used to support iteration over molecules, residues, # chains, and rings: class _AtomIterable: """ Collection of all `_StructureAtom` objects in a _Molecule, _Chain, _Residue or _Ring. """ def __init__(self, st, atoms): """Constructor takes a list of atom numbers to iterate over. """ self._st = st self._atoms = atoms def __len__(self): """Return number of atoms in this molecule/chain/residue/ring. """ return len(self._atoms) def __iter__(self): """Iterator through atoms in this molecule/chain/residue/ring. """ for ai in self._atoms: yield self._st.atom[ai] def __getitem__(self, idx): """ Return atom <idx> of the molecule/chain/residue/ring. Starts at 1. """ if idx < 1: raise IndexError("Tried to get atom %d but indexes start at 1" % idx) try: # Subtract one because this list starts at index 1 instead of 0. return self._st.atom[self._atoms[idx - 1]] except IndexError: raise IndexError( "Tried to get atom %d but there are only %d atoms" % (idx, len(self._st.atom))) def __next__(self): raise AttributeError( "This object is an iterable, not an iterator. If you need an iterator, call iter() on it." ) class _RingEdgeIterable: """ Collection of edges(bonds) in a _Ring. """ def __init__(self, ring): """ ring argument is an instance of _Ring class. """ self._ring = ring def __len__(self): return len(self._ring._atoms) def __iter__(self): nummem = len(self._ring._atoms) for atomi in range(nummem): a1 = self._ring._atoms[atomi] if atomi == nummem - 1: # On the last atom of the ring: a2 = self._ring._atoms[0] else: a2 = self._ring._atoms[atomi + 1] edge = self._ring._st.getBond(a1, a2) if edge is None: raise RuntimeError("structure._Ring.edge: Invalid ring (make " "sure it's sorted)") yield edge def __getitem__(self, idx): """ Return i'th edge. Index starts at 1 """ i = 0 nummem = len(self._ring._atoms) for atomi in range(nummem): i += 1 if i == idx: a1 = self._ring._atoms[atomi] if atomi == nummem - 1: # On the last atom of the ring: a2 = self._ring._atoms[0] else: a2 = self._ring._atoms[atomi + 1] edge = self._ring._st.getBond(a1, a2) if edge is None: raise RuntimeError("structure._Ring.edge: Invalid ring " "(make sure it's sorted)") return edge def __next__(self): raise AttributeError( "This object is an iterable, not an iterator. If you need an iterator, call iter() on it." )
[docs]class _AtomCollection(object): """ A set of atoms, usually comprising a subset of the total atoms in a Structure. Initialize using a structure and an iterable of current atom indices. Important methods include `extractStructure` and `getAtomIndices`. Use the `atom` attribute to iterate over all contained atoms. 1-based indexed access to the atoms is also possible using `atom` (e.g. atom[1] gets the first atom in the _AtomCollection). The number of atoms can be determined via `len(self.atom)` or `len(self)`. Intended as a base class for _Ring, _Molecule, and _Chain. """
[docs] def __init__(self, st, atoms): self._st = st self._atoms = atoms
@property def structure(self): """ Return the parent Structure object for this atom collection. """ return self._st @property def atom(self): """ Iterate over all atoms. Also allows 1-based indexed access to the atoms. """ return _AtomIterable(self._st, self._atoms)
[docs] def __len__(self): """Return number of atoms.""" return len(self._atoms)
[docs] def getAtomIndices(self): """ Return a list of atom indices for all atoms in this object. :return: List of atom indicies. :rtype: list of ints """ return self._atoms
[docs] def getAtomList(self): """ Deprecated. Use getAtomIndices() method instead. """ warnings.warn("The %s.getAtomList() method" % self.__class__.__name__ + " is deprecated; use getAtomIndices() instead.", DeprecationWarning, stacklevel=2) return self._atoms
[docs] def extractStructure(self, copy_props=False): """ Return a new Structure containing only the atoms associated with this substructure. Structure properties, including the title, are inherited only if copy_props is set to True. """ return self._st.extract(self.getAtomIndices(), copy_props)
@property def temperature_factor(self): """ Average B (temperature) factor for all atoms that have it assigned. Setting this property will set the B factor to the given value for each atom. """ bfactors = [] for atom in self.atom: temperature_factor = atom.temperature_factor if temperature_factor is not None and temperature_factor > 0: bfactors.append(temperature_factor) else: # This atom did not have a positive temperature_factor # leave it out when calculating the average value continue # Default to zero if none of the atoms had positive temperature_factor if not bfactors: return 0.0 # Return the average temperature factor: return sum(bfactors) / len(bfactors) @temperature_factor.setter def temperature_factor(self, tfactor): # Set the temperature factor for all atoms in the object. for atom in self.atom: if atom.atomic_number > 1: atom.temperature_factor = tfactor def __eq__(self, that): """ Return True if this object and "that" object are equivalent. """ if type(self) != type(that): return False return self._st == that._st and self._atoms == that._atoms def __hash__(self): """ Hash value for this atom collection. This hash uniquely identifies the object (e.g. when using it as keys to dicts or in sets). """ # Use Structure.handle, so that different Structures that reference # the same MMCT object are considered equivalent. return hash((self._st.handle, tuple(self._atoms)))
[docs]class _Ring(_AtomCollection): """ Class representing a ring. Important methods include `extractStructure` and `getAtomIndices`. The `atom` attribute returns an iterator over all atoms in the ring, and the number of atoms can be determined via `len(molecule.atom)`. The `edge` attribute works in a similar manner for bonds in the ring. """
[docs] def __init__(self, st, ringnum, atoms, iterator): super(_Ring, self).__init__(st, atoms) self._ringnum = ringnum self._iterator = iterator
def __str__(self): return "ring(%i)" % self._ringnum @property def edge(self): """ Returns a bond iterator for all edges in the ring. """ return _RingEdgeIterable(self)
[docs] def isAromatic(self): if self._iterator is None: raise Exception("isAromatic() cannot be used on rings without " "iterators") return self._iterator._isAromatic(self._atoms)
[docs] def isHeteroaromatic(self): if not self.isAromatic(): return False # This ring is aromatic, check for N, O, or S atom presence: n_o_s = {7, 8, 16} return any(a.atomic_number in n_o_s for a in self.atom)
[docs] def applyStyle(self, atoms=ATOM_BALLNSTICK, bonds=BOND_BALLNSTICK): """ Applies the given display styles to the atoms and bonds of the ring. :type atoms: int :param atoms: display style for atoms given by structure module constants ATOM_NOSTYLE, ATOM_CIRCLE, ATOM_CPK, ATOM_BALLNSTICK. Default is ATOM_BALLNSTICK. :type bonds: int :param atoms: display style for bonds given by structure module constants BOND_NOSTYLE, BOND_WIRE, BOND_TUBE, BOND_BALLNSTICK. Default is BOND_BALLNSTICK. """ self._st.applyStyle(atoms, bonds, self.atom)
[docs]class _Molecule(_AtomCollection): """ A class used to return molecule information when iterating over molecules in a Structure object. Important methods include `extractStructure` and `getAtomIndices`. The `atom` attribute can be used to iterate over all atoms in the molecule, and the number of atoms can be determined via `len(molecule.atom)`. The `residue` iterator allows for iteration over residues of the molecule, returning `_Residue` instances. """
[docs] def __init__(self, st, molnum, atoms): """Initialize the Molecule object.""" super(_Molecule, self).__init__(st, atoms) self._molnum = molnum
@property def residue(self): """ Returns residue iterator for all residues in the molecule. """ return _ResidueIterable(self._st, self.atom) def __index__(self): """Return an integer rep - the molecule number.""" return self._molnum @property def number(self): """ Returns the molecule number of this molecule. """ return int(self) @property def number_by_entry(self): """ Return molecule number of this molecule by entry. """ return self.atom[1].molecule_number_by_entry def __str__(self): return "molecule(%i)" % self._molnum
[docs] def applyStyle(self, atoms=ATOM_BALLNSTICK, bonds=BOND_BALLNSTICK): """ Applies the given display styles to the atoms and bonds of the molecule. :type atoms: int :param atoms: display style for atoms given by structure module constants ATOM_NOSTYLE, ATOM_CIRCLE, ATOM_CPK, ATOM_BALLNSTICK. Default is ATOM_BALLNSTICK. :type bonds: int :param atoms: display style for bonds given by structure module constants BOND_NOSTYLE, BOND_WIRE, BOND_TUBE, BOND_BALLNSTICK. Default is BOND_BALLNSTICK. """ self._st.applyStyle(atoms, bonds, self.atom)
[docs]class _Residue(_AtomCollection): """ A class which is returned by the ResidueIterator and contains information about the residue including the atoms which make it up. Important methods include `extractStructure` and `getAtomIndices`. The `atom` attribute can be used to iterate over all atoms in the molecule, and the number of atoms can be determined via `len(molecule.atom)`. """
[docs] def __init__(self, st, resnum, inscode, chain, atoms=[]): # noqa: M511 super(_Residue, self).__init__(st, atoms) self._resnum = resnum self._inscode = inscode self._chain = chain
def __str__(self): """ Return a string representation - chain:resnum[inscode]. Insertion code is included only it is present. If the residue has no chain ID, then underscore ("_") is used. """ chain = self._chain if chain == " ": chain = "_" s = "%s:%d" % (chain, self._resnum) if self._inscode != " ": s += self.inscode return s # See Ev:113093 @property def pdbres(self): """ Returns PDB residue name. """ return self.atom[1].pdbres @pdbres.setter def pdbres(self, newvalue): for atom in self.atom: atom.pdbres = newvalue @property def chain(self): """ Return chain name. """ return self._chain @chain.setter def chain(self, newvalue): for atom in self.atom: atom.chain = newvalue self._chain = newvalue @property def resnum(self): """ Returns PDB residue number. """ return self._resnum @resnum.setter def resnum(self, newvalue): for atom in self.atom: atom.resnum = newvalue self._resnum = newvalue @property def inscode(self): """ Returns PDB residue insertion code. """ return self._inscode @inscode.setter def inscode(self, newvalue): for atom in self.atom: atom.inscode = newvalue self._inscode = newvalue @property def molecule_number(self): """ Return molecule number. """ return self.atom[1].molecule_number @property def molecule_number_by_entry(self): """ Return molecule number of this residue by entry. """ return self.atom[1].molecule_number_by_entry @property def secondary_structure(self): return self.atom[1].secondary_structure
[docs] def getCode(self): """Return the one-letter residue code for this residue.""" return RESIDUE_MAP_3_TO_1_LETTER.get(self.pdbres.strip(), "X")
[docs] def hasMissingAtoms(self): """ Returns True is this residue doesn't have the expected number of heavy atoms. Will return False is this residue has the correct number of heavy atoms or if it is of a type we don't know """ # res_sizes is a global dictionary if self.pdbres in _res_sizes: # Count the number of heavy atoms: heavy_cnt = sum(1 for at in self.atom if at.atomic_number != 1) # If we don't get the expected number then say True if heavy_cnt < _res_sizes[self.pdbres]: return True return False
[docs] def isStandardResidue(self): """ Returns True if this residue is on the list of standard PDB residues """ return bool(mm.mmct_atom_is_standard_residue(self._st, self.atom[1]))
[docs] def isConnectedToResidue(self, other_res): """ Returns True if the given residue is connected (C->N) to this residue. If the "C" PDB atom of this residue is connected to the "N" PDB atom of the other_res, then the residues are connected. Otherwise, they are not considered connected. """ if self._st.handle != other_res._st.handle: raise ValueError("Given residue is not in the same structure") return bool( mm.mmct_res_connected(self._st.handle, self._atoms[0], other_res._atoms[0]))
[docs] def applyStyle(self, atoms=ATOM_BALLNSTICK, bonds=BOND_BALLNSTICK): """ Applies the given display styles to the atoms and bonds of the residue. :type atoms: int :param atoms: display style for atoms given by structure module constants ATOM_NOSTYLE, ATOM_CIRCLE, ATOM_CPK, ATOM_BALLNSTICK. Default is ATOM_BALLNSTICK. :type bonds: int :param atoms: display style for bonds given by structure module constants BOND_NOSTYLE, BOND_WIRE, BOND_TUBE, BOND_BALLNSTICK. Default is BOND_BALLNSTICK. """ self._st.applyStyle(atoms, bonds, self.atom)
[docs] def getAtomByPdbName(self, pdbname): """ Returns the atom of this residue that matches the given PDB name, or None if no such atom is found. :param pdbname: 4-letter PDB atom name. E.g. " C " to get the C terminal atom of a protein residue. :type pdbname: str :return: Atom with given PDB name or None :rtype: `_StructureAtom` or None """ if len(pdbname) != 4: raise ValueError('String pdbname must be 4 characters long.') return next((at for at in self.atom if at.pdbname == pdbname), None)
[docs] def getBackboneNitrogen(self): """ Returns the backbone nitrogen of the residue, or None. NOTE: For use with protein residues only. :return: Nitrogen atom or None :rtype: `_StructureAtom` or None """ return self.getAtomByPdbName(' N ')
[docs] def getCarbonylCarbon(self): """ Returns the carbonyl backbone carbon of the residue, or None. NOTE: For use with protein residues only. :return: Carbon atom or None :rtype: `_StructureAtom` or None """ return self.getAtomByPdbName(' C ')
[docs] def getBackboneOxygen(self): """ Returns the oxygen of the backbone, or None. NOTE: For use with protein residues only. :return: Oxygen atom or None :rtype: `_StructureAtom` or None """ atom = self.getAtomByPdbName(' O ') # Even though this method is not expected to work with non-protein # residues; still doesn't hurt to add an exception for waters: if atom is not None and self.pdbres == 'HOH ': atom = None return atom
[docs] def getAlphaCarbon(self): """ Returns the backbone alpha carbon atom of this residue, or None. NOTE: For use with protein residues only. :return: Alpha carbon atom or None :rtype: `_StructureAtom` or None """ return self.getAtomByPdbName(' CA ')
[docs] def getBetaCarbon(self, gly_hydrogen=False): """ Returns the beta carbon atom of this residue, or None. NOTE: For use with protein residues only. :param gly_hydrogen: Whether to return the hydrogen atom if the residue is a glycine. :type gly_hydrogen: bool :return: Beta carbon atom or None :rtype: `_StructureAtom` or None """ resname = self.pdbres for atom in self.atom: name = atom.pdbname if name == ' CB ': return atom elif gly_hydrogen and name in (' HA3', '2HA ') and resname == 'GLY ': return atom return None
[docs] def getDihedralAtoms(self, angle_name): """ Return a list of 4 atom objects for the named dihedral angle in this residue. For backbone bonds, atoms are listed in the N->C order; for side-chain bonds, atoms are listed in order of increasing bond count from the backbone. Omega dihedral is the one to the previous residue (the one bonded to the N atom of this residue). :param name: Name of the dihedral angle to fine. Supported names are: Phi, Psi, Omega, Chi1, Chi2, Chi3, Chi4, Chi5. :type name: str :raises ValueError: if specified dihedral name is not valid or if it was not found in the database. """ def get_atom(name): """ Return the atom object for the atom with the given PDB name. Raises ValueError if such atom is not present. """ atom = self.getAtomByPdbName(name) if atom is None: raise ValueError('Could not find atom "%s" in residue %s' % (name, self)) return atom def get_bonded_atom(atom, name): """ Given an atom object, return the connected atom with specified name. If it's not present, raises ValueError. """ for neighbor in atom.bonded_atoms: if neighbor.pdbname == name: return neighbor raise ValueError('No atom "%s" bonded to "%s" of residue %s' % (name, atom.pdbname, self)) if angle_name == 'Phi': # N-CA bond. Use the C atom of the previous residue as the first # atom instead of the hydrogen bound to N in order to have this # code work when hydrogens are not present, and to work with PRO. # For termini residues, this code will report the dihedral as # missing - which is fine, since in that case the bond is not # rotatable anyway. nitrogen = get_atom(' N ') prev_c = get_bonded_atom(nitrogen, ' C ') atoms = [prev_c, nitrogen, get_atom(' CA '), get_atom(' C ')] elif angle_name == 'Psi': # CA-C bond. All 4 atoms returned are in the backbone; so last # nitrogen is from the next residue. If no next residue is present, # the bond is not detected (it's not rotatable). nitrogen = get_atom(' N ') carbon = get_atom(' C ') ca = get_atom(' CA ') next_n = get_bonded_atom(carbon, ' N ') atoms = [nitrogen, ca, carbon, next_n] elif angle_name == 'Omega': # Inter-residue bond: C(prev res)-N(this res) # All 4 atoms must be from the backbone to get 180° for trans # conformation. nitrogen = get_atom(' N ') ca = get_atom(' CA ') # Find the C and CA atoms of the previous residue: prev_c = get_bonded_atom(nitrogen, ' C ') prev_ca = get_bonded_atom(prev_c, ' CA ') atoms = [prev_ca, prev_c, nitrogen, ca] elif angle_name in CHI_DIHEDRAL_NAMES: # Chi angle (1-5) at_names = _get_atom_names_for_chi_angle(self.pdbres, angle_name) atoms = list(map(get_atom, at_names)) else: raise ValueError("Unsupported angle name: %s" % angle_name) return atoms
[docs] def getAsl(self): """ Return an ASL that uniquely identifies this residue, by chain ID, residue number, and insertion code. """ return '(chain.name "%s" AND res.num %i AND res.inscode "%s")' % ( self._chain, self._resnum, self._inscode)
[docs]class _Chain(_AtomCollection): """ A class used to return chain information when iterating over chains in a Structure object. Important methods include `extractStructure` and `getAtomIndices`. The `atom` attribute can be used to iterate over all atoms in the molecule, and the number of atoms can be determined via `len(molecule.atom)`. The `residue` iterator allows for iteration over residues of the chain, returning `_Residue` instances. """
[docs] def __init__(self, st, chain, atoms): """Initialize the Chain object.""" super(_Chain, self).__init__(st, atoms) self._chain = chain
@property def residue(self): """ Returns residue iterator for all residues in the chain """ return _ResidueIterable(self._st, self.atom) @property def name(self): """ Return name of the chain. """ return self._chain @name.setter def name(self, newname): for atom in self.atom: atom.chain = newname self._chain = newname def __str__(self): return "chain(%s)" % self._chain
[docs] def applyStyle(self, atoms=ATOM_BALLNSTICK, bonds=BOND_BALLNSTICK): """ Applies the given display styles to the atoms and bonds of the chain. :type atoms: int :param atoms: display style for atoms given by structure module constants ATOM_NOSTYLE, ATOM_CIRCLE, ATOM_CPK, ATOM_BALLNSTICK. Default is ATOM_BALLNSTICK. :type bonds: int :param atoms: display style for bonds given by structure module constants BOND_NOSTYLE, BOND_WIRE, BOND_TUBE, BOND_BALLNSTICK. Default is BOND_BALLNSTICK. """ self._st.applyStyle(atoms, bonds, self.atom)
class _StructureMoleculeIterable: """ Collection of _Molecules in a Structure. """ def __init__(self, st): self._st = st self.molecule_atoms_dict = None def recalc(self): """Obsolete""" # keys() method is not provided to avoid user thinking it's a dictionary def __len__(self): return self._st.mol_total def __getitem__(self, molnum): """ Return molecule object for molecule <molnum>. """ molatoms = Bitset(size=self._st.atom_total) mm.mmct_get_mol_atoms(self._st.handle, molnum, molatoms) if molatoms.count() == 0: raise KeyError(f"Molecule key out of range (starts at 1): {molnum}") return _Molecule(self._st, molnum, list(molatoms)) def __delitem__(self, molnum): """ Delete the molecule <molnum> from the Structure. Note that this immediately updates the Structure and therefore renumbers the atoms. """ molatoms = Bitset(size=self._st.atom_total) mm.mmct_get_mol_atoms(self._st.handle, molnum, molatoms) if molatoms.count() == 0: raise KeyError(f"Molecule key out of range (starts at 1): {molnum}") self._st.deleteAtoms(list(molatoms)) def __iter__(self): molecule_atoms_dict = {} for atom in self._st.atom: molnum = atom.molecule_number ai = atom.index try: molecule_atoms_dict[molnum].append(ai) except: molecule_atoms_dict[molnum] = [ai] for molnum in range(1, len(molecule_atoms_dict) + 1): atoms = molecule_atoms_dict[molnum] yield _Molecule(self._st, molnum, atoms) def __next__(self): raise AttributeError( "This object is an iterable, not an iterator. If you need an iterator, call iter() on it." ) class _ResidueIterable(object): """ Residue iterator. Each Structure, Chain and Molecule will have one of these. """ def __init__(self, st, atoms, sort=True, connectivity_sort=False): """ Residue iterator constructor. :type st: Structure :param st: The Structure that the atoms list belongs to. :type atoms: list(_StructureAtom) :param atoms: An atom iterator from which to select residues, e.g. chain.atom. :type sort: bool :param sort: If False, the residues will be returned in the order they are listed in the structure. If True, they will be sorted according to (chain, resnum, inscode, atom index). :type connectivity_sort: bool :param connectivity_sort: If True, residues are sorted according to N->C order. A given chain of connectivity will be sorted will be sorted contiguously, but the order of several different chains or unconnected residues is undefined. Modifying the connectivity of CT during iteration will also produce undefined results. """ self._st = st self._atoms = atoms # Atom iterator (_Chain.atom or _Molecule.atom) # Allow sorting to be turned off so that the residue iterator acts # as a simple means of returning residue groups; EV 70232. self._sort = sort if sort and connectivity_sort: raise ValueError("sort and connectivity_sort are mutually " "exclusive arguments.") self._connectivity_sort = connectivity_sort def __len__(self): """Number of residues in this st, chain, or molecule.""" assigned_atoms = set() # Atoms that we assigned to a residue already numres = 0 for a in self._atoms: ai = a.index if ai not in assigned_atoms: bs = cppstructure.get_residue_atoms(a) assigned_atoms.update(bs) numres += 1 return numres def _get_atom_residue_mapping(self): res_list = [] # List of (chain, resnum, inscode, atoms) atom_res_lookup = {} # key - atom, val = res_list index assigned_atoms = set() # Atoms that we assigned to a residue already for a in self._atoms: ai = a.index if ai not in assigned_atoms: bs = cppstructure.get_residue_atoms(a) resatoms = [] for atom in bs: resatoms.append(atom) assigned_atoms.add(atom) atom_res_lookup[atom] = len(res_list) res_list.append([a.chain, a.resnum, a.inscode, resatoms]) return atom_res_lookup, res_list def __iter__(self): """Iterate over residues in this st, chain, or molecule.""" atom_res_lookup, res_list = self._get_atom_residue_mapping() if self._connectivity_sort: mm.mmpdb_initialize(mm.error_handler) new_indexes = mm.mmpdb_get_sequence_order(self._st, mm.MMPDB_SEQUENCE_ORDER) mm.mmpdb_terminate() visited_residues = set() # indexes of original list # Ignore 0 element, since these correspond to real atom numbers sorted_res_list = [] for index in new_indexes[1:]: try: res_list_index = atom_res_lookup[index] except KeyError: continue if res_list_index not in visited_residues: sorted_res_list.append(res_list[res_list_index]) visited_residues.add(res_list_index) res_list = sorted_res_list # Sort the residues by chain, resnum, then by inscode before iterating: elif self._sort: res_list.sort() for chain, resnum, inscode, atoms in res_list: yield _Residue(self._st, resnum, inscode, chain, atoms) # __getitem__() method was NOT implemented, because there is no completely # unique way to identify a residue. Any way we could think of was not # problem free. def __next__(self): raise AttributeError( "This object is an iterable, not an iterator. If you need an iterator, call iter() on it." ) class _StructureChainIterable: """ Collection of chains in a Structure. """ def __init__(self, st): self._st = st def recalc(self): """ Obsolete """ # keys() method is not provided to avoid user thinking it's a dictionary def chainNames(self): """ Return a list of chain letters """ # Key: chain letter, value: list of atoms chain_atoms_dict = {} for atom in self._st.atom: chain = atom.chain i = atom.index try: chain_atoms_dict[chain].append(i) except KeyError: chain_atoms_dict[chain] = [i] chain_list = list(chain_atoms_dict) chain_list.sort() return chain_list def __len__(self): """ Return number of chains in the structure """ # Key: chain letter, value: list of atoms chain_atoms_dict = {} for atom in self._st.atom: chain = atom.chain i = atom.index try: chain_atoms_dict[chain].append(i) except KeyError: chain_atoms_dict[chain] = [i] chain_list = list(chain_atoms_dict) return len(chain_list) def __getitem__(self, name): """ Return chain object for chain <name> """ chain_atoms = [] for atom in self._st.atom: chain = atom.chain if chain == name: chain_atoms.append(atom.index) if not chain_atoms: raise KeyError("No such chain: %s" % name) return _Chain(self._st, name, chain_atoms) def __delitem__(self, name): """ Delete the chain <name> from the Structure. Note that this immediately updates the Structure and therefore renumbers the atoms. """ chain_atoms = [] for atom in self._st.atom: chain = atom.chain if chain == name: chain_atoms.append(atom.index) if not chain_atoms: raise KeyError(f"No such chain: {name}") self._st.deleteAtoms(chain_atoms) def __iter__(self): """Iterate through all chains in the structure.""" # Key: chain letter, value: list of atoms chain_atoms_dict = {} for atom in self._st.atom: chain = atom.chain i = atom.index try: chain_atoms_dict[chain].append(i) except KeyError: chain_atoms_dict[chain] = [i] chain_list = sorted(chain_atoms_dict) for i in chain_list: atoms = chain_atoms_dict[i] yield _Chain(self._st, i, atoms) def __next__(self): raise AttributeError( "This object is an iterable, not an iterator. If you need an iterator, call iter() on it." ) class _StructureRingIterable: """ Container for all rings in the Structure. Represents a dynamic list of the smallest set of smallest rings (SSSR) in the structure. """ def __init__(self, st): self._st = st self._aromatic_atoms = None def recalc(self): """ Obsolete """ def __len__(self): """Return the number of rings in the structure.""" ring_atom_list = self._st.find_rings(sort=False) return len(ring_atom_list) def __getitem__(self, ringnum): """Return ring object for ring <i>. Index starts at 1.""" ring_atom_list = self._st.find_rings(sort=True) try: ring_atoms = ring_atom_list[ringnum - 1] except IndexError: raise KeyError("Ring key out of range (starts at 1): %i" % ringnum) return _Ring(self._st, ringnum, ring_atoms, self) def __iter__(self): """Iterate through all rings in the structure.""" ring_atom_list = self._st.find_rings(sort=True) for ringnum, ring_atoms in enumerate(ring_atom_list): yield _Ring(self._st, ringnum, ring_atoms, self) def _isAromatic(self, ring_atoms): """ Returns true if all of ring_atoms are aromatic """ if self._aromatic_atoms is None: # We don't yet have a list of aromatic atoms, create them now adaptor = canvas.ChmMmctAdaptor() # Check out license in case it hasn't already happened yet _ = canvasutils.get_license() # noqa: F841 self._aromatic_atoms = adaptor.getAromaticityInfo(self._st.handle) # Test if this ring is aromatic. Start assuming it's true but # if any of the atoms are not aromatic then it's false: is_aromatic = True for iatom in ring_atoms: if not self._aromatic_atoms[iatom - 1]: is_aromatic = False break return is_aromatic def __next__(self): raise AttributeError( "This object is an iterable, not an iterator. If you need an iterator, call iter() on it." ) class _StructureAtomProperty(MutableMapping): """ Dictionary-like container of atom based properties. These can be accessed via the property name as it appears in the maestro file. Property names must be m2io data names, which are in the format '<type>_<author>_<property_name>', where '<type>' is a data type prefix, '<author>' is a source specification, and '<property_name>' is the actual name of the data. The data type prefix can specified as 's' for string, 'i' for integer, 'r' for real and 'b' for boolean. The author specification should be 'user' for user created properties. The property name can have embedded underscores. Some example m2io datanames are 'r_m_x_coord', which indicates a real maestro property named 'x coord', and 'i_user_my_count' which indicates an integer user property named 'my count'. """ __slots__ = ('_ct', '_cpp_atom') def __init__(self, ct, cpp_atom): """Create an instance of the property dictionary. """ self._ct = ct self._cpp_atom = cpp_atom def __getitem__(self, item): """ Return the given item if it is a valid property for this atom, raise a KeyError if not. :param item: Key object for property dict, must be a string starting with s, r, i, or b :type item: string :raises: KeyError """ # Check to see if property is built-in: builtin = ATOM_PROP_LINK.get(item) if builtin: index = self._cpp_atom.getIndex() return getattr(self._ct.atom[index], builtin) return self._cpp_atom.getProperty(item) def get(self, item, default=None): builtin = ATOM_PROP_LINK.get(item) if builtin: index = self._cpp_atom.getIndex() return getattr(self._ct.atom[index], builtin) return self._cpp_atom.getProperty(item, default) def __setitem__(self, item, value): """Set atom properties for the given atom:""" builtin = ATOM_PROP_LINK.get(item) if builtin: index = self._cpp_atom.getIndex() setattr(self._ct.atom[index], builtin, value) return try: if item.startswith('s'): self._cpp_atom.setPropertyString(item, value) elif item.startswith('r'): self._cpp_atom.setPropertyReal(item, value) elif item.startswith('i'): self._cpp_atom.setPropertyInt(item, value) elif item.startswith('b'): self._cpp_atom.setPropertyBool(item, bool(value)) else: raise KeyError(f'"{item}" is not a valid property name.') except IndexError as err: raise KeyError(str(err)) def __delitem__(self, item): """Delete the atom property. """ if item in ATOM_PROP_LINK: raise ValueError( f'"{item}" is a built-in property and cannot be deleted.') try: self._cpp_atom.getProperty(item) except IndexError as err: raise KeyError(*err.args) self._cpp_atom.deleteProperty(item) def keys(self): """Returns a list of the datanames of all unrequested items. """ props = list(self._cpp_atom.getPropertyNames()) # Add built-in property names: props.extend(list(ATOM_PROP_LINK)) return props def clear(self): """ Clear all properties (except the built-in ones). """ for key in self._cpp_atom.getPropertyNames(): del self[key] index = self._cpp_atom.getIndex() for builtin, null_value in RESETABLE_BUILTIN_ATOM_PROPS.values(): setattr(self._ct.atom[index], builtin, null_value) def __len__(self): return len(self.keys()) def __iter__(self): return iter(self.keys())
[docs]class _StructureAtom(_NotSortable): """ Access of mmct atoms properties pythonically. """ __slots__ = ('_ct', '_cpp_atom', '_property')
[docs] def __init__(self, ct, cpp_atom): """ Create an instance from the Structure object and the atom index. Note that the index used starts at 1 as per the underlying mmct lib. """ self._ct = ct self._cpp_atom = cpp_atom self._property = None
# These three ways to get the index are all submicrosecond. If you have # performance issues, you can try calling atom.__index__(), as it is slightly # faster. def __index__(self): "The atom index. I{Read only.}" return self._cpp_atom.getIndex() @property def index(self): return int(self) @property def _index(self): return int(self) def __eq__(self, that): """Compare on mmct handle and index. """ return type(self) == type(that) and self._cpp_atom == that._cpp_atom def __ne__(self, other): """ Check for inequality based on mmct handle and index. """ return not self.__eq__(other) def __hash__(self): return hash(self._cpp_atom) def __str__(self): return "atom(%d)" % self.index
[docs] def addBond(self, atom2, bond_order): """ Add a bond between the current atom and atom2. :param bond_order Takes an integer bond order or a BondType """ return _addBond(self._ct, self.index, atom2, bond_order)
[docs] def deleteBond(self, atom2): """Delete the bond between the current atom and atom2. """ self._ct.deleteBond(self, atom2)
[docs] def retype(self): """ Reassign the MacroModel atom type based on the bond orders and formal charge. This function should be called after either of these have been changed. """ mm.mmtype_retype_atom(self._ct, self._index)
# Methods for returning Residue, Molecule, and Chain objects for this atom:
[docs] def getResidue(self): """ Return a _Residue object for the residue that this atom is part of. """ bs = Bitset(cppstructure.get_residue_atoms(self)) res = _Residue(self._ct, self.resnum, self.inscode, self.chain, list(bs)) return res
[docs] def getMolecule(self): """ Return a _Molecule object for the molecule that this atom is part of. """ bs = Bitset(size=self._ct.atom_total) mm.mmct_atom_get_mol_atoms(self._ct.handle, self._index, bs) mol = _Molecule(self._ct, self.molecule_number, list(bs)) return mol
[docs] def getChain(self): """ Return a _Chain object for the molecule that this atom is part of. """ bs = Bitset(size=self._ct.atom_total) mm.mmct_atom_get_chain_atoms(self._ct.handle, self._index, bs) chain = _Chain(self._ct, self.chain, list(bs)) return chain
# Properties @property def structure(self): """ Return the parent Structure object for this atom. """ return self._ct @property def bond_total(self): """ Get total number of bonds to this atom. """ return self._cpp_atom.getNeighborCount() @property def bond(self): """ List of bonds to the atom (`_StructureBond` objects). """ # It is cheap to create a bond container, so don't cache it (PPREP-1240) return _AtomBondContainer(self._ct, self._index) @property def bonded_atoms(self): """ Iterator for atoms bonded to this atom (`_StructureAtom` objects). """ for neighbor in self._cpp_atom.getNeighbors(): yield _StructureAtom(self._ct, neighbor) @property def atomic_weight(self): """ Return the atomic weight of the atom. If implicit hydrogens are present, these are included with the weight of the atom they are attached to. """ return self._cpp_atom.getAtomicWeight() @property def growname(self): """ Returns Maestro grow name. """ return self._cpp_atom.getGrowName() @growname.setter def growname(self, value): self._cpp_atom.setGrowName(value) @property def pdbname(self): """ Returns PDB atom name. """ return self._cpp_atom.getPDBAtomName() @pdbname.setter def pdbname(self, value): """ Return PDB residue name. """ self._cpp_atom.setPDBAtomName(value) @property def pdbres(self): return self._cpp_atom.getPDBResidue() @pdbres.setter def pdbres(self, value): if len(value) < 4: value = value.rjust(3) + ' ' self._cpp_atom.setPDBResidue(value) @property def pdbcode(self): """ Returns one-letter PDB residue code. """ return RESIDUE_MAP_3_TO_1_LETTER.get( self._cpp_atom.getPDBResidue().strip(), "X") @property def resnum(self): """ Returns PDB residue number. """ return self._cpp_atom.getResidueId().residue_number @resnum.setter def resnum(self, rnum): icode = self._cpp_atom.getResidueId().insertion_code mm.mmct_atom_set_resnum(self._ct, self._index, rnum, icode) @property def inscode(self): """ Returns PDB residue insertion code. """ return self._cpp_atom.getResidueId().insertion_code @inscode.setter def inscode(self, icode): resnum = self._cpp_atom.getResidueId().residue_number self._cpp_atom.setResidueNumber(resnum, icode) @property def atom_type(self): return self._cpp_atom.getAtomType() @atom_type.setter def atom_type(self, type): """ Set MacroModel atom type. Note that changing the atom type does not automatically update the charge. See `schrodinger.structure.Structure.retype`. """ self._cpp_atom.setAtomType(type) @property def atom_type_name(self): """ Returns MacroModel atom type name. """ return mm.mmat_get_mmod_name(self.atom_type) @property def _color_index(self): # Support for i_m_color property, which is not handled by # _StructureAtomProperty container. return mm.mmct_atom_get_color(self._ct, self._index) @_color_index.setter def _color_index(self, value): # Support for i_m_color property, which is not handled by # _StructureAtomProperty container. if type(value) != int: raise TypeError('i_m_color property must be set to an integer.') self._cpp_atom.setColor(value) @property def color(self): """ Get property method for _StructureAtom.color attribute. :return Color object. :rtype: `color.Color` """ rgb = mm.mmct_atom_get_color_vector(self._ct, self._index) return color.Color(rgb) @color.setter def color(self, co): """ Set property method for _StructureAtom.color attribute. :param co: Desired color to use. :param co: `color.Color`, int, str, or tuple. """ if type(co) == int: mm.mmct_atom_set_color(self._ct, self._index, co) return if not isinstance(co, color.Color): # Color was passed in as a string (e.g. 'red' or 'FF0000'), # or a tuple of (R, G, B) values co = color.Color(co) mm.mmct_atom_set_color_vector(self._ct, self._index, co.rgb)
[docs] def setColorRGB(self, red, green, blue): """ Set the RGB color of this atom as a tuple (R,G.B). Each color value should be an integer between 0 and 255. """ rgb_list = [] rgb_list.append(red) rgb_list.append(green) rgb_list.append(blue) mm.mmct_atom_set_color_vector(self._ct, self._index, rgb_list)
@property def chain(self): """ First letter of the chain name for legacy applications which expect a 1-letter chain identifier. The 'chain_name' property holds the full chain name. """ return mm.mmct_atom_get_chain(self._ct, self._index) @chain.setter def chain(self, chain): if len(chain) != 1: raise ValueError( "Use the 'chain_name' property for multi-letter chains") mm.mmct_atom_set_chain(self._ct, self._index, chain) @property def chain_name(self): """ Chain name (mmCIF "auth_asym_id") """ return mm.mmct_atom_get_chainstr(self._ct, self._index) @chain_name.setter def chain_name(self, chain): mm.mmct_atom_set_chain(self._ct, self._index, chain) def _atomNameWarning(self): msg = "The 'atom_name' property is pending deprecation, use the " msg += "atom's 'name' property instead." warnings.warn( msg, PendingDeprecationWarning, stacklevel=3 # emitted at the atom_name property level. ) @property def atom_name(self): self._atomNameWarning() return mm.mmct_atom_get_atom_name(self._ct, self._index) @atom_name.setter def atom_name(self, name): self._atomNameWarning() mm.mmct_atom_set_atom_name(self._ct, self._index, name) @property def name(self): """ Return name of atom. """ return mm.mmct_atom_get_atom_name(self._ct, self._index) @name.setter def name(self, name): mm.mmct_atom_set_atom_name(self._ct, self._index, name) @property def entry_id(self): """ Return maestro entry id, may be None. """ entry_id = self._cpp_atom.getEntryName() if not entry_id: return None return entry_id def _getCtEntryId(self): """ Return the entry id from the structure's property dictionary :return: The structure entry id, or None if no entry id was found :rtype: str or NoneType """ try: return self._ct.property["s_m_entry_id"] except KeyError: return None @property def element(self): """ Element symbol of the atom. """ # Changing this code will expose SHARED-2708 an = self.atomic_number el = mm.mmat_get_element_by_atomic_number(an) return el.rstrip() @element.setter def element(self, element): at = mm.mmelement_get_atomic_number_by_symbol(element) if at == -1 and element != "Lp": raise ValueError(f"Unrecognized element {element}") self.atomic_number = at @property def partial_charge(self): """ Return partial charge of the atom. """ return self._cpp_atom.getPartialCharge() @partial_charge.setter def partial_charge(self, pcharge): self._cpp_atom.setPartialCharge(pcharge) @property def solvation_charge(self): return self._cpp_atom.getSolvationCharge() @solvation_charge.setter def solvation_charge(self, scharge): self._cpp_atom.setSolvationCharge(scharge) @property def formal_charge(self): return self._cpp_atom.getFormalCharge() @formal_charge.setter def formal_charge(self, fcharge): self._cpp_atom.setFormalCharge(fcharge) @property def isotope(self): """ Returns mass number charge of the atom. """ return mm.mmct_atom_get_isotope(self._ct, self._index) @isotope.setter def isotope(self, mass_number): mm.mmct_atom_set_isotope(self._ct, self._index, mass_number) @property def secondary_structure(self): return self._cpp_atom.getSecondaryStructure() @secondary_structure.setter def secondary_structure(self, secondary_struct): self._cpp_atom.setSecondaryStructure(secondary_struct) @property def temperature_factor(self): return self.property.get('r_m_pdb_tfactor') @temperature_factor.setter def temperature_factor(self, tfactor): self.property['r_m_pdb_tfactor'] = tfactor @property def radius(self): return self._cpp_atom.getRadius() @property def vdw_radius(self): warnings.warn( "The atom 'vdw_radius' property is deprecated." "Please use the 'radius' property instead.", DeprecationWarning, stacklevel=2) return self.radius @property def is_halogen(self): # Fluorine, Chlorine, Bromine, Iodine, Astatine, Tennessine halo = [9, 17, 35, 53, 85, 117] return self._cpp_atom.getAtomicNumber() in halo @property def molecule_number(self): return self._cpp_atom.getMoleculeNumber() @property def molecule_number_by_entry(self): """ Return the molecule number of this atom, by entry. """ return mm.mmct_atom_get_entry_mol_num(self._ct, self._index) @property def number_by_molecule(self): return mm.mmct_atom_get_atom_mol(self._ct, self._index) @property def number_by_entry(self): return mm.mmct_atom_get_atom_entry(self._ct, self._index) @property def atomic_number(self): """ "Atomic number of the atom's element. """ return self._cpp_atom.getAtomicNumber() @atomic_number.setter def atomic_number(self, value): self._cpp_atom.setAtomicNumber(value) @property def x(self): return self._cpp_atom.x() @x.setter def x(self, value): mm.mmct_atom_set_x(self._ct, self._index, value) @property def y(self): return self._cpp_atom.y() @y.setter def y(self, value): mm.mmct_atom_set_y(self._ct, self._index, value) @property def z(self): return self._cpp_atom.z() @z.setter def z(self, value): mm.mmct_atom_set_z(self._ct, self._index, value) @property def xyz(self): """ XYZ-coordinates of the atom. """ return mm.mmct_atom_get_xyz(self._ct, self._index) @xyz.setter def xyz(self, xyz): mm.mmct_atom_set_xyz(self._ct, self._index, xyz) @property def alt_xyz(self): """ Alternative XYZ-coordinates of the atom, if available, otherwise returns None. """ if not mm.mmct_atom_has_alt_position(self._ct, self._index): return None return mm.mmct_atom_get_alt_xyz(self._ct, self._index) @alt_xyz.setter def alt_xyz(self, xyz): if xyz is None: for prop in [ mm.M2IO_DATA_ALT_X_COORD, mm.M2IO_DATA_ALT_Y_COORD, mm.M2IO_DATA_ALT_Z_COORD ]: try: del self.property[prop] except KeyError: pass else: mm.mmct_atom_set_alt_xyz(self._ct, self._index, xyz) @property def chirality(self): """ Returns chirality of the atom. R, S, ANR, ANS, undef, or None. """ # The mmstereo library is already initialized by now handle = mm.mmstereo_new(self._ct) chirality = mm.mmstereo_atom_stereo(handle, self._index) mm.mmstereo_delete(handle) if chirality == mm.MMSTEREO_NO_CHIRALITY: return None if chirality == mm.MMSTEREO_CHIRALITY_R: return 'R' elif chirality == mm.MMSTEREO_CHIRALITY_S: return 'S' elif chirality == mm.MMSTEREO_CHIRALITY_ANR: return 'ANR' elif chirality == mm.MMSTEREO_CHIRALITY_ANS: return 'ANS' else: return 'undef' # Deprecate atom_style - it's redundant and inconsistent with # bond.style. def _atomStyleWarning(self): warnings.warn( "The atom 'atom_style' property is deprecated." "Please use the 'style' property instead.", DeprecationWarning, stacklevel=3) @property def atom_style(self): self._atomStyleWarning() return mm.mmctg_atom_get_style(self._ct, self._index) @atom_style.setter def atom_style(self, style): self._atomStyleWarning() mm.mmctg_atom_set_style(self._ct, self._index, style) @property def style(self): return mm.mmctg_atom_get_style(self._ct, self._index) @style.setter def style(self, style): mm.mmctg_atom_set_style(self._ct, self._index, style) @property def visible(self): return mm.mmctg_atom_get_visible(self._ct, self._index) @visible.setter def visible(self, visible): mm.mmctg_atom_set_visible(self._ct, self._index, visible) @property def label_format(self): return mm.mmctg_atom_get_label_format(self._ct, self._index) @label_format.setter def label_format(self, fmt): mm.mmctg_atom_set_label_format(self._ct, self._index, fmt) @property def label_color(self): return mm.mmctg_atom_get_label_color(self._ct, self._index) @label_color.setter def label_color(self, col): mm.mmctg_atom_set_label_color(self._ct, self._index, col) @property def label_user_text(self): return mm.mmctg_atom_get_label_user_text(self._ct, self._index) @label_user_text.setter def label_user_text(self, utext): mm.mmctg_atom_set_label_user_text(self._ct, self._index, utext) @property def property(self): """ Dictionary-like container of Atom-level properties. Keys are strings of the form `type_family_name` as described in the "`PropertyName` documentation. """ if self._property is None: self._property = _StructureAtomProperty(self._ct, self._cpp_atom) return self._property
class _StructureBondProperty(MutableMapping): """ Dictionary-like container of bond based properties. These can be accessed via the property name as it appears in the maestro file. Property names must be m2io data names, which are in the format '<type>_<author>_<property_name>', where '<type>' is a data type prefix, '<author>' is a source specification, and '<property_name>' is the actual name of the data. The data type prefix can specified as 's' for string, 'i' for integer, 'r' for real and 'b' for boolean. The author specification should be 'user' for user created properties. The property name can have embedded underscores. Some example m2io datanames are 'r_m_x_coord', which indicates a real maestro property named 'x coord', and 'i_user_my_count' which indicates an integer user property named 'my count'. """ def __init__(self, ct, atomindex, bondnum): """Create an instance of the property dictionary. """ self._ct = ct self._atom_index1 = atomindex self._atom_index2 = mm.mmct_atom_get_bond_atom(ct, self._atom_index1, bondnum) def __getitem__(self, item): """ Return the given item if it is a valid property for this atom, None if not. """ try: if item[0] == "s": ret = mm.mmct_bond_property_get_string(self._ct, item, self._atom_index1, self._atom_index2) elif item[0] == "i": ret = mm.mmct_bond_property_get_int(self._ct, item, self._atom_index1, self._atom_index2) elif item[0] == "r": ret = mm.mmct_bond_property_get_real(self._ct, item, self._atom_index1, self._atom_index2) elif item[0] == "b": ret = mm.mmct_bond_property_get_bool(self._ct, item, self._atom_index1, self._atom_index2) else: raise KeyError("%s is an invalid dataname" % item) except mm.MmException as e: if e.rc == mm.MMCT_BOND_PROPERTY_UNDEFINED_BOND or \ e.rc == mm.MMCT_BOND_PROPERTY_NEVER_DEFINED_IN_SESSION: raise KeyError("'%s' property is not defined" % item) else: raise KeyError("%s is not the name of an atom property" % item) except IndexError as e: if item == "": raise KeyError("The empty string is an invalid dataname.") else: raise return ret def __setitem__(self, item, value): """Set atom properties for the given atom:""" if item[0] == "s": mm.mmct_bond_property_set_string(self._ct, item, self._atom_index1, self._atom_index2, value) elif item[0] == "i": mm.mmct_bond_property_set_int(self._ct, item, self._atom_index1, self._atom_index2, value) elif item[0] == "r": mm.mmct_bond_property_set_real(self._ct, item, self._atom_index1, self._atom_index2, value) elif item[0] == "b": mm.mmct_bond_property_set_bool(self._ct, item, self._atom_index1, self._atom_index2, value) else: raise KeyError("%s is an invalid property name; see " "_StructureBondProperty documentation for details." % item) def __delitem__(self, item): """Delete the atom property. """ try: mm.mmct_bond_property_delete(self._ct, item, self._atom_index1, self._atom_index2) except mm.MmException as e: if e.rc == mm.MMCT_BOND_PROPERTY_NEVER_DEFINED_IN_SESSION: raise KeyError("bond property %s doesn't exist" % item) elif e.rc == mm.MMCT_INVALID_PROPERTY_NAME: raise KeyError("bond property name %s is invalid" % item) else: raise def keys(self): """Returns a list of the datanames of all unrequested items. """ props = mm.mmct_bond_get_property_names(self._ct, self._atom_index1, self._atom_index2) return props def __len__(self): return len(self.keys()) def __iter__(self): return iter(self.keys())
[docs]class _StructureBond(_NotSortable): """ A class for pythonic access to bond properties. Attributes - atom1: The first atom, by which the bond is defined. - atom2: The atom bonded to atom1. """
[docs] def __init__(self, ct, atom_index, index): """ A bond is defined by its underlying Structure, the atom index that it is anchored to, and the bond index. """ self._ct = ct self._index = index self._property = None self.atom1 = self._ct.atom[atom_index] bonded_index = mm.mmct_atom_get_bond_atom(self._ct, atom_index, self._index) # Check that the bonded atom is not zero, as this means that the # bond doesn't really exist. # if bonded_index == 0: if index > mm.mmct_atom_get_bond_total(self._ct, atom_index): raise IndexError(f"bond index {index} out of range") else: # I don't know if this case will ever happen, because I # haven't checked that bond deletion collapses the bond # array in the mmct lib. raise Exception("bonded atom is not defined") self.atom2 = self._ct.atom[bonded_index]
def __eq__(self, that): """Test _StructureBond equality by the bonded atoms. """ try: return ((self.atom1 == that.atom1 and self.atom2 == that.atom2) or (self.atom1 == that.atom2 and self.atom2 == that.atom1)) except AttributeError: return False def __ne__(self, other): """ Test _StructureBond inequality based on the bonded atoms. """ return not self.__eq__(other) def __hash__(self): """ Hash value for bond, based on the index values of the atoms involved and the handle of the underlying Structure. """ if self.atom1._index < self.atom2._index: lesser = self.atom1._index greater = self.atom2._index else: lesser = self.atom2._index greater = self.atom1._index return hash((self._ct.handle, lesser, greater)) def __str__(self): return "bond(%d, %d)" % (self.atom1.index, self.atom2.index)
[docs] def delete(self): """ Delete this bond. Use with care. Iteration over bonds may be affected. """ mm.mmct_atom_delete_bond(self._ct, self.atom1, self.atom2)
def _getBondIndex(self): """ Get current internal to mmct bond index. Returns None if not currently a represented bond. This is regrettably necessary to deal with certain mmct calls. """ # The number of bonds may have changed since we got this bond object # therefore we should locate the attached atom rather than # just use the internal bond index: num_bonds = mm.mmct_atom_get_bond_total(self._ct, self.atom1._index) for ibond in range(1, num_bonds + 1): if self.atom2._index == mm.mmct_atom_get_bond_atom( self._ct, self.atom1._index, ibond): return ibond else: return None @property def length(self): """ Length of the bond in angstroms. """ return mm.mmct_atom_get_distance_s(self._ct, self.atom1, self._ct, self.atom2) @property def order(self): """ Return bond order. Returns None for the MMCT_NONE type, integer values for zero through three. """ bond_index = self._getBondIndex() if bond_index is None: return None order = mm.mmct_atom_get_bond_order(self._ct, self.atom1._index, bond_index) if order == mm.MMCT_NONE: return None else: return order @order.setter def order(self, order): """ Set bond order. Using an order of None sets order to MMCT_NONE, otherwise must be an int from 0 to 3. """ bond_index = self._getBondIndex() if bond_index is None: return None if order is None: order = mm.MMCT_NONE elif order < 0 or order > 3: raise Exception("A bond order of %d is not acceptable." % order) mm.mmct_atom_set_bond_order(self._ct, self.atom1._index, self.atom2._index, order) @property def type(self): """ :returns schrodinger.structure.BondType """ bond_type = mm.mmct_bond_get_type(self._ct, self.atom1._index, self.atom2._index) return bond_type @type.setter def type(self, bond_type): """ :type bond_type schrodinger.structure.BondType """ mm.mmct_bond_set_type(self._ct, self.atom1._index, self.atom2._index, bond_type) @property def from_style(self): """Return bond's "from" style. """ # The number of bonds may have changed since we got this bond object # therefore we should locate the attached atom rather than # just use the internal bond index: num_bonds = mm.mmct_atom_get_bond_total(self._ct, self.atom1._index) bond_index = -1 for ibond in range(1, num_bonds + 1): if self.atom2._index == mm.mmct_atom_get_bond_atom( self._ct, self.atom1._index, ibond): bond_index = ibond break if bond_index == -1: return None style = mm.mmctg_atom_get_bond_style(self._ct, self.atom1._index, bond_index) return style @from_style.setter def from_style(self, style): """Set bond "from" style. """ # The number of bonds may have changed since we got this bond object # therefore we should locate the attached atom rather than # just use the internal bond index: num_bonds = mm.mmct_atom_get_bond_total(self._ct, self.atom1._index) bond_index = -1 for ibond in range(1, num_bonds + 1): if self.atom2._index == mm.mmct_atom_get_bond_atom( self._ct, self.atom1._index, ibond): bond_index = ibond break mm.mmctg_atom_set_bond_style(self._ct, self.atom1._index, bond_index, style) @property def to_style(self): """Return bond's "to" style. """ # FIXME Merge with the _getFromStyle above? (DRY principle) # The number of bonds may have changed since we got this bond object # therefore we should locate the attached atom rather than # just use the internal bond index: num_bonds = mm.mmct_atom_get_bond_total(self._ct, self.atom2._index) bond_index = -1 for ibond in range(1, num_bonds + 1): if self.atom1._index == mm.mmct_atom_get_bond_atom( self._ct, self.atom2._index, ibond): bond_index = ibond break if bond_index == -1: return None style = mm.mmctg_atom_get_bond_style(self._ct, self.atom2._index, bond_index) return style @to_style.setter def to_style(self, style): """Set bond "to" style. """ # FIXME Merge with the _setFromStyle above? (DRY principle) # The number of bonds may have changed since we got this bond object # therefore we should locate the attached atom rather than # just use the internal bond index: num_bonds = mm.mmct_atom_get_bond_total(self._ct, self.atom2._index) bond_index = -1 for ibond in range(1, num_bonds + 1): if self.atom1._index == mm.mmct_atom_get_bond_atom( self._ct, self.atom2._index, ibond): bond_index = ibond break mm.mmctg_atom_set_bond_style(self._ct, self.atom2._index, bond_index, style) # The "style" property is deprecated. See Ev:118009: @property def style(self): msg = "The 'style' property is pending deprecation, use the bond's " msg += "'from_style' and 'to_style' properties instead." warnings.warn( msg, PendingDeprecationWarning, stacklevel=3 # emitted at the style property level. ) return self.from_style @style.setter def style(self, style): msg = "The 'style' property is pending deprecation, use the bond's " msg += "'from_style' and 'to_style' properties instead." warnings.warn( msg, PendingDeprecationWarning, stacklevel=3 # emitted at the style property level. ) self.from_style = style
[docs] def setStyle(self, style): """ Set the bond's style in both directions ("from" and "to") """ self.from_style = style self.to_style = style
@property def atom(self): """ Returns an iterable of the atoms and can also be used for easy inclusion checks """ return frozenset((self.atom1, self.atom2)) @property def property(self): """ Dictionary-like container of Bond properties. """ if self._property is None: self._property = _StructureBondProperty(self._ct, self.atom1.index, self._index) return self._property
[docs] def otherAtom(self, atom): """ Given one atom in the bond, return the other atom in the bond, :type atom: _StructureAtom or int :param atom: atom object or its index which is one side of the bond :rtype: _StructureAtom :return: the atom in the bond that is not the input atom """ if int(atom) not in {self.atom1.index, self.atom2.index}: raise ValueError('Bond does not contain supplied atom') return self.atom2 if int(atom) == self.atom1.index else self.atom1
class _StructureAtomContainer(object): """The class to provide access to _StructureAtom instances. """ def __init__(self, st): """ Initialize the container. The underlying mmct is always present. """ self._st = st self._ct_handle = st.handle def __delitem__(self, index): """ Delete an atom from the Structure. Note that this immediately updates the Structure and therefore renumbers any atoms following the one deleted. You can use Structure.deleteAtoms() to easily delete multiple atoms. """ atom_total = mm.mmct_ct_get_atom_total(self._ct_handle) if index < 1: raise IndexError( "Tried to delete atom %d but atom indexes start at 1" % index) elif index > atom_total: raise IndexError( "Tried to delete atom %d but there are only %d atoms" % (index, atom_total)) mm.mmct_ct_delete_atom(self._ct_handle, index) def __getitem__(self, index): """ Return the wrapper class for atom based properties. Note that the initial index is 1, as per the underlying mmct library, not 0 as is expected for python. """ try: cpp_atom = self._st._cpp_structure[index] return _StructureAtom(self._st, cpp_atom) except TypeError: if isinstance(index, _StructureAtom): return index elif isinstance(index, cppstructure.StructureAtom): return _StructureAtom(self._st, index) raise def __iter__(self): """ Provide iteration access. """ for i in range(1, len(self) + 1): cpp_atom = self._st._cpp_structure[i] yield _StructureAtom(self._st, cpp_atom) def __len__(self): """ Return the number of atoms by querying the Structure class. """ return mm.mmct_ct_get_atom_total(self._ct_handle) # TODO: define __setitem__ ? Would need to decide what properties get # copied in an assignment. class _StructureBondContainer(object): """ The class to provide access to _StructureBond instances for each bond in the structure. """ def __init__(self, ct): """ Initialize with a Structure. """ self._ct = ct def __iter__(self): """Provide iteration access. """ for atom in self._ct.atom: for bond in atom.bond: if bond.atom2.index < atom.index: continue yield bond def __len__(self): """Return the total number of bonds in the Structure """ num_bonds = sum(len(atom.bond) for atom in self._ct.atom) # Divide by 2, because we counted from both directions: return num_bonds // 2
[docs]class _AtomBondContainer(object): """ The class to provide access to _StructureBond instances for each bond of an atom. """
[docs] def __init__(self, ct, atom_index): """ Initialize with a Structure and an atom index. """ self._ct = ct self._atom_index = atom_index
def __delitem__(self, index): """Delete the specified bond. """ if index < 1 or index > len(self): raise IndexError("bond index out of range (starts at 1)") mm.mmct_atom_set_bond(self._ct, self._atom_index, index, 0, mm.MMCT_NONE) def __getitem__(self, index): """ Return the wrapper class for bond based properties. Note that as in _StructureAtomContainer the initial index is 1, as expected by the C mmlibraries, not 0 as is expected for python. """ if index < 1 or index > len(self): raise IndexError("bond index out of range (starts at 1)") return _StructureBond(self._ct, self._atom_index, index) def __iter__(self): """Provide iteration access. """ for i in range(1, len(self) + 1): yield self.__getitem__(i)
[docs] def __len__(self): """Return the number of bonds by querying the Structure class. """ return mm.mmct_atom_get_bond_total(self._ct, self._atom_index)
[docs]class _StructureProperty(MutableMapping): """ Dictionary-like container of Structure based properties, with all dict methods. Properties can be accessed via the m2io dataname as it appears in the maestro file. Property names must be m2io data names, which are in the format '<type>_<author>_<property_name>', where '<type>' is a data type prefix, '<author>' is a source specification, and '<property_name>' is the actual name of the data. The data type prefix can specified as 's' for string, 'i' for integer, 'r' for real and 'b' for boolean. The author specification should be 'user' for user created properties. The property name can have embedded underscores. Some example m2io datanames are 'r_m_x_coord', which indicates a real maestro property named 'x coord', and 'i_user_my_count' which indicates an integer user property named 'my count'. To convert to the _StructureProperty to a real dictionary:: d = dict(st.property) To set all properties from dictionary:: st.property = {...} """
[docs] def __init__(self, st, read_only=False): """ Create an instance of the property 'dictionary' The instance is created when st.property is first used. If read-only is True then only read-access will be supported. """ self._st = st # Fix for PYTHON-3097 self._ct_handle = st.handle self._read_only = read_only
def _get_ct_property(self, item): try: lookup_func = ct_property_getters[item[0]] except KeyError: raise KeyError(f"{item} is not a valid data name") return lookup_func(self._ct_handle, item) def __getitem__(self, item): """ Return an item from the unrequested or additional data handle. Usage: value = st.property[<name>] """ # From EV 39515 - special case 's_m_title' if item == 's_m_title': return mm.mmct_ct_get_title(self._ct_handle) try: return self._get_ct_property(item) except mm.MmException as e: if e.rc == mm.MMCT_ERROR: raise KeyError( "%s is not the name of a property in Structure(%d)" % (item, self._ct_handle)) except IndexError as e: if item == "": raise KeyError("The empty string is an invalid dataname.") else: raise def __delitem__(self, item): """ Delete an item from the additional and unrequested data handle. Usage: del st.property[<name>] """ if self._read_only: raise AttributeError("It is not possible to delete a property " "from a TextualStructure object") if item == 's_m_title': raise ValueError("The title (s_m_title) can not be deleted") #trigger key error if not present: self.__getitem__(item) self._st._cpp_structure.deleteProperty(item) def __setitem__(self, item, value): """ Set the item into the additional data handle. Usage: st.property[<name>] = value """ if self._read_only: raise AttributeError("It is not possible to set a property for a " "TextualStructure object") try: if item == 's_m_title': self._st._cpp_structure.setTitle(value) elif item.startswith('s'): self._st._cpp_structure.setPropertyString(item, value) elif item.startswith('r'): self._st._cpp_structure.setPropertyReal(item, value) elif item.startswith('i'): self._st._cpp_structure.setPropertyInt(item, value) elif item.startswith('b'): self._st._cpp_structure.setPropertyBool(item, bool(value)) else: raise KeyError("%s is an invalid property name; see " "_StructureProperty documentation for details." % item) # thrown at the C++ layer if the property doesn't conform to the # correct format except IndexError as err: raise KeyError(str(err))
[docs] def keys(self): """ Return a list of the names of all properties. """ props = self._st._cpp_structure.getPropertyNames() if 's_m_title' not in props: props = ('s_m_title',) + props return props
[docs] def clear(self): """ Clear all properties (except the title). """ if self._read_only: raise AttributeError("It is not possible to clear properties from " "a TextualStructure object") for key in list(self): if key != 's_m_title': del self[key]
[docs] def __len__(self): return len(self.keys())
def __iter__(self): return iter(self.keys())
[docs]class Structure(object): """ A general chemical structure object, which may contain multiple molecules. Structure is an object-oriented wrapper for the underlying MMCT library, where all state is stored. There are several ways to create Structure instances. The structure.StructureReader provides a way to read Structures from a file, and the `schrodinger.maestro.maestro.workspace_get` function returns the workspace Structure from a Maestro session. The `schrodinger.project` module provides access to Structures in a Maestro project. Properties of the Structure entry can be accessed from the `property` dictionary using the `.mae` file data name. For example, the Glide score of a docked pose may be accessed as:: glide_score = st.property['r_i_glide_gscore'] A few additional Structure attributes are available as instance attributes (actually, as python properties). For example, the title of the structure can be accessed (and assigned) via the `title` attribute. See the Structure properties documentation for a full list. Atom objects are accessed from the list-like `schrodinger.structure.Structure.atom` attribute. Each atom is an instance of the `_StructureAtom` class. See the "Properties" section of the `schrodinger.structure._StructureAtom` documentation for a list of available attributes (implemented again as python properties). For example, the atomic number of the first atom in a structure is accessed as:: atomic_num = st.atom[1].atomic_number Note that indices used to access atoms and bonds start at 1, not 0 as with regular python lists. Iteration over the atoms and bonds works as expected. Structure atom properties may also be accessed from the atom's `property` dictionary. For example, the x-coordinate of the first atom may be accessed as:: x_coord = st.atom[1].property['r_m_x_coord'] (However, it is preferable to simply use the `x` attribute - i.e. `st.atom[1].x`.) Bond objects are instances of the `schrodinger.structure._StructureBond` class and are usually accessed via atoms using the `schrodinger.structure._StructureAtom.bond` attribute. Like atoms, bonds have some built-in attributes and a general `property` dictionary. Bonds can also be accessed from the `schrodinger.structure.Structure.bond` iterator, which iterates over all bonds in the Structure. Iterators for various substructures can be accessed from the `molecule`, `chain`, `residue`, and `ring` attributes. Each of these yields an object that has a `getAtomIndices` method to get a list of atom indices, and an `extractStructure` method that can be used to create a separate `Structure` instance corresponding to the substructure. Please see the Python Module Overview for a non-technical introduction and additional examples. """
[docs] def __init__(self, handle, error_handler=None): """ Initialize an object with an existing MMCT handle or a C++ Structure object. """ if error_handler is None: error_handler = mm.error_handler if isinstance(handle, cppstructure.Structure): self.handle = handle.getHandle() self._cpp_structure = handle else: # Check that the handle passed in isn't a bad value and raise an # exception if it is. if not mm.mmct_ct_in_use(handle): raise ValueError("There is no active Structure for the handle " "%d." % handle) self.handle = int(handle) self._cpp_structure = cppstructure.Structure.get_structure( self.handle) # Create the StructureProperty object when it is first accessed. # The unrequested and additional data handles are opened at that # time. self._property = None
def __repr__(self): return "Structure(%d)" % self.handle def __copy__(self): """ Allows the structure to be copied by copy.copy """ st = cppstructure.copy(self) new_structure = Structure(st) return new_structure def __index__(self): """ Return the underlying mmct handle when coerced to an integer. """ return self.handle
[docs] def copy(self): """ Returns a copy of the structure. """ return self.__copy__()
def __eq__(self, other): """ Check for equality with other Structure instances based on the MMCT handle. """ return isinstance(other, Structure) and self.handle == other.handle def __hash__(self): return hash(self.handle) def __ne__(self, other): """ Check for inequality with other Structure instances based on the MMCT handle. """ return not self.__eq__(other) def __getstate__(self): """ Return a string representation for use by pickle. """ return self.writeToString(MAESTRO) def __setstate__(self, state): """ Allows Structure to be unpickled by reading in a string representation """ from schrodinger.structure._io import StructureReader with StructureReader.fromString(state) as reader: ct = next(reader) self.__dict__ = ct.__dict__
[docs] def getXYZ(self, copy=True): """ Get a numpy array of the xyz coordinates of all atoms in the molecule with shape (atom_total, 3). Note that numpy arrays are indexed starting with 0. You can avoid copying the underlying data by specifying copy=False, in which case modifying any values will modify the coordinate values in the Structure. Note that if coordinates are retrieved with copy=False they will become invalid after their source Structure has been garbage collected. Any use of them after this point will likely cause a core dump. This is because the python numpy array provides access directly to the underlying C data. """ if copy: return mm.mmct_ct_get_all_xyz_copy(self.handle) else: return mm.mmct_ct_get_all_xyz_live(self.handle)
[docs] def setXYZ(self, xyz): """ Set all xyz coordinates for the molecule from a numpy array. """ shape = getattr(xyz, "shape", None) if shape != (self.atom_total, 3): raise Exception("Numpy arrays provided to setXYZ() must be of " "shape (atom_total, 3).") mm.mmct_ct_set_all_xyz(self.handle, xyz)
[docs] def findResidue(self, query): """ Returns a `_Residue` object matching the given string (e.g. "A:123"). Currently only protein residues are supported. If no residues were found that match the given string, or if the given string is of improper format, ValueError is raised. :note: If the structure has more than one matching residue, then only the first match will be returned. """ # Ev:115517 # If a string enclosed with "residue(" & ")" is given, strip those: if query.startswith("residue("): query = query[8:-1] s = query.split(":") if len(s) != 2: raise ValueError("The residue string must contain exactly one " "colon (:) character") query_chain = s[0] if query_chain == "_": query_chain = " " query_resnum_str = s[1] try: query_resnum = int(query_resnum_str) except ValueError: try: query_resnum = int(query_resnum_str[:-1]) except ValueError: raise ValueError("Invalid residue number (not an int)") else: query_inscode = query_resnum_str[-1] else: query_inscode = " " # Now iterate over all residues, and return the first match: for res in self.residue: if res._chain == query_chain and res._resnum == query_resnum and res._inscode == query_inscode: return res raise ValueError("Residue not found: %s" % query)
# # This section of the class definition consists of attributes to create # on the fly, to avoid the cost of constructing them up front. # # The CreateWhenNeeded class is a descriptor that allows an attribute # to be created on the fly. The first time the attribute is accessed, # the function is called; whatever it returns is stored as the # attribute and used in all future access. # def _createProxy(self): """ A method to create a proxy to be passed to subobjects that need to hold a reference to the parent Structure. This prevents cyclic references and therefore allows Structure instances to be deallocated by reference counting rather than waiting for a garbage collection sweep. """ return Structure(self._cpp_structure) _proxy = CreateWhenNeeded(_createProxy, "_proxy") _doc = """ An iterable of structure atoms, each of which is a `_StructureAtom` instance. Example usage, where `st` is a Structure instance:: # Access an atom (indices start at 1) atomobj = st.atom[n] # Delete an atom del st.atom[n] # Find the number of atoms len(st.atom) # Iterate over all atoms for atom in st.atom: take_some_action(atom) :note: As with many other collections, the contents of the atom list should not be modified through additions or deletions while you are iterating over it. """ def _createAtomContainer(self): return _StructureAtomContainer(self._proxy) atom = CreateWhenNeeded(_createAtomContainer, "atom", _doc) _doc = """ An iterable of structure bonds, each of which is a `_StructureBond` instance. To iterate over bonds:: for bond in st.bond: take_some_action(bond) :note: Atoms and bonds should not be added or deleted while you are iterating over bonds. :note: Bonds are not accessible by index. """ def _createBondContainer(self): return _StructureBondContainer(self._proxy) bond = CreateWhenNeeded(_createBondContainer, "bond", _doc) # molecule _doc = """ An iterable of molecules in the structure, each of which is a `_Molecule` instance. Example usage:: # Find the number of molecules in the structure len(st.molecule) # Retrieve a molecule by number (indices start at 1) mol = st.molecule[molnum] # Iterate over all molecules for mol in st.molecule: take_some_action(mol) :note: Atoms and bonds should not be added or deleted while you are iterating over molecules. """ def _createStructureMoleculeIterable(self): return _StructureMoleculeIterable(self._proxy) molecule = CreateWhenNeeded(_createStructureMoleculeIterable, "molecule", doc=_doc) # chain _doc = """ An iterable of chains in the structure, each of which is a `_Chain` instance. Example usage:: # Find the number of chains in the structure len(st.chain) # Retrieve a _Chain instance by letter chain = st.chain[letter] # Iterate over chains for chain in st.chain: take_some_action(chain) :note: Atoms and bonds should not be added or deleted while you are iterating over chains. """ def _createStructureChainIterable(self): return _StructureChainIterable(self._proxy) chain = CreateWhenNeeded(_createStructureChainIterable, "chain", doc=_doc) # ring _doc = """ An iterable of rings in the structure, each of which is a `_Ring` instance. To iterate over rings:: for ring in st.ring: take_some_action(ring) :note: Atoms and bonds should not be added or deleted while you are iterating over rings. """ def _createStructureRingIterable(self): return _StructureRingIterable(self._proxy) ring = CreateWhenNeeded(_createStructureRingIterable, "ring", doc=_doc) @property def title(self): """ Get the title for this structure """ return mm.mmct_ct_get_title(self.handle) @title.setter def title(self, title): """ Set the title for this structure """ mm.mmct_ct_set_title(self.handle, title) @property def atom_total(self): """ Get total number of atoms in this structure """ return mm.mmct_ct_get_atom_total(self.handle) @property def mol_total(self): """ Get total number of molecules in this structure """ return mm.mmct_ct_get_mol_total(self.handle) @property def formal_charge(self): """ Get the sum of formal charges for the structure. """ return self._cpp_structure.getTotalCharge() @property def total_weight(self): """ The sum of atomic weights for the whole structure. The weight of implicit hydrogens is automatically included. Accessing this property is an O(N) operation. """ return sum([a.atomic_weight for a in self.atom]) @property def residue(self): """ An iterable of residues in the structure, each of which is a `_Residue` instance. To iterate over all residues:: for residue in st.residue: take_some_action(residue) :note: Atoms and bonds should not be added or deleted while you are iterating over residues. :note: residues are not accessible by index. See `Structure.findResidue()` """ return _ResidueIterable(self._proxy, self.atom) @property def pbc(self): """Access the PBC of the Structure. Throws KeyError if no PBC exists""" return cppstructure.PBC.get_pbc(self) @pbc.setter def pbc(self, pbc): """ Set the PBC of the Structure to the given PBC. :type pbc: schrodinger.infra.structure.PBC or list of floats. :param pbc: PBC object, or list of floats that is accepted by the PBC constructor. Can be: - Orthorhombic PBC based on the box side lengths: 3 floats (A) - PBC based on the box side lengths and angles in angstroms and degrees: 6 floats - PBC based on the Desmond box chorus properties: 9 floats :raise: ValueError: If PBC object failed to initialize """ if not isinstance(pbc, cppstructure.PBC): # Try to create a PBC object try: pbc = cppstructure.PBC(*pbc) except NotImplementedError as err: # cppstructure.PBC already raises ValueError for wrong cell # parameters, keep it within the same exception type raise ValueError( 'Invalid number or type of arguments.') from err pbc.applyToStructure(self) @pbc.deleter def pbc(self): """Remove the PBC of the Structure""" cppstructure.PBC.clearFromStructure(self) @property def property(self): """ Dictionary-like container of Structure-level properties. Keys are strings of the form `type_family_name` as described in the `PropertyName` documentation. """ if self._property is None: self._property = _StructureProperty(self._proxy) return self._property @property.setter def property(self, d): if self._property is None: self._property = _StructureProperty(self._proxy) self._property.clear() self._property.update(d)
[docs] def retype(self): """ Reassign all the MacroModel atom types based on the bond orders and formal charges. This function should be called after either of these have been changed. """ mm.mmtype_retype(self.handle)
[docs] @staticmethod def read(filename, index=1): """ Convenience method for the preferred call to StructureReader.read() """ from schrodinger.structure._io import StructureReader with StructureReader(filename, index=index) as reader: return next(reader)
[docs] def writeToString(self, format): """ Write the structure to a string representation and return the string. The format parameter is required. """ if format == MAESTRO: return write_ct_to_string(self.handle) if format == SD: return write_ct_to_sd_string(self.handle) raise ValueError( "Write to string only supported for Maestro and SD formats.")
[docs] def write(self, filename, format=None): """ Convenience method for the preferred call to StructureWriter.write() """ from schrodinger.structure._io import StructureWriter with StructureWriter(filename, format=format) as writer: writer.append(self)
[docs] def append(self, filename, format=None): """ Convenience method for the preferred StructureWriter context manager """ from schrodinger.structure._io import StructureWriter with StructureWriter(filename, overwrite=False, format=format) as writer: writer.append(self)
[docs] def putToM2ioFile(self, filehandle): """ Used by the Maestro writer - put a single structure to the (already open) filehandle """ mm.mmct_ct_m2io_put(self.handle, filehandle)
[docs] def closeBlockIfNecessary(self, filehandle): """ Used by the Maestro writer to leave the header block if necessary. For Structure objects this is not needed so it only returns """ return
[docs] def deleteAtoms(self, indices, renumber_map=False): """ Delete multiple atoms from the Structure. The argument indices must be a sequence or an iterable, and able to be interpreted as ints. After deletion, indices are renumbered from 1 to len(atoms). Pre-existing references to Structure atoms will not be correct, as they store index values. If renumber_map is set to True, will return a renumbering dictionary. Keys are atom numbers before deleting, and value for each is the new atom number, or None if that atom was deleted. """ indices = [int(i) for i in indices] if renumber_map: mm.mmct_ct_begin_atom_renumbering(self.handle) self._cpp_structure.deleteAtoms(indices) if renumber_map: rmap = mm.mmct_ct_end_atom_renumbering(self.handle) if renumber_map: return rmap
[docs] def addAtom(self, element, x, y, z, color=None, atom_type=None): """ Add a new atom to the structure. Return the created `_StructureAtom` object. """ mm.mmct_ct_new_atoms(self.handle, 1) num_atoms = self.atom_total atom = self.atom[num_atoms] try: atom.element = element atom.xyz = (x, y, z) if color is not None: atom.color = color if atom_type is not None: atom.atom_type = atom_type return atom except Exception: del self.atom[num_atoms] raise
[docs] def addAtoms(self, num_atoms): """ Add the specified number of atoms to this structure. The following atom attributes will have to be set for each atom afterwards: - `element` - `x`, `y`, `z` - `color` - `atom_type` """ mm.mmct_ct_new_atoms(self.handle, num_atoms)
[docs] def extract(self, indices, copy_props=False): """ Return a new structure object which contains the atoms of the current structure that appear in the specified list. After extractions, indices are renumbered from 1 to len(atoms). Pre-existing references to Structure atoms will not be correct, as they store index values. :type indices: iterable or sequence :param indices: List of atom indices to extract :param bool copy_props: Whether to copy structure properties :rtype: Structure :return: Extracted structure """ bs = Bitset.from_list(self.atom_total, indices) new_ct = cppstructure.extract(self, bs) new_st = Structure(new_ct, True) if copy_props: new_st.property = dict(self.property) return new_st
[docs] def extend(self, other_structure): """ Add the atoms in other_structure to the end of the current structure. The other_structure is left unchanged. :raises ValueError: Extending a structure with itself is not allowed. """ try: cppstructure.append(other_structure, self) except RuntimeError as err: if 'itself' in str(err): raise ValueError(*err.args) raise
[docs] def merge(self, other_structure, copy_props=False): """ Return a new structure object which contains the atoms of the current structure and the atoms of other_structure. If copy_props is True, properties from the current structure and other_structure will be added to the new structure. If the same property is specifed in both the current structure and other_structure, the current value will take precedence. """ new_struct = self.copy() new_struct.extend(other_structure) if copy_props: for k, v in other_structure.property.items(): new_struct.property.setdefault(k, v) return new_struct
[docs] def areBound(self, atom1, atom2): """ Returns True if atom1 and atom2 have a bond of any order between them and False is there is no bond. """ return bool(mm.mmct_is_atom_bonded(self.handle, int(atom1), int(atom2)))
[docs] def getBond(self, atom1, atom2): """ Returns a `_StructureBond` object for the bond between atom1 and atom2. The atom parameters can be `_StructureAtom` objects or integer indices from 1 to the number of atoms in the structure. """ num_bond = mm.mmct_atom_get_bond_total(self.handle, atom1) for ibond in range(1, num_bond + 1): if mm.mmct_atom_get_bond_atom(self.handle, atom1, ibond) == int(atom2): return _StructureBond(self._proxy, int(atom1), ibond) return None
[docs] def addBond(self, atom1, atom2, bond_type): """ Add a bond of the specified type between the two atoms atom1 and atom2. The atom parameters can be `_StructureAtom` objects or integer indices from 1 to the number of atoms in the structure. If the two atoms are already bound then the bond type is just changed. :param bond_type bond type (legacy integer 0-3 bond order) """ return _addBond(self, atom1, atom2, bond_type)
[docs] def addBonds(self, bonds_list): """ Add multiple bonds to this structure. This is much faster than multiple calls to addBond() method when many bonds need to be added. Bonds are specified by a list of integer lists: (atom1, atom2, bond_type). Example:: st.addBonds([(10, 11, 1), (12, 13, 2)]) This will add a single-order bond between atoms 10 and 11, and a double-order bond between atoms 12 and 13. """ new_bonds = [] with update_once(): for atom1, atom2, bond_type in bonds_list: new_bonds.append(_addBond(self, atom1, atom2, bond_type)) return new_bonds
[docs] def deleteBond(self, atom1, atom2): """ Delete the bond between atom1 and atom2. Raises an Exception if there is no bond between these two. """ try: mm.mmct_atom_delete_bond(self, atom1, atom2) except mmcheck.MmException as err: if err.rc == -5: msg = f"Error - atoms {atom1} and {atom2} are not bound" raise Exception(msg) else: raise
[docs] def measure(self, atom1, atom2, atom3=None, atom4=None): """ Return the measurement for the provided atoms. If atom3 is None, return the distance between atom1 and atom2. If atom4 is None, return the angle with atoms 1 through 3, and if all atoms are provided, return the dihedral angle. All atom arguments can be integers or `_StructureAtom` objects. If Periodic Boundary Condition CT-level properties are defined, uses the PBC measurement. See also the structutil.measure module, which has functions to make measurements between atoms in different structures, and can also measure angles between planes. """ if atom3 is None: return mm.mmct_atom_get_distance_pbc_s(self.handle, atom1, self.handle, atom2) elif atom4 is None: return mm.mmct_atom_get_bond_angle_pbc_s(self.handle, atom1, self.handle, atom2, self.handle, atom3) else: return mm.mmct_atom_get_dihedral_angle_pbc_s( self.handle, atom1, self.handle, atom2, self.handle, atom3, self.handle, atom4)
[docs] def adjust(self, value, atom1, atom2, atom3=None, atom4=None): """ Adjust a distance, angle or dihedral angle. If atom3 is None then the distance between atom1 and atom2 will be set to value, atom2 and all atoms attached to that atom will be moved. If atom4 is None then the angle between atom1, atom2 and atom3 will set to value, atom3 and all other atoms attached to that will be moved. If all atoms are specified then the dihedral angle made by atom1, atom2, atom3 and atom4 will be set to value and atom4 and all other atoms attached to that will be moved. All distances are specified in Angstroms, all angles in degrees. NOTE: To adjust improper dihedrals, use build.adjustImproperDihedral instead :type value: float :param value: value the internal coordinate will be set to :type atom1: int or _StructureAtom :param atom1: first atom in the coordinate :type atom2: int or _StructureAtom :param atom2: second atom in the coordinate :type atom3: int or _StructureAtom or None :param atom3: third atom in the coordinate (if None, the coordinate is a bond) :type atom4: int or _StructureAtom or None :param atom4: fourth atom in the coordinate (if None, the coordinate is an angle) :raises AtomsInRingError: if specified atoms are within a ring system. """ # Create a bitset for the moving atoms fixed_atom = atom1 if atom3 is None else atom2 moving_atom = atom2 if atom3 is None else atom3 bs = Bitset(size=mm.mmct_ct_get_atom_total(self.handle)) mm.mmct_atom_get_moving(self.handle, fixed_atom, self.handle, moving_atom, bs) if bs.get(fixed_atom): raise AtomsInRingError( "Can't do adjustment - atoms are in a ring: %i, %i" % (fixed_atom, moving_atom)) if atom3 is None: mm.mmct_atom_set_distance(value, self.handle, atom1, self.handle, atom2, bs) elif atom4 is None: mm.mmct_atom_set_bond_angle(value, mm.MM_ANGLE_DEG, self.handle, atom1, self.handle, atom2, self.handle, atom3, bs) else: mm.mmct_atom_set_dihedral_angle(value, mm.MM_ANGLE_DEG, self.handle, atom1, self.handle, atom2, self.handle, atom3, self.handle, atom4, bs) return
[docs] def getMovingAtoms(self, fixed_atom, moving_atom): """ Returns all atoms that would move if <moving_atom> is moved while <fixed_atom> is frozen. This effectively returns all atoms in the same molecule substructure as <moving_atom> (atoms in the same substructure as fixed_atom are excluded). In other words, if the bond between the moving_atom and fixed_atom (or towards the direction of fixed_atom) were to be broken, the atoms that would be in the same molecule as moving_atom are returned. Can be used for detecting things like residue side-chain atoms, etc. :note: If fixed_atom and moving_atom are part of different molecules, then all atoms in the moving_atom's molecule will be returned. If fixed_atom and moving_atom are not bound directly, the intervening atoms will not be included in the result. If fixed_atom and moving_atom are connected with more than one path (are in a ring), then ValueError is raised. :type fixed_atom: Atom index or `_StructureAtom`. :param fixed_atom: Atom which is part of the molecule that is to be excluded from the result (frozen, not being moved). :type moving_atom: Atom index or `_StructureAtom`. :param moving_atom: Atom of interest (atom to be moved); a set of atoms that would be moved with it (connected to it) will be returned. :rtype: Set of ints :return: Set of atom indices for atoms "connected" to moving_atom - those atoms that would be moved with it if it was moved. For example, if called with alpha carbon and beta carbon atoms of a protein residue, then all side-chain atoms would be returned. Atom moving_atom will also be included. Raises ValueError if the given atoms are part of a ring (in other words, moving_atom is connected to fixed_atom via more than one path). This may happen if part of the moving_atom's "chain" is bonded to something unexpected; e.g. ZOBed to metals, or involved in a di-sulfide bond. """ bs = Bitset(size=self.atom_total) mm.mmct_atom_get_moving(self.handle, int(fixed_atom), self.handle, int(moving_atom), bs) if bs.get(fixed_atom): raise ValueError( "fixed_atom (%i) and moving_atom (%i) are in a ring" % (fixed_atom, moving_atom)) return set(bs)
[docs] def getResidueAtoms(self, atom): """ Return a list of atom objects that are in the same residue as 'atom'. """ bs = cppstructure.get_residue_atoms(self.atom[atom]) # Convert the Bitset into a list of atom objects: return [self.atom[ix] for ix in bs]
[docs] def getMoleculeAtoms(self, atom): """ Return a list of atom objects that are in the same molecule as 'atom'. """ num_atoms = mm.mmct_ct_get_atom_total(self.handle) bs = Bitset(size=num_atoms) mm.mmct_atom_get_mol_atoms(self.handle, atom, bs) # Convert the Bitset into a list of atom objects: return [self.atom[ix] for ix in bs]
[docs] def getChainAtoms(self, atom): """ Return a list of atom objects that are in the same chain as 'atom'. """ num_atoms = mm.mmct_ct_get_atom_total(self.handle) bs = Bitset(size=num_atoms) mm.mmct_atom_get_chain_atoms(self.handle, atom, bs) # Convert the Bitset into a list of atom objects: return [self.atom[ix] for ix in bs]
[docs] def getAtomIndices(self): """ Return a list of all atom indices in this structure. """ return list(range(1, self.atom_total + 1))
[docs] def getPropertyNames(self): # See corresponding docstring in mmshare/include/structure.h return self._cpp_structure.getPropertyNames()
[docs] def getAtomPropertyNames(self, include_builtin=False): """ Return a tuple of atom-level property names present in this CT. :param: include_builtin: Whether to include built-in properties. :type include_builtin: bool """ # See corresponding docstring in mmshare/include/structure.h # TODO consider returning list instead of a tuple props = self._cpp_structure.getAtomPropertyNames() if include_builtin: props = props + tuple(ATOM_PROP_LINK.keys()) return props
[docs] def deletePropertyFromAllAtoms(self, prop_name): """ Deletes a property from all atoms present in structure :type prop_name: string :param: prop_name: The name of the atom-level property to delete. """ self._cpp_structure.deletePropertyFromAllAtoms(prop_name)
[docs] def isEquivalent(self, struct, check_stereo=True): """ Return True if the 2 structures are equivalent Return False if the 2 structures are different struct: Another structure class object check_stereo: Specifies whether or not to check stereo chemistry. """ if (check_stereo): return cppstructure.topologically_equivalent( self._cpp_structure, struct._cpp_structure) else: return cppstructure.topologically_equivalent_without_stereo( self._cpp_structure, struct._cpp_structure)
[docs] def find_rings(self, sort=True): """ Find all rings in the structure using SSSR. Each ring is returned in connectivity order. :type sort: bool :param sort: Deprecated and unused :return: A list of lists of integers corresponding to the atom indices of the rings. """ cpprings = cppstructure.get_sssr(self) rings = [[a.getIndex() for a in r.getAtoms()] for r in cpprings] return rings
[docs] def applyTubeStyle(self, atom_list=None): """ Applies CPK styles to the atoms and bonds of the entire structure (by default) or to the atoms (and their bonds) given in atom_list. :type atom_list: iterable :param atom_list: An iterable of atom objects or atom indices to apply the given styles to. If not included the styles are applied to all atoms in the structure. """ self.applyStyle(atoms=ATOM_NOSTYLE, bonds=BOND_TUBE, atom_list=atom_list)
[docs] def applyCPKStyle(self, atom_list=None): """ Applies CPK styles to the atoms and bonds of the entire structure (by default) or to the atoms (and their bonds) given in atom_list. :type atom_list: iterable :param atom_list: An iterable of atom objects or atom indices to apply the given styles to. If not included the styles are applied to all atoms in the structure. """ self.applyStyle(atoms=ATOM_CPK, bonds=BOND_WIRE, atom_list=atom_list)
[docs] def applyWireStyle(self, atom_list=None): """ Applies wire styles to the atoms and bonds of the entire structure (by default) or to the atoms (and their bonds) given in atom_list. :type atom_list: iterable :param atom_list: An iterable of atom objects or atom indices to apply the given styles to. If not included the styles are applied to all atoms in the structure. """ self.applyStyle(atoms=ATOM_NOSTYLE, bonds=BOND_WIRE, atom_list=atom_list)
[docs] def applyStyle(self, atoms=ATOM_BALLNSTICK, bonds=BOND_BALLNSTICK, atom_list=None): """ Applies the given display styles to the atoms and bonds of the entire structure (by default) or to the atoms (and their bonds) given in atom_list. :type atoms: int :param atoms: Display style for atoms, given by structure module constants ATOM_NOSTYLE, ATOM_CIRCLE, ATOM_CPK, ATOM_BALLNSTICK. Default is ATOM_BALLNSTICK. :type bonds: int :param atoms: Display style for bonds, given by structure module constants BOND_NOSTYLE, BOND_WIRE, BOND_TUBE, BOND_BALLNSTICK. Default is BOND_BALLNSTICK. :type atom_list: iterable :param atom_list: An iterable of atom objects or atom indices to apply the given styles to. If not included the styles are applied to all atoms in the structure. Possible examples include:: [1, 3, 5] ring.atom schrodinger.structutils.analyze.evalulate_asl(asl_expr) [structure.atom[x] for x in xrange(50)] maestro.selected_atoms_get() """ if atom_list is None: atom_list = self.atom else: # Test to see if atom_list is a list of atom indices try: # This test will be passed if it is a list of atom objects atom_list[0].style except AttributeError: # Attribute Errors are raise by lists of atom indices atom_list = [self.atom[x] for x in atom_list] except IndexError: # Iterable atom containers (such as self.atom) throw IndexErrors pass for atom in atom_list: atom.style = atoms for bond in atom.bond: bond.setStyle(bonds)
[docs] def has3dCoords(self): """ Returns True if any atom in the structure has a non-zero z-coordinate. """ return any((a.z != 0.0 for a in self.atom))
[docs] def get3dStructure(self, require_stereo=True): """ :deprecated: Use generate3dConformation() instead. """ msg = ('Structure.get3dStructure() is deprecated. Use ' 'Structure.generate3dConformation() instead.') warnings.warn(msg, DeprecationWarning, stacklevel=2) copy_st = self.copy() copy_st.generate3dConformation(require_stereo) return copy_st
[docs] def generate3dConformation(self, require_stereo=True): """ Generate new 3D coordinates for the current structure, and add hydrogens if any are missing. This method is useful for "volumizing" a 2D structure into 3D. NOTE: For 2D inputs, annotation properties must be present for chiral centers to be processed correctly. :type require_stereo: bool :param require_stereo: Whether to require all chiral centers to have defined stereochemistry via annotation properties. Defaults to True. UndefinedStereochemistry exception is raised if any chiral atom has ambiguous chirality. If set to False, ambiguous chiralities will be expanded arbitrarily. """ global analyze if analyze is None: # To prevent any circular imports, import modules here from schrodinger.structutils import analyze # Verify that no ambiguous stereochemistry is present: if require_stereo: for anum, chirality in analyze.get_chiral_atoms(self).items(): if chirality == "undef": raise UndefinedStereochemistry( "No chirality specified for atom %i" % anum) fast3d_volumizer = fast3d.Volumizer() fast3d_volumizer.run( self, True, # add Hydrogens True) # run stereoizer
[docs]def write_ct_to_string(ct, error_handler=None): """ Return a string representation of the provided Structure, formatted as a Maestro file. :type ct: Structure """ return mm.mmct_ct_to_string(ct)
def write_ct_to_sd_string(ct, error_handler=None): """ Return a string representation of the provided Structure, formatted as an SD file. :type ct: Structure """ if error_handler is None: error_handler = mm.error_handler mm.mmmdl_initialize(error_handler) fh = mm.mmmdl_new_to_string() mm.mmmdl_sdfile_put_ct(fh, ct) ret = mm.mmmdl_get_string(fh) mm.mmmdl_delete(fh) return ret # # PropertyName and associated constants # # Currently, assume 1:1 correspondence between short (i.e., actual property # owners) and long names (i.e., more readable names). PROP_LONG_NAME = { 'm': 'Maestro', 'qp': 'QikProp', 'vsw': 'VSW', 'st': 'Stereoizer', 'lp': 'LigPrep', 'mmod': 'MacroModel', 'glide': 'Glide', 'i': 'Impact', 'pdb': 'PDB', 'epik': 'Epik', 'sd': 'SD', 'chorus': 'Desmond', 'j': 'Jaguar', 'livedesign': 'LiveDesign', } PROP_SHORT_NAME = {} for prop in list(PROP_LONG_NAME): PROP_SHORT_NAME[PROP_LONG_NAME[prop]] = prop PROP_STRING = 's' PROP_FLOAT = 'r' PROP_REAL = PROP_FLOAT PROP_INTEGER = 'i' PROP_BOOLEAN = 'b'
[docs]class PropertyName(object): """ The PropertyName class can be used to display the "user-readable" version of an entry or atom property to the user. Properties are stored in `.mae` files with names in the form `type_family_name`, where `type` is a datatype indicator, `family` indicates the owner or creator of the property, and `name` is the name of the property. These strings are used as the keys in the `property` dictionary attributes of `Structure`, `_StructureAtom`, and `_StructureBond`. Examples include `s_m_title`, which is a string created by Maestro with the name "title", and `i_m_residue_number`, which is an integer created by Maestro with the name "residue number". """
[docs] def __init__(self, dataname=None, type=None, family=None, username=None): """ The PropertyName constructor operates in one of two modes - either a dataname needs to be provided, or each of type, family, and username needs to be provided. :type dataname: str :param dataname: The full property name, e.g. 's_m_title'. :type type: enum :param type: The property value type, which must be 's', 'r', 'i', or 'b'. You can also use the predefined module constants `PROP_STRING`, `PROP_FLOAT` (or equivalently `PROP_REAL`), `PROP_INTEGER`, and `PROP_BOOLEAN`. :type family: str :param family: The family/owner of the property. If the family is one of the recognized long family names (e.g. 'QikProp' - see the keys of the `PROP_SHORT_NAME` dict), the short family name is assigned automatically. :type username: str :param username: The name a user would see displayed in the Maestro project table. Underscores are replaced with spaces unless protected by a backslash (in which case the backslash is not displayed). Internally, PropertyName will store the 'name', which is the fragment of the dataname that is not the type or family. """ # Is the family/owner required? if not dataname and (not type or not family or not username): msg = "PropertyName requires a dataname or type/family/username" raise Exception(msg) # Note: data/usernames with '\ ' will convert incorrectly if dataname: s = dataname.split('_') if len(s) < 3: raise Exception(" PropertyName dataname '%s' must include " "type, family, and name" % dataname) else: self.type = s[0] self.family = PROP_SHORT_NAME.get(s[1], s[1]) self.name = "_".join(s[2:]) if self.name.find(r'\ ') >= 0: raise Exception( r"Unsupported string '\ ' in the dataname '%s'" % dataname) else: self.type = type self.family = family # Protect the underscores and replace spaces with underscores if username.find(r'\ ') >= 0: raise Exception( r"Unsupported string '\ ' in the username '%s'" % username) self.name = username.replace('_', r'\_').replace(' ', '_') if self.type not in (PROP_STRING, PROP_FLOAT, PROP_INTEGER, PROP_BOOLEAN): raise Exception("'%s' is not a valid property type" % self.type)
[docs] def dataName(self): """ Returns the m2io data name of form `type_family_name`. This is the fully qualified name that includes the type, owner and name and should be used for all lookup, indexing, etc. """ return "_".join((self.type, self.family, self.name))
[docs] def userName(self): r""" Returns the user name of this property. User name is the shortened, user-readable name that should *only* be used when presenting this property to the user, such as in a GUI pull down menu. Since data names can NOT have spaces in them, while user names can, we have a convention where a space in a user name is represented as an underscore in the data name, and an underscore in the user name has to be escaped with a backslash in the data name. Replace '\_' with '_', and '_' with ' ' before returning the user name """ return self.name.replace('_', ' ').replace(r'\ ', '_')
[docs] def userNameWithFamily(self): """ Returns the property name to be displayed in UI widgets, such as combo menus and tables. The display name is the "user" name, followed by the family in parentheses. :return: Property name with family name in parentheses. :rtype: str """ # Lookup full family name: family = PROP_LONG_NAME.get(self.family, self.family) return "%s (%s)" % (self.userName(), family)
def __str__(self): """ Convert this PropertyName to a string """ return self.dataName() def __eq__(self, other): """ Compare this property to a string property name """ try: # Works with PropertyName instances and strings: return str(self) == str(other) except: return False def __ne__(self, other): """ Check for inequality; opposite of above """ return not self.__eq__(other)
[docs]def create_new_structure(num_atoms=0): """ Returns a new Structure object. If the Structure is created without atoms, they can be added with the `Structure.addAtom` or `Structure.addAtoms` methods. Otherwise, the following atom attributes must be set for each atom afterwards: - `element` - `x`, `y`, `z` - `color` - `atom_type` :param num_atoms: The number of atoms to create the structure with. """ ct = cppstructure.create_structure(num_atoms) return Structure(ct)
[docs]def get_residues_by_connectivity(atom_container): """ Access residues in N->C connectivity order :param atom_container: Anything with an atom() method that returns atom indexes :type atom_container: `Structure._Chain`, `Structure._Structure`, or `Structure._Molecule` :return: A residue iterator where the atoms are returned in N->C connectivity order :rtype: `Structure._ResidueIterable` """ return _get_residues_by(atom_container, sort=False, connectivity_sort=True)
[docs]def get_residues_unsorted(atom_container): """ Access residues in the unsorted input order :param atom_container: Anything with an atom() method that returns atom indexes :type atom_container: `Structure._Chain`, `Structure._Structure`, or `Structure._Molecule` :return: A residue iterator where the atoms are returned in unsorted order :rtype: `Structure._ResidueIterable` """ return _get_residues_by(atom_container, sort=False, connectivity_sort=False)
def _get_residues_by(atom_container, **kwargs): """ Access residues in the specified order. All keyword arguments are passed to `_ResidueIterable.__init__`. :param atom_container: Anything with an atom() method that returns atom indexes :type atom_container: `Structure._Chain`, `Structure._Structure`, or `Structure._Molecule` :return: A residue iterator where the atoms are returned in the requested order :rtype: `Structure._ResidueIterable` """ if isinstance(atom_container, Structure): ct = atom_container else: ct = atom_container._st return _ResidueIterable(ct, atom_container.atom, **kwargs)
[docs]def get_pyatom_from_cppatom(cppatom): """ Given a C++ atom object, return the corresponding Python atom. :param cppatom: C++ atom object :type cppatom: `schrodinger.infra.structure.StructureAtom` :return: The corresponding Python atom object :rtype: `structure._StructureAtom` """ return _StructureAtom(Structure(cppatom.getStructure()), cppatom)