Source code for BFEE2.postTreatment

# post-treatment of BFEE

import math

import numpy as np

# Boltazann constant for NAMD unit convention
BOLTZMANN = 0.0019872041
BOLTZMANN_GMX = 0.0019872041 * 4.184

# standard concentration
CSTAR = 1661
CSTAR_GMX = 1.661

[docs]class postTreatment: """the post-treatment of BFEE outputs """ def __init__(self, temperature, unit, jobType='geometric'): """do post treatment, internally, all the unit should be converted into the NAMD/Colvars unit Args: temperature (float): temperature of the simulation unit (str): unit convention used by MD engine, 'namd' or 'gromacs' jobType (str): 'geometric' or 'alchemical'. Actually this arg is not used yet. Default to geometric. """ if unit == 'namd': self.BOLTZMANN = BOLTZMANN self.CSTAR = CSTAR elif unit == 'gromacs': self.BOLTZMANN = BOLTZMANN_GMX self.CSTAR = CSTAR_GMX self.unit = unit self.beta = 1 / (self.BOLTZMANN * temperature) self.temperature = float(temperature) def _readPMF(self, filePath): """read a 1D PMF file Args: filePath (str): the path of the PMF file Returns: np.array (float, 2*N): ((x0,x1,x2, ...), (y0, y1, y2, ...)) """ data = np.loadtxt(filePath) x = data[:,0] y = data[:,1] return np.array((x, y)) def _geometricRestraintContribution(self, pmf, forceConstant, rmsd=False, unbound=False): """calculate the contribution of RMSD and angle restraints Args: pmf (np.array, float, 2*N): ((x0,x1,x2, ...), (y0, y1, y2, ...)) forceConstant (float): the force constant of the restraint rmsd (bool): whether the contribution of RMSD is being calculated. Defaults to False. unbound (bool, optional): whether unbound-state contribution is being calculated. Defaults to False. Returns: float: contribution of the geometric restraint """ width = pmf[0][1] - pmf[0][0] if rmsd: # for RMSD, the restraintCenter is zero restraintCenter = 0 else: # the minimum of pmf restraintCenter = pmf[0][np.argmin(pmf[1])] # integration numerator = 0 denominator = 0 for x, y in zip(pmf[0], pmf[1]): numerator += math.exp(-self.beta * y) denominator += math.exp((-self.beta) * (y + 0.5 * forceConstant * ((x - restraintCenter)**2))) contribution = math.log(numerator / denominator) / self.beta if unbound: return contribution else: return -contribution def _geometricRestraintContributionBulk( self, theta, forceConstantTheta, forceConstantPhi, forceConstantPsi ): """contribution of rotational restraints in the unbounded state Args: theta (float): restraining center of the theta angle forceConstantTheta (float): restraining force constant for Theta forceConstantPhi (float): restraining force constant for Phi forceConstantPsi (float): restraining force constant for Psi Returns: float: contribution of the geometric restraint in the unbound state """ # all the units in radian theta0 = math.radians(theta) # periodic CV then the u(phi) and u(psi) should be the same in all cases phi0 = math.radians(180) psi0 = math.radians(180) forceConstantTheta *= (180 / math.pi)**2 forceConstantPhi *= (180 / math.pi)**2 forceConstantPsi *= (180 / math.pi)**2 contributionTheta = 0 contributionPhi = 0 contributionPsi = 0 # integration for i in range(1000): theta = i / 1000.0 * math.pi - math.pi / 2 contributionTheta += 1.0/1000.0 * math.pi * math.sin(theta + math.pi / 2) * \ math.exp(-self.beta * 0.5 * forceConstantTheta * ((theta - theta0)**2)) phi = i / 1000.0 * 2 * math.pi contributionPhi += 1.0/1000.0 * 2 * math.pi * \ math.exp(-self.beta * 0.5 * forceConstantPhi* ((phi - phi0)**2)) psi = i / 1000.0 * 2 * math.pi contributionPsi += 1.0/1000.0 * 2 * math.pi * \ math.exp(-self.beta * 0.5 * forceConstantPsi* ((psi - psi0)**2)) return -math.log((contributionTheta * contributionPhi * contributionPsi) / 8 / math.pi**2) / self.beta def _geometricJacobianCorrection(self, pmf): """correct the Jacobian contribution of separation pmf Args: pmf (np.array, float, 2*N): ((x0,x1,x2, ...), (y0, y1, y2, ...)), separation pmf """ for i in range(len(pmf[0])): pmf[1][i] += 2 * self.BOLTZMANN * self.temperature * math.log(pmf[0][i]) pmf[1] -= np.min(pmf[1]) def _geometricCalculateSI( self, rStar, pmf, polarTheta, polarPhi, forceConstantPolarTheta, forceConstantPolarPhi): """calculation the contribution of S* and I* in the separation simulation Args: rStar (float): r* in integration pmf (np.array, float, 2*N): ((x0,x1,x2, ...), (y0, y1, y2, ...)), separation pmf polarTheta0 (float): restraining center of polarTheta polarPhi0 (float): restraining center of polarPhi forceConstantPolarTheta (float): restraining force constant for polarTheta forceConstantPolarPhi (float): restraining force constant for polarPhi Returns: float: contribution of S* and I* in the separation simulation """ assert(rStar <= pmf[0][-1]) polarTheta0 = math.radians(polarTheta) polarPhi0 = math.radians(polarPhi) forceConstantPolarTheta *= (180 / math.pi)**2 forceConstantPolarPhi *= (180 / math.pi)**2 contributionPolarTheta = 0 contributionPolarPhi = 0 # integration for i in range(1000): polarTheta = i / 1000.0 * math.pi contributionPolarTheta += 1.0 / 1000.0 * math.pi * math.sin(polarTheta) * \ math.exp(-self.beta * 0.5 * forceConstantPolarTheta * (polarTheta - polarTheta0)**2) polarPhi = i / 1000.0 * 2 * math.pi - math.pi contributionPolarPhi += 1.0 / 1000.0 * 2 * math.pi * \ math.exp(-self.beta * 0.5 * forceConstantPolarPhi * (polarPhi - polarPhi0)**2) S = rStar**2 * contributionPolarTheta * contributionPolarPhi # w(r*) wrStar = pmf[1][0] for x, y in zip(pmf[0], pmf[1]): if x >= rStar: wrStar = y break # integration width = pmf[0][1] - pmf[0][0] I = 0 for x, y in zip(pmf[0], pmf[1]): I += width * math.exp(-self.beta * (y - wrStar)) if x >= rStar: break return -1 / self.beta * math.log(S * I / self.CSTAR)
[docs] def geometricBindingFreeEnergy(self, filePathes, parameters): """calculate binding free energy for geometric route Args: filePathes (list of strings, 8): pathes of PMF files for step1 - step8 parameters (np.array, floats, 8): (forceConstant1, FC2, FC3, FC4, FC5, FC6, r*, FC8) Returns: np.array, float, 10: (contributions for step1, 2, 3, 4 ... 8, bulk restraining contribution, free energy) """ assert len(parameters) == 8 assert len(filePathes) == 8 pmfs = [] for path in filePathes: pmfs.append(self._readPMF(path)) self._geometricJacobianCorrection(pmfs[6]) contributions = np.zeros(10) contributions[0] = self._geometricRestraintContribution(pmfs[0], parameters[0], True, False) contributions[1] = self._geometricRestraintContribution(pmfs[1], parameters[1], False, False) contributions[2] = self._geometricRestraintContribution(pmfs[2], parameters[2], False, False) contributions[3] = self._geometricRestraintContribution(pmfs[3], parameters[3], False, False) contributions[4] = self._geometricRestraintContribution(pmfs[4], parameters[4], False, False) contributions[5] = self._geometricRestraintContribution(pmfs[5], parameters[5], False, False) contributions[6] = self._geometricCalculateSI( parameters[6], pmfs[6], pmfs[4][0][np.argmin(pmfs[4][1])], pmfs[5][0][np.argmin(pmfs[5][1])], parameters[4], parameters[5] ) contributions[7] = self._geometricRestraintContribution(pmfs[7], parameters[7], True, True) contributions[8] = self._geometricRestraintContributionBulk( pmfs[1][0][np.argmin(pmfs[1][1])], parameters[1], parameters[2], parameters[3] ) contributions[9] = np.sum(contributions[:9]) if self.unit == 'namd': return contributions elif self.unit == 'gromacs': return contributions / 4.184
def _alchemicalRestraintContributionBulk( self, eulerTheta, polarTheta, R, forceConstantTheta=0.1, forceConstantPhi=0.1, forceConstantPsi=0.1, forceConstanttheta=0.1, forceConstantphi=0.1, forceConstantR=10 ): """contribution of (standard concentration corrected) rotational and orienetational restraints in the unbounded state Args: eulerTheta (float): restraining center of the Euler angle theta polarTheta (float): restraining center of the polar angle theta R (float): restraining center of anger R forceConstantTheta (float): restraining force constant for euler Theta. Defaults to 0.1. forceConstantPhi (float, optional): restraining force constant for euler Phi. Defaults to 0.1. forceConstantPsi (float, optional): restraining force constant for euler Psi. Defaults to 0.1. forceConstanttheta (float, optional): restraining force constant for polar theta. Defaults to 0.1. forceConstantphi (float, optional): restraining force constant for polar phi. Defaults to 0.1. forceConstantR (int, optional): restraining force constant for distance R. Defaults to 10. Returns: float: contribution of the geometric restraint in the unbound state """ # degrees to rad eulerTheta = math.radians(eulerTheta + 90) polarTheta = math.radians(polarTheta) forceConstantTheta *= (180 / math.pi)**2 forceConstantPhi *= (180 / math.pi)**2 forceConstantPsi *= (180 / math.pi)**2 forceConstanttheta *= (180 / math.pi)**2 forceConstantphi *= (180 / math.pi)**2 contribution = self.BOLTZMANN * self.temperature * math.log( 8 * (math.pi**2) * self.CSTAR / ((R**2) * math.sin(eulerTheta) * math.sin(polarTheta)) * \ math.sqrt(forceConstantTheta * forceConstantPhi * forceConstantPsi * forceConstanttheta * \ forceConstantphi * forceConstantR ) / ((2 * math.pi * self.BOLTZMANN * self.temperature)**3) ) return contribution def _alchemicalFepoutFile(self, filePath, fileType = 'fepout'): """parse a fepout/log file and return the total free energy change Args: filePath (str): path of the fepout file fileType (str): 'fepout' (decouping atoms) or 'log' (decoupling restraints). Defaults to 'fepout'. Returns: float: free-energy change """ freeEnergy = 0 # for TI simulation results of the .log file Lambda = [] dA_dLambda = [] with open(filePath, 'r', encoding='utf-8') as fepoutFile: for line in fepoutFile.readlines(): if fileType == 'fepout': if not line.startswith('#Free energy'): continue if fileType == 'log': if not ('dA/dLambda' in line): continue splitedLine = line.strip().split() if fileType == 'fepout': freeEnergy += float(splitedLine[11]) if fileType == 'log': Lambda.append(float(splitedLine[4])) dA_dLambda.append(float(splitedLine[6])) # integraion of TI results if fileType == 'log': # backward simulation if Lambda[0] > Lambda[7]: Lambda.reverse() dA_dLambda.reverse() # step 2 if Lambda[0] == Lambda[1]: for i in range(7, len(Lambda)): freeEnergy += dA_dLambda[i] * (Lambda[i] - Lambda[i-7]) else: # step 4 for i in range(1, len(Lambda)): freeEnergy += dA_dLambda[i] * (Lambda[i] - Lambda[i-1]) return freeEnergy
[docs] def alchemicalBindingFreeEnergy(self, filePathes, parameters): """calculate binding free energy for geometric route Args: filePathes (list of strings, 8): pathes of alchemical output files (step1-forward, step1-backward, step2-forward ...) parameters (np.array, floats, 9): (eulerTheta, polarTheta, r, forceConstant1, FC2, FC3, FC4, FC5, FC6) Returns: tuple: np.array, float, 6: (contributions for step1, 2, 3, 4, bulk restraining contribution, free energy) np.array, float, 6: errors corresponding each contribution """ assert len(parameters) == 9 assert len(filePathes) == 8 # get free energies from fep outputs freeEnergies = [] for i in range(len(filePathes)): if filePathes[i] != '': if (i // 2) % 2 == 0: freeEnergies.append(self._alchemicalFepoutFile(filePathes[i], 'fepout')) else: freeEnergies.append(self._alchemicalFepoutFile(filePathes[i], 'log')) else: # backward file can be empty freeEnergies.append(None) contributions = np.zeros(6) errors = np.zeros(6) if freeEnergies[1] is not None: contributions[0] = (freeEnergies[0] - freeEnergies[1]) / 2 errors[0] = abs((freeEnergies[0] + freeEnergies[1]) / 1.414) else: contributions[0] = freeEnergies[0] errors[0] = 99999 if freeEnergies[3] is not None: contributions[1] = -(freeEnergies[2] + freeEnergies[3]) / 2 errors[1] = abs((freeEnergies[2] - freeEnergies[3]) / 1.414) else: contributions[1] = -freeEnergies[2] errors[1] = 99999 if freeEnergies[5] is not None: contributions[2] = -(freeEnergies[4] - freeEnergies[5]) / 2 errors[2] = abs((freeEnergies[4] + freeEnergies[5]) / 1.414) else: contributions[2] = -freeEnergies[4] errors[2] = 99999 if freeEnergies[7] is not None: contributions[3] = (freeEnergies[6] + freeEnergies[7]) / 2 errors[3] = abs((freeEnergies[6] - freeEnergies[7]) / 1.414) else: contributions[3] = freeEnergies[6] errors[3] = 99999 contributions[4] = self._alchemicalRestraintContributionBulk(*parameters) errors[4] = 0 contributions[5] = contributions[0] + contributions[1] + contributions[2] + contributions[3] + contributions[4] errors[5] = math.sqrt(errors[0]**2 + errors[1]**2 +errors[2]**2 + errors[3]**2 + errors[4]**2) return contributions, errors