Source code for pdb2pqr.hydrogens.optimize

"""Hydrogen optimization routines.

.. codeauthor:: Todd Dolinsky
.. codeauthor:: Jens Erik Nielsen
.. codeauthor:: Yong Huang
.. codeauthor:: Nathan Baker
"""
import logging
import math
from .. import quatfit as quat
from .. import utilities as util
from ..config import ANGLE_CUTOFF, DIST_CUTOFF


_LOGGER = logging.getLogger(__name__)


[docs] class Optimize: """The holder class for the hydrogen optimization routines. Individual optimization types inherit off of this class. Any functions used by multiple types appear here. """
[docs] def __init__(self): self.residue = None self.optinstance = None self.routines = None
def __str__(self): return f"{self.residue} ({self.optinstance.opttype})"
[docs] @staticmethod def get_hbond_angle(atom1, atom2, atom3): """Get the angle between three atoms :param atom1: the first atom :type atom1: Atom :param atom2: the second (vertex) atom :type atom2: Atom :param atom3: the third atom :type atom3: Atom :return: the angle between the atoms in degrees :rtype: float """ atom_coords = atom2.coords coords1 = util.subtract(atom3.coords, atom_coords) coords2 = util.subtract(atom1.coords, atom_coords) norm1 = util.normalize(coords1) norm2 = util.normalize(coords2) dotted = util.dot(norm1, norm2) if dotted > 1.0: # If normalized, this is due to rounding error dotted = 1.0 elif dotted < -1.0: # If normalized, this is due to rounding error dotted = -1.0 rad = abs(math.acos(dotted)) angle = rad * 180.0 / math.pi if angle > 180.0: angle = 360.0 - angle return angle
[docs] def is_hbond(self, donor, acc): """Determine whether this donor acceptor pair is a hydrogen bond. .. todo:: Remove hard-coded hydrogen bond distance and angles. :param donor: donor atom :param acc: acceptor atom :return: whether this pair is a hydrogen bond :rtype: bool """ for donorhatom in donor.bonds: if not donorhatom.is_hydrogen: continue # Check the H(D)-A distance dist = util.distance(donorhatom.coords, acc.coords) if dist > DIST_CUTOFF: continue # Ensure no conflicts if H(A)s if present flag = True for acchatom in acc.bonds: if not acchatom.is_hydrogen: continue flag = False # Check the H(D)-H(A) distance hdist = util.distance(donorhatom.coords, acchatom.coords) if hdist < 1.5: continue # Check the H(D)-H(A)-A angle angle = self.get_hbond_angle(donorhatom, acchatom, acc) if angle < 110.0: flag = True if not flag: continue # Check the A-D-H(D) angle angle = self.get_hbond_angle(acc, donor, donorhatom) if angle <= ANGLE_CUTOFF: _LOGGER.debug(f"Found HBOND! {dist:.4f} {angle:.4f}") return True # If we get here, no bond is formed return False
[docs] @staticmethod def get_pair_energy(donor, acceptor): """Get the energy between two atoms .. todo:: Lots of code in this function could be accelerated with :mod:`numpy`. .. todo:: Lots of hard-coded parameters in this function that need to be abstracted out. :param donor: the first atom in the pair :type donor: Atom :param acceptor: the second atom in the pair :type acceptor: Atom :return: the energy of the pair :rtype: float """ # Initialize some variables bump_energy = 10.0 bump_distance = 1.5 max_hbond_energy = -10.0 max_ele_energy = -1.0 adh_angle_cutoff = ANGLE_CUTOFF dhaha_angle_cutoff = 110.0 max_dha_dist = DIST_CUTOFF max_ele_dist = 5.0 energy = 0.0 if not (donor.hdonor and acceptor.hacceptor): return energy # See if hydrogens are presently bonded to the acceptor and donor donorhs = (bond for bond in donor.bonds if bond.is_hydrogen) acceptorhs = [bond for bond in acceptor.bonds if bond.is_hydrogen] for donorhatom in donorhs: dist = util.distance(donorhatom.coords, acceptor.coords) if dist > max_dha_dist and dist < max_ele_dist: energy += max_ele_energy / (dist * dist) continue # Case 1: Both donor and acceptor hydrogens are present for acceptorhatom in acceptorhs: # Penalize if H(D) is too close to H(A) hdist = util.distance(donorhatom.coords, acceptorhatom.coords) if hdist < bump_distance: energy += bump_energy continue # Assign energies based on angles angle1 = Optimize.get_hbond_angle(acceptor, donor, donorhatom) if angle1 <= adh_angle_cutoff: angle2 = Optimize.get_hbond_angle( donorhatom, acceptorhatom, acceptor ) if angle2 < dhaha_angle_cutoff: angle2 = 1.0 else: angle2 = ( dhaha_angle_cutoff - angle2 ) / dhaha_angle_cutoff angleterm = (adh_angle_cutoff - angle1) / adh_angle_cutoff energy += ( max_hbond_energy / pow(dist, 3) * angleterm * angle2 ) # Case 2: Only donor hydrogens are present if not acceptorhs: # Assign energies based on A-D-H(D) angle alone angle1 = Optimize.get_hbond_angle(acceptor, donor, donorhatom) if angle1 <= adh_angle_cutoff: angleterm = (adh_angle_cutoff - angle1) / adh_angle_cutoff energy += max_hbond_energy / pow(dist, 2) * angleterm return energy
[docs] def make_atom_with_no_bonds(self, atom, closeatom, addname): """Create an atom with no bonds. Called for water oxygen atoms with no current bonds. Uses the closeatom to place the new atom directly colinear with the atom and the closeatom. :param atom: the oxygen atom of the water :type atom: Atom :param closeatom: the nearby atom (donor/acceptor) :type closeatom: Atom :param addname: the name of the atom to add :type addname: str """ residue = atom.residue # Place along line, 1 A away vec = util.subtract(closeatom.coords, atom.coords) dist = util.distance(atom.coords, closeatom.coords) newcoords = [vec[i] / dist + atom.coords[i] for i in range(3)] residue.create_atom(addname, newcoords) newatom = residue.get_atom(addname) self.routines.cells.add_cell(newatom) # Set the bonds (since not in reference structure) if newatom not in atom.bonds: atom.bonds.append(newatom) if atom not in newatom.bonds: newatom.bonds.append(atom)
[docs] @classmethod def make_water_with_one_bond(cls, atom, addname): """Add an atom to a water residue that already has one bond. Uses the water reference structure to align the new atom. .. todo:: Does this need to be a classmethod or could it be a staticmethod? :param atom: existing atom :type atom: Atom :param addname: name of atom to add :type addname: str """ residue = atom.residue nextatom = atom.bonds[0] coords = [atom.coords, nextatom.coords] refcoords = [ residue.reference.map[atom.name].coords, residue.reference.map["H1"].coords, ] refatomcoords = residue.reference.map["H2"].coords # Make the atom newcoords = quat.find_coordinates(2, coords, refcoords, refatomcoords) residue.create_atom(addname, newcoords) # Set the bonds (since not in reference structure) newatom = residue.get_atom(addname) if newatom not in atom.bonds: atom.bonds.append(newatom) if atom not in newatom.bonds: newatom.bonds.append(atom)
[docs] @classmethod def make_atom_with_one_bond_h(cls, atom, addname): """Add a hydrogen to an alcoholic donor with one existing bond. .. todo:: Does this need to be a classmethod or could it be a staticmethod? :param atom: existing atom :type atom: Atom :param addname: name of atom to add :type addname: str """ residue = atom.residue nextatom = atom.bonds[0] coords = [atom.coords, nextatom.coords] refcoords = [ residue.reference.map[atom.name].coords, residue.reference.map[nextatom.name].coords, ] refatomcoords = residue.reference.map[addname].coords # Make the atom newcoords = quat.find_coordinates(2, coords, refcoords, refatomcoords) residue.create_atom(addname, newcoords)
[docs] @classmethod def make_atom_with_one_bond_lp(cls, atom, addname): """Add a lone pair to an alcoholic donor with one existing bond. .. todo:: Does this need to be a classmethod or could it be a staticmethod? :param atom: existing atom :type atom: Atom :param addname: name of atom to add :type addname: str """ # Initialize some variables residue = atom.residue the_refname = "" for refname in atom.reference.bonds: if refname.startswith("H"): the_refname = refname break nextatom = atom.bonds[0] coords = [atom.coords, nextatom.coords] refcoords = [ residue.reference.map[atom.name].coords, residue.reference.map[nextatom.name].coords, ] refatomcoords = residue.reference.map[the_refname].coords # Make the atom newcoords = quat.find_coordinates(2, coords, refcoords, refatomcoords) residue.create_atom(addname, newcoords) # Set the bonds (since not in reference structure) newatom = residue.get_atom(addname) if newatom not in atom.bonds: atom.bonds.append(newatom) if atom not in newatom.bonds: newatom.bonds.append(atom)
[docs] def try_single_alcoholic_h(self, donor, acc, newatom): """Attempt to add an atom to make a hydrogen bond. .. todo:: Remove some hard-coded values in this function. After a new bond has been added using :func:`makeAtomWithOneBond`, try to find the best orientation by rotating to form a hydrogen bond. If a bond cannot be formed, remove the :makevar:`newatom` (thereby returning to a single bond). :param donor: hydrogen bond donor :type donor: Atom :param acc: hydrogen bond acceptor :type acc: Atom :param newatom: new atom to add :type newatom: Atom :return: indication whether atom was added :rtype: bool """ # Initialize some variables besten = 999.99 bestcoords = [] residue = donor.residue pivot = donor.bonds[0] for _ in range(72): residue.rotate_tetrahedral(pivot, donor, 5.0) if self.is_hbond(donor, acc): energy = self.get_pair_energy(donor, acc) if energy < besten: bestcoords = newatom.coords besten = energy # If a hydrogen bond was made, set at best coordinates if bestcoords != []: newatom.x = bestcoords[0] newatom.y = bestcoords[1] newatom.z = bestcoords[2] self.routines.cells.add_cell(newatom) return True residue.remove_atom(newatom.name) return False
[docs] def try_single_alcoholic_lp(self, acc, donor, newatom): """Attempt to add an atom to make a hydrogen bond. .. todo:: Remove some hard-coded values in this function and figure out where others (e.g., "72") come from. After a new bond has been added using :func:`makeAtomWithOneBond`, ensure that a hydrogen bond has been made. If so, try to minimze the H(D)-A-LP angle. If that cannot be minimized, ignore the bond and remove the atom. :param donor: hydrogen bond donor :type donor: Atom :param acc: hydrogen bond acceptor :type acc: Atom :param newatom: new atom to add :type newatom: Atom :return: indication whether atom was added :rtype: bool """ # Initialize some variables residue = acc.residue pivot = acc.bonds[0] bestangle = 180.00 bestcoords = [] the_donorhatom = "" # If a hydrogen bond was made, set at best distance if not self.is_hbond(donor, acc): residue.remove_atom(newatom.name) return False # Grab the H(D) that caused the bond for donorhatom in donor.bonds: if ( donorhatom.is_hydrogen and self.get_hbond_angle(acc, donor, donorhatom) < ANGLE_CUTOFF ): the_donorhatom = donorhatom break for _ in range(72): residue.rotate_tetrahedral(pivot, acc, 5.0) angle = abs(self.get_hbond_angle(the_donorhatom, acc, newatom)) if angle < bestangle: bestangle = angle bestcoords = newatom.coords # Remove if geometry does not work if bestangle > (ANGLE_CUTOFF * 2.0): _LOGGER.debug( f"Removing due to geometry {bestangle:.2f} > " f"{ANGLE_CUTOFF * 2.0:.2f}" ) residue.remove_atom(newatom.name) return False # Otherwise set to best coordinates newatom.x = bestcoords[0] newatom.y = bestcoords[1] newatom.z = bestcoords[2] self.routines.cells.add_cell(newatom) return True
[docs] @classmethod def get_positions_with_two_bonds(cls, atom): """Return possible coordinates for new bonds. .. todo:: Remove some hard-coded values in this function. Given a tetrahedral geometry with two existing bonds, return the two potential sets of coordinates that are possible for a new bond. :param atom: atom to test for bonds :type atom: Atom :return: 2-tuple of coordinates (floats) :rtype: tuple """ # Initialize some variables residue = atom.residue fixed = atom.bonds[0] rotate = atom.bonds[1] # Rotate by 120 degrees twice residue.rotate_tetrahedral(fixed, atom, 120) loc1 = rotate.coords residue.rotate_tetrahedral(fixed, atom, 120) loc2 = rotate.coords # Set rotate back to original by one more rotation residue.rotate_tetrahedral(fixed, atom, 120) return loc1, loc2
[docs] def try_positions_with_two_bonds_h(self, donor, acc, newname, loc1, loc2): """Try adding a new hydrogen to the two potential locations. If both form hydrogen bonds, place at whatever returns the best bond as determined by get_pair_energy. :param donor: donor atom :type donor: Atom :param acc: acceptor atom :type acc: Atom :param newname: new atom name :type newname: str :param loc1: first coordinate to try :type loc1: (float, float, float) :param loc2: first coordinate to try :type loc2: (float, float, float) :return: indication whether atom was added :rtype: bool """ # Initialize some variables besten = 999.99 bestcoords = [] residue = donor.residue # Try the first position residue.create_atom(newname, loc1) if self.is_hbond(donor, acc): besten = self.get_pair_energy(donor, acc) bestcoords = loc1 # Try the second newatom = residue.get_atom(newname) newatom.x = loc2[0] newatom.y = loc2[1] newatom.z = loc2[2] if self.is_hbond(donor, acc): energy = self.get_pair_energy(donor, acc) if energy < besten: bestcoords = loc2 # Set at best coords if bestcoords != []: newatom.x = bestcoords[0] newatom.y = bestcoords[1] newatom.z = bestcoords[2] self.routines.cells.add_cell(newatom) return 1 residue.remove_atom(newname) return 0
[docs] def try_positions_with_two_bonds_lp(self, acc, donor, newname, loc1, loc2): """Attempt to place a :abbr:`LP (lone pair)` on an atom. Try placing an LP on a tetrahedral geometry with two existing bonds. If this isn't a hydrogen bond it can return - otherwise ensure that the H(D)-A-LP angle is minimized. :param acc: hydrogen bond acceptor :type acc: Atom :param donor: hydrogen bond donor :type donor: Atom :param newname: name for lone pair :type newname: str :param loc1: first location to try :type loc1: (float, float, float) :param loc2: second location to try :type loc2: (float, float, float) :return: indication of whether LP was added :rtype: bool """ # Initialize some variables bestangle = 180.00 bestcoords = [] residue = acc.residue the_donorhatom = "" # If the donor/acceptor pair is not an hbond return if not self.is_hbond(donor, acc): return False # Grab the H(D) that caused the bond for donorhatom in donor.bonds: if ( donorhatom.is_hydrogen and self.get_hbond_angle(acc, donor, donorhatom) < ANGLE_CUTOFF ): the_donorhatom = donorhatom break # Try the first position residue.create_atom(newname, loc1) newatom = residue.get_atom(newname) angle = abs(self.get_hbond_angle(the_donorhatom, acc, newatom)) if angle < bestangle: bestangle = angle bestcoords = loc1 # Try the second newatom.x = loc2[0] newatom.y = loc2[1] newatom.z = loc2[2] angle = self.get_hbond_angle(the_donorhatom, acc, newatom) if angle < bestangle: bestcoords = loc2 # Remove if geometry does not work if bestangle > (ANGLE_CUTOFF * 2.0): residue.remove_atom(newname) return False # Otherwise set at best coords newatom.x = bestcoords[0] newatom.y = bestcoords[1] newatom.z = bestcoords[2] self.routines.cells.add_cell(newatom) # Set the bonds (since not in reference structure) if newatom not in acc.bonds: acc.bonds.append(newatom) if acc not in newatom.bonds: newatom.bonds.append(acc) return True
[docs] @classmethod def get_position_with_three_bonds(cls, atom): """Find position for last bond in tetrahedral geometry. .. todo:: Should this be a staticmethod rather than a classmethod? .. todo:: Remove hard-coded values in function. If there's three bonds in a tetrahedral geometry, there's only one available position. Find that position. :param atom: atom to check :type atom: Atom :return: coordinates :rtype: (float, float, float) """ # Initialize some variables residue = atom.residue pivot = atom.bonds[0] rot1 = atom.bonds[1] rot2 = atom.bonds[2] # Find the two new positions residue.rotate_tetrahedral(pivot, atom, 120) newcoords1 = rot1.coords residue.rotate_tetrahedral(pivot, atom, 120) newcoords2 = rot1.coords residue.rotate_tetrahedral(pivot, atom, 120) # Determine which is unoccupied if util.distance(rot2.coords, newcoords1) > 0.1: return newcoords1 return newcoords2
[docs] def try_positions_three_bonds_h(self, donor, acc, newname, loc): """Try making a hydrogen bond with the lone available position. :param donor: hydrogen bond donor :type donor: Atom :param acc: hydrogen bond acceptor :type acc: Atom :param newname: name for new atom :type newname: str :return: indication of whether atom was added :rtype: bool """ residue = donor.residue residue.create_atom(newname, loc) if self.is_hbond(donor, acc): newatom = residue.get_atom(newname) self.routines.cells.add_cell(newatom) return True residue.remove_atom(newname) return False
[docs] def try_positions_three_bonds_lp(self, acc, donor, newname, loc): """Make a hydrogen bond in the only position possible. Try making a hydrogen bond using the lone available hydrogen position. :param acc: hydrogen bond acceptor :type acc: Atom :param donor: hydrogen bond donor :type donor: Atom :param newname: new atom name :type newname: str :param loc: location for atom (coordinates) :type loc: (float, float, float) :return: indication whether atom was added :rtype: bool """ residue = acc.residue if not self.is_hbond(donor, acc): return False # Grab the H(D) that caused the bond for donorhatom in donor.bonds: if ( donorhatom.is_hydrogen and self.get_hbond_angle(acc, donor, donorhatom) < ANGLE_CUTOFF ): the_donorhatom = donorhatom break residue.create_atom(newname, loc) newatom = residue.get_atom(newname) # Remove if geometry does not work # WARNING: Nathan? What if the the_donorhatom is not set? angle = abs(self.get_hbond_angle(the_donorhatom, acc, newatom)) if angle > (ANGLE_CUTOFF * 2.0): residue.remove_atom(newname) return False # Otherwise keep it newatom = residue.get_atom(newname) self.routines.cells.add_cell(newatom) # Set the bonds (since not in reference structure) if newatom not in acc.bonds: acc.bonds.append(newatom) if acc not in newatom.bonds: newatom.bonds.append(acc) return True
[docs] class OptimizationHolder: """A holder class for the XML parser."""
[docs] def __init__(self): self.name = "" self.map = {} self.opttype = "" self.optangle = ""
def __str__(self): text = f"{self.name}\n" text += f"Type: {self.opttype}\n" if self.optangle != "": text += f"Optimization Angle: {self.optangle}\n" text += "Atoms: \n" for atomname in self.map: text += f"\t{self.map[atomname]}\n" return text