Source code for meeko.atomtyper

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import warnings

import numpy as np

from .utils import pdbutils


[docs] class AtomTyper:
[docs] @classmethod def type_everything( cls, molsetup, atom_params, charge_model, offatom_params=None, dihedral_params=None, ): cls._type_atoms(molsetup, atom_params) # offatoms must be typed after charges, because offsites pull charge if offatom_params is not None: cached_offatoms = cls._cache_offatoms(molsetup, offatom_params) coords = {atom.index: atom.coord for atom in molsetup.atoms if not atom.is_dummy} cls._set_offatoms(molsetup, cached_offatoms, coords) if dihedral_params not in (None, "espaloma"): cls._type_dihedrals(molsetup, dihedral_params) return
@staticmethod def _type_atoms(molsetup, atom_params): # ensure every "atompar" is defined in a single "smartsgroup" ensure = {} # go over all "smartsgroup"s for smartsgroup in atom_params: if smartsgroup == "comment": continue for line in atom_params[ smartsgroup ]: # line is a dict, e.g. {"smarts": "[#1][#7,#8,#9,#15,#16]","atype": "HD"} smarts = str(line["smarts"]) # get indices of the atoms in the smarts to which the parameters will be assigned idxs = [ 0 ] # by default, the first atom in the smarts gets parameterized if "IDX" in line: idxs = [i for i in line["IDX"]] # match SMARTS hits = molsetup.find_pattern(smarts) for atompar in line: if atompar in ["smarts", "comment", "IDX"]: continue if atompar not in molsetup.atom_params: molsetup.atom_params[atompar] = [None] * len(molsetup.atoms) value = line[atompar] # keep track of every "smartsgroup" that modified "atompar" ensure.setdefault(atompar, []) ensure[atompar].append(smartsgroup) # Each "hit" is a tuple of atom indices that matched the smarts # The length of each "hit" is the number of atoms in the smarts for hit in hits: # Multiple atoms may be targeted by a single smarts: # For example: both oxygens in NO2 are parameterized by a single smarts pattern. # "idxs" are 1-indices of atoms in the smarts to which parameters are to be assigned. for idx in idxs: if atompar == "atype": molsetup.set_atom_type( hit[idx], value ) # overrides previous calls molsetup.atom_params[atompar][hit[idx]] = value # guarantee that each atompar is exclusive of a single group for atompar in ensure: if len(set(ensure[atompar])) > 1: msg = "%s is modified in multiple smartsgroups: %s" % ( atompar, set(ensure[atompar]), ) warnings.warn(msg) return @staticmethod def _cache_offatoms(molsetup, offatom_params): """precalculate off-site atoms""" cached_offatoms = {} n_offatoms = 0 atoms_with_offchrg = set() # each parent atom can only be matched once in each smartsgroup for smartsgroup in offatom_params: if smartsgroup == "comment": continue tmp = {} for line in offatom_params[smartsgroup]: # SMARTS smarts = str(line["smarts"]) hits = molsetup.find_pattern(smarts) # atom indexes in smarts string smarts_idxs = [0] if "IDX" in line: smarts_idxs = [i for i in line["IDX"]] for smarts_idx in smarts_idxs: for hit in hits: parent_idx = hit[smarts_idx] tmp.setdefault( parent_idx, [] ) # TODO tmp[parent_idx] = [], yeah? for offatom in line["OFFATOMS"]: # set defaults tmp[parent_idx].append( { "offatom": { "distance": 1.0, "x90": False, "phi": 0.0, "theta": 0.0, "z": [], "x": [], }, "atom_params": {}, } ) for key in offatom: if key in ["distance", "x90"]: tmp[parent_idx][-1]["offatom"][key] = offatom[key] # replace SMARTS indexes by the atomic index elif key in ["z", "x"]: for i in offatom[key]: idx = hit[i] tmp[parent_idx][-1]["offatom"][key].append(idx) # convert degrees to radians elif key in ["theta", "phi"]: tmp[parent_idx][-1]["offatom"][key] = np.radians( offatom[key] ) # ignore comments elif key in ["comment"]: pass elif key == "atype": tmp[parent_idx][-1]["atom_params"][key] = offatom[ key ] elif key == "pull_charge_fraction": if parent_idx in atoms_with_offchrg: raise RuntimeError( "atom %d has charge pulled more than once" % parent_idx ) atoms_with_offchrg.add(parent_idx) tmp[parent_idx][-1]["atom_params"][key] = offatom[ key ] else: pass for parent_idx in tmp: for offatom_dict in tmp[parent_idx]: atom_params = offatom_dict["atom_params"] offatom = offatom_dict["offatom"] atomgeom = AtomicGeometry( parent_idx, neigh=offatom["z"], xneigh=offatom["x"], x90=offatom["x90"], ) if "pull_charge_fraction" in atom_params: pull_charge_fraction = atom_params["pull_charge_fraction"] else: pull_charge_fraction = 0.0 args = ( atom_params["atype"], offatom["distance"], offatom["theta"], offatom["phi"], pull_charge_fraction, ) # number of coordinates (before adding new offatom) cached_offatoms[n_offatoms] = (atomgeom, args) n_offatoms += 1 return cached_offatoms @staticmethod def _set_offatoms(molsetup, cached_offatoms, coords): """add cached offatoms""" for k, (atomgeom, args) in cached_offatoms.items(): (atom_type, dist, theta, phi, pull_charge_fraction) = args offatom_coords = atomgeom.calc_point(dist, theta, phi, coords) tmp = molsetup.get_pdbinfo(atomgeom.parent + 1) pdbinfo = pdbutils.PDBAtomInfo( "G", tmp.resName, tmp.resNum, tmp.icode, tmp.chain ) q_parent = (1 - pull_charge_fraction) * molsetup.get_charge(atomgeom.parent) q_offsite = pull_charge_fraction * molsetup.get_charge(atomgeom.parent) pseudo_atom = { "coord": offatom_coords, "anchor_list": [atomgeom.parent], "charge": q_offsite, "pdbinfo": pdbinfo, "atom_type": atom_type, "rotatable": False, } molsetup.atoms[atomgeom.parent].charge = q_parent molsetup.add_pseudoatom(**pseudo_atom) return @staticmethod def _type_dihedrals(molsetup, dihedral_params): dihedrals = {} for line in dihedral_params: smarts = str(line["smarts"]) hits = molsetup.find_pattern(smarts) if len(hits) == 0: continue # non-rotatable bonds get dihedrals idxs = [i for i in line["IDX"]] tid = line["id"] if "id" in line else None fourier_series = [] term_indices = {} for key in line: for keyword in ["phase", "k", "periodicity", "idivf"]: if key.startswith(keyword): t = int(key.replace(keyword, "")) # e.g. "phase2" -> int(2) if t not in term_indices: term_indices[t] = len(fourier_series) fourier_series.append({}) index = term_indices[t] fourier_series[index][keyword] = line[key] break for index in range(len(fourier_series)): if "idivf" in fourier_series[index]: idivf = fourier_series[index].pop("idivf") fourier_series[index]["k"] /= idivf dihedral_index = molsetup.add_dihedral_interaction(fourier_series) for hit in hits: atom_idxs = tuple([hit[j] for j in idxs]) molsetup.dihedral_partaking_atoms[atom_idxs] = dihedral_index molsetup.dihedral_labels[atom_idxs] = tid
class AtomicGeometry: """generate reference frames and add extra sites""" def __init__(self, parent, neigh, xneigh=[], x90=False, planar_tol=0.1): """arguments are indices of atoms""" self.planar_tol = planar_tol # angstroms, length of neighbor vecs for z-axis # real atom hosting extra sites if type(parent) != int: raise RuntimeError("parent must be int") self.parent = parent # list of bonded atoms (used to define z-axis) self.neigh = [] for i in neigh: if type(i) != int: raise RuntimeError("neigh indices must be int") self.neigh.append(i) # list of atoms that self.xneigh = [] for i in xneigh: if type(i) != int: raise RuntimeError("xneigh indices must be int") self.xneigh.append(i) self.calc_x = len(self.xneigh) > 0 self.x90 = x90 # y axis becomes x axis (useful to rotate in-plane by 90 deg) def calc_point(self, distance, theta, phi, coords): """return coordinates of point specified in spherical coordinates""" z = self._calc_z(coords) # return pt aligned with z-axis if phi == 0.0: return z * distance + np.array(coords[self.parent]) # need x-vec if phi != 0 elif self.calc_x == False: raise RuntimeError("phi must be zero if X undefined") else: x = self._calc_x(coords) if self.x90: x = np.cross(self.z, x) y = np.cross(z, x) pt = z * distance pt = self.rot3D(pt, y, phi) pt = self.rot3D(pt, z, theta) pt += np.array(coords[self.parent]) return pt def _calc_z(self, coords): """maximize distance from neigh""" z = np.zeros(3) cumsum = np.zeros(3) for i in self.neigh: v = np.array(coords[self.parent]) - np.array(coords[i]) cumsum += v z += self.normalized(v) z = self.normalized(z) if np.sum(cumsum**2) < self.planar_tol**2: raise RuntimeError("Refusing to place Z axis on planar atom") return z def _calc_x(self, coords): x = np.zeros(3) for i in self.xneigh: v = np.array(coords[self.parent]) - np.array(coords[i]) x += self.normalized(v) x = self.normalized(x) return x @staticmethod def rot3D(pt, ax, rad): """ Rotate point: pt = (x,y,z) coordinates to be rotated ax = vector around wich rotation is performed rad = rotate by "rad" radians """ # If axis has len=0, rotate by 0.0 rad on any axis # Make sure ax has unitary length len_ax = (ax[0] ** 2 + ax[1] ** 2 + ax[2] ** 2) ** 0.5 if len_ax == 0.0: u, v, w = (1, 0, 0) rad = 0.0 else: u, v, w = [i / len_ax for i in ax] x, y, z = pt ux, uy, uz = u * x, u * y, u * z vx, vy, vz = v * x, v * y, v * z wx, wy, wz = w * x, w * y, w * z sa = np.sin(rad) ca = np.cos(rad) p0 = ( u * (ux + vy + wz) + (x * (v * v + w * w) - u * (vy + wz)) * ca + (-wy + vz) * sa ) p1 = ( v * (ux + vy + wz) + (y * (u * u + w * w) - v * (ux + wz)) * ca + (wx - uz) * sa ) p2 = ( w * (ux + vy + wz) + (z * (u * u + v * v) - w * (ux + vy)) * ca + (-vx + uy) * sa ) return (p0, p1, p2) def normalized(self, vec): l = sum([x**2 for x in vec]) ** 0.5 if type(vec) == list: return [x / l for x in vec] else: # should be np.array return vec / l