Source code for meeko.hydrate

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

import json
import numpy as np
from rdkit import Chem
from rdkit.Geometry import Point3D
import warnings

from .atomtyper import AtomicGeometry
from .molsetup import RDKitMoleculeSetup
from .utils import geomutils
from .utils import pdbutils


class Waters:

    @staticmethod
    def make_molsetup_OPC() -> RDKitMoleculeSetup:
        """
        Builds an RDKitMoleculeSetup for an OPC water.

        Returns
        -------
        molsetup: RDKitMoleculeSetup
            An RDKitMoleculeSetup for an OPC water.
        """

        raise NotImplementedError
        rdmol = Chem.MolFromSmiles("O")
        conformer = Chem.Conformer(rdmol.GetNumAtoms())
        conformer.SetAtomPosition(0, Point3D(0, 0, 0))
        rdmol.AddConformer(conformer)
        molsetup = RDKitMoleculeSetup.from_mol(rdmol)
        return molsetup

    @staticmethod
    def make_molsetup_TIP3P_AA() -> RDKitMoleculeSetup:
        """
        Builds an RDKitMoleculeSetup for an all atom (AA) TIP3P water, i.e. explicit Hs

        Returns
        -------
        molsetup: RDKitMoleculeSetup
            An RDKitMoleculeSetup for an all atom (AA) TIP3P water.
        """

        rdmol = Chem.MolFromSmiles("O")
        rdmol = Chem.AddHs(rdmol)
        conformer = Chem.Conformer(rdmol.GetNumAtoms())
        dist_oh = 0.9572
        ang_hoh = np.radians(104.52)
        conformer.SetAtomPosition(0, Point3D(0, 0, 0))
        conformer.SetAtomPosition(1, Point3D(dist_oh, 0, 0))
        conformer.SetAtomPosition(
            2, Point3D(np.cos(ang_hoh) * dist_oh, np.sin(ang_hoh) * dist_oh, 0)
        )
        rdmol.AddConformer(conformer)
        molsetup = RDKitMoleculeSetup.from_mol(rdmol)
        molsetup.add_bond(0, 1, rotatable=False)
        molsetup.add_bond(0, 2, rotatable=False)
        molsetup.atoms[0].atom_type = "n-tip3p-O"
        molsetup.atoms[1].atom_type = "n-tip3p-H"
        molsetup.atoms[2].atom_type = "n-tip3p-H"
        molsetup.atoms[0].charge = -0.834
        molsetup.atoms[1].charge = 0.417
        molsetup.atoms[2].charge = 0.417
        return molsetup


