Source code for molecular_simulations.simulate.constantph.reference_energy

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


[docs] class ReferenceEnergyFinder:
[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 = next(iter(model.titrations.keys())) 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 * kilojoules_per_mole, 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 _ in range(1000): self.model.simulation.step(substeps) self.model.attemptMCStep(self.temperature) fractions = [[] for _ in range(len(self.model.pH))] for _ 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