Source code for schrodinger.application.matsci.geometry
"""
Utilities for geometry manipulation or property calculation
Copyright Schrodinger, LLC. All rights reserved.
"""
import numpy
from schrodinger import geometry as infrageom
from schrodinger.application.matsci import graph
from schrodinger.application.matsci import msprops
from schrodinger.structutils import analyze
[docs]def get_backbone_atoms(struct, exclude_hydrogen=True, max_fix=True):
    """
    Return dictionary of backbone atoms for each molecule in the structure.
    :param `schrodinger.structure.Structure` struct: The structure to find
        backbone atoms in.
    :param bool exclude_hydrogen: If true hydrogen will be excluded in
        creation of graph and searching for backbone atoms. This results in
        much faster calculation.
    :param max_fix: Whether to fix internal degeneracy when maximum number of
        path checks are reached
    :type max_fix: bool
    :return dict: the key of the dictionary is molecule number and the
        value is the list of backbone atoms. If there are less than two atoms
        in the molecule the associated value will be an empty list.
    """
    heavy_atom_asl = 'heavy_atoms'
    backbone = {}
    # Create graph
    atom_list = None
    if exclude_hydrogen:
        atom_list = analyze.evaluate_asl(struct, heavy_atom_asl)
    struct_graph = analyze.create_nx_graph(struct, atoms=atom_list)
    # Iterate through molecule to find backbone in each
    for sub_graph in graph.get_sub_graphs(struct_graph):
        # Get molecule number
        nodes = list(sub_graph.nodes())
        rep_node = nodes[0]
        mol_num = struct.atom[rep_node].molecule_number
        # Find backbone path
        backbone_path = graph.find_backbone(sub_graph, max_fix=max_fix)
        # Add to the dictionary if path is found, for single atoms there will be
        # no path
        if backbone_path:
            backbone[mol_num] = backbone_path
    return backbone 
[docs]def get_ordered_polymer_backbone(struct, backbone_path, remove_side_chain):
    """
    Backbone of a polymer chain should follow H--TH--TH--T format, where
    backbone starts with head and ends with tail.
    :param `schrodinger.structure.Structure` struct: The structure to find
        backbone atoms in.
    :param list backbone_path: list of atom indexes in the backbone
    :param bool remove_side_chain: If true it will remove the side chain atoms
        from the backbone in the start and end.
    :return list: list of atom indexes in the backbone such that first atom is
        always a head.
    """
    ht_prop_name = [msprops.HEAD_ATOM_PROP, msprops.TAIL_ATOM_PROP]
    # Check if head and tail information is present in backbone atoms
    for atom_id in backbone_path:
        # Check if atom is head or tail
        atom = struct.atom[atom_id]
        is_head, is_tail = [atom.property.get(x) for x in ht_prop_name]
        if is_head or is_tail:
            break
    else:
        # return unchanged list if no head and tail are available
        return backbone_path
    # In case tail is first backbone needs to be reversed
    if is_tail:
        backbone_path.reverse()
    if remove_side_chain:
        # Find index of first head atom and last tail atom in the backbone
        ht_index = {}
        found_prop, not_found_prop = [
            ht_prop_name[0] if x else ht_prop_name[1]
            for x in [is_head, is_tail]
        ]
        ht_index[found_prop] = backbone_path.index(atom_id)
        for index, atom_id in enumerate(backbone_path):
            if struct.atom[atom_id].property.get(not_found_prop):
                ht_index[not_found_prop] = index
                if is_tail:
                    # Break only if finding first head
                    break
        # Remove initial and final side chain atoms by selecting atoms between
        # first head atom and last tail atom in the backbones
        backbone_path = backbone_path[
            ht_index[ht_prop_name[0]]:ht_index[ht_prop_name[1]]]
    return backbone_path 
[docs]def get_ordered_backbone_atoms(struct):
    """
    Return dictionary of backbone atoms for each molecule in the structure.
    :param `schrodinger.structure.Structure` struct: The structure to find
        backbone atoms in.
    :return dict(list): the key of the dictionary is molecule number and the
        value is the list of backbone atoms
    """
    fixed_struct = struct.copy()
    # Get backbone atoms
    backbone = get_backbone_atoms(fixed_struct)
    # Fix order for polymeric systems
    for mol_num, backbone_path in backbone.items():
        backbone[mol_num] = get_ordered_polymer_backbone(
            fixed_struct, backbone_path, False)
    return backbone 
[docs]def radius_of_gyration(struct, molecule_coms=False):
    """
    Calculate radius of gyration for a structure
    :param `structure.Structure` struct: The structure to calculate RG for
    :param bool molecule_coms: Whether to use center of mass of each molecule in
         the structure for calculating Rg
    :rtype: float
    :return: The radius of gyration
    """
    xyzs = struct.getXYZ()
    weights = [atom.atomic_weight for atom in struct.atom]
    center_of_mass = numpy.average(xyzs, weights=weights, axis=0)
    if molecule_coms:
        # Method from https://pubs.acs.org/doi/abs/10.1021/acs.jpcb.0c02856
        mol_coms = []
        for mol in struct.molecule:
            mol_xyzs = [atom.xyz for atom in mol.atom]
            atomic_weights = [atom.atomic_weight for atom in mol.atom]
            mol_coms.append(
                numpy.average(mol_xyzs, weights=atomic_weights, axis=0))
        mol_coms = numpy.array(mol_coms)
        sum_distance_squared = numpy.sum(numpy.square(mol_coms -
                                                      center_of_mass))
        r_g = numpy.sqrt(sum_distance_squared / len(struct.molecule))
    else:
        distance_squared = numpy.sum(numpy.square(xyzs - center_of_mass),
                                     axis=1)
        r_g = numpy.sqrt(
            numpy.sum(weights * distance_squared) / struct.total_weight)
    return r_g 
[docs]def fit_structure_to_ellipsoid(struct):
    """
    Fit the structure to an ellipsoid and return the ellipsoid semi-axes and
    principal moments of inertia
    The method is taken from https://pubs.acs.org/doi/abs/10.1021/jp047138e
    and uses the following equations to calculate the semi-axes values:
    I1 = M/5 * (a**2 + b**2)
    I2 = M/5 * (a**2 + c**2)
    I3 = M/5 * (b**2 + c**2)
    Also see https://en.wikipedia.org/wiki/Ellipsoid#Dynamical_properties
    :param `structure.Structure` struct: The structure to fit to an ellipsoid
    :rtype: tuple of (float, float, float, [float, float, float])
    :return: Tuple of semi-axes values in Angstroms and principal moments of
        inertia in u * A**2
    """
    moments = infrageom.principal_moments(struct).moments
    moments = sorted(moments, reverse=True)
    multiplier = struct.total_weight / 5
    coefficients = numpy.array([[1, 1, 0], [1, 0, 1], [0, 1, 1]]) * multiplier
    a_val, b_val, c_val = numpy.sqrt(numpy.linalg.solve(coefficients, moments))
    return a_val, b_val, c_val, moments