Source code for schrodinger.structutils.check
"""
Tools for detecting and reporting distortions in structures.
"""
from typing import Optional
from schrodinger import structure
from schrodinger.infra import mm
from schrodinger.structutils import analyze
from schrodinger.structutils import measure
from schrodinger.structutils import rings
[docs]class AngleDistortion(Distortion):
[docs]    def __init__(self, atoms, ideal_angle, actual_angle):
        self.atoms = atoms
        self.ideal_angle = ideal_angle
        self.actual_angle = round(actual_angle, 2) 
    def __str__(self):
        return f'Angle Distortion ({self.atoms[0].index}-{self.atoms[1].index}-{self.atoms[2].index}): {self.actual_angle} != {self.ideal_angle}' 
[docs]class BondDistortion(Distortion):
[docs]    def __init__(self, bond, ideal_length, actual_length):
        self.bond = bond
        self.ideal_length = round(ideal_length, 3)
        self.actual_length = round(actual_length, 2) 
    def __str__(self):
        return f'Bond Distortion ({self.bond.atom1.index}-{self.bond.atom2.index}): {self.actual_length} != {self.ideal_length}' 
def _translate_angle_geom(mmideal_geo):
    """
    Returns the MMIdeal_Geo enum from the MMAT atom type
    """
    if mmideal_geo == mm.MMAT_LINEAR:
        return mm.MMIDEAL_LINEAR
    elif mmideal_geo == mm.MMAT_TRIGONAL:
        return mm.MMIDEAL_TRIGONAL
    elif mmideal_geo == mm.MMAT_TETRAHEDRAL:
        return mm.MMIDEAL_TETRAHEDRAL
    else:
        return mm.MMIDEAL_ERROR
def _find_small_ring_distortions(st, tolerance):
    """
    Finds angle distortions in 3 and 4 membered rings
    """
    distortions = []
    small_ring_atoms = set()
    # All angles in 3/4-membered rings should be 60/90 degrees
    sssr = rings.find_rings(st)
    for ring in sssr:
        if len(ring) > 4:
            continue
        ideal_angle = 60 if len(ring) == 3 else 90
        full_ring = ring + ring[:2]
        all_angles = []
        for a1, a2, a3 in zip(full_ring, full_ring[1:], full_ring[2:]):
            atoms = [st.atom[a1], st.atom[a2], st.atom[a3]]
            all_angles.append(atoms)
            small_ring_atoms.add(atoms[1].index)
        for angle_atoms in all_angles:
            angle = measure.measure_bond_angle(*angle_atoms)
            if abs(angle - ideal_angle) > tolerance:
                # distortion found
                distortions.append(
                    AngleDistortion(angle_atoms, ideal_angle, angle))
    return [distortions, small_ring_atoms]
def _find_angle_distortions(st, tolerance):
    """
    Determines if st has any distorted angles. An angle is distorted if it is not
    within tolerance of the ideal angle.
    """
    ring_angle_distortions = _find_small_ring_distortions(st, tolerance)
    distortions = ring_angle_distortions[0]
    ring_angle_atoms = ring_angle_distortions[1]
    # Check linear/triagonal/tetrahedral angles
    for angle_atom_indices in analyze.angle_iterator(st):
        # Ignore angles in/attatched to 3 or 4 membered rings
        if angle_atom_indices[1] in ring_angle_atoms:
            continue
        angle_atoms = [st.atom[a] for a in angle_atom_indices]
        central_geometry = mm.mmat_get_central_geometry(
            angle_atoms[1].atom_type)
        mmideal_central_geometry = _translate_angle_geom(central_geometry)
        if mmideal_central_geometry == mm.MMIDEAL_ERROR:
            # Atom is not LIN/TRI/TET
            continue
        if mm.mmat_is_wildcard(angle_atoms[1].atom_type) and len(
                angle_atoms[1].bond) == 3:
            # Atom is automatically labeled as TET but may TRI
            mmideal_central_geometry = mm.MMIDEAL_TRIGONAL
        ideal_angle = mm.mmideal_get_angle(mmideal_central_geometry)
        angle = measure.measure_bond_angle(*angle_atoms)
        diff = abs(ideal_angle - angle)
        if diff > tolerance:
            distortions.append(AngleDistortion(angle_atoms, ideal_angle, angle))
    return distortions
def _find_bond_distortions(st, tolerance):
    """
    Finds any bond distortions. A bond is distorted if it is not within tolerance
    of its ideal length.
    """
    distortions = []
    for bond in st.bond:
        ideal_length = mm.mmideal_get_stretch(st, [bond.atom1, bond.atom2])
        bond_length = measure.measure_distance(bond.atom1, bond.atom2)
        diff = abs(ideal_length - bond_length)
        if diff > tolerance:
            distortions.append(BondDistortion(bond, ideal_length, bond_length))
    return distortions
[docs]def find_distortions(st: structure.Structure,
                     angle_tolerance: float = 20.0,
                     bond_tolerance: float = 0.5) -> Optional[list]:
    """
    Determines whether a given structure is distorted or not. Specifically,
    this function checks that checks that:
        1. All angles in 3-membered rings are within angle_tolerance (in degrees) of 60 deg
        2. All angles in 4-membered rings are within angle_tolerance of 90 deg
        3. All linear/trigonol/tetrahedral angles are within angle_tolerance of 180/120/109.5
        4. All bonds are within bond_tolerance of their ideal length
    Returns None or a list of Distortions
    """
    angle_distortions = _find_angle_distortions(st, angle_tolerance)
    bond_distortions = _find_bond_distortions(st, bond_tolerance)
    if not (angle_distortions or bond_distortions):
        return None
    return angle_distortions + bond_distortions