implementing GaMD in openmm with a LangevinIntegrator
See the notebook for the full derivation. In OpenMM, the easiest way to implement the boost potential is to modify the force, which is done like this:
Custom integrator that does this, along with Langevin dynamics:
class CustomGaMDLangevinIntegrator(CustomIntegrator):
def __init__(self, temperature, friction, dt, ktot, Etot, kgrp, Egrp, forceGroup):
self.ktot = ktot
self.Etot = Etot
self.kgrp = kgrp
self.Egrp = Egrp
self.forceGroup = str(forceGroup)
CustomIntegrator.__init__(self, dt)
#lew added:
self.addGlobalVariable("ktot", self.ktot)
self.addGlobalVariable("Etot", self.Etot)
self.addGlobalVariable("kgrp", self.ktot)
self.addGlobalVariable("Egrp", self.Egrp)
self.addGlobalVariable("groupEnergy", 0)
#normal langevin:
self.addGlobalVariable("temperature", temperature);
self.addGlobalVariable("friction", friction);
self.addGlobalVariable("vscale", 0);
self.addGlobalVariable("fscale", 0);
self.addGlobalVariable("noisescale", 0);
self.addPerDofVariable("x0", 0);
self.addPerDofVariable("fgrp", 0)
#normal langevin:
self.addUpdateContextState();
self.addComputeGlobal("groupEnergy", "energy"+self.forceGroup)
self.addComputePerDof("fgrp", "f"+self.forceGroup)
self.addComputeGlobal("vscale", "exp(-dt*friction)");
self.addComputeGlobal("fscale", "(1-vscale)/friction");
#original line:
self.addComputeGlobal("noisescale", "sqrt(kT*(1-vscale*vscale)); kT=0.00831451*temperature");
self.addComputePerDof("x0", "x");
#original langevin line:
#self.addComputePerDof("v", "vscale*v + fscale*f/m + noisescale*gaussian/sqrt(m)");
#GaMD:
dof_string = "vscale*v + fscale*fprime/m + noisescale*gaussian/sqrt(m);"
dof_string+= "fprime= fprime1 + fprime2;"
#fprime2 is the dihedral force modified by the boost. Boot calculated using group only.
dof_string+= "fprime2 = fgrp*((1-modifyGroup) + modifyGroup* (1 - kgrp*(Egrp - groupEnergy)) ) ;"
#fprime1 is the other forces modified by the boost, but the boost is calculated using TOTAL energy.
dof_string+= "fprime1 = ftot*((1-modifyTotal) + modifyTotal* (1 - ktot*(Etot - energy)) );"
dof_string+= "ftot=f-fgrp;"
dof_string+= "modifyGroup=step(Egrp-groupEnergy);"
dof_string+= "modifyTotal=step(Etot-energy);"
self.addComputePerDof("v", dof_string);
#normal langevin
self.addComputePerDof("x", "x+dt*v");
self.addConstrainPositions();
self.addComputePerDof("v", "(x-x0)/dt");
self.addComputePerDof("veloc", "v")
def setKtot(self, newK):
if not is_quantity(newK):
newK = newK/kilojoules_per_mole
self.setGlobalVariableByName('ktot', newK)
def setEtot(self, newE):
if not is_quantity(newE):
newE = newE*kilojoules_per_mole
self.setGlobalVariableByName('Etot', newE)
def setKgrp(self, newK):
if not is_quantity(newK):
newK = newK/kilojoules_per_mole
self.setGlobalVariableByName('kgrp', newK)
def setEgrp(self, newE):
#if not is_quantity(newE):
# newE = newE*kilojoules_per_mole
# print(newE)
self.setGlobalVariableByName('Egrp', newE)
def getGrpBoost(self, grpEnergy):
kgrp = self.getGlobalVariableByName('kgrp')/kilojoules_per_mole
Egrp = self.getGlobalVariableByName('Egrp')*kilojoules_per_mole
if not is_quantity(grpEnergy):
grpEnergy = grpEnergy*kilojoules_per_mole # Assume kJ/mole
if (grpEnergy > Egrp):
return 0*kilojoules_per_mole #no boosting
return ( 0.5 * kgrp * (Egrp-grpEnergy)**2 ) # 'k' parameter should instead be per kj/mol
def getTotBoost(self, totEnergy):
ktot = self.getGlobalVariableByName('ktot')/kilojoules_per_mole
Etot = self.getGlobalVariableByName('Etot')*kilojoules_per_mole
if not is_quantity(totEnergy):
totEnergy = totEnergy*kilojoules_per_mole # Assume kJ/mole
if (totEnergy > Etot):
return 0*kilojoules_per_mole #no boosting
return ( 0.5 * ktot * (Etot-totEnergy)**2 ) # 'k' parameter should instead be per kj/mol
def getEffectiveEnergy(self, totEnergy, grpEnergy):
if not is_quantity(totEnergy):
totEnergy = totEnergy*kilojoules_per_mole # Assume kJ/mole
if not is_quantity(grpEnergy):
grpEnergy = grpEnergy*kilojoules_per_mole # Assume kJ/mole
group_boost = self.getGrpBoost(grpEnergy)
total_boost = self.getTotBoost(totEnergy)
return totEnergy + group_boost + total_boost
now, when setting E
to Vmax
, estimate GaMD parameters like this:
pe = np.array(my_potential_energies)
#set the desired maximum standard deviation of the boost potential to be 10kT:
sigma_0 = (MOLAR_GAS_CONSTANT_R * TEMPERATURE ).value_in_unit(kilojoule_per_mole) * 10
print(f'Sigma0: {sigma_0}')
#potential energy statistics:
Vmax = pe.max()
Vmin = pe.min()
Vavg = pe.mean()
Vstd = np.std(pe)
print(f'Vmax: {Vmax},\nVmin: {Vmin},\nVavg: {Vavg},\nVstd: {Vstd}')
k_0 = min(1, sigma_0/Vstd * ((Vmax-Vmin)/(Vmax-Vavg)))
k = k_0 * (1 / (Vmax - Vmin) )
print(f'k_0: {k_0},\nk: {k}')