Source code for molecular_simulations.simulate.constantph.reference_energy

from openmm.unit import kilojoules_per_mole, kelvin, is_quantity, MOLAR_GAS_CONSTANT_R
from scipy.optimize import curve_fit
import numpy as np

[docs] class ReferenceEnergyFinder(object):
[docs] def __init__(self, model, pKa, temperature): """ Construct a ReferenceEnergyFinder. Parameters ---------- model: ConstantPH The model for which to determine reference energies. It must contain a single titratable residue with exactly two states. It does not matter what pH or reference energies were specified when it was created, because they will both be overwritten. pKa: float The experimental pKa of the titratable residue. Reference energies will be chosen to match it. temperature: openmm.unit.Quantity The temperature at which the simulation will be run. """ if len(model.titrations) != 1: raise ValueError("The model compound must contain a single titratable residue") self.model = model self.pKa = pKa if not is_quantity(temperature): temperature = temperature*kelvin self.temperature = temperature self.residueIndex = list(model.titrations.keys())[0] self.titration = model.titrations[self.residueIndex] if len(self.titration.explicitStates) != 2: raise ValueError("Only residues with two states are currently supported")
[docs] def findReferenceEnergies(self, iterations=20000, substeps=20): """ Compute the reference energies for the states of the model compound. On exit, they will be stored in the ConstantPH object. Parameters ---------- iterations: int The number of Monte Carlo moves to attempt. The larger the number, the more tightly converged the results will be. subsets: int The number of dynamics steps to integrate between Monte Carlo moves. """ # Find an initial estimate of the reference energies just by computing the potential # energies of the states. self.model.setResidueState(self.residueIndex, 0) energy0 = self.model.implicitContext.getState(getEnergy=True).getPotentialEnergy() self.model.setResidueState(self.residueIndex, 1) energy1 = self.model.implicitContext.getState(getEnergy=True).getPotentialEnergy() deltaN = self.titration.implicitStates[1].numHydrogens - self.titration.implicitStates[0].numHydrogens scale = MOLAR_GAS_CONSTANT_R*self.temperature*deltaN*np.log(10.0) self.titration.referenceEnergies[0] = 0.0*kilojoules_per_mole self.titration.referenceEnergies[1] = energy1-energy0 self.model.simulation.minimizeEnergy() self.model.simulation.context.setVelocitiesToTemperature(self.temperature) # If our initial estimate is exact, the fractions should be equal at pH 0. Since it probably # isn't, simulate it at various pHs to refine the estimate. while True: self.model.setPH([-3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0]) for i in range(1000): self.model.simulation.step(substeps) self.model.attemptMCStep(self.temperature) fractions = [[] for _ in range(len(self.model.pH))] for i in range(iterations): self.model.simulation.step(substeps) self.model.attemptMCStep(self.temperature) fractions[self.model.currentPHIndex].append(1.0 if self.titration.protonatedIndex == self.titration.currentIndex else 0.0) # Fit a curve to the data to better estimate when the fraction is exactly 0.5, # and compute the reference energy based on it. x = [] y = [] for i in range(len(fractions)): if len(fractions[i]) > 0: x.append(self.model.pH[i]) y.append(np.average(fractions[i])) def f(ph, pka): return 1/(1+10**(ph-pka)) popt, pcov = curve_fit(f, x, y, [0.0]) root = popt[0] if root > -2 and root < 2: self.titration.referenceEnergies[1] += scale*(self.pKa-root) break self.titration.referenceEnergies[1] -= scale*root