Source code for chilife.scoring

from abc import abstractmethod
import numpy as np
from scipy.spatial import cKDTree
from scipy.spatial.distance import cdist
from numba import njit
import MDAnalysis as mda

from .base_classes import Ensemble


from .MolSys import MolSys, MolecularSystemBase


[docs] @njit(cache=True) def get_lj_energy(r, rmin, eps, forgive=1, cap=10, rmax=10): """Return a vector with the energy values for the flat bottom lennard-jones potential from a set of atom pairs with distance ``r``, rmin values of ``rmin`` and epsilon values of ``eps``. Parameters ---------- r : numpy.ndarray Vector of inter-atomic distances between non-bonded atoms of a system. rmin : numpy.ndarray Vector of rmin parameters, in angstroms, corresponding to atoms pairs of ``r``. eps : numpy.ndarray Vector of epsilon parameters corresponding to atom pairs of ``r`` forgive : numpy.ndarray The ``forgive`` factor is a softening term to mitigate rigid body artifacts. It is set to a value between 0 and 1 and modifies the ``rmin`` parameter of all atom pairs in ``r`` to be the fraction, ``forgive`` of the original value. This allows atoms to be closer than otherwise allowed to prevent explosion of the Lennard-jones repulsion in situations that would otherwise be resolved with minor atomic displacements. cap : float Maximum allowed energy factor. Sets a cap on the maximum energy contribution of one atom pair interaction as another mechanism for softening. ``cap`` is sets the actual max value as a multiple of the ``eps`` parameter. i.e. the maximum allowed energy from a single atom pair interaction is the ``eps`` parameter multiplied by ``cap``. rmax : float Maximum distance to consider for potential calculation. Any atom pairs with ``r`` > ``rmax`` will be set to 0. Returns ------- lj_energy: numpy.ndarray Vector of atom pair energies calculated using the modified lj potential function. """ r = np.atleast_2d(r) lj_energy = np.zeros_like(r) rmin_lower = forgive * rmin # Piecewise function for flat lj potential near rmin for j, rotamer in enumerate(r): for i in range(len(rotamer)): if rotamer[i] < rmin_lower[i]: lj = rmin_lower[i] / rotamer[i] lj = lj * lj * lj lj = lj * lj lj_energy[j, i] = np.minimum(eps[i] * (lj**2 - 2 * lj), cap * eps[i]) elif rmin_lower[i] <= rotamer[i] < rmin[i]: lj_energy[j, i] = -eps[i] elif rotamer[i] < rmax: lj = rmin[i] / rotamer[i] lj = lj * lj * lj lj = lj * lj lj_energy[j, i] = eps[i] * (lj**2 - 2 * lj) return lj_energy
[docs] @njit(cache=True) def get_lj_scwrl(r, rmin, eps, forgive=1): """Calculate a scwrl-like lennard-jones potential from a set of atom pairs with distance ``r``, rmin values of ``rmin`` and epsilon values of ``eps``. Parameters ---------- r : numpy.ndarray Vector of inter-atomic distances between non-bonded atoms of a system. rmin : numpy.ndarray Vector of rmin parameters, in angstroms, corresponding to atoms pairs of ``r``. eps : numpy.ndarray Vector of epsilon parameters corresponding to atom pairs of ``r``. forgive : numpy.ndarray The ``forgive`` factor is a softening term to mitigate rigid body artifacts. It is set to a value between 0 and 1 and modifies the ``rmin`` parameter of all atom pairs in ``r`` to be the fraction, ``forgive`` of the original value. This allows atoms to be closer than otherwise allowed to prevent explosion of the Lennard-jones repulsion in situations that would otherwise be resolved with minor atomic displacements. Returns ------- lj_energy: numpy.ndarray Vector of atom pair energies calculated using the modified lj potential function. """ r = np.atleast_2d(r) lj_energy = np.empty_like(r) rmin_lower = rmin * forgive for j, rotamer in enumerate(r): # Piecewise function for flat lj potential near rmin for i in range(len(rotamer)): rat = rotamer[i] / (rmin_lower[i] / 1.12246204831) if rat < 0.8254: lj_energy[j, i] = 10 * eps[i] elif rat <= 1: lj_energy[j, i] = 57.273 * (1 - rat) * eps[i] elif rat < 10 / 9: lj_energy[j, i] = eps[i] * (10 - 9 * rat) ** (57.273 / (9 * eps[i])) - eps[i] elif rat < 4 / 3: lj_energy[j, i] = (eps[i] / 4) * (9 * rat - 10) ** 2 - eps[i] else: lj_energy[j, i] = 0 return lj_energy
[docs] @njit(cache=True) def get_lj_rep(r, rmin, eps, forgive=0.9, cap=10): """Calculate only the repulsive terms of the lennard-jones potential from a set of atom pairs with distance ``r``, rmin values of ``rmin`` and epsilon values of ``eps``. Parameters ---------- r : numpy.ndarray Vector of inter-atomic distances between non-bonded atoms of a system. rmin : numpy.ndarray Vector of ``rmin`` parameters, in angstroms, corresponding to atoms pairs of ``r``. eps : numpy.ndarray Vector of epsilon parameters corresponding to atom pairs of ``r``. forgive : numpy.ndarray The ``forgive`` factor is a softening term to mitigate rigid body artifacts. It is set to a value between 0 and 1 and modifies the ``rmin`` parameter of all atom pairs in ``r`` to be the fraction, ``forgive`` of the original value. This allows atoms to be closer than otherwise allowed to prevent explosion of the Lennard-jones repulsion in situations that would otherwise be resolved with minor atomic displacements. cap : float Maximum allowed energy factor. Sets a cap on the maximum energy contribution of one atom pair interaction as another mechanism for softening. ``cap`` is sets the actual max value as a multiple of the ``eps`` parameter. i.e. the maximum allowed energy from a single atom pair interaction is the ``eps`` parameter multiplied by ``cap``. Returns ------- lj_energy: numpy.ndarray Vector of atom pair energies calculated using the modified lj potential function. """ r = np.atleast_2d(r) lj_energy = np.empty_like(r) rmin_lower = forgive * rmin # Piecewise function for flat lj potential near rmin for i, rotamer in enumerate(r): for j in range(len(rotamer)): lj = rmin_lower[j] / rotamer[j] lj = lj * lj * lj lj = lj * lj lj_energy[i, j] = np.minimum(eps[j] * lj**2, cap * eps[j]) return lj_energy
[docs] @njit(cache=True) def get_lj_attr(r, rmin, eps, forgive=0.9, floor=-2): """Calculate only the attractive terms of the lennard-jones potential from a set of atom pairs with distance ``r``, ``rmin`` values of `rmin` and epsilon values of `eps`. Parameters ---------- r : numpy.ndarray Vector of inter-atomic distances between non-bonded atoms of a system. rmin : numpy.ndarray Vector of rmin parameters, in angstroms, corresponding to atoms pairs of ``r``. eps : numpy.ndarray Vector of epsilon parameters corresponding to atom pairs of ``r`` forgive : numpy.ndarray The `forgive` factor is a softening term to mitigate rigid body artifacts. It is set to a value between 0 and 1 and modifies the ``rmin`` parameter of all atom pairs in ``r`` to be the fraction, ``forgive`` of the original value. This allows atoms to be closer than otherwise allowed to prevent explosion of the Lennard-jones repulsion in situations that would otherwise be resolved with minor atomic displacements. cap : flaat Maximum allowed energy factor. Sets a cap on the maximum energy contribution of one atom pair interaction as another mechanism for softening. ``cap`` is sets the actual max value as a multiple of the ``eps`` parameter. i.e. the maximum allowed energy from a single atom pair interaction is the ``eps`` parameter multiplied by ``cap``. Returns ------- lj_energy: numpy.ndarray Vector of atom pair energies calculated using the modified lj potential function. """ r = np.atleast_2d([r]) lj_energy = np.empty_like(r) rmin_lower = forgive * rmin for j, rotamer in enumerate(r): # Piecewise function for flat lj potential near rmin for i in range(len(rotamer)): lj = rmin_lower[i] / rotamer[i] lj = lj * lj * lj lj = lj * lj lj_energy[j, i] = np.maximum(-2 * eps[i] * lj, eps[i] * floor) return lj_energy
[docs] def reweight_rotamers(energies, temp, weights): """Adjust rotamer population weights based on external energies (from clash evaluations). Parameters ---------- energies : numpy.ndarray Array of external energies, in kcal/mol temp : scalar Temperature, in kelvin weights : numpy.ndarray Current weights of rotamers return_partition : bool If True, return the value of the partition function an additional argument; see below Returns ------- new_weights : numpy.ndarray Adjusted weights partition : float (optional) The partition function relative to the free label. A small partition function suggests the interactions with neighboring atoms are unfavorable while a large partition function suggests the opposite. """ probabilities = np.exp(-energies / (GAS_CONST * temp)) p = probabilities * weights p_sum = np.sum(p) new_weights = p/p_sum partition = p_sum / weights.sum() return new_weights, partition
[docs] def join_geom(a, b, flat=False): """ Function to join Lennard-Jones parameters (``rmin`` or ``eps``) using their geometric mean. Parameters can be joined in two different ways (see keyword argument ``flat``) Parameters ---------- a : numpy.ndarray Single atom parameters of atoms of group a b : numpy.ndarray Single atom parameters of atoms of group b flat : bool Only join parameters i of group a and j of group b if i=j. If false all combinations of i,j are computed. Returns ------- param : numpy.ndarray The joined parameters of group a and b """ if flat: return np.sqrt(a * b) else: return np.sqrt(np.outer(a, b))
[docs] def join_arith(a, b, flat=False): """ Function to join Lennard-Jones parameters (``rmin`` or ``eps``) using their arithmatic mean. Parameters can be joined in two different ways (see keyword argument ``flat``) Parameters ---------- a : numpy.ndarray Single atom parameters of atoms of group a b : numpy.ndarray Single atom parameters of atoms of group b flat : bool Only join parameters i of group a and j of group b if i=j. If false all combinations of i,j are computed. Returns ------- param : numpy.ndarray The joined parameters of group a and b """ if flat: return a + b else: return np.add.outer(a, b)
rmin_charmm = { "C": 2.0000, "H": 1.2700, "N": 1.8500, "O": 1.7000, "S": 2.0000, "Se": 2.0000, # Default Selenium to Sulfur "Br": 1.9800, "Cu": 0.7251, # ion 2+ 'B': 2.5500, 'F': 1.6300, 'Na': 1.3638, # ion 'Mg': 1.1850, 'P': 2.1500, 'Cl': 1.9100, 'K': 1.7638, # ion 'Ca': 1.3670, # ion 'Fe': 0.6500, # HEM 'Ni': 1.2760, 'Zn': 1.0900, # ion 'Gd': 1.5050, # ion 3+ 'Au': 0.6510, 'I': 2.5670, "join_protocol": join_arith, } # Bromobenzene from GutiƩrrez et al. 2016 eps_charmm = { "C": -0.110, "H": -0.022, "N": -0.200, "O": -0.120, "S": -0.450, "Se": -0.450, "Br": -0.320, "Cu": -0.1505, 'B': -0.0380, 'F': -0.105, 'Na': -0.0469, 'Mg': -0.0150, 'P': -0.5850, 'Cl': -0.3430, 'K': -0.0870, 'Ca': -0.120, 'Fe': 0.000, 'Ni': -5.65, 'Zn': -0.250, 'Gd': -0.1723, 'Au': -0.1330, 'I': -0.7927, "join_protocol": join_geom, } # Bromobenzene from GutiƩrrez et al. 2016 rmin_uff = { "C": 3.851, "H": 2.886, "N": 3.660, "O": 3.500, "S": 4.035, "Se": 4.035, # Default Selenium to Sulfur "Br": 4.189, "Na": 2.983, "Mg": 3.021, "P" : 4.147, "Cl": 3.947, "K" : 3.812, "Ca": 3.399, "Mn": 2.961, "Fe": 2.912, "Co": 2.872, "Ni": 2.834, "Cu": 3.495, "Gd": 3.368, "join_protocol": join_geom, } eps_uff = { "C": -0.105, "H": -0.044, "N": -0.069, "O": -0.060, "S": -0.274, "Se": -0.274, # Default Selenium to Sulfur "Br": -0.251, "Na": -0.030, "Mg": -0.111, "P" : -0.305, "Cl": -0.227, "K" : -0.035, "Ca": -0.238, "Mn": -0.013, "Fe": -0.013, "Co": -0.014, "Ni": -0.015, "Cu": -0.005, "Gd": -0.009, "join_protocol": join_geom, } for dictionary in rmin_charmm, eps_charmm, rmin_uff, eps_uff: tdict = {**{key.upper(): val for key, val in dictionary.items()}, **{key.lower(): val for key, val in dictionary.items()}} dictionary.update(tdict) lj_params = {"uff": [rmin_uff, eps_uff], "charmm": [rmin_charmm, eps_charmm]} class EnergyFunc: @abstractmethod def __call__(self, system): pass @abstractmethod def forces(self, system): pass @abstractmethod def prepare_system(self, system): pass class ljEnergyFunc(EnergyFunc): def __init__(self, functional=None, params=None, extra_params=None, **kwargs): functional = get_lj_rep if functional is None else functional params = 'charmm' if params is None else params self.functional = functional self.rmax = kwargs.get("rmax", 10) self.forgive = kwargs.pop("forgive", 1) self.kwargs = kwargs if isinstance(params, dict): self._rmin_func, self._eps_func = params['rmin'], params['eps'] elif params not in lj_params: raise RuntimeError(f'`{params}` is not a recognized forcefield.') else: self.name = params self._rmin_func, self._eps_func = lj_params[params] if extra_params is not None: self._rmin_func.update(extra_params['rmin']) self._eps_func.update(extra_params['eps']) self.join_rmin = self._rmin_func["join_protocol"] self.join_eps = self._eps_func["join_protocol"] self._system_hash = {} def get_lj_rmin(self, atypes): return np.array([self._rmin_func[a] for a in atypes]) def get_lj_eps(self, atypes): return np.array([self._eps_func[a] for a in atypes]) def prepare_system(self, system): if isinstance(system, Ensemble): # Prepare internal a, b = system.aidx, system.bidx a_eps = self.get_lj_eps(system.atom_types[a]) a_radii = self.get_lj_rmin(system.atom_types[a]) b_eps = self.get_lj_eps(system.atom_types[b]) b_radii = self.get_lj_rmin(system.atom_types[b]) system.irmin_ij = self.join_rmin(a_radii * self.forgive, b_radii * self.forgive, flat=True) system.ieps_ij = self.join_eps(a_eps, b_eps, flat=True) # Prepare external if system.protein is not None: environment_atypes = system.protein.atoms.types[system.protein_clash_idx] system.rmin2 = self.get_lj_rmin(system.atom_types[system.side_chain_idx]) system.eps = self.get_lj_eps(system.atom_types[system.side_chain_idx]) protein_lj_radii = self.get_lj_rmin(environment_atypes) protein_lj_eps = self.get_lj_eps(environment_atypes) system.ermin_ij = self.join_rmin(system.rmin2 * self.forgive, protein_lj_radii * self.forgive).reshape(-1) system.eeps_ij = self.join_eps(system.eps, protein_lj_eps).reshape((-1)) elif isinstance(system, (mda.Universe, mda.AtomGroup, MolecularSystemBase)): bonds = ( {(a, b) for a, b in system.atoms.bonds} if isinstance(system, MolecularSystemBase) else {(a, b) for a, b in system.atoms.bonds.indices} ) tree = cKDTree(system.atoms.positions) pairs = tree.query_pairs(self.rmax) pairs = pairs - bonds pairs = np.array(list(pairs)) lj_radii_1 = self.get_lj_rmin(system.atoms.types[pairs[:, 0]]) lj_radii_2 = self.get_lj_rmin(system.atoms.types[pairs[:, 1]]) lj_eps_1 = self.get_lj_eps(system.atoms.types[pairs[:, 0]]) lj_eps_2 = self.get_lj_eps(system.atoms.types[pairs[:, 1]]) rmin_ij = self.join_rmin(lj_radii_1 * self.forgive, lj_radii_2*self.forgive, flat=True) eps_ij = self.join_eps(lj_eps_1, lj_eps_2, flat=True) if isinstance(system, MolSys): system.rmin_ij = rmin_ij system.eps_ij = eps_ij system.pairs = pairs else: self._system_hash[system] = rmin_ij, eps_ij, pairs def _get_functional_input(self, system, internal): if isinstance(system, Ensemble): if internal: rmin, eps = system.irmin_ij, system.ieps_ij r = np.linalg.norm(system.coords[:, system.aidx] - system.coords[:, system.bidx], axis=-1) shape = len(system), len(system.aidx) else: rmin, eps = system.ermin_ij, system.eeps_ij c1 = system.coords[:, system.side_chain_idx].reshape(-1, 3) c2 = system.protein_tree.data[system.protein_clash_idx] r = cdist(c1, c2).reshape(len(system), -1) shape = len(system), len(system.side_chain_idx) else: if isinstance(system, (mda.Universe, mda.AtomGroup)): rmin, eps, pairs = self._system_hash[system] else: rmin, eps, pairs = system.rmin_ij, system.eps_ij, system.pairs r = np.linalg.norm(system.positions[pairs[:,0]] - system.positions[pairs[:,1]], axis=-1) shape = (1, len(pairs)) return r, rmin, eps, shape def __call__(self, system, internal = False, **kwargs): tkwargs = {k: v for k, v in self.kwargs.items()} tkwargs.update(kwargs) r, rmin, eps, shape = self._get_functional_input(system, internal) E = self.functional(r, rmin, eps, **tkwargs) E = E.reshape(*shape, -1) if isinstance(system, Ensemble): system.atom_energies = E.sum(axis=2) return E.sum(axis=(1, 2)) KCAL2J = 4.184e3 # conversion factor form kcal to J (exact) BOLTZ_CONST = 1.380649e-23 # Boltzmann constant, J K^-1 (exact) AVOGADRO_CONST = 6.02214076e23 # Avogadro constant, mol^-1 (exact) GAS_CONST = BOLTZ_CONST*AVOGADRO_CONST/KCAL2J # molar gas constant, kcal mol^1 K^-1