[docs] class Hydrate: defaults = [ { "smarts": "[#7X2;v3;!+](=,:[*])[*]", "IDX": 1, "z": [2, 3], "is_donor": False, "geometries": [ {"phi": 0.0, "distance": 3.0}, ], }, { "smarts": "[#1][#7,#8,#9]", "IDX": 1, "z": [2], "is_donor": True, "geometries": [ {"phi": 0.0, "distance": 2.0}, ], }, { "smarts": "[#8X1]=[X3][*]", "IDX": 1, "z": [2], "x": [3], "is_donor": False, "geometries": [ {"phi": 60.0, "theta": 0, "distance": 3.0}, {"phi": 60.0, "theta": 180, "distance": 3.0}, ], }, ] def __init__(self, water_model="tip3p", planar_tol=0.05): self.rule_list = json.loads(json.dumps(self.defaults)) self.planar_tol = planar_tol if water_model == "tip3p": self.make_water = Waters.make_molsetup_TIP3P_AA else: raise RuntimeError("unknown water model: %s" % water_model) def __call__(self, molsetup: RDKitMoleculeSetup): coordinates = [molsetup.get_coord(i) for i in range(molsetup.true_atom_count)] water_molsetup_list = [] for rule in self.rule_list: hits = molsetup.find_pattern(rule["smarts"]) if len(hits) == 0: continue matched_idxs = set() smarts_idx = rule.get("IDX", 1) - 1 # default to first atom in SMARTS for hit in hits: parent_index = hit[smarts_idx] parent_center = coordinates[parent_index] if parent_index in matched_idxs: warnings.warn( "SMARTS <%s> matches same target atom more than once, ignoring" % rule["smarts"] ) continue matched_idxs.add(parent_index) # required settings z = [hit[i - 1] for i in rule["z"]] is_donor = rule["is_donor"] # optional settings x = rule.get("x", []) x = [hit[i - 1] for i in x] x90 = rule.get("x90", False) atomgeom = AtomicGeometry(parent_index, z, x, x90, self.planar_tol) for geometry in rule["geometries"]: # required values distance = geometry["distance"] phi = np.radians(geometry["phi"]) # optional values theta = geometry.get("theta", 0) theta = np.radians(theta) # place water water_center = atomgeom.calc_point( distance, theta, phi, coordinates ) watersetup = self.make_water() watercoords = watersetup.coord self.orient_water( watercoords, water_center, parent_center, is_donor ) for i in range(len(watercoords)): watersetup.coord[i] = watercoords[i] water_molsetup_list.append(watersetup) return water_molsetup_list
[docs] @staticmethod def orient_water(coords, target_xyz, anchor_xyz, anchor_is_donor): """ Coordinates will be changed in place target_xyz is where the water oxygen will be anchor_xyz is the atom to which this molecule belongs anchor_is_donor is True for H, and False for O, N, S expects starting O to be at (0, 0, 0) and an H along x-axis Parameters ---------- coords target_xyz anchor_xyz anchor_is_donor Returns ------- """ if anchor_is_donor: ang_hoh = np.radians(104.52) ang_lp = np.radians(109.5) # probably good enough for lone pairs x = np.cos(np.pi - ang_hoh / 2) * np.cos(ang_lp / 2) y = -np.sin(np.pi - ang_hoh / 2) * np.cos(ang_lp / 2) z = np.sin(ang_lp / 2) axis = (x, y, z) else: axis = (1, 0, 0) # rotate v = np.array(anchor_xyz - target_xyz) v /= np.sqrt(np.dot(v, v)) rotaxis = np.cross(axis, v) magnitude = np.sqrt(np.dot(rotaxis, rotaxis)) if magnitude > 1e-6: # rotangle = np.arccos(np.dot(axis, v)) rotangle = np.arcsin(magnitude) for i in range(len(coords)): coords_tuple = AtomicGeometry.rot3D( coords[i], rotaxis, rotangle ) # normalizes rotaxis coords[i] = list(coords_tuple) # translate for i in range(len(coords)): for j in range(3): coords[i][j] = coords[i][j] + target_xyz[j]
class HydrateMoleculeLegacy: DEFAULT_HYDRATE_DISTANCE = 3.0 DEFAULT_HYDRATE_CHARGE = 0.0 DEFAULT_ATOM_TYPE = "W" DEFAULT_HB_LENGTH = 3.0 def __init__( self, distance: float = DEFAULT_HYDRATE_DISTANCE, charge: float = DEFAULT_HYDRATE_CHARGE, atom_type: str = DEFAULT_ATOM_TYPE, ): """ Initialize the legacy hydrate typer for AutoDock 4.2.x Parameters ---------- distance: float Distance between water molecules and ligand heavy atoms. charge: float Partial charge of the water molecule. Not use for the hydrated docking. atom_type: str Atom type of the water molecule. """ self._distance = distance self._charge = charge self._atom_type = atom_type self._bond_type = 1 self._rotatable = False self._hb_config = { "HD": {1: (1, 1)}, # neigh: 1, wat: 1, sp1 "OA": { 1: (2, 2), # neigh: 1, wat: 2, sp2 2: (2, 3), # neigh: 2, wat: 2, sp3 }, "SA": { 1: (2, 2), # neigh: 1, wat: 2, sp2 2: (2, 3), # neigh: 2, wat: 2, sp3 }, "NA": { 1: (1, 1), # neigh: 1, wat: 3, sp1 2: (1, 2), # neigh: 2, wat: 1, sp2 3: (1, 3), # neigh: 3, wat: 1, sp3 }, } def _place_sp1_one_water( self, anchor_xyz, neighbor_xyz, hb_length=DEFAULT_HB_LENGTH ): position = anchor_xyz + geomutils.vector(neighbor_xyz, anchor_xyz) position = geomutils.resize_vector(position, hb_length, anchor_xyz) positions = np.array([position]) return positions def _place_sp2_one_water( self, anchor_xyz, neighbor1_xyz, neighbor2_xyz, hb_length=DEFAULT_HB_LENGTH ): position = geomutils.atom_to_move(anchor_xyz, [neighbor1_xyz, neighbor2_xyz]) position = geomutils.resize_vector(position, hb_length, anchor_xyz) positions = np.array([position]) return positions def _place_sp2_two_waters( self, anchor_xyz, neighbor1_xyz, neighbor2_xyz, hb_lengths, angles ): if len(hb_lengths) != 2: raise ValueError() if len(angles) != 2: raise ValueError() positions = [] r = geomutils.rotation_axis( neighbor1_xyz, anchor_xyz, neighbor2_xyz, origin=anchor_xyz ) p = neighbor1_xyz # We rotate p to get each vectors if necessary for hb_length, angle in zip(hb_lengths, angles): vector = p if angle != 0.0: position = geomutils.rotate_point(vector, anchor_xyz, r, angle) position = geomutils.resize_vector(position, hb_length, anchor_xyz) positions.append(position) positions = np.array(positions) return positions def _place_sp3_one_water( self, anchor_xyz, neighbor1_xyz, neighbor2_xyz, neighbor3_xyz, hb_length ): # We have to normalize bonds, otherwise the water molecule is not well placed v1 = anchor_xyz + geomutils.normalize( geomutils.vector(anchor_xyz, neighbor1_xyz) ) v2 = anchor_xyz + geomutils.normalize( geomutils.vector(anchor_xyz, neighbor2_xyz) ) v3 = anchor_xyz + geomutils.normalize( geomutils.vector(anchor_xyz, neighbor3_xyz) ) position = geomutils.atom_to_move(anchor_xyz, [v1, v2, v3]) position = geomutils.resize_vector(position, hb_length, anchor_xyz) positions = np.array([position]) return positions def _place_sp3_two_waters( self, anchor_xyz, neighbor1_xyz, neighbor2_xyz, hb_lengths, angles ): if len(hb_lengths) != 2: raise ValueError() if len(angles) != 2: raise ValueError() positions = [] v1 = anchor_xyz + geomutils.normalize( geomutils.vector(anchor_xyz, neighbor1_xyz) ) v2 = anchor_xyz + geomutils.normalize( geomutils.vector(anchor_xyz, neighbor2_xyz) ) r = anchor_xyz + geomutils.normalize(geomutils.vector(v1, v2)) p = geomutils.atom_to_move(anchor_xyz, [v1, v2]) # We rotate p to get each vectors if necessary for hb_length, angle in zip(hb_lengths, angles): vector = p if angle != 0.0: position = geomutils.rotate_point(vector, anchor_xyz, r, angle) position = geomutils.resize_vector(position, hb_length, anchor_xyz) positions.append(position) positions = np.array(positions) return positions def hydrate(self, setup: RDKitMoleculeSetup): """Add water molecules to the ligand Args: setup: MoleculeSetup object """ water_anchors = [] water_positions = [] # It will be the same distance for all of the water molecules hb_length = self._distance for atom in setup.atoms: neighbors = atom.graph atom_type = atom.atom_type anchor_xyz = atom.coord neighbor1_xyz = setup.get_coord(neighbors[0]) positions = np.array([]) n_wat = None hyb = None if atom_type in self._hb_config: try: n_wat, hyb = self._hb_config[atom_type][len(neighbors)] except KeyError: raise RuntimeError( "Cannot place water molecules on atom %d of type %s with %d neighbors." % (atom.index, atom_type, len(neighbors)) ) water_anchors.append(atom.index) if hyb == 1: if n_wat == 1: # Example: X-HD positions = self._place_sp1_one_water( anchor_xyz, neighbor1_xyz, hb_length - 1.0 ) elif hyb == 2: if n_wat == 1: # Example: X-Nitrogen-X neighbor2_xyz = setup.get_coord(neighbors[1]) positions = self._place_sp2_one_water( anchor_xyz, neighbor1_xyz, neighbor2_xyz, hb_length ) elif n_wat == 2: # Example: C=0 (backbone oxygen) tmp_neighbors = [ x for x in setup.get_neighbors(neighbors[0]) if not x == atom.index ] neighbor2_xyz = setup.get_coord(tmp_neighbors[0]) positions = self._place_sp2_two_waters( anchor_xyz, neighbor1_xyz, neighbor2_xyz, [hb_length, hb_length], [-np.radians(120), np.radians(120)], ) elif n_wat == 3: hyb = 3 elif hyb == 3: if n_wat == 1: # Example: Ammonia neighbor2_xyz = setup.get_coord(neighbors[1]) neighbor3_xyz = setup.get_coord(neighbors[2]) positions = self._place_sp3_one_water( anchor_xyz, neighbor1_xyz, neighbor2_xyz, neighbor3_xyz, hb_length, ) elif n_wat == 2: # Example: O-HD (Oxygen in hydroxyl group) neighbor2_xyz = setup.get_coord(neighbors[1]) positions = self._place_sp3_two_waters( anchor_xyz, neighbor1_xyz, neighbor2_xyz, [hb_length, hb_length], [-np.radians(60), np.radians(60)], ) elif n_wat == 3: positions = np.array([]) if positions.size: water_positions.append(positions) for water_anchor, waters_on_anchor in zip(water_anchors, water_positions): for water_on_anchor in waters_on_anchor: tmp = setup.get_pdbinfo(water_anchor) pdbinfo = pdbutils.PDBAtomInfo( "WAT", tmp.resName, tmp.resNum, tmp.icode, tmp.chain ) setup.add_pseudoatom( coord=water_on_anchor, charge=self._charge, anchor_list=[water_anchor], atom_type=self._atom_type, rotatable=self._rotatable, pdbinfo=pdbinfo, )