ForceBalance API  1.3
Automated optimization of force fields and empirical potentials
openmmio.py
Go to the documentation of this file.
1 """ @package forcebalance.openmmio OpenMM input/output.
2 
3 @author Lee-Ping Wang
4 @date 04/2012
5 """
6 from __future__ import division
7 
8 from builtins import zip
9 from builtins import range
10 import os
11 from forcebalance import BaseReader
12 from forcebalance.abinitio import AbInitio
13 from forcebalance.binding import BindingEnergy
14 from forcebalance.liquid import Liquid
15 from forcebalance.interaction import Interaction
16 from forcebalance.moments import Moments
17 from forcebalance.hydration import Hydration
18 from forcebalance.vibration import Vibration
19 from forcebalance.opt_geo_target import OptGeoTarget
20 from forcebalance.torsion_profile import TorsionProfileTarget
21 import networkx as nx
22 import numpy as np
23 import sys
24 import pickle
25 import shutil
26 from copy import deepcopy
27 from forcebalance.engine import Engine
28 from forcebalance.molecule import *
29 from forcebalance.chemistry import *
30 from forcebalance.nifty import *
31 from forcebalance.nifty import _exec
32 from collections import OrderedDict
33 from forcebalance.output import getLogger
34 logger = getLogger(__name__)
35 try:
36  from simtk.openmm.app import *
37  from simtk.openmm import *
38  from simtk.unit import *
39  import simtk.openmm._openmm as _openmm
40 except:
41  pass
42 
43 def get_mask(grps):
44  """ Given a list of booleans [1, 0, 1] return the bitmask that sets
45  these force groups appropriately in Context.getState(). Any values
46  not provided are defaulted to 1. """
47  mask = 0
48  for i, j in enumerate(grps):
49  # print i, j, 2**i
50  mask += 2**i*j
51  for k in range(i+1, 31):
52  # print k, 1, 2**k
53  mask += 2**k
54  return mask
55 
56 def energy_components(Sim, verbose=False):
57  # Before using EnergyComponents, make sure each Force is set to a different group.
58  EnergyTerms = OrderedDict()
59  if type(Sim.integrator) in [LangevinIntegrator, VerletIntegrator]:
60  for i in range(Sim.system.getNumForces()):
61  EnergyTerms[Sim.system.getForce(i).__class__.__name__] = Sim.context.getState(getEnergy=True,groups=2**i).getPotentialEnergy() / kilojoules_per_mole
62  return EnergyTerms
63 
64 def get_multipoles(simulation,q=None,mass=None,positions=None,rmcom=True):
65  """Return the current multipole moments in Debye and Buckingham units. """
66  dx = 0.0
67  dy = 0.0
68  dz = 0.0
69  qxx = 0.0
70  qxy = 0.0
71  qxz = 0.0
72  qyy = 0.0
73  qyz = 0.0
74  qzz = 0.0
75  enm_debye = 48.03204255928332 # Conversion factor from e*nm to Debye
76  for i in simulation.system.getForces():
77  if isinstance(i, AmoebaMultipoleForce):
78  mm = i.getSystemMultipoleMoments(simulation.context)
79  dx += mm[1]
80  dy += mm[2]
81  dz += mm[3]
82  qxx += mm[4]
83  qxy += mm[5]
84  qxz += mm[6]
85  qyy += mm[8]
86  qyz += mm[9]
87  qzz += mm[12]
88  if isinstance(i, NonbondedForce):
89  # Get array of charges.
90  if q is None:
91  q = np.array([i.getParticleParameters(j)[0]._value for j in range(i.getNumParticles())])
92  # Get array of positions in nanometers.
93  if positions is None:
94  positions = simulation.context.getState(getPositions=True).getPositions()
95  if mass is None:
96  mass = np.array([simulation.context.getSystem().getParticleMass(k).value_in_unit(dalton) \
97  for k in range(simulation.context.getSystem().getNumParticles())])
98  x = np.array(positions.value_in_unit(nanometer))
99  if rmcom:
100  com = np.sum(x*mass.reshape(-1,1),axis=0) / np.sum(mass)
101  x -= com
102  xx, xy, xz, yy, yz, zz = (x[:,k]*x[:,l] for k, l in [(0,0),(0,1),(0,2),(1,1),(1,2),(2,2)])
103  # Multiply charges by positions to get dipole moment.
104  dip = enm_debye * np.sum(x*q.reshape(-1,1),axis=0)
105  dx += dip[0]
106  dy += dip[1]
107  dz += dip[2]
108  qxx += enm_debye * 15 * np.sum(q*xx)
109  qxy += enm_debye * 15 * np.sum(q*xy)
110  qxz += enm_debye * 15 * np.sum(q*xz)
111  qyy += enm_debye * 15 * np.sum(q*yy)
112  qyz += enm_debye * 15 * np.sum(q*yz)
113  qzz += enm_debye * 15 * np.sum(q*zz)
114  tr = qxx+qyy+qzz
115  qxx -= tr/3
116  qyy -= tr/3
117  qzz -= tr/3
118  # This ordering has to do with the way TINKER prints it out.
119  return [dx,dy,dz,qxx,qxy,qyy,qxz,qyz,qzz]
120 
121 def get_dipole(simulation,q=None,mass=None,positions=None):
122  """Return the current dipole moment in Debye.
123  Note that this quantity is meaningless if the system carries a net charge."""
124  return get_multipoles(simulation, q=q, mass=mass, positions=positions, rmcom=False)[:3]
125 
126 def PrepareVirtualSites(system):
127  """ Prepare a list of function wrappers and vsite parameters from the system. """
128  isvsites = []
129  vsfuncs = []
130  vsidxs = []
131  vswts = []
132  for i in range(system.getNumParticles()):
133  if system.isVirtualSite(i):
134  isvsites.append(1)
135  vs = system.getVirtualSite(i)
136  if isinstance(vs, TwoParticleAverageSite):
137  vsidx = [_openmm.VirtualSite_getParticle(vs, 0), _openmm.VirtualSite_getParticle(vs, 1)]
138  vswt = [_openmm.TwoParticleAverageSite_getWeight(vs, 0), _openmm.TwoParticleAverageSite_getWeight(vs, 1)]
139  def vsfunc(pos, idx_, wt_):
140  return wt_[0]*pos[idx_[0]] + wt_[1]*pos[idx_[1]]
141  elif isinstance(vs, ThreeParticleAverageSite):
142  vsidx = [_openmm.VirtualSite_getParticle(vs, 0), _openmm.VirtualSite_getParticle(vs, 1), _openmm.VirtualSite_getParticle(vs, 2)]
143  vswt = [_openmm.ThreeParticleAverageSite_getWeight(vs, 0), _openmm.ThreeParticleAverageSite_getWeight(vs, 1), _openmm.ThreeParticleAverageSite_getWeight(vs, 2)]
144  def vsfunc(pos, idx_, wt_):
145  return wt_[0]*pos[idx_[0]] + wt_[1]*pos[idx_[1]] + wt_[2]*pos[idx_[2]]
146  elif isinstance(vs, OutOfPlaneSite):
147  vsidx = [_openmm.VirtualSite_getParticle(vs, 0), _openmm.VirtualSite_getParticle(vs, 1), _openmm.VirtualSite_getParticle(vs, 2)]
148  vswt = [_openmm.OutOfPlaneSite_getWeight12(vs), _openmm.OutOfPlaneSite_getWeight13(vs), _openmm.OutOfPlaneSite_getWeightCross(vs)]
149  def vsfunc(pos, idx_, wt_):
150  v1 = pos[idx_[1]] - pos[idx_[0]]
151  v2 = pos[idx_[2]] - pos[idx_[0]]
152  cross = np.array([v1[1]*v2[2]-v1[2]*v2[1], v1[2]*v2[0]-v1[0]*v2[2], v1[0]*v2[1]-v1[1]*v2[0]])
153  return pos[idx_[0]] + wt_[0]*v1 + wt_[1]*v2 + wt_[2]*cross
154  else:
155  isvsites.append(0)
156  vsfunc = None
157  vsidx = None
158  vswt = None
159  vsfuncs.append(deepcopy(vsfunc))
160  vsidxs.append(deepcopy(vsidx))
161  vswts.append(deepcopy(vswt))
162  return (isvsites, vsfuncs, vsidxs, vswts)
163 
164 def ResetVirtualSites_fast(positions, vsinfo):
165  """Given a set of OpenMM-compatible positions and a System object,
166  compute the correct virtual site positions according to the System."""
167  isvsites, vsfuncs, vsidxs, vswts = vsinfo
168  if any(isvsites):
169  pos = np.array(positions.value_in_unit(nanometer))
170  for i in range(len(positions)):
171  if isvsites[i]:
172  pos[i] = vsfuncs[i](pos, vsidxs[i], vswts[i])
173  newpos = [Vec3(*i) for i in pos]*nanometer
174  return newpos
175  else:
176  return positions
177 
178 def ResetVirtualSites(positions, system):
179  """Given a set of OpenMM-compatible positions and a System object,
180  compute the correct virtual site positions according to the System."""
181  if any([system.isVirtualSite(j) for j in range(system.getNumParticles())]):
182  pos = np.array(positions.value_in_unit(nanometer))
183  for i in range(system.getNumParticles()):
184  if system.isVirtualSite(i):
185  vs = system.getVirtualSite(i)
186  # Faster code to work around Python API slowness.
187  if isinstance(vs, TwoParticleAverageSite):
188  vspos = _openmm.TwoParticleAverageSite_getWeight(vs, 0)*pos[_openmm.VirtualSite_getParticle(vs, 0)] \
189  + _openmm.TwoParticleAverageSite_getWeight(vs, 1)*pos[_openmm.VirtualSite_getParticle(vs, 1)]
190  elif isinstance(vs, ThreeParticleAverageSite):
191  vspos = _openmm.ThreeParticleAverageSite_getWeight(vs, 0)*pos[_openmm.VirtualSite_getParticle(vs, 0)] \
192  + _openmm.ThreeParticleAverageSite_getWeight(vs, 1)*pos[_openmm.VirtualSite_getParticle(vs, 1)] \
193  + _openmm.ThreeParticleAverageSite_getWeight(vs, 2)*pos[_openmm.VirtualSite_getParticle(vs, 2)]
194  elif isinstance(vs, OutOfPlaneSite):
195  v1 = pos[_openmm.VirtualSite_getParticle(vs, 1)] - pos[_openmm.VirtualSite_getParticle(vs, 0)]
196  v2 = pos[_openmm.VirtualSite_getParticle(vs, 2)] - pos[_openmm.VirtualSite_getParticle(vs, 0)]
197  cross = Vec3(v1[1]*v2[2]-v1[2]*v2[1], v1[2]*v2[0]-v1[0]*v2[2], v1[0]*v2[1]-v1[1]*v2[0])
198  vspos = pos[_openmm.VirtualSite_getParticle(vs, 0)] + _openmm.OutOfPlaneSite_getWeight12(vs)*v1 \
199  + _openmm.OutOfPlaneSite_getWeight13(vs)*v2 + _openmm.OutOfPlaneSite_getWeightCross(vs)*cross
200  pos[i] = vspos
201  newpos = [Vec3(*i) for i in pos]*nanometer
202  return newpos
203  else: return positions
204 
205 def GetVirtualSiteParameters(system):
206  """Return an array of all virtual site parameters in the system. Used for comparison purposes."""
207  vsprm = []
208  for i in range(system.getNumParticles()):
209  if system.isVirtualSite(i):
210  vs = system.getVirtualSite(i)
211  vstype = vs.__class__.__name__
212  if vstype == 'TwoParticleAverageSite':
213  vsprm.append(_openmm.TwoParticleAverageSite_getWeight(vs, 0))
214  vsprm.append(_openmm.TwoParticleAverageSite_getWeight(vs, 1))
215  elif vstype == 'ThreeParticleAverageSite':
216  vsprm.append(_openmm.ThreeParticleAverageSite_getWeight(vs, 0))
217  vsprm.append(_openmm.ThreeParticleAverageSite_getWeight(vs, 1))
218  vsprm.append(_openmm.ThreeParticleAverageSite_getWeight(vs, 2))
219  elif vstype == 'OutOfPlaneSite':
220  vsprm.append(_openmm.OutOfPlaneSite_getWeight12(vs))
221  vsprm.append(_openmm.OutOfPlaneSite_getWeight13(vs))
222  vsprm.append(_openmm.OutOfPlaneSite_getWeightCross(vs))
223  return np.array(vsprm)
224 
225 def CopyAmoebaBondParameters(src,dest):
226  dest.setAmoebaGlobalBondCubic(src.getAmoebaGlobalBondCubic())
227  dest.setAmoebaGlobalBondQuartic(src.getAmoebaGlobalBondQuartic())
228  for i in range(src.getNumBonds()):
229  dest.setBondParameters(i,*src.getBondParameters(i))
230 
232  dest.setAmoebaGlobalOutOfPlaneBendCubic(src.getAmoebaGlobalOutOfPlaneBendCubic())
233  dest.setAmoebaGlobalOutOfPlaneBendQuartic(src.getAmoebaGlobalOutOfPlaneBendQuartic())
234  dest.setAmoebaGlobalOutOfPlaneBendPentic(src.getAmoebaGlobalOutOfPlaneBendPentic())
235  dest.setAmoebaGlobalOutOfPlaneBendSextic(src.getAmoebaGlobalOutOfPlaneBendSextic())
236  for i in range(src.getNumOutOfPlaneBends()):
237  dest.setOutOfPlaneBendParameters(i,*src.getOutOfPlaneBendParameters(i))
238 
240  dest.setAmoebaGlobalAngleCubic(src.getAmoebaGlobalAngleCubic())
241  dest.setAmoebaGlobalAngleQuartic(src.getAmoebaGlobalAngleQuartic())
242  dest.setAmoebaGlobalAnglePentic(src.getAmoebaGlobalAnglePentic())
243  dest.setAmoebaGlobalAngleSextic(src.getAmoebaGlobalAngleSextic())
244  for i in range(src.getNumAngles()):
245  dest.setAngleParameters(i,*src.getAngleParameters(i))
246  return
248 def CopyAmoebaInPlaneAngleParameters(src, dest):
249  dest.setAmoebaGlobalInPlaneAngleCubic(src.getAmoebaGlobalInPlaneAngleCubic())
250  dest.setAmoebaGlobalInPlaneAngleQuartic(src.getAmoebaGlobalInPlaneAngleQuartic())
251  dest.setAmoebaGlobalInPlaneAnglePentic(src.getAmoebaGlobalInPlaneAnglePentic())
252  dest.setAmoebaGlobalInPlaneAngleSextic(src.getAmoebaGlobalInPlaneAngleSextic())
253  for i in range(src.getNumAngles()):
254  dest.setAngleParameters(i,*src.getAngleParameters(i))
255  return
257 def CopyAmoebaVdwParameters(src, dest):
258  for i in range(src.getNumParticles()):
259  dest.setParticleParameters(i,*src.getParticleParameters(i))
260 
261 def CopyAmoebaMultipoleParameters(src, dest):
262  for i in range(src.getNumMultipoles()):
263  dest.setMultipoleParameters(i,*src.getMultipoleParameters(i))
264 
266  for i in range(src.getNumBonds()):
267  dest.setBondParameters(i,*src.getBondParameters(i))
268 
270  for i in range(src.getNumAngles()):
271  dest.setAngleParameters(i,*src.getAngleParameters(i))
272 
274  for i in range(src.getNumTorsions()):
275  dest.setTorsionParameters(i,*src.getTorsionParameters(i))
276 
277 def CopyNonbondedParameters(src, dest):
278  dest.setReactionFieldDielectric(src.getReactionFieldDielectric())
279  for i in range(src.getNumParticles()):
280  dest.setParticleParameters(i,*src.getParticleParameters(i))
281  for i in range(src.getNumExceptions()):
282  dest.setExceptionParameters(i,*src.getExceptionParameters(i))
283 
284 def CopyGBSAOBCParameters(src, dest):
285  dest.setSolventDielectric(src.getSolventDielectric())
286  dest.setSoluteDielectric(src.getSoluteDielectric())
287  for i in range(src.getNumParticles()):
288  dest.setParticleParameters(i,*src.getParticleParameters(i))
289 
290 def CopyCustomNonbondedParameters(src, dest):
291  '''
292  copy whatever updateParametersInContext can update:
293  per-particle parameters
294  '''
295  for i in range(src.getNumParticles()):
296  dest.setParticleParameters(i, list(src.getParticleParameters(i)))
297 
298 def do_nothing(src, dest):
299  return
300 
301 def CopySystemParameters(src,dest):
302  """Copy parameters from one system (i.e. that which is created by a new force field)
303  sto another system (i.e. the one stored inside the Target object).
304  DANGER: These need to be implemented manually!!!"""
305  Copiers = {'AmoebaBondForce':CopyAmoebaBondParameters,
306  'AmoebaOutOfPlaneBendForce':CopyAmoebaOutOfPlaneBendParameters,
307  'AmoebaAngleForce':CopyAmoebaAngleParameters,
308  'AmoebaInPlaneAngleForce':CopyAmoebaInPlaneAngleParameters,
309  'AmoebaVdwForce':CopyAmoebaVdwParameters,
310  'AmoebaMultipoleForce':CopyAmoebaMultipoleParameters,
311  'HarmonicBondForce':CopyHarmonicBondParameters,
312  'HarmonicAngleForce':CopyHarmonicAngleParameters,
313  'PeriodicTorsionForce':CopyPeriodicTorsionParameters,
314  'NonbondedForce':CopyNonbondedParameters,
315  'CustomNonbondedForce':CopyCustomNonbondedParameters,
316  'GBSAOBCForce':CopyGBSAOBCParameters,
317  'CMMotionRemover':do_nothing}
318  for i in range(src.getNumForces()):
319  nm = src.getForce(i).__class__.__name__
320  if nm in Copiers:
321  Copiers[nm](src.getForce(i),dest.getForce(i))
322  else:
323  warn_press_key('There is no Copier function implemented for the OpenMM force type %s!' % nm)
324 
325 def UpdateSimulationParameters(src_system, dest_simulation):
326  CopySystemParameters(src_system, dest_simulation.system)
327  for i in range(src_system.getNumForces()):
328  if hasattr(dest_simulation.system.getForce(i),'updateParametersInContext'):
329  dest_simulation.system.getForce(i).updateParametersInContext(dest_simulation.context)
330  if isinstance(dest_simulation.system.getForce(i), CustomNonbondedForce):
331  force = src_system.getForce(i)
332  for j in range(force.getNumGlobalParameters()):
333  pName = force.getGlobalParameterName(j)
334  pValue = force.getGlobalParameterDefaultValue(j)
335  dest_simulation.context.setParameter(pName, pValue)
336 
337 
338 def SetAmoebaVirtualExclusions(system):
339  if any([f.__class__.__name__ == "AmoebaMultipoleForce" for f in system.getForces()]):
340  # logger.info("Cajoling AMOEBA covalent maps so they work with virtual sites.\n")
341  vss = [(i, [system.getVirtualSite(i).getParticle(j) for j in range(system.getVirtualSite(i).getNumParticles())]) \
342  for i in range(system.getNumParticles()) if system.isVirtualSite(i)]
343  for f in system.getForces():
344  if f.__class__.__name__ == "AmoebaMultipoleForce":
345  # logger.info("--- Before ---\n")
346  # for i in range(f.getNumMultipoles()):
347  # logger.info("%s\n" % f.getCovalentMaps(i))
348  for i, j in vss:
349  f.setCovalentMap(i, 0, j)
350  f.setCovalentMap(i, 4, j+[i])
351  for k in j:
352  f.setCovalentMap(k, 0, list(f.getCovalentMap(k, 0))+[i])
353  f.setCovalentMap(k, 4, list(f.getCovalentMap(k, 4))+[i])
354  # logger.info("--- After ---\n")
355  # for i in range(f.getNumMultipoles()):
356  # logger.info("%s\n" % f.getCovalentMaps(i))
357 
358 def AddVirtualSiteBonds(mod, ff):
359  # print "In AddVirtualSiteBonds"
360  for ir, R in enumerate(list(mod.topology.residues())):
361  A = list(R.atoms())
362  # print "Residue", ir, ":", R.name
363  for vs in ff._templates[R.name].virtualSites:
364  vi = vs.index
365  for ai in vs.atoms:
366  bi = sorted([A[ai], A[vi]])
367  # print "Adding Bond", ai, vi
368  mod.topology.addBond(*bi)
369 
370 def SetAmoebaNonbondedExcludeAll(system, topology):
371  """ Manually set the AmoebaVdwForce, AmoebaMultipoleForce to exclude all atoms belonging to the same residue """
372  # find atoms and residues
373  atom_residue_index = [a.residue.index for a in topology.atoms()]
374  residue_atoms = [[a.index for a in r.atoms()] for r in topology.residues()]
375  for f in system.getForces():
376  if f.__class__.__name__ == "AmoebaVdwForce":
377  for i in range(f.getNumParticles()):
378  f.setParticleExclusions(i, residue_atoms[atom_residue_index[i]])
379  elif f.__class__.__name__ == "AmoebaMultipoleForce":
380  for i in range(f.getNumMultipoles()):
381  f.setCovalentMap(i, 0, residue_atoms[atom_residue_index[i]])
382  for m in range(1, 4):
383  f.setCovalentMap(i, m, [])
384 
385 def MTSVVVRIntegrator(temperature, collision_rate, timestep, system, ninnersteps=4):
386  """
387  Create a multiple timestep velocity verlet with velocity randomization (VVVR) integrator.
388 
389  ARGUMENTS
390 
391  temperature (Quantity compatible with kelvin) - the temperature
392  collision_rate (Quantity compatible with 1/picoseconds) - the collision rate
393  timestep (Quantity compatible with femtoseconds) - the integration timestep
394  system (simtk.openmm.System) - system whose forces will be partitioned
395  ninnersteps (int) - number of inner timesteps (default: 4)
396 
397  RETURNS
398 
399  integrator (openmm.CustomIntegrator) - a VVVR integrator
400 
401  NOTES
402 
403  This integrator is equivalent to a Langevin integrator in the velocity Verlet discretization with a
404  timestep correction to ensure that the field-free diffusion constant is timestep invariant. The inner
405  velocity Verlet discretization is transformed into a multiple timestep algorithm.
406 
407  REFERENCES
408 
409  VVVR Langevin integrator:
410  * http://arxiv.org/abs/1301.3800
411  * http://arxiv.org/abs/1107.2967 (to appear in PRX 2013)
412 
413  TODO
414 
415  Move initialization of 'sigma' to setting the per-particle variables.
416 
417  """
418  # Multiple timestep Langevin integrator.
419  for i in system.getForces():
420  if i.__class__.__name__ in ["NonbondedForce", "CustomNonbondedForce", "AmoebaVdwForce", "AmoebaMultipoleForce"]:
421  # Slow force.
422  logger.info(i.__class__.__name__ + " is a Slow Force\n")
423  i.setForceGroup(1)
424  else:
425  logger.info(i.__class__.__name__ + " is a Fast Force\n")
426  # Fast force.
427  i.setForceGroup(0)
428 
429  kB = BOLTZMANN_CONSTANT_kB * AVOGADRO_CONSTANT_NA
430  kT = kB * temperature
431 
432  integrator = CustomIntegrator(timestep)
433 
434  integrator.addGlobalVariable("dt_fast", timestep/float(ninnersteps)) # fast inner timestep
435  integrator.addGlobalVariable("kT", kT) # thermal energy
436  integrator.addGlobalVariable("a", np.exp(-collision_rate*timestep)) # velocity mixing parameter
437  integrator.addGlobalVariable("b", np.sqrt((2/(collision_rate*timestep)) * np.tanh(collision_rate*timestep/2))) # timestep correction parameter
438  integrator.addPerDofVariable("sigma", 0)
439  integrator.addPerDofVariable("x1", 0) # position before application of constraints
440 
441  #
442  # Pre-computation.
443  # This only needs to be done once, but it needs to be done for each degree of freedom.
444  # Could move this to initialization?
445  #
446  integrator.addComputePerDof("sigma", "sqrt(kT/m)")
447 
448  #
449  # Velocity perturbation.
450  #
451  integrator.addComputePerDof("v", "sqrt(a)*v + sqrt(1-a)*sigma*gaussian")
452  integrator.addConstrainVelocities();
453 
454  #
455  # Symplectic inner multiple timestep.
456  #
457  integrator.addUpdateContextState();
458  integrator.addComputePerDof("v", "v + 0.5*b*dt*f1/m")
459  for innerstep in range(ninnersteps):
460  # Fast inner symplectic timestep.
461  integrator.addComputePerDof("v", "v + 0.5*b*dt_fast*f0/m")
462  integrator.addComputePerDof("x", "x + v*b*dt_fast")
463  integrator.addComputePerDof("x1", "x")
464  integrator.addConstrainPositions();
465  integrator.addComputePerDof("v", "v + 0.5*b*dt_fast*f0/m + (x-x1)/dt_fast")
466  integrator.addComputePerDof("v", "v + 0.5*b*dt*f1/m") # TODO: Additional velocity constraint correction?
467  integrator.addConstrainVelocities();
468 
469  #
470  # Velocity randomization
471  #
472  integrator.addComputePerDof("v", "sqrt(a)*v + sqrt(1-a)*sigma*gaussian")
473  integrator.addConstrainVelocities();
474 
475  return integrator
476 
477 
478 """Dictionary for building parameter identifiers. As usual they go like this:
479 Bond/length/OW.HW
480 The dictionary is two-layered because the same interaction type (Bond)
481 could be under two different parent types (HarmonicBondForce, AmoebaHarmonicBondForce)"""
482 suffix_dict = { "HarmonicBondForce" : {"Bond" : ["class1","class2"]},
483  "HarmonicAngleForce" : {"Angle" : ["class1","class2","class3"],},
484  "PeriodicTorsionForce" : {"Proper" : ["class1","class2","class3","class4"],},
485  "NonbondedForce" : {"Atom": ["type"]},
486  "CustomNonbondedForce" : {"Atom": ["class"]},
487  "GBSAOBCForce" : {"Atom": ["type"]},
488  "AmoebaBondForce" : {"Bond" : ["class1","class2"]},
489  "AmoebaAngleForce" : {"Angle" : ["class1","class2","class3"]},
490  "AmoebaStretchBendForce" : {"StretchBend" : ["class1","class2","class3"]},
491  "AmoebaVdwForce" : {"Vdw" : ["class"]},
492  "AmoebaMultipoleForce" : {"Multipole" : ["type","kz","kx"], "Polarize" : ["type"]},
493  "AmoebaUreyBradleyForce" : {"UreyBradley" : ["class1","class2","class3"]},
494  "Residues.Residue" : {"VirtualSite" : ["index"]},
495 
496  "ForceBalance" : {"GB": ["type"]},
497  }
498 
499 
500 pdict = "XML_Override"
501 
502 class OpenMM_Reader(BaseReader):
503  """ Class for parsing OpenMM force field files. """
504  def __init__(self,fnm):
505 
506  super(OpenMM_Reader,self).__init__(fnm)
507 
508  self.pdict = pdict
509 
510  def build_pid(self, element, parameter):
511  """ Build the parameter identifier (see _link_ for an example)
512  @todo Add a link here """
513  #InteractionType = ".".join([i.tag for i in list(element.iterancestors())][::-1][1:] + [element.tag])
514  ParentType = ".".join([i.tag for i in list(element.iterancestors())][::-1][1:])
515  InteractionType = element.tag
516  try:
517  if ParentType == "Residues.Residue":
518  pfx = list(element.iterancestors())[0].attrib["name"]
519  Involved = '.'.join([pfx+"-"+element.attrib[i] for i in suffix_dict[ParentType][InteractionType]])
520  else:
521  Involved1 = '.'.join([element.attrib[i] for i in suffix_dict[ParentType][InteractionType] if i in element.attrib])
522  suffix2 = [i.replace('class','type') for i in suffix_dict[ParentType][InteractionType]]
523  suffix3 = [i.replace('type','class') for i in suffix_dict[ParentType][InteractionType]]
524  Involved2 = '.'.join([element.attrib[i] for i in suffix2 if i in element.attrib])
525  Involved3 = '.'.join([element.attrib[i] for i in suffix3 if i in element.attrib])
526  # Keep the Involved string that is the longest (assuming that is the one that properly matched)
527  Involved = [Involved1, Involved2, Involved3][np.argmax(np.array([len(Involved1),len(Involved2),len(Involved3)]))]
528  return "/".join([InteractionType, parameter, Involved])
529  except:
530  logger.info("Minor warning: Parameter ID %s doesn't contain any atom types, redundancies are possible\n" % ("/".join([InteractionType, parameter])))
531  return "/".join([InteractionType, parameter])
532 
533 class OpenMM(Engine):
534 
535  """ Derived from Engine object for carrying out general purpose OpenMM calculations. """
536 
537  def __init__(self, name="openmm", **kwargs):
538  if not hasattr(self, 'valkwd'):
539  self.valkwd = ['ffxml', 'pdb', 'platname', 'precision', 'mmopts', 'vsite_bonds', 'implicit_solvent', 'restrain_k', 'freeze_atoms']
540  super(OpenMM,self).__init__(name=name, **kwargs)
541 
542  def setopts(self, platname="CUDA", precision="single", **kwargs):
543 
544  """ Called by __init__ ; Set OpenMM-specific options. """
545 
546 
547  if hasattr(self,'target'):
548  self.platname = self.target.platname
549  self.precision = self.target.precision
550  else:
551  self.platname = platname
552  self.precision = precision
553 
554  valnames = [Platform.getPlatform(i).getName() for i in range(Platform.getNumPlatforms())]
555  if self.platname not in valnames:
556  warn_press_key("Platform %s does not exist (valid options are %s (case-sensitive))" % (self.platname, valnames))
557  self.platname = 'Reference'
558  self.precision = self.precision.lower()
559  valprecs = ['single','mixed','double']
560  if self.precision not in valprecs:
561  logger.error("Please specify one of %s for precision\n" % valprecs)
562  raise RuntimeError
563 
564  if self.verbose: logger.info("Setting Platform to %s\n" % self.platname)
565  self.platform = Platform.getPlatformByName(self.platname)
566  if self.platname == 'CUDA':
567 
568  device = os.environ.get('CUDA_DEVICE',"0")
569  if self.verbose: logger.info("Setting CUDA Device to %s\n" % device)
570  self.platform.setPropertyDefaultValue("CudaDeviceIndex", device)
571  if self.verbose: logger.info("Setting CUDA Precision to %s\n" % self.precision)
572  self.platform.setPropertyDefaultValue("CudaPrecision", self.precision)
573  elif self.platname == 'OpenCL':
574  device = os.environ.get('OPENCL_DEVICE',"0")
575  if self.verbose: logger.info("Setting OpenCL Device to %s\n" % device)
576  self.platform.setPropertyDefaultValue("OpenCLDeviceIndex", device)
577  if self.verbose: logger.info("Setting OpenCL Precision to %s\n" % self.precision)
578  self.platform.setPropertyDefaultValue("OpenCLPrecision", self.precision)
579  self.simkwargs = {}
580  if 'implicit_solvent' in kwargs:
581  # Force implicit solvent to either On or Off.
582  self.ism = kwargs['implicit_solvent']
583  else:
584  self.ism = None
585 
586  def readsrc(self, **kwargs):
587 
588  """ Called by __init__ ; read files from the source directory.
589  Provide a molecule object or a coordinate file.
590  Add an optional PDB file for residues, atom names etc. """
591 
592  pdbfnm = None
593  # Determine the PDB file name.
594  if 'pdb' in kwargs and os.path.exists(kwargs['pdb']):
595  # Case 1. The PDB file name is provided explicitly
596  pdbfnm = kwargs['pdb']
597  if not os.path.exists(pdbfnm):
598  logger.error("%s specified but doesn't exist\n" % pdbfnm)
599  raise RuntimeError
600 
601  if 'mol' in kwargs:
602  self.mol = kwargs['mol']
603  elif 'coords' in kwargs:
604  self.mol = Molecule(kwargs['coords'])
605  if pdbfnm is None and kwargs['coords'].endswith('.pdb'):
606  pdbfnm = kwargs['coords']
607  else:
608  logger.error('Must provide either a molecule object or coordinate file.\n')
609  raise RuntimeError
610 
611  # If the PDB file exists, then it is copied directly to create
612  # the OpenMM pdb object rather than being written by the
613  # Molecule class.
614  if pdbfnm is not None:
615  self.abspdb = os.path.abspath(pdbfnm)
616  mpdb = Molecule(pdbfnm)
617  for i in ["chain", "atomname", "resid", "resname", "elem"]:
618  self.mol.Data[i] = mpdb.Data[i]
619 
620  # Store a separate copy of the molecule for reference restraint positions.
621  self.ref_mol = deepcopy(self.mol)
622 
623  def prepare(self, pbc=False, mmopts={}, **kwargs):
624 
625  """
626  Prepare the calculation. Note that we don't create the
627  Simulation object yet, because that may depend on MD
628  integrator parameters, thermostat, barostat etc.
629  """
630  # Introduced to attempt to fix a bug, but didn't work,
631  # Might be sensible code anyway.
632  # if hasattr(self.mol, 'boxes') and not pbc:
633  # del self.mol.Data['boxes']
634  # if pbc and not hasattr(self.mol, 'boxes'):
635  # logger.error('Requested periodic boundary conditions but coordinate file contains no boxes')
636  # raise RuntimeError
637 
638  if hasattr(self, 'abspdb'):
639  self.pdb = PDBFile(self.abspdb)
640  else:
641  pdb1 = "%s-1.pdb" % os.path.splitext(os.path.basename(self.mol.fnm))[0]
642  self.mol[0].write(pdb1)
643  self.pdb = PDBFile(pdb1)
644  os.unlink(pdb1)
645 
646 
647  if hasattr(self, 'FF'):
648  self.ffxml = [self.FF.openmmxml]
649  self.forcefield = ForceField(os.path.join(self.root, self.FF.ffdir, self.FF.openmmxml))
650  else:
651  self.ffxml = listfiles(kwargs.get('ffxml'), 'xml', err=True)
652  self.forcefield = ForceField(*self.ffxml)
653 
654 
656  self.vbonds = kwargs.get('vsite_bonds', 0)
657 
658 
659  self.mmopts = dict(mmopts)
660 
661 
662  self.AMOEBA = any(['Amoeba' in f.__class__.__name__ for f in self.forcefield._forces])
663 
664 
665  if 'restrain_k' in kwargs:
666  self.restrain_k = kwargs['restrain_k']
667  if 'freeze_atoms' in kwargs:
668  self.freeze_atoms = kwargs['freeze_atoms'][:]
669 
670 
671  if hasattr(self,'FF'):
672  if self.AMOEBA:
673  if self.FF.amoeba_pol is None:
674  logger.error('You must specify amoeba_pol if there are any AMOEBA forces.\n')
675  raise RuntimeError
676  if self.FF.amoeba_pol == 'mutual':
677  self.mmopts['polarization'] = 'mutual'
678  self.mmopts.setdefault('mutualInducedTargetEpsilon', self.FF.amoeba_eps if self.FF.amoeba_eps is not None else 1e-6)
679  self.mmopts['mutualInducedMaxIterations'] = 500
680  elif self.FF.amoeba_pol == 'direct':
681  self.mmopts['polarization'] = 'direct'
682  self.mmopts['rigidWater'] = self.FF.rigid_water
683  if self.FF.constrain_h is True:
684  self.mmopts['constraints'] = HBonds
685  logger.info('Constraining hydrogen bond lengths (SHAKE)')
686 
687 
688  self.pbc = pbc
689  if pbc:
690  minbox = min([self.mol.boxes[0].a, self.mol.boxes[0].b, self.mol.boxes[0].c])
691 
693  self.SetPME = True
694  # LPW: THIS CAUSES ISSUES! (AMOEBA system refuses to be created)
695  # self.mmopts.setdefault('nonbondedMethod', CutoffPeriodic)
696  self.mmopts.setdefault('nonbondedMethod', PME)
697  if self.AMOEBA:
698  nonbonded_cutoff = kwargs.get('nonbonded_cutoff', 7.0)
699  vdw_cutoff = kwargs.get('nonbonded_cutoff', 8.5)
700  vdw_cutoff = kwargs.get('vdw_cutoff', vdw_cutoff)
701  # Conversion to nanometers
702  nonbonded_cutoff /= 10
703  vdw_cutoff /= 10
704  if 'nonbonded_cutoff' in kwargs and 'vdw_cutoff' not in kwargs:
705  warn_press_key('AMOEBA detected and nonbonded_cutoff is set, but not vdw_cutoff; it will be set equal to nonbonded_cutoff')
706  if nonbonded_cutoff > 0.05*(float(int(minbox - 1))):
707  warn_press_key("nonbonded_cutoff = %.1f should be smaller than half the box size = %.1f Angstrom" % (nonbonded_cutoff*10, minbox))
708  if vdw_cutoff > 0.05*(float(int(minbox - 1))):
709  warn_press_key("vdw_cutoff = %.1f should be smaller than half the box size = %.1f Angstrom" % (vdw_cutoff*10, minbox))
710  self.mmopts.setdefault('nonbondedCutoff', nonbonded_cutoff*nanometer)
711  self.mmopts.setdefault('vdwCutoff', vdw_cutoff*nanometer)
712  self.mmopts.setdefault('aEwald', 5.4459052)
713  #self.mmopts.setdefault('pmeGridDimensions', [24,24,24])
714  else:
715  if 'vdw_cutoff' in kwargs:
716  warn_press_key('AMOEBA not detected, your provided vdw_cutoff will not be used')
717  nonbonded_cutoff = kwargs.get('nonbonded_cutoff', 8.5)
718  # Conversion to nanometers
719  nonbonded_cutoff /= 10
720  if nonbonded_cutoff > 0.05*(float(int(minbox - 1))):
721  warn_press_key("nonbonded_cutoff = %.1f should be smaller than half the box size = %.1f Angstrom" % (nonbonded_cutoff*10, minbox))
722 
723  self.mmopts.setdefault('nonbondedCutoff', nonbonded_cutoff*nanometer)
724  self.mmopts.setdefault('useSwitchingFunction', True)
725  self.mmopts.setdefault('switchingDistance', (nonbonded_cutoff-0.1)*nanometer)
726  self.mmopts.setdefault('useDispersionCorrection', True)
727  else:
728  if 'nonbonded_cutoff' in kwargs or 'vdw_cutoff' in kwargs:
729  warn_press_key('No periodic boundary conditions, your provided nonbonded_cutoff and vdw_cutoff will not be used')
730  self.SetPME = False
731  self.mmopts.setdefault('nonbondedMethod', NoCutoff)
732  self.mmopts['removeCMMotion'] = False
733 
734 
735  mod = self.generate_xyz_omm(self.mol)
736 
737  Top = mod.getTopology()
738  Atoms = list(Top.atoms())
739  Bonds = [(a.index, b.index) for a, b in list(Top.bonds())]
740 
741  # vss = [(i, [system.getVirtualSite(i).getParticle(j) for j in range(system.getVirtualSite(i).getNumParticles())]) \
742  # for i in range(system.getNumParticles()) if system.isVirtualSite(i)]
743  self.AtomLists = defaultdict(list)
744  self.AtomLists['Mass'] = [a.element.mass.value_in_unit(dalton) if a.element is not None else 0 for a in Atoms]
745  self.AtomLists['ParticleType'] = ['A' if m >= 1.0 else 'D' for m in self.AtomLists['Mass']]
746  self.AtomLists['ResidueNumber'] = [a.residue.index for a in Atoms]
747  self.AtomMask = [a == 'A' for a in self.AtomLists['ParticleType']]
748  self.realAtomIdxs = [i for i, a in enumerate(self.AtomMask) if a is True]
749 
750  def generate_xyz_omm(self, mol):
751 
752  self.xyz_omms = []
753  for I in range(len(mol)):
754  xyz = mol.xyzs[I]
755  xyz_omm = [Vec3(i[0],i[1],i[2]) for i in xyz]*angstrom
756  # An extra step with adding virtual particles
757  mod = Modeller(self.pdb.topology, xyz_omm)
758  mod.addExtraParticles(self.forcefield)
759  if self.pbc:
760  # Obtain the periodic box
761  if mol.boxes[I].alpha != 90.0 or mol.boxes[I].beta != 90.0 or mol.boxes[I].gamma != 90.0:
762  logger.error('OpenMM cannot handle nonorthogonal boxes.\n')
763  raise RuntimeError
764  box_omm = [Vec3(mol.boxes[I].a, 0, 0)*angstrom,
765  Vec3(0, mol.boxes[I].b, 0)*angstrom,
766  Vec3(0, 0, mol.boxes[I].c)*angstrom]
767  else:
768  box_omm = None
769  # Finally append it to list.
770  self.xyz_omms.append((mod.getPositions(), box_omm))
771  return mod
772 
773  def create_simulation(self, timestep=1.0, faststep=0.25, temperature=None, pressure=None, anisotropic=False, mts=False, collision=1.0, nbarostat=25, rpmd_beads=0, **kwargs):
774 
775  """
776  Create simulation object. Note that this also takes in some
777  options pertinent to system setup, including the type of MD
778  integrator and type of pressure control.
779  """
780 
781  # Divisor for the temperature (RPMD sets it to nonzero.)
782  self.tdiv = 1
783 
784 
785  if temperature:
786 
787  if mts:
788  if rpmd_beads > 0:
789  logger.error("No multiple timestep integrator without temperature control.\n")
790  raise RuntimeError
791  integrator = MTSVVVRIntegrator(temperature*kelvin, collision/picosecond,
792  timestep*femtosecond, self.system, ninnersteps=int(timestep/faststep))
793  else:
794  if rpmd_beads > 0:
795  logger.info("Creating RPMD integrator with %i beads.\n" % rpmd_beads)
796  self.tdiv = rpmd_beads
797  integrator = RPMDIntegrator(rpmd_beads, temperature*kelvin, collision/picosecond, timestep*femtosecond)
798  else:
799  integrator = LangevinIntegrator(temperature*kelvin, collision/picosecond, timestep*femtosecond)
800  else:
801 
802  if rpmd_beads > 0:
803  logger.error("No RPMD integrator without temperature control.\n")
804  raise RuntimeError
805  if mts: warn_once("No multiple timestep integrator without temperature control.")
806  integrator = VerletIntegrator(timestep*femtoseconds)
807 
808 
809  if pressure is not None:
810  if anisotropic:
811  barostat = MonteCarloAnisotropicBarostat([pressure, pressure, pressure]*atmospheres,
812  temperature*kelvin, nbarostat)
813  else:
814  barostat = MonteCarloBarostat(pressure*atmospheres, temperature*kelvin, nbarostat)
815  if self.pbc and pressure is not None: self.system.addForce(barostat)
816  elif pressure is not None: warn_once("Pressure is ignored because pbc is set to False.")
817 
818  # Add a restraint force if we have one.
819  self.restraint_frc_index = None
820  if hasattr(self, 'restrain_k') and self.restrain_k != 0.0:
821  restraint_frc = CustomExternalForce("0.5*k*((x-x0)^2+(y-y0)^2+(z-z0)^2)")
822  restraint_frc.addGlobalParameter("k", self.restrain_k * kilocalorie_per_mole / angstrom**2)
823  restraint_frc.addPerParticleParameter("x0")
824  restraint_frc.addPerParticleParameter("y0")
825  restraint_frc.addPerParticleParameter("z0")
826  for i, j in enumerate(self.realAtomIdxs):
827  restraint_frc.addParticle(j)
828  restraint_frc.setParticleParameters(i, j, [0.0, 0.0, 0.0])
829  self.restraint_frc_index = self.system.addForce(restraint_frc)
830 
831  # Freeze atoms if we have any.
832  if hasattr(self, 'freeze_atoms'):
833  for i in self.freeze_atoms:
834  j = self.realAtomIdxs[i]
835  self.system.setParticleMass(j, 0.0)
836 
837 
838  GrpTogether = ['AmoebaGeneralizedKirkwoodForce', 'AmoebaMultipoleForce','AmoebaWcaDispersionForce',
839  'CustomNonbondedForce', 'NonbondedForce']
840  GrpNums = {}
841  if not mts:
842  for j in self.system.getForces():
843  i = -1
844  if j.__class__.__name__ in GrpTogether:
845  for k in GrpNums:
846  if k in GrpTogether:
847  i = GrpNums[k]
848  break
849  if i == -1: i = len(set(GrpNums.values()))
850  GrpNums[j.__class__.__name__] = i
851  j.setForceGroup(i)
852 
853 
854  SetAmoebaVirtualExclusions(self.system)
855 
856  # test: exclude all Amoeba Nonbonded Forces within each residue
857  #SetAmoebaNonbondedExcludeAll(self.system, self.mod.topology)
858 
859 
860  self.simulation = Simulation(self.mod.topology, self.system, integrator, self.platform)
861 
862 
867 
868  def update_simulation(self, **kwargs):
869 
870  """
871  Create the simulation object, or update the force field
872  parameters in the existing simulation object. This should be
873  run when we write a new force field XML file.
874  """
875  if len(kwargs) > 0:
876  self.simkwargs = kwargs
877  self.forcefield = ForceField(*self.ffxml)
878  # OpenMM classes for force generators
879  ismgens = [forcefield.AmoebaGeneralizedKirkwoodGenerator, forcefield.AmoebaWcaDispersionGenerator,
880  forcefield.CustomGBGenerator, forcefield.GBSAOBCGenerator]
881  if self.ism is not None:
882  if self.ism == False:
883  self.forcefield._forces = [f for f in self.forcefield._forces if not any([isinstance(f, f_) for f_ in ismgens])]
884  elif self.ism == True:
885  if len([f for f in self.forcefield._forces if any([isinstance(f, f_) for f_ in ismgens])]) == 0:
886  logger.error("There is no implicit solvent model!\n")
887  raise RuntimeError
888  self.mod = Modeller(self.pdb.topology, self.pdb.positions)
889  self.mod.addExtraParticles(self.forcefield)
890  # Add bonds for virtual sites. (Experimental)
891  if self.vbonds: AddVirtualSiteBonds(self.mod, self.forcefield)
892  #printcool_dictionary(self.mmopts, title="Creating/updating simulation in engine %s with system settings:" % (self.name))
893  # for b in list(self.mod.topology.bonds()):
894  # print b[0].index, b[1].index
895  self.system = self.forcefield.createSystem(self.mod.topology, **self.mmopts)
896  self.vsinfo = PrepareVirtualSites(self.system)
897  self.nbcharges = np.zeros(self.system.getNumParticles())
898 
899  for i in self.system.getForces():
900  if isinstance(i, NonbondedForce):
901  self.nbcharges = np.array([i.getParticleParameters(j)[0]._value for j in range(i.getNumParticles())])
902  if self.SetPME:
903  i.setNonbondedMethod(i.PME)
904  if isinstance(i, AmoebaMultipoleForce):
905  if self.SetPME:
906  i.setNonbondedMethod(i.PME)
907 
908  #----
909  # If the virtual site parameters have changed,
910  # the simulation object must be remade.
911  #----
912  vsprm = GetVirtualSiteParameters(self.system)
913  if hasattr(self,'vsprm') and len(self.vsprm) > 0 and np.max(np.abs(vsprm - self.vsprm)) != 0.0:
914  if hasattr(self, 'simulation'):
915  delattr(self, 'simulation')
916  self.vsprm = vsprm.copy()
917 
918  if hasattr(self, 'simulation'):
919  UpdateSimulationParameters(self.system, self.simulation)
920  else:
921  self.create_simulation(**self.simkwargs)
922 
923  def set_restraint_positions(self, shot):
924  """
925  Set reference positions for energy restraints. This may be a different set of positions
926  from the "current" positions that are stored in self.mol and self.xyz_omm.
927  """
928  if self.restraint_frc_index is not None:
929 
930  xyz = self.ref_mol.xyzs[shot] / 10.0
931  frc = self.simulation.system.getForce(self.restraint_frc_index)
932  for i, j in enumerate(self.realAtomIdxs):
933  frc.setParticleParameters(i, j, xyz[i])
934  frc.updateParametersInContext(self.simulation.context)
935  else:
936  raise RuntimeError('Asked to set restraint positions, but no restraint force has been added to the system')
937 
938  def set_positions(self, shot):
939 
940  """
941  Set the positions and periodic box vectors to one of the
942  stored coordinates.
943 
944  *** NOTE: If you run a MD simulation, then the coordinates are
945  overwritten by the MD trajectory. ***
946  """
947  #----
948  # Ideally the virtual site parameters would be copied but they're not.
949  # Instead we update the vsite positions manually.
950  #----
951  # if self.FF.rigid_water:
952  # simulation.context.applyConstraints(1e-8)
953  # else:
954  # simulation.context.computeVirtualSites()
955  #----
956  # NOTE: Periodic box vectors must be set FIRST
957  if self.pbc:
958  self.simulation.context.setPeriodicBoxVectors(*self.xyz_omms[shot][1])
959  # self.simulation.context.setPositions(ResetVirtualSites(self.xyz_omms[shot][0], self.system))
960  # self.simulation.context.setPositions(ResetVirtualSites_fast(self.xyz_omms[shot][0], self.vsinfo))
961  self.simulation.context.setPositions(self.xyz_omms[shot][0])
962  self.simulation.context.computeVirtualSites()
963 
964  def get_charges(self):
965  logger.error('OpenMM engine does not have get_charges (should be trivial to implement however.)')
966  raise NotImplementedError
967 
968  def compute_volume(self, box_vectors):
969  """ Compute the total volume of an OpenMM system. """
970  [a,b,c] = box_vectors
971  A = np.array([a/a.unit, b/a.unit, c/a.unit])
972  # Compute volume of parallelepiped.
973  volume = np.linalg.det(A) * a.unit**3
974  return volume
975 
976  def compute_mass(self, system):
977  """ Compute the total mass of an OpenMM system. """
978  mass = 0.0 * amu
979  for i in range(system.getNumParticles()):
980  mass += system.getParticleMass(i)
981  return mass
982 
983  def evaluate_one_(self, force=False, dipole=False):
984  """ Perform a single point calculation on the current geometry. """
985 
986  state = self.simulation.context.getState(getPositions=dipole, getEnergy=True, getForces=force)
987  Result = {}
988  Result["Energy"] = state.getPotentialEnergy() / kilojoules_per_mole
989  if force:
990  Force = state.getForces(asNumpy=True).value_in_unit(kilojoule/(nanometer*mole))
991  # Extract forces belonging to real atoms only
992  Result["Force"] = Force[self.realAtomIdxs].flatten()
993  if dipole:
994  Result["Dipole"] = get_dipole(self.simulation, q=self.nbcharges, mass=self.AtomLists['Mass'], positions=state.getPositions())
995  return Result
996 
997  def evaluate_(self, force=False, dipole=False, traj=False):
998 
999  """
1000  Utility function for computing energy, and (optionally) forces and dipoles using OpenMM.
1001 
1002  Inputs:
1003  force: Switch for calculating the force.
1004  dipole: Switch for calculating the dipole.
1005  traj: Trajectory (listing of coordinate and box 2-tuples). If provide, will loop over these snapshots.
1006  Otherwise will do a single point evaluation at the current geometry.
1007 
1008  Outputs:
1009  Result: Dictionary containing energies, forces and/or dipoles.
1010  """
1011 
1012  self.update_simulation()
1013 
1014  # If trajectory flag set to False, perform a single-point calculation.
1015  if not traj: return evaluate_one_(force, dipole)
1016  Energies = []
1017  Forces = []
1018  Dipoles = []
1019  for I in range(len(self.xyz_omms)):
1020  self.set_positions(I)
1021  R1 = self.evaluate_one_(force, dipole)
1022  Energies.append(R1["Energy"])
1023  if force: Forces.append(R1["Force"])
1024  if dipole: Dipoles.append(R1["Dipole"])
1025  # Compile it all into the dictionary object
1026  Result = OrderedDict()
1027 
1028  Result["Energy"] = np.array(Energies)
1029  if force: Result["Force"] = np.array(Forces)
1030  if dipole: Result["Dipole"] = np.array(Dipoles)
1031  return Result
1032 
1033  def energy_one(self, shot):
1034  self.set_positions(shot)
1035  return self.evaluate_()["Energy"]
1036 
1037  def energy_force_one(self, shot):
1038  self.set_positions(shot)
1039  Result = self.evaluate_(force=True)
1040  return np.hstack((Result["Energy"].reshape(-1,1), Result["Force"]))
1041 
1042  def energy(self):
1043  return self.evaluate_(traj=True)["Energy"]
1044 
1045  def energy_force(self):
1046  """ Loop through the snapshots and compute the energies and forces using OpenMM. """
1047  Result = self.evaluate_(force=True, traj=True)
1048  E = Result["Energy"].reshape(-1,1)
1049  F = Result["Force"]
1050  return np.hstack((Result["Energy"].reshape(-1,1), Result["Force"]))
1051 
1052  def energy_dipole(self):
1053  """ Loop through the snapshots and compute the energies and forces using OpenMM. """
1054  Result = self.evaluate_(dipole=True, traj=True)
1055  return np.hstack((Result["Energy"].reshape(-1,1), Result["Dipole"]))
1056 
1057  def build_mass_weighted_hessian(self, shot=0, optimize=True):
1058  """OpenMM single frame hessian evaluation
1059  Since OpenMM doesnot provide a Hessian evaluation method, we used finite difference on forces
1060 
1061  Parameters
1062  ----------
1063  shot: int
1064  The frame number in the trajectory of this target
1065 
1066  Returns
1067  -------
1068  hessian: np.array with shape 3N x 3N, N = number of "real" atoms
1069  The result hessian matrix.
1070  The row indices are fx0, fy0, fz0, fx1, fy1, ...
1071  The column indices are x0, y0, z0, x1, y1, ..
1072  The unit is kilojoule / (nanometer^2 * mole * dalton) => 10^24 s^-2
1073  """
1074  self.update_simulation()
1075  if optimize is True:
1076  self.optimize(shot, crit=1e-8)
1077  else:
1078  warn_once("Computing mass-weighted hessian without geometry optimization")
1079  self.set_positions(shot)
1080  context = self.simulation.context
1081  pos = context.getState(getPositions=True).getPositions(asNumpy=True)
1082  # pull real atoms and their mass
1083  massList = np.array(self.AtomLists['Mass'])[self.realAtomIdxs] # unit in dalton
1084  # initialize an empty hessian matrix
1085  noa = len(self.realAtomIdxs)
1086  hessian = np.empty((noa*3, noa*3), dtype=float)
1087  # finite difference step size
1088  diff = Quantity(0.0001, unit=nanometer)
1089  coef = 1.0 / (0.0001 * 2) # 1/2h
1090  for i, i_atom in enumerate(self.realAtomIdxs):
1091  massWeight = 1.0 / np.sqrt(massList * massList[i])
1092  # loop over the x, y, z coordinates
1093  for j in range(3):
1094  # plus perturbation
1095  pos[i_atom][j] += diff
1096  context.setPositions(pos)
1097  grad_plus = context.getState(getForces=True).getForces(asNumpy=True).value_in_unit(kilojoule/(nanometer*mole))
1098  grad_plus = -grad_plus[self.realAtomIdxs] # gradients are negative forces
1099  # minus perturbation
1100  pos[i_atom][j] -= 2*diff
1101  context.setPositions(pos)
1102  grad_minus = context.getState(getForces=True).getForces(asNumpy=True).value_in_unit(kilojoule/(nanometer*mole))
1103  grad_minus = -grad_minus[self.realAtomIdxs] # gradients are negative forces
1104  # set the perturbation back to zero
1105  pos[i_atom][j] += diff
1106  # fill one row of the hessian matrix
1107  hessian[i*3+j] = np.ravel((grad_plus - grad_minus) * coef * massWeight[:, np.newaxis])
1108  # make hessian symmetric by averaging upper right and lower left
1109  hessian += hessian.T
1110  hessian *= 0.5
1111  # recover the original position
1112  context.setPositions(pos)
1113  return hessian
1114 
1115  def normal_modes(self, shot=0, optimize=True):
1116  """OpenMM Normal Mode Analysis
1117  Since OpenMM doesnot provide a Hessian evaluation method, we used finite difference on forces
1118 
1119  Parameters
1120  ----------
1121  shot: int
1122  The frame number in the trajectory of this target
1123  optimize: bool, default True
1124  Optimize the geometry before evaluating the normal modes
1125 
1126  Returns
1127  -------
1128  freqs: np.array with shape (3N - 6) x 1, N = number of "real" atoms
1129  Harmonic frequencies, sorted from smallest to largest, with the 6 smallest removed, in unit cm^-1
1130  normal_modes: np.array with shape (3N - 6) x (3N), N = number of "real" atoms
1131  The normal modes corresponding to each of the frequencies, scaled by mass^-1/2.
1132  """
1133  if self.precision == 'single':
1134  warn_once("Single-precision OpenMM engine used for normal mode analysis - recommend that you use mix or double precision.")
1135  if not optimize:
1136  warn_once("Asking for normal modes without geometry optimization?")
1137  # step 0: check number of real atoms
1138  noa = len(self.realAtomIdxs)
1139  if noa < 2:
1140  error('normal mode analysis not suitable for system with one or less atoms')
1141  # step 1: build a full hessian matrix
1142  hessian_matrix = self.build_mass_weighted_hessian(shot=shot, optimize=optimize)
1143  # step 2: diagonalize the hessian matrix
1144  eigvals, eigvecs = np.linalg.eigh(hessian_matrix)
1145  # step 3: convert eigenvalues to frequencies
1146  coef = 0.5 / np.pi * 33.3564095 # 10^12 Hz => cm-1
1147  negatives = (eigvals >= 0).astype(int) * 2 - 1 # record the negative ones
1148  freqs = np.sqrt(np.abs(eigvals)) * coef * negatives
1149  # step 4: convert eigenvectors to normal modes
1150  # re-arange to row index and shape
1151  normal_modes = eigvecs.T.reshape(noa*3, noa, 3)
1152  # step 5: Remove mass weighting from eigenvectors
1153  massList = np.array(self.AtomLists['Mass'])[self.realAtomIdxs] # unit in dalton
1154  for i in range(normal_modes.shape[0]):
1155  mode = normal_modes[i]
1156  mode /= np.sqrt(massList[:,np.newaxis])
1157  mode /= np.linalg.norm(mode)
1158  # step 5: remove the 6 freqs with smallest abs value and corresponding normal modes
1159  n_remove = 5 if len(self.realAtomIdxs) == 2 else 6
1160  larger_freq_idxs = np.sort(np.argpartition(np.abs(freqs), n_remove)[n_remove:])
1161  freqs = freqs[larger_freq_idxs]
1162  normal_modes = normal_modes[larger_freq_idxs]
1163  return freqs, normal_modes
1164 
1165  def optimize(self, shot, crit=1e-4, disable_vsite=False, align=True, include_restraint_energy=False):
1166 
1167  """
1168  Optimize the geometry and align the optimized
1169  geometry to the starting geometry.
1170 
1171  Parameters
1172  ----------
1173  shot : int
1174  The snapshot number to be minimized
1175  crit : float
1176  Convergence criterion in kJ/mol
1177  disable_vsite : bool
1178  Disable virtual sites (needed for SMIRNOFF)
1179  include_restraint_energy : bool
1180  Include energy component from CustomExternalForce
1181 
1182  Returns
1183  -------
1184  E : float
1185  Potential energy of the system
1186  rmsd : float
1187  RMSD of the system (w/r.t. starting geometry) in Angstrom
1188  """
1189 
1190  steps = int(max(1, -1*np.log10(crit)))
1191  self.update_simulation()
1192  self.set_positions(shot)
1193  if self.restraint_frc_index is not None:
1194  self.set_restraint_positions(shot)
1195  # Get the previous geometry.
1196  X0 = self.simulation.context.getState(getPositions=True).getPositions(asNumpy=True).value_in_unit(angstrom)[self.realAtomIdxs]
1197  # printcool_dictionary(energy_components(self.simulation), title='Energy component analysis before minimization, shot %i' % shot)
1198  # Minimize the energy. Optimizer works best in "steps".
1199  for logc in np.linspace(0, np.log10(crit), steps):
1200  self.simulation.minimizeEnergy(tolerance=10**logc*kilojoule/mole, maxIterations=100000)
1201  # check if energy minimization is successful
1202  # try 1000 times with 10 steps each as openmm minimizer is not very stable at the tolerance
1203  for _ in range(1000):
1204  e_minimized = self.simulation.context.getState(getEnergy=True).getPotentialEnergy().value_in_unit(kilojoule_per_mole)
1205  self.simulation.minimizeEnergy(tolerance=crit*kilojoule_per_mole, maxIterations=10)
1206  e_new = self.simulation.context.getState(getEnergy=True).getPotentialEnergy().value_in_unit(kilojoule_per_mole)
1207  if abs(e_new - e_minimized) < crit * 10:
1208  break
1209  else:
1210  logger.error("Energy minimization did not converge")
1211  raise RuntimeError("Energy minimization did not converge")
1212  # Remove the restraint energy from the total energy if desired.
1213  groups = set(range(32))
1214  if self.restraint_frc_index is not None and not include_restraint_energy:
1215  frc = self.simulation.system.getForce(self.restraint_frc_index)
1216  groups.remove(frc.getForceGroup())
1217  # printcool_dictionary(energy_components(self.simulation), title='Energy component analysis after minimization, shot %i' % shot)
1218  S = self.simulation.context.getState(getPositions=True, getEnergy=True, groups=groups)
1219  # Get the optimized geometry.
1220  X1 = S.getPositions(asNumpy=True).value_in_unit(angstrom)[self.realAtomIdxs]
1221  E = S.getPotentialEnergy().value_in_unit(kilocalorie_per_mole)
1222  # Align to original geometry.
1223  M = deepcopy(self.mol[0])
1224  M += deepcopy(M)
1225  M.xyzs = [X0, X1]
1226  if not self.pbc and align:
1227  M.align(center=False)
1228  X1 = M.xyzs[1]
1229  if disable_vsite:
1230  self.simulation.context.setPositions(X1 * angstrom)
1231  else:
1232  # Create virtual sites before setting positions
1233  mod = Modeller(self.pdb.topology, X1*angstrom)
1234  mod.addExtraParticles(self.forcefield)
1235  self.simulation.context.setPositions(ResetVirtualSites_fast(mod.getPositions(), self.vsinfo))
1236  return E, M.ref_rmsd(0)[1], M[1]
1237 
1238  def getContextPosition(self, removeVirtual=False):
1239  """
1240  Get current position from simulation context.
1241 
1242  Parameters
1243  ----------
1244  removeVirtual: bool
1245  Remove positions of virtual atoms, result will only have positions of real atoms.
1246 
1247  Returns
1248  -------
1249  pos: np.ndarray of shape (N x 3)
1250  Position array in unit of Angstrom. If removeVirtual=True, N = No. real atoms, else N = No. all atoms.
1251 
1252  """
1253  pos = self.simulation.context.getState(getPositions=True).getPositions(asNumpy=True).value_in_unit(angstrom)
1254  if removeVirtual:
1255  pos = pos[self.realAtomIdxs]
1256  return pos
1257 
1258  def multipole_moments(self, shot=0, optimize=True, polarizability=False):
1259 
1260  """ Return the multipole moments of the i-th snapshot in Debye and Buckingham units. """
1261 
1262  self.update_simulation()
1263 
1264  if polarizability:
1265  logger.error("Polarizability calculation is available in TINKER only.\n")
1266  raise NotImplementedError
1267 
1268  if (self.platname in ['CUDA', 'OpenCL'] and self.precision in ['single', 'mixed']):
1269  crit = 1e-4
1270  else:
1271  crit = 1e-6
1272 
1273  if optimize: self.optimize(shot, crit=crit)
1274  else: self.set_positions(shot)
1275 
1276  moments = get_multipoles(self.simulation)
1277 
1278  dipole_dict = OrderedDict(zip(['x','y','z'], moments[:3]))
1279  quadrupole_dict = OrderedDict(zip(['xx','xy','yy','xz','yz','zz'], moments[3:10]))
1280 
1281  calc_moments = OrderedDict([('dipole', dipole_dict), ('quadrupole', quadrupole_dict)])
1282 
1283  return calc_moments
1284 
1285  def energy_rmsd(self, shot=0, optimize=True):
1286 
1287  """ Calculate energy of the 1st structure (optionally minimize and return the minimized energy and RMSD). In kcal/mol. """
1288 
1289  self.update_simulation()
1290 
1291  if (self.platname in ['CUDA', 'OpenCL'] and self.precision in ['single', 'mixed']):
1292  crit = 1e-4
1293  else:
1294  crit = 1e-6
1295 
1296  rmsd = 0.0
1297  if optimize:
1298  E, rmsd, _ = self.optimize(shot, crit=crit)
1299  else:
1300  self.set_positions(shot)
1301  E = self.simulation.context.getState(getEnergy=True).getPotentialEnergy().value_in_unit(kilocalories_per_mole)
1302 
1303  return E, rmsd
1304 
1305  def interaction_energy(self, fraga, fragb):
1306 
1307  """ Calculate the interaction energy for two fragments. """
1308 
1309  self.update_simulation()
1310 
1311  if self.name == 'A' or self.name == 'B':
1312  logger.error("Don't name the engine A or B!\n")
1313  raise RuntimeError
1314 
1315  # Create two subengines.
1316  if hasattr(self,'target'):
1317  if not hasattr(self,'A'):
1318  self.A = OpenMM(name="A", mol=self.mol.atom_select(fraga), target=self.target)
1319  if not hasattr(self,'B'):
1320  self.B = OpenMM(name="B", mol=self.mol.atom_select(fragb), target=self.target)
1321  else:
1322  if not hasattr(self,'A'):
1323  self.A = OpenMM(name="A", mol=self.mol.atom_select(fraga), platname=self.platname, \
1324  precision=self.precision, ffxml=self.ffxml, mmopts=self.mmopts)
1325  if not hasattr(self,'B'):
1326  self.B = OpenMM(name="B", mol=self.mol.atom_select(fragb), platname=self.platname, \
1327  precision=self.precision, ffxml=self.ffxml, mmopts=self.mmopts)
1328 
1329  # Interaction energy needs to be in kcal/mol.
1330  D = self.energy()
1331  A = self.A.energy()
1332  B = self.B.energy()
1333 
1334  return (D - A - B) / 4.184
1335 
1336  def molecular_dynamics(self, nsteps, timestep, temperature=None, pressure=None, nequil=0, nsave=1000, minimize=True, anisotropic=False, save_traj=False, verbose=False, **kwargs):
1337 
1338  """
1339  Method for running a molecular dynamics simulation.
1340 
1341  Required arguments:
1342  nsteps = (int) Number of total time steps
1343  timestep = (float) Time step in FEMTOSECONDS
1344  temperature = (float) Temperature control (Kelvin)
1345  pressure = (float) Pressure control (atmospheres)
1346  nequil = (int) Number of additional time steps at the beginning for equilibration
1347  nsave = (int) Step interval for saving and printing data
1348  minimize = (bool) Perform an energy minimization prior to dynamics
1349 
1350  Returns simulation data:
1351  Rhos = (array) Density in kilogram m^-3
1352  Potentials = (array) Potential energies
1353  Kinetics = (array) Kinetic energies
1354  Volumes = (array) Box volumes
1355  Dips = (3xN array) Dipole moments
1356  EComps = (dict) Energy components
1357  """
1358 
1359  if float(int(float(nequil)/float(nsave))) != float(nequil)/float(nsave):
1360  logger.error("Please set nequil to an integer multiple of nsave\n")
1361  raise RuntimeError
1362  iequil = int(nequil/nsave)
1363 
1364  if float(int(float(nsteps)/float(nsave))) != float(nsteps)/float(nsave):
1365  logger.error("Please set nsteps to an integer multiple of nsave\n")
1366  raise RuntimeError
1367  isteps = int(nsteps/nsave)
1368  nsave = int(nsave)
1369 
1370  if hasattr(self, 'simulation'):
1371  logger.warning('Deleting the simulation object and re-creating for MD\n')
1372  delattr(self, 'simulation')
1373 
1374  self.update_simulation(timestep=timestep, temperature=temperature, pressure=pressure, anisotropic=anisotropic, **kwargs)
1375  self.set_positions(0)
1376 
1377  # Minimize the energy.
1378  if minimize:
1379  if verbose: logger.info("Minimizing the energy... (starting energy % .3f kJ/mol)" %
1380  self.simulation.context.getState(getEnergy=True).getPotentialEnergy().value_in_unit(kilojoule_per_mole))
1381  self.simulation.minimizeEnergy()
1382  if verbose: logger.info("Done (final energy % .3f kJ/mol)\n" %
1383  self.simulation.context.getState(getEnergy=True).getPotentialEnergy().value_in_unit(kilojoule_per_mole))
1384 
1385  # Serialize the system if we want.
1386  Serialize = 0
1387  if Serialize:
1388  serial = XmlSerializer.serializeSystem(system)
1389  with wopen('%s_system.xml' % phase) as f: f.write(serial)
1390 
1391  # Determine number of degrees of freedom; the center of mass motion remover is also a constraint.
1392  kB = BOLTZMANN_CONSTANT_kB * AVOGADRO_CONSTANT_NA
1393 
1394  # Compute total mass.
1395  self.mass = self.compute_mass(self.system).in_units_of(gram / mole) / AVOGADRO_CONSTANT_NA
1396 
1397  # Determine number of degrees of freedom.
1398  self.ndof = 3*(self.system.getNumParticles() - sum([self.system.isVirtualSite(i) for i in range(self.system.getNumParticles())])) \
1399  - self.system.getNumConstraints() - 3*self.pbc
1400 
1401  # Initialize statistics.
1402  edecomp = OrderedDict()
1403  # Stored coordinates, box vectors
1404  self.xyz_omms = []
1405  # Densities, potential and kinetic energies, box volumes, dipole moments
1406  Rhos = []
1407  Potentials = []
1408  Kinetics = []
1409  Volumes = []
1410  Dips = []
1411  Temps = []
1412  #========================#
1413  # Now run the simulation #
1414  #========================#
1415  # Initialize velocities.
1416  self.simulation.context.setVelocitiesToTemperature(temperature*kelvin)
1417  # Equilibrate.
1418  if iequil > 0:
1419  if verbose: logger.info("Equilibrating...\n")
1420  if self.pbc:
1421  if verbose: logger.info("%6s %9s %9s %13s %10s %13s\n" % ("Iter.", "Time(ps)", "Temp(K)", "Epot(kJ/mol)", "Vol(nm^3)", "Rho(kg/m^3)"))
1422  else:
1423  if verbose: logger.info("%6s %9s %9s %13s\n" % ("Iter.", "Time(ps)", "Temp(K)", "Epot(kJ/mol)"))
1424  for iteration in range(-1, iequil):
1425  if iteration >= 0:
1426  self.simulation.step(nsave)
1427  state = self.simulation.context.getState(getEnergy=True,getPositions=True,getVelocities=False,getForces=False)
1428  kinetic = state.getKineticEnergy()/self.tdiv
1429  potential = state.getPotentialEnergy()
1430  if self.pbc:
1431  box_vectors = state.getPeriodicBoxVectors()
1432  volume = self.compute_volume(box_vectors)
1433  density = (self.mass / volume).in_units_of(kilogram / meter**3)
1434  else:
1435  volume = 0.0 * nanometers ** 3
1436  density = 0.0 * kilogram / meter ** 3
1437  kinetic_temperature = 2.0 * kinetic / kB / self.ndof # (1/2) ndof * kB * T = KE
1438  if self.pbc:
1439  if verbose: logger.info("%6d %9.3f %9.3f % 13.3f %10.4f %13.4f\n" % (iteration+1, state.getTime() / picoseconds,
1440  kinetic_temperature / kelvin, potential / kilojoules_per_mole,
1441  volume / nanometers**3, density / (kilogram / meter**3)))
1442  else:
1443  if verbose: logger.info("%6d %9.3f %9.3f % 13.3f\n" % (iteration+1, state.getTime() / picoseconds,
1444  kinetic_temperature / kelvin, potential / kilojoules_per_mole))
1445  # Collect production data.
1446  if verbose: logger.info("Production...\n")
1447  if self.pbc:
1448  if verbose: logger.info("%6s %9s %9s %13s %10s %13s\n" % ("Iter.", "Time(ps)", "Temp(K)", "Epot(kJ/mol)", "Vol(nm^3)", "Rho(kg/m^3)"))
1449  else:
1450  if verbose: logger.info("%6s %9s %9s %13s\n" % ("Iter.", "Time(ps)", "Temp(K)", "Epot(kJ/mol)"))
1451  if save_traj:
1452  self.simulation.reporters.append(PDBReporter('%s-md.pdb' % self.name, nsteps))
1453  self.simulation.reporters.append(DCDReporter('%s-md.dcd' % self.name, nsave))
1454 
1455  for iteration in range(-1, isteps):
1456  # Propagate dynamics.
1457  if iteration >= 0: self.simulation.step(nsave)
1458  # Compute properties.
1459  state = self.simulation.context.getState(getEnergy=True,getPositions=True,getVelocities=False,getForces=False)
1460  kinetic = state.getKineticEnergy()/self.tdiv
1461  potential = state.getPotentialEnergy()
1462  kinetic_temperature = 2.0 * kinetic / kB / self.ndof
1463  if self.pbc:
1464  box_vectors = state.getPeriodicBoxVectors()
1465  volume = self.compute_volume(box_vectors)
1466  density = (self.mass / volume).in_units_of(kilogram / meter**3)
1467  else:
1468  box_vectors = None
1469  volume = 0.0 * nanometers ** 3
1470  density = 0.0 * kilogram / meter ** 3
1471  positions = state.getPositions(asNumpy=True).astype(np.float32) * nanometer
1472  self.xyz_omms.append([positions, box_vectors])
1473  # Perform energy decomposition.
1474  for comp, val in energy_components(self.simulation).items():
1475  if comp in edecomp:
1476  edecomp[comp].append(val)
1477  else:
1478  edecomp[comp] = [val]
1479  if self.pbc:
1480  if verbose: logger.info("%6d %9.3f %9.3f % 13.3f %10.4f %13.4f\n" % (iteration+1, state.getTime() / picoseconds,
1481  kinetic_temperature / kelvin, potential / kilojoules_per_mole,
1482  volume / nanometers**3, density / (kilogram / meter**3)))
1483  else:
1484  if verbose: logger.info("%6d %9.3f %9.3f % 13.3f\n" % (iteration+1, state.getTime() / picoseconds,
1485  kinetic_temperature / kelvin, potential / kilojoules_per_mole))
1486  Temps.append(kinetic_temperature / kelvin)
1487  Rhos.append(density.value_in_unit(kilogram / meter**3))
1488  Potentials.append(potential / kilojoules_per_mole)
1489  Kinetics.append(kinetic / kilojoules_per_mole)
1490  Volumes.append(volume / nanometer**3)
1491  Dips.append(get_dipole(self.simulation,positions=self.xyz_omms[-1][0]))
1492  Rhos = np.array(Rhos)
1493  Potentials = np.array(Potentials)
1494  Kinetics = np.array(Kinetics)
1495  Volumes = np.array(Volumes)
1496  Dips = np.array(Dips)
1497  Ecomps = OrderedDict([(key, np.array(val)) for key, val in edecomp.items()])
1498  Ecomps["Potential Energy"] = Potentials
1499  Ecomps["Kinetic Energy"] = Kinetics
1500  Ecomps["Temperature"] = Temps
1501  Ecomps["Total Energy"] = Potentials + Kinetics
1502  # Initialized property dictionary.
1503  prop_return = OrderedDict()
1504  prop_return.update({'Rhos': Rhos, 'Potentials': Potentials, 'Kinetics': Kinetics, 'Volumes': Volumes, 'Dips': Dips, 'Ecomps': Ecomps})
1505  return prop_return
1506 
1507  def scale_box(self, x=1.0, y=1.0, z=1.0):
1508  """ Scale the positions of molecules and box vectors. Molecular structures will be kept the same.
1509  Input: x, y, z :scaling factors (float)
1510  Output: None
1511  After this function call, self.xyz_omms will be overwritten with the new positions and box_vectors.
1512  """
1513  if not hasattr(self, 'xyz_omms'):
1514  logger.error("molecular_dynamics has not finished correctly!")
1515  raise RuntimeError
1516  # record the indices of each residue
1517  if not hasattr(self, 'residues_idxs'):
1518  self.residues_idxs = np.array([[a.index for a in r.atoms()] for r in self.simulation.topology.residues()])
1519  scale_xyz = np.array([x,y,z])
1520  # loop over each frame and replace items
1521  for i in range(len(self.xyz_omms)):
1522  pos, box = self.xyz_omms[i]
1523  # scale the box vectors
1524  new_box = np.array(box/nanometer) * scale_xyz
1525  # convert pos to np.array
1526  positions = np.array(pos/nanometer)
1527  # Positions of each residue
1528  residue_positions = np.take(positions, self.residues_idxs, axis=0)
1529  # Center of each residue
1530  res_center_positions = np.mean(residue_positions, axis=1)
1531  # Shift of each residue
1532  center_pos_shift = res_center_positions * (scale_xyz-1)
1533  # New positions
1534  new_pos = (residue_positions + center_pos_shift[:,np.newaxis,:]).reshape(-1,3)
1535  # update this frame
1536  self.xyz_omms[i] = [new_pos.astype(np.float32)*nanometer, new_box*nanometer]
1537 
1538 class Liquid_OpenMM(Liquid):
1539  """ Condensed phase property matching using OpenMM. """
1540  def __init__(self,options,tgt_opts,forcefield):
1541  # Time interval (in ps) for writing coordinates
1542  self.set_option(tgt_opts,'force_cuda',forceprint=True)
1543  # Enable multiple timestep integrator
1544  self.set_option(tgt_opts,'mts_integrator',forceprint=True)
1545  # Enable ring polymer MD
1546  self.set_option(options,'rpmd_beads',forceprint=True)
1547  # OpenMM precision
1548  self.set_option(tgt_opts,'openmm_precision','precision',default="mixed")
1549  # OpenMM platform
1550  self.set_option(tgt_opts,'openmm_platform','platname',default="CUDA")
1551  # Name of the liquid coordinate file.
1552  self.set_option(tgt_opts,'liquid_coords',default='liquid.pdb',forceprint=True)
1553  # Name of the gas coordinate file.
1554  self.set_option(tgt_opts,'gas_coords',default='gas.pdb',forceprint=True)
1555  # Name of the surface tension coordinate file. (e.g. an elongated box with a film of water)
1556  self.set_option(tgt_opts,'nvt_coords',default='surf.pdb',forceprint=True)
1557  # Set the number of steps between MC barostat adjustments.
1558  self.set_option(tgt_opts,'mc_nbarostat')
1559  # Class for creating engine object.
1560  self.engine_ = OpenMM
1561  # Name of the engine to pass to npt.py.
1562  self.engname = "openmm"
1563  # Command prefix.
1564  self.nptpfx = "bash runcuda.sh"
1565  if tgt_opts['remote_backup']:
1566  self.nptpfx += " -b"
1567  # Extra files to be linked into the temp-directory.
1568  self.nptfiles = []
1569  self.nvtfiles = []
1570  # Set some options for the polarization correction calculation.
1571  self.gas_engine_args = {}
1572  # Scripts to be copied from the ForceBalance installation directory.
1573  self.scripts = ['runcuda.sh']
1574  # Initialize the base class.
1575  super(Liquid_OpenMM,self).__init__(options,tgt_opts,forcefield)
1576  # Send back the trajectory file.
1577  if self.save_traj > 0:
1578  self.extra_output = ['liquid-md.pdb', 'liquid-md.dcd']
1579  # These functions need to be called after self.nptfiles is populated
1580  self.post_init(options)
1581 
1582 class AbInitio_OpenMM(AbInitio):
1583  """ Force and energy matching using OpenMM. """
1584  def __init__(self,options,tgt_opts,forcefield):
1585 
1586  self.set_option(tgt_opts,'pdb',default="conf.pdb")
1587  self.set_option(tgt_opts,'coords',default="all.gro")
1588  self.set_option(tgt_opts,'openmm_precision','precision',default="double", forceprint=True)
1589  self.set_option(tgt_opts,'openmm_platform','platname',default="CUDA", forceprint=True)
1590  self.engine_ = OpenMM
1591 
1592  super(AbInitio_OpenMM,self).__init__(options,tgt_opts,forcefield)
1593 
1594 class BindingEnergy_OpenMM(BindingEnergy):
1595  """ Binding energy matching using OpenMM. """
1596 
1597  def __init__(self,options,tgt_opts,forcefield):
1598  self.engine_ = OpenMM
1599  self.set_option(tgt_opts,'openmm_precision','precision',default="double", forceprint=True)
1600  self.set_option(tgt_opts,'openmm_platform','platname',default="Reference", forceprint=True)
1601 
1602  super(BindingEnergy_OpenMM,self).__init__(options,tgt_opts,forcefield)
1603 
1604 class Interaction_OpenMM(Interaction):
1605  """ Interaction matching using OpenMM. """
1606  def __init__(self,options,tgt_opts,forcefield):
1607 
1608  self.set_option(tgt_opts,'coords',default="all.pdb")
1609  self.set_option(tgt_opts,'openmm_precision','precision',default="double", forceprint=True)
1610  self.set_option(tgt_opts,'openmm_platform','platname',default="Reference", forceprint=True)
1611  self.engine_ = OpenMM
1612 
1613  super(Interaction_OpenMM,self).__init__(options,tgt_opts,forcefield)
1614 
1615 class Moments_OpenMM(Moments):
1616  """ Multipole moment matching using OpenMM. """
1617  def __init__(self,options,tgt_opts,forcefield):
1618 
1619  self.set_option(tgt_opts,'coords',default="input.pdb")
1620  self.set_option(tgt_opts,'openmm_precision','precision',default="double", forceprint=True)
1621  self.set_option(tgt_opts,'openmm_platform','platname',default="Reference", forceprint=True)
1622  self.engine_ = OpenMM
1623 
1624  super(Moments_OpenMM,self).__init__(options,tgt_opts,forcefield)
1625 
1626 class Hydration_OpenMM(Hydration):
1627  """ Single point hydration free energies using OpenMM. """
1628 
1629  def __init__(self,options,tgt_opts,forcefield):
1630 
1632  self.set_option(tgt_opts,'openmm_precision','precision',default="double", forceprint=True)
1633  self.set_option(tgt_opts,'openmm_platform','platname',default="CUDA", forceprint=True)
1634  self.engine_ = OpenMM
1635  self.engname = "openmm"
1636 
1637  self.scripts = ['runcuda.sh']
1638 
1639  self.crdsfx = '.pdb'
1640 
1641  self.prefix = "bash runcuda.sh"
1642  if tgt_opts['remote_backup']:
1643  self.prefix += " -b"
1644 
1645  super(Hydration_OpenMM,self).__init__(options,tgt_opts,forcefield)
1646 
1647  if self.save_traj > 0:
1648  self.extra_output = ['openmm-md.dcd']
1649 
1650 class Vibration_OpenMM(Vibration):
1651  """ Vibrational frequency matching using TINKER. """
1652  def __init__(self,options,tgt_opts,forcefield):
1653 
1654  self.set_option(tgt_opts,'coords',default="input.pdb")
1655  self.set_option(tgt_opts,'openmm_precision','precision',default="double", forceprint=True)
1656  self.set_option(tgt_opts,'openmm_platform','platname',default="Reference", forceprint=True)
1657  self.engine_ = OpenMM
1658 
1659  super(Vibration_OpenMM,self).__init__(options,tgt_opts,forcefield)
1660 
1661 class OptGeoTarget_OpenMM(OptGeoTarget):
1662  """ Optimized geometry matching using OpenMM. """
1663  def __init__(self,options,tgt_opts,forcefield):
1664  self.engine_ = OpenMM
1665  self.set_option(tgt_opts,'openmm_precision','precision',default="double", forceprint=True)
1666  self.set_option(tgt_opts,'openmm_platform','platname',default="Reference", forceprint=True)
1667 
1668  super(OptGeoTarget_OpenMM,self).__init__(options,tgt_opts,forcefield)
1669 
1670  def create_engines(self, engine_args):
1671  """ create a dictionary of self.engines = {sysname: Engine} """
1672  self.engines = OrderedDict()
1673  for sysname, sysopt in self.sys_opts.items():
1674  # path to pdb file
1675  pdbpath = os.path.join(self.root, self.tgtdir, sysopt['topology'])
1676  # use the PDB file with topology
1677  M = Molecule(pdbpath)
1678  # replace geometry with values from xyz file for higher presision
1679  M0 = Molecule(os.path.join(self.root, self.tgtdir, sysopt['geometry']))
1680  M.xyzs = M0.xyzs
1681  # here mol=M is given for the purpose of using the topology from the input pdb file
1682  # if we don't do this, pdb=top.pdb option will only copy some basic information but not the topology into OpenMM.mol (openmmio.py line 615)
1683  self.engines[sysname] = self.engine_(target=self, mol=M, name=sysname, pdb=pdbpath, **engine_args)
1684 
1685 class TorsionProfileTarget_OpenMM(TorsionProfileTarget):
1686  """ Optimized geometry matching using OpenMM. """
1687  def __init__(self,options,tgt_opts,forcefield):
1688 
1689  self.set_option(tgt_opts,'pdb',default="conf.pdb")
1690  self.set_option(tgt_opts,'coords',default="scan.xyz")
1691  self.set_option(tgt_opts,'openmm_precision','precision',default="double", forceprint=True)
1692  self.set_option(tgt_opts,'openmm_platform','platname',default="Reference", forceprint=True)
1693  self.engine_ = OpenMM
1694 
1695  super(TorsionProfileTarget_OpenMM,self).__init__(options,tgt_opts,forcefield)
def CopyAmoebaAngleParameters(src, dest)
Definition: openmmio.py:247
def CopyGBSAOBCParameters(src, dest)
Definition: openmmio.py:292
def AddVirtualSiteBonds(mod, ff)
Definition: openmmio.py:368
Optimized Geometry fitting module.
Vibrational mode fitting module.
def CopyAmoebaOutOfPlaneBendParameters(src, dest)
Definition: openmmio.py:239
Nifty functions, intended to be imported by any module within ForceBalance.
Ab-initio fitting module (energies, forces, resp).
def PrepareVirtualSites(system)
Prepare a list of function wrappers and vsite parameters from the system.
Definition: openmmio.py:132
def CopyNonbondedParameters(src, dest)
Definition: openmmio.py:285
def ResetVirtualSites_fast(positions, vsinfo)
Given a set of OpenMM-compatible positions and a System object, compute the correct virtual site posi...
Definition: openmmio.py:172
def CopySystemParameters(src, dest)
Copy parameters from one system (i.e.
Definition: openmmio.py:314
def CopyAmoebaVdwParameters(src, dest)
Definition: openmmio.py:265
def energy_components(Sim, verbose=False)
Definition: openmmio.py:58
def GetVirtualSiteParameters(system)
Return an array of all virtual site parameters in the system.
Definition: openmmio.py:214
Torsion profile fitting module.
Binding energy fitting module.
def SetAmoebaVirtualExclusions(system)
Definition: openmmio.py:348
def UpdateSimulationParameters(src_system, dest_simulation)
Definition: openmmio.py:335
def CopyAmoebaBondParameters(src, dest)
Definition: openmmio.py:233
def do_nothing(src, dest)
Definition: openmmio.py:307
def CopyAmoebaInPlaneAngleParameters(src, dest)
Definition: openmmio.py:256
def warn_press_key(warning, timeout=10)
Definition: nifty.py:1599
Hydration free energy fitting module.
def CopyHarmonicAngleParameters(src, dest)
Definition: openmmio.py:277
def CopyCustomNonbondedParameters(src, dest)
copy whatever updateParametersInContext can update: per-particle parameters
Definition: openmmio.py:303
def SetAmoebaNonbondedExcludeAll(system, topology)
Manually set the AmoebaVdwForce, AmoebaMultipoleForce to exclude all atoms belonging to the same resi...
Definition: openmmio.py:382
Interaction energy fitting module.
def CopyAmoebaMultipoleParameters(src, dest)
Definition: openmmio.py:269
def get_mask(grps)
Given a list of booleans [1, 0, 1] return the bitmask that sets these force groups appropriately in C...
Definition: openmmio.py:48
def listfiles(fnms=None, ext=None, err=False, dnm=None)
Definition: nifty.py:1173
def ResetVirtualSites(positions, system)
Given a set of OpenMM-compatible positions and a System object, compute the correct virtual site posi...
Definition: openmmio.py:187
def warn_once(warning, warnhash=None)
Prints a warning but will only do so once in a given run.
Definition: nifty.py:1611
Multipole moment fitting module.
def wopen(dest, binary=False)
If trying to write to a symbolic link, remove it first.
Definition: nifty.py:1304
def CopyHarmonicBondParameters(src, dest)
Definition: openmmio.py:273
Matching of liquid bulk properties.
def MTSVVVRIntegrator(temperature, collision_rate, timestep, system, ninnersteps=4)
Create a multiple timestep velocity verlet with velocity randomization (VVVR) integrator.
Definition: openmmio.py:429
def get_dipole(simulation, q=None, mass=None, positions=None)
Return the current dipole moment in Debye.
Definition: openmmio.py:127
def CopyPeriodicTorsionParameters(src, dest)
Definition: openmmio.py:281
def get_multipoles(simulation, q=None, mass=None, positions=None, rmcom=True)
Return the current multipole moments in Debye and Buckingham units.
Definition: openmmio.py:68