ForceBalance API  1.3
Automated optimization of force fields and empirical potentials
smirnoffio.py
Go to the documentation of this file.
1 """ @package forcebalance.smirnoff SMIRNOFF force field support.
2 
3 @author Lee-Ping Wang
4 @date 12/2018
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
25 import pickle
26 import shutil
27 from copy import deepcopy
28 from forcebalance.engine import Engine
29 from forcebalance.molecule import *
30 from forcebalance.chemistry import *
31 from forcebalance.nifty import *
32 from forcebalance.nifty import _exec
33 from collections import OrderedDict, defaultdict, Counter
34 from forcebalance.output import getLogger
35 from forcebalance.openmmio import OpenMM, UpdateSimulationParameters
36 import json
37 
38 logger = getLogger(__name__)
39 try:
40  from simtk.openmm.app import *
41  from simtk.openmm import *
42  from simtk.unit import *
43  import simtk.openmm._openmm as _openmm
44 except:
45  pass
46 
47 try:
48  # import the hack for openforcefield to improve performance by 10x
49  from forcebalance import smirnoff_hack
50  # Import the SMIRNOFF forcefield engine and some useful tools
51  from openforcefield.typing.engines.smirnoff import ForceField as OpenFF_ForceField
52  # QYD: name of class are modified to avoid colliding with ForceBalance Molecule
53  from openforcefield.topology import Molecule as OffMolecule
54  from openforcefield.topology import Topology as OffTopology
55  import openforcefield
56  from pkg_resources import parse_version
57  if parse_version(openforcefield.__version__) < parse_version('0.7'):
58  raise RuntimeError('This version of FB is incompatible with OpenFF toolkit version <0.7.0')
59 except ImportError:
60  pass
61 
62 
63 pdict = "XML_Override"
64 
65 def smirnoff_analyze_parameter_coverage(forcefield, tgt_opts):
66  printcool("SMIRNOFF Parameter Coverage Analysis")
67  assert hasattr(forcefield, 'offxml'), "Only SMIRNOFF Force Field is supported"
68  parameter_assignment_data = defaultdict(list)
69  parameter_counter = Counter()
70  # The openforcefield.typing.engines.smirnoff.ForceField object should now be contained in forcebalance.forcefield.FF
71  ff = forcefield.openff_forcefield
72  # analyze each target
73  for tgt_option in tgt_opts:
74  target_path = os.path.join('targets', tgt_option['name'])
75  # aggregate mol2 file paths from all targets
76  mol2_paths = []
77  if tgt_option['type'] == 'OPTGEOTARGET_SMIRNOFF':
78  # parse optgeo_options_txt and get the names of the mol2 files
79  optgeo_options_txt = os.path.join(target_path, tgt_option['optgeo_options_txt'])
80  sys_opts = forcebalance.opt_geo_target.OptGeoTarget.parse_optgeo_options(optgeo_options_txt)
81  mol2_paths = [os.path.join(target_path,fnm) for sysopt in sys_opts.values() for fnm in sysopt['mol2']]
82  elif tgt_option['type'].endswith('_SMIRNOFF'):
83  mol2_paths = [os.path.join(target_path,fnm) for fnm in tgt_option['mol2']]
84  # analyze SMIRKs terms
85  for mol_fnm in mol2_paths:
86  # we work with one file at a time to avoid the topology sliently combine "same" molecules
87  openff_mol = OffMolecule.from_file(mol_fnm)
88  off_topology = OffTopology.from_molecules([openff_mol])
89  molecule_force_list = ff.label_molecules(off_topology)
90  for mol_idx, mol_forces in enumerate(molecule_force_list):
91  for force_tag, force_dict in mol_forces.items():
92  # e.g. force_tag = 'Bonds'
93  for atom_indices, parameter in force_dict.items():
94  param_dict = {'id': parameter.id, 'smirks': parameter.smirks, 'type':force_tag, 'atoms': list(atom_indices),}
95  parameter_assignment_data[mol_fnm].append(param_dict)
96  parameter_counter[parameter.smirks] += 1
97  # write out parameter assignment data
98  out_json_path = os.path.join(forcefield.root, 'smirnoff_parameter_assignments.json')
99  with open(out_json_path, 'w') as jsonfile:
100  json.dump(parameter_assignment_data, jsonfile, indent=2)
101  logger.info("Force field assignment data written to %s\n" % out_json_path)
102  # print parameter coverages
103  logger.info("%4s %-100s %10s\n" % ("idx", "Parameter", "Count"))
104  logger.info("-"*118 + '\n')
105  n_covered = 0
106  for i,p in enumerate(forcefield.plist):
107  smirks = p.split('/')[-1]
108  logger.info('%4i %-100s : %10d\n' % (i, p, parameter_counter[smirks]))
109  if parameter_counter[smirks] > 0:
110  n_covered += 1
111  logger.info("SNIRNOFF Parameter Coverage Analysis result: %d/%d parameters are covered.\n" % (n_covered, len(forcefield.plist)))
112  logger.info("-"*118 + '\n')
113 
114 class SMIRNOFF_Reader(BaseReader):
115  """ Class for parsing OpenMM force field files. """
116  def __init__(self,fnm):
117 
118  super(SMIRNOFF_Reader,self).__init__(fnm)
119 
120  self.pdict = pdict
121 
122  def build_pid(self, element, parameter):
123  """ Build the parameter identifier (see _link_ for an example)
124  @todo Add a link here """
125  ParentType = ".".join([i.tag for i in list(element.iterancestors())][::-1][1:])
126  InteractionType = element.tag
127  try:
128  Involved = element.attrib["smirks"]
129  return "/".join([ParentType, InteractionType, parameter, Involved])
130  except:
131  logger.info("Minor warning: Parameter ID %s doesn't contain any SMIRKS patterns, redundancies are possible\n" % ("/".join([InteractionType, parameter])))
132  return "/".join([ParentType, InteractionType, parameter])
133 
134 def assign_openff_parameter(ff, new_value, pid):
135  """
136  Assign a SMIRNOFF parameter given the openforcefield.ForceField object, the desired parameter value,
137  and the parameter's unique ID.
138  """
139  # Split the parameter's unique ID into four fields using a slash:
140  # Input: ProperTorsions/Proper/k1/[*:1]~[#6X3:2]:[#6X3:3]~[*:4]
141  # Output: ProperTorsions, Proper, k1, [*:1]~[#6X3:2]:[#6X3:3]~[*:4]
142  # The first, third and fourth fields will be used for parameter assignment.
143  # We use "value_name" to describe names of individual numerical values within a single parameter type
144  # e.g. k1 in the above example.
145 
146  # QYD: cache the parameter finding procedure, then directly change the _value of the quantity
147  # Note: This cache requires the quantity does not get overwritten, which is True since this function is the only
148  # place we modify the OpenFF ForceField parameters.
149  if not hasattr(ff, '_forcebalance_assign_parameter_map'):
150  ff._forcebalance_assign_parameter_map = dict()
151  if pid not in ff._forcebalance_assign_parameter_map:
152  (handler_name, tag_name, value_name, smirks) = pid.split('/')
153  # Get the OpenFF parameter object
154  parameter = ff.get_parameter_handler(handler_name).parameters[smirks]
155  if hasattr(parameter, value_name):
156  # If the value name is an attribute of the parameter then we set it directly.
157  unit = getattr(parameter, value_name).unit
158  # Get the quantity of the parameter in the OpenFF forcefield object
159  param_quantity = getattr(parameter, value_name)
160  elif value_name in parameter._cosmetic_attribs:
161  param_quantity = None
162  else:
163  # If the value name is a periodic attribute (say, k1) then we need to use
164  # a regex to split the value name into 'k' and '1', then set the appropriate
165  # value in the k-list
166  attribute_split = re.split(r'(\d+)', value_name)
167  # print(attribute_split)
168  # assert len(attribute_split) == 2
169  assert hasattr(parameter, attribute_split[0]), "%s.%s not exist" % (parameter, attribute_split[0])
170  # attribute_split[0] is a string such as 'k'
171  value_name = attribute_split[0]
172  # parameter_index is the position of k1 in the values associated with 'k'
173  parameter_index = int(attribute_split[1]) - 1
174  # Get the list of values, update the appropriate one and then set the new attribute to the updated list
175  value_list = getattr(parameter, value_name)
176  # Get the quantity of the parameter in the OpenFF forcefield object
177  param_quantity = value_list[parameter_index]
178  # save the found quantity in cache
179  ff._forcebalance_assign_parameter_map[pid] = param_quantity
180  else:
181  param_quantity = ff._forcebalance_assign_parameter_map[pid]
182  # set new_value directly in the quantity
183  if param_quantity is not None:
184  param_quantity._value = new_value
185 
186 class SMIRNOFF(OpenMM):
187 
188  """ Derived from Engine object for carrying out OpenMM calculations that use the SMIRNOFF force field. """
189 
190  def __init__(self, name="openmm", **kwargs):
191  self.valkwd = ['ffxml', 'pdb', 'mol2', 'platname', 'precision', 'mmopts', 'vsite_bonds', 'implicit_solvent', 'restrain_k', 'freeze_atoms']
192  super(SMIRNOFF,self).__init__(name=name, **kwargs)
193 
194  def readsrc(self, **kwargs):
195  """
196  SMIRNOFF simulations always require the following passed in via kwargs:
197 
198  Parameters
199  ----------
200  pdb : string
201  Name of a .pdb file containing the topology of the system
202  mol2 : list
203  A list of .mol2 file names containing the molecule/residue templates of the system
204 
205  Also provide 1 of the following, containing the coordinates to be used:
206  mol : Molecule
207  forcebalance.Molecule object
208  coords : string
209  Name of a file (readable by forcebalance.Molecule)
210  This could be the same as the pdb argument from above.
211  """
212 
213  pdbfnm = kwargs.get('pdb')
214  # Determine the PDB file name.
215  if not pdbfnm:
216  raise RuntimeError('Name of PDB file not provided.')
217  elif not os.path.exists(pdbfnm):
218  logger.error("%s specified but doesn't exist\n" % pdbfnm)
219  raise RuntimeError
220 
221  if 'mol' in kwargs:
222  self.mol = kwargs['mol']
223  elif 'coords' in kwargs:
224  if not os.path.exists(kwargs['coords']):
225  logger.error("%s specified but doesn't exist\n" % kwargs['coords'])
226  raise RuntimeError
227  self.mol = Molecule(kwargs['coords'])
228  else:
229  logger.error('Must provide either a molecule object or coordinate file.\n')
230  raise RuntimeError
231 
232  # Here we cannot distinguish the .mol2 files linked by the target
233  # vs. the .mol2 files to be provided by the force field.
234  # But we can assume that these files should exist when this function is called.
235 
236  self.mol2 = kwargs.get('mol2')
237  if self.mol2:
238  for fnm in self.mol2:
239  if not os.path.exists(fnm):
240  if hasattr(self, 'FF') and fnm in self.FF.fnms: continue
241  logger.error("%s doesn't exist" % fnm)
242  raise RuntimeError
243  else:
244  logger.error("Must provide a list of .mol2 files.\n")
245 
246  self.abspdb = os.path.abspath(pdbfnm)
247  mpdb = Molecule(pdbfnm)
248  for i in ["chain", "atomname", "resid", "resname", "elem"]:
249  self.mol.Data[i] = mpdb.Data[i]
250 
251  # Store a separate copy of the molecule for reference restraint positions.
252  self.ref_mol = deepcopy(self.mol)
253 
254  def prepare(self, pbc=False, mmopts={}, **kwargs):
255 
256  """
257  Prepare the calculation. Note that we don't create the
258  Simulation object yet, because that may depend on MD
259  integrator parameters, thermostat, barostat etc.
260 
261  This is mostly copied and modified from openmmio.py's OpenMM.prepare(),
262  but we are calling ForceField() from the OpenFF toolkit and ignoring
263  AMOEBA stuff.
264  """
265  self.pdb = PDBFile(self.abspdb)
266 
267  # Create the OpenFF ForceField object.
268  if hasattr(self, 'FF'):
269  self.offxml = [self.FF.offxml]
270  self.forcefield = self.FF.openff_forcefield
271  else:
272  self.offxml = listfiles(kwargs.get('offxml'), 'offxml', err=True)
273  self.forcefield = OpenFF_ForceField(*self.offxml)
274 
275 
276  openff_mols = []
277  for fnm in self.mol2:
278  try:
279  mol = OffMolecule.from_file(fnm)
280  except Exception as e:
281  logger.error("Error when loading %s" % fnm)
282  raise e
283  openff_mols.append(mol)
284  self.off_topology = OffTopology.from_openmm(self.pdb.topology, unique_molecules=openff_mols)
285 
286  # used in create_simulation()
287  self.mod = Modeller(self.pdb.topology, self.pdb.positions)
288 
289 
290  self.mmopts = dict(mmopts)
292 
293  if 'restrain_k' in kwargs:
294  self.restrain_k = kwargs['restrain_k']
295  if 'freeze_atoms' in kwargs:
296  self.freeze_atoms = kwargs['freeze_atoms'][:]
298 
299  fftmp = False
300  if hasattr(self,'FF'):
301  self.mmopts['rigidWater'] = self.FF.rigid_water
302  if not all([os.path.exists(f) for f in self.FF.fnms]):
303  # If the parameter files don't already exist, create them for the purpose of
304  # preparing the engine, but then delete them afterward.
305  fftmp = True
306  self.FF.make(np.zeros(self.FF.np))
307 
308 
309  self.pbc = pbc
310 
311  if 'nonbonded_cutoff' in kwargs:
312  logger.warning("nonbonded_cutoff keyword ignored because it's set in the offxml file\n")
313 
314 
315  self.xyz_omms = []
316  for I in range(len(self.mol)):
317  position = self.mol.xyzs[I] * angstrom
318  # xyz_omm = [Vec3(i[0],i[1],i[2]) for i in xyz]*angstrom
319  # An extra step with adding virtual particles
320  # mod = Modeller(self.pdb.topology, xyz_omm)
321  # LPW commenting out because we don't have virtual sites yet.
322  # mod.addExtraParticles(self.forcefield)
323  if self.pbc:
324  # Obtain the periodic box
325  if self.mol.boxes[I].alpha != 90.0 or self.mol.boxes[I].beta != 90.0 or self.mol.boxes[I].gamma != 90.0:
326  logger.error('OpenMM cannot handle nonorthogonal boxes.\n')
327  raise RuntimeError
328  box_omm = np.diag([self.mol.boxes[I].a, self.mol.boxes[I].b, self.mol.boxes[I].c]) * angstrom
329  else:
330  box_omm = None
331  # Finally append it to list.
332  self.xyz_omms.append((position, box_omm))
333 
334 
335  Top = self.pdb.topology
336  Atoms = list(Top.atoms())
337  Bonds = [(a.index, b.index) for a, b in list(Top.bonds())]
338 
339  # vss = [(i, [system.getVirtualSite(i).getParticle(j) for j in range(system.getVirtualSite(i).getNumParticles())]) \
340  # for i in range(system.getNumParticles()) if system.isVirtualSite(i)]
341  self.AtomLists = defaultdict(list)
342  self.AtomLists['Mass'] = [a.element.mass.value_in_unit(dalton) if a.element is not None else 0 for a in Atoms]
343  self.AtomLists['ParticleType'] = ['A' if m >= 1.0 else 'D' for m in self.AtomLists['Mass']]
344  self.AtomLists['ResidueNumber'] = [a.residue.index for a in Atoms]
345  self.AtomMask = [a == 'A' for a in self.AtomLists['ParticleType']]
346  self.realAtomIdxs = [i for i, a in enumerate(self.AtomMask) if a is True]
347  if hasattr(self,'FF') and fftmp:
348  for f in self.FF.fnms:
349  os.unlink(f)
350 
351  def update_simulation(self, **kwargs):
353  """
354  Create the simulation object, or update the force field
355  parameters in the existing simulation object. This should be
356  run when we write a new force field XML file.
357  """
358  if len(kwargs) > 0:
359  self.simkwargs = kwargs
360 
361  # Because self.forcefield is being updated in forcebalance.forcefield.FF.make()
362  # there is no longer a need to create a new force field object here.
363  try:
364  self.system = self.forcefield.create_openmm_system(self.off_topology)
365  except Exception as error:
366  logger.error("Error when creating system for %s" % self.mol2)
367  raise error
368  # Commenting out all virtual site stuff for now.
369  # self.vsinfo = PrepareVirtualSites(self.system)
370  self.nbcharges = np.zeros(self.system.getNumParticles())
371 
372  #----
373  # If the virtual site parameters have changed,
374  # the simulation object must be remade.
375  #----
376  # vsprm = GetVirtualSiteParameters(self.system)
377  # if hasattr(self,'vsprm') and len(self.vsprm) > 0 and np.max(np.abs(vsprm - self.vsprm)) != 0.0:
378  # if hasattr(self, 'simulation'):
379  # delattr(self, 'simulation')
380  # self.vsprm = vsprm.copy()
381 
382  if hasattr(self, 'simulation'):
383  UpdateSimulationParameters(self.system, self.simulation)
384  else:
385  self.create_simulation(**self.simkwargs)
386 
387  def optimize(self, shot=0, align=True, crit=1e-4):
388  return super(SMIRNOFF,self).optimize(shot=shot, align=align, crit=crit, disable_vsite=True)
389 
390  def interaction_energy(self, fraga, fragb):
391 
392  """
393  Calculate the interaction energy for two fragments.
394  Because this creates two new objects and requires passing in the mol2 argument,
395  the codes are copied and modified from the OpenMM class.
396  """
397 
398  self.update_simulation()
399 
400  if self.name == 'A' or self.name == 'B':
401  logger.error("Don't name the engine A or B!\n")
402  raise RuntimeError
403 
404  # Create two subengines.
405  if hasattr(self,'target'):
406  if not hasattr(self,'A'):
407  self.A = SMIRNOFF(name="A", mol=self.mol.atom_select(fraga), mol2=self.mol2, target=self.target)
408  if not hasattr(self,'B'):
409  self.B = SMIRNOFF(name="B", mol=self.mol.atom_select(fragb), mol2=self.mol2, target=self.target)
410  else:
411  if not hasattr(self,'A'):
412  self.A = SMIRNOFF(name="A", mol=self.mol.atom_select(fraga), mol2=self.mol2, platname=self.platname, \
413  precision=self.precision, offxml=self.offxml, mmopts=self.mmopts)
414  if not hasattr(self,'B'):
415  self.B = SMIRNOFF(name="B", mol=self.mol.atom_select(fragb), mol2=self.mol2, platname=self.platname, \
416  precision=self.precision, offxml=self.offxml, mmopts=self.mmopts)
417 
418  # Interaction energy needs to be in kcal/mol.
419  D = self.energy()
420  A = self.A.energy()
421  B = self.B.energy()
422 
423  return (D - A - B) / 4.184
424 
425  def get_smirks_counter(self):
426  """Get a counter for the time of appreance of each SMIRKS"""
427  smirks_counter = Counter()
428  molecule_force_list = self.forcefield.label_molecules(self.off_topology)
429  for mol_idx, mol_forces in enumerate(molecule_force_list):
430  for force_tag, force_dict in mol_forces.items():
431  # e.g. force_tag = 'Bonds'
432  for parameter in force_dict.values():
433  smirks_counter[parameter.smirks] += 1
434  return smirks_counter
435 
436 class Liquid_SMIRNOFF(Liquid):
437  """ Condensed phase property matching using OpenMM. """
438  def __init__(self,options,tgt_opts,forcefield):
439  # Time interval (in ps) for writing coordinates
440  self.set_option(tgt_opts,'force_cuda',forceprint=True)
441  # Enable multiple timestep integrator
442  self.set_option(tgt_opts,'mts_integrator',forceprint=True)
443  # Enable ring polymer MD
444  self.set_option(options,'rpmd_beads',forceprint=True)
445  # List of .mol2 files for SMIRNOFF to set up the system
446  self.set_option(tgt_opts,'mol2',forceprint=True)
447  # OpenMM precision
448  self.set_option(tgt_opts,'openmm_precision','precision',default="mixed")
449  # OpenMM platform
450  self.set_option(tgt_opts,'openmm_platform','platname',default="CUDA")
451  # Name of the liquid coordinate file.
452  self.set_option(tgt_opts,'liquid_coords',default='liquid.pdb',forceprint=True)
453  # Name of the gas coordinate file.
454  self.set_option(tgt_opts,'gas_coords',default='gas.pdb',forceprint=True)
455  # Name of the surface tension coordinate file. (e.g. an elongated box with a film of water)
456  self.set_option(tgt_opts,'nvt_coords',default='surf.pdb',forceprint=True)
457  # Set the number of steps between MC barostat adjustments.
458  self.set_option(tgt_opts,'mc_nbarostat')
459  # Class for creating engine object.
460  self.engine_ = SMIRNOFF
461  # Name of the engine to pass to npt.py.
462  self.engname = "smirnoff"
463  # Command prefix.
464  self.nptpfx = "bash runcuda.sh"
465  if tgt_opts['remote_backup']:
466  self.nptpfx += " -b"
467  # Extra files to be linked into the temp-directory.
468  self.nptfiles = []
469  self.nvtfiles = []
470  # Set some options for the polarization correction calculation.
471  self.gas_engine_args = {}
472  # Scripts to be copied from the ForceBalance installation directory.
473  self.scripts = ['runcuda.sh']
474  # Initialize the base class.
475  super(Liquid_SMIRNOFF,self).__init__(options,tgt_opts,forcefield)
476  # Send back the trajectory file.
477  if self.save_traj > 0:
478  self.extra_output = ['liquid-md.pdb', 'liquid-md.dcd']
479  # These functions need to be called after self.nptfiles is populated
480  self.post_init(options)
481 
482 class AbInitio_SMIRNOFF(AbInitio):
483  """ Force and energy matching using OpenMM. """
484  def __init__(self,options,tgt_opts,forcefield):
485 
486  self.set_option(tgt_opts,'pdb',default="conf.pdb")
487  # List of .mol2 files for SMIRNOFF to set up the system
488  self.set_option(tgt_opts,'mol2',forceprint=True)
489  self.set_option(tgt_opts,'coords',default="all.gro")
490  self.set_option(tgt_opts,'openmm_precision','precision',default="double", forceprint=True)
491  self.set_option(tgt_opts,'openmm_platform','platname',default="Reference", forceprint=True)
492  self.engine_ = SMIRNOFF
493 
494  super(AbInitio_SMIRNOFF,self).__init__(options,tgt_opts,forcefield)
496  def submit_jobs(self, mvals, AGrad=False, AHess=False):
497  # we update the self.pgrads here so it's not overwritten in rtarget.py
499 
500  def smirnoff_update_pgrads(self):
501  """
502  Update self.pgrads based on smirks present in mol2 files
503 
504  This can greatly improve gradients evaluation in big optimizations
505 
506  Note
507  ----
508  1. This function assumes the names of the forcefield parameters has the smirks as the last item
509  2. This function assumes params only affect the smirks of its own. This might not be true if parameter_eval is used.
510  """
511  orig_pgrad_set = set(self.pgrad)
512  # smirks to param_idxs map
513  smirks_params_map = defaultdict(list)
514  # New code for mapping smirks to mathematical parameter IDs
515  for pname in self.FF.pTree:
516  smirks = pname.rsplit('/',maxsplit=1)[-1]
517  for pidx in self.FF.get_mathid(pname):
518  smirks_params_map[smirks].append(pidx)
519  pgrads_set = set()
520  # get the smirks for this target, keep only the pidx corresponding to these smirks
521  smirks_counter = self.engine.get_smirks_counter()
522  for smirks in smirks_counter:
523  if smirks_counter[smirks] > 0:
524  pidx_list = smirks_params_map[smirks]
525  # update the set of parameters present in this target
526  pgrads_set.update(pidx_list)
527  # this ensure we do not add any new items into self.pgrad
528  pgrads_set.intersection_update(orig_pgrad_set)
529  self.pgrad = sorted(list(pgrads_set))
530 
531 
532 class Vibration_SMIRNOFF(Vibration):
533  """ Vibrational frequency matching using TINKER. """
534  def __init__(self,options,tgt_opts,forcefield):
535 
536  self.set_option(tgt_opts,'coords',default="input.pdb")
537  self.set_option(tgt_opts,'pdb',default="conf.pdb")
538  self.set_option(tgt_opts,'mol2',forceprint=True)
539  self.set_option(tgt_opts,'openmm_precision','precision',default="double", forceprint=True)
540  self.set_option(tgt_opts,'openmm_platform','platname',default="Reference", forceprint=True)
541  self.engine_ = SMIRNOFF
542 
543  super(Vibration_SMIRNOFF,self).__init__(options,tgt_opts,forcefield)
544 
545  def submit_jobs(self, mvals, AGrad=False, AHess=False):
546  # we update the self.pgrads here so it's not overwritten in rtarget.py
549  def smirnoff_update_pgrads(self):
550  """
551  Update self.pgrads based on smirks present in mol2 files
552 
553  This can greatly improve gradients evaluation in big optimizations
554 
555  Note
556  ----
557  1. This function assumes the names of the forcefield parameters has the smirks as the last item
558  2. This function assumes params only affect the smirks of its own. This might not be true if parameter_eval is used.
559  """
560  orig_pgrad_set = set(self.pgrad)
561  # smirks to param_idxs map
562  smirks_params_map = defaultdict(list)
563  for pname in self.FF.pTree:
564  smirks = pname.rsplit('/',maxsplit=1)[-1]
565  for pidx in self.FF.get_mathid(pname):
566  smirks_params_map[smirks].append(pidx)
567  pgrads_set = set()
568  # get the smirks for this target, keep only the pidx corresponding to these smirks
569  smirks_counter = self.engine.get_smirks_counter()
570  for smirks in smirks_counter:
571  if smirks_counter[smirks] > 0:
572  pidx_list = smirks_params_map[smirks]
573  # update the set of parameters present in this target
574  pgrads_set.update(pidx_list)
575  # this ensure we do not add any new items into self.pgrad
576  pgrads_set.intersection_update(orig_pgrad_set)
577  self.pgrad = sorted(list(pgrads_set))
578 
579 
580 class OptGeoTarget_SMIRNOFF(OptGeoTarget):
581  """ Optimized geometry fitting using SMIRNOFF format powered by OpenMM """
582  def __init__(self,options,tgt_opts,forcefield):
583  self.set_option(tgt_opts,'openmm_precision','precision',default="double", forceprint=True)
584  self.set_option(tgt_opts,'openmm_platform','platname',default="Reference", forceprint=True)
585  self.engine_ = SMIRNOFF
586 
587  super(OptGeoTarget_SMIRNOFF,self).__init__(options,tgt_opts,forcefield)
588 
589  def create_engines(self, engine_args):
590  """ create a dictionary of self.engines = {sysname: Engine} """
591  self.engines = OrderedDict()
592  for sysname, sysopt in self.sys_opts.items():
593  # SMIRNOFF is a subclass of OpenMM engine but it requires the mol2 input
594  # note: OpenMM.mol is a Molecule class instance; mol2 is a file format.
595  # path to .pdb file
596  pdbpath = os.path.join(self.root, self.tgtdir, sysopt['topology'])
597  # a list of paths to .mol2 files
598  mol2path = [os.path.join(self.root, self.tgtdir, f) for f in sysopt['mol2']]
599  # use the PDB file with topology
600  M = Molecule(os.path.join(self.root, self.tgtdir, sysopt['topology']))
601  # replace geometry with values from xyz file for higher presision
602  M0 = Molecule(os.path.join(self.root, self.tgtdir, sysopt['geometry']))
603  M.xyzs = M0.xyzs
604  # here mol=M is given for the purpose of using the topology from the input pdb file
605  # 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)
606  self.engines[sysname] = self.engine_(target=self, mol=M, name=sysname, pdb=pdbpath, mol2=mol2path, **engine_args)
609  def build_system_mval_masks(self):
610  """
611  Build a mask of mvals for each system, to speed up finite difference gradients
612 
613  Note
614  ----
615  1. This function assumes the names of the forcefield parameters has the smirks as the last item
616  2. This function assumes params only affect the smirks of its own. This might not be true if parameter_eval is used.
617  """
618  # only need to build once
619  if hasattr(self, 'system_mval_masks'): return
620  n_params = len(self.FF.map)
621  # default mask with all False
622  system_mval_masks = {sysname: np.zeros(n_params, dtype=bool) for sysname in self.sys_opts}
623  orig_pgrad_set = set(self.pgrad)
624  # smirks to param_idxs map
625  smirks_params_map = defaultdict(list)
626  # New code for mapping smirks to mathematical parameter IDs
627  for pname in self.FF.pTree:
628  smirks = pname.rsplit('/',maxsplit=1)[-1]
629  # print("pname %s mathid %s -> smirks %s" % (pname, str(self.FF.get_mathid(pname)), smirks))
630  for pidx in self.FF.get_mathid(pname):
631  smirks_params_map[smirks].append(pidx)
632  # Old code for mapping smirks to mathematical parameter IDs
633  # for pname, pidx in self.FF.map.items():
634  # smirks = pname.rsplit('/',maxsplit=1)[-1]
635  # smirks_params_map[smirks].append(pidx)
636  # go over all smirks for each system
637  for sysname in self.sys_opts:
638  engine = self.engines[sysname]
639  smirks_counter = engine.get_smirks_counter()
640  for smirks in smirks_counter:
641  if smirks_counter[smirks] > 0:
642  pidx_list = smirks_params_map[smirks]
643  # set mask value to True for present smirks
644  system_mval_masks[sysname][pidx_list] = True
645  # finish
646  logger.info("system_mval_masks is built for faster gradient evaluations")
647  self.system_mval_masks = system_mval_masks
648 
649 class TorsionProfileTarget_SMIRNOFF(TorsionProfileTarget):
650  """ Force and energy matching using SMIRKS native Open Force Field (SMIRNOFF). """
651  def __init__(self,options,tgt_opts,forcefield):
652 
653  self.set_option(tgt_opts,'pdb',default="conf.pdb")
654  # List of .mol2 files for SMIRNOFF to set up the system
655  self.set_option(tgt_opts,'mol2',forceprint=True)
656  self.set_option(tgt_opts,'coords',default="scan.xyz")
657  self.set_option(tgt_opts,'openmm_precision','precision',default="double", forceprint=True)
658  self.set_option(tgt_opts,'openmm_platform','platname',default="Reference", forceprint=True)
659  self.engine_ = SMIRNOFF
660 
661  super(TorsionProfileTarget_SMIRNOFF,self).__init__(options,tgt_opts,forcefield)
662 
663  def submit_jobs(self, mvals, AGrad=False, AHess=False):
664  # we update the self.pgrads here so it's not overwritten in rtarget.py
665  self.smirnoff_update_pgrads()
666 
667  def smirnoff_update_pgrads(self):
668  """
669  Update self.pgrads based on smirks present in mol2 files
671  This can greatly improve gradients evaluation in big optimizations
672 
673  Note
674  ----
675  1. This function assumes the names of the forcefield parameters has the smirks as the last item
676  2. This function assumes params only affect the smirks of its own. This might not be true if parameter_eval is used.
677  """
678  orig_pgrad_set = set(self.pgrad)
679  # smirks to param_idxs map
680  smirks_params_map = defaultdict(list)
681  # New code for mapping smirks to mathematical parameter IDs
682  for pname in self.FF.pTree:
683  smirks = pname.rsplit('/',maxsplit=1)[-1]
684  for pidx in self.FF.get_mathid(pname):
685  smirks_params_map[smirks].append(pidx)
686  pgrads_set = set()
687  # get the smirks for this target, keep only the pidx corresponding to these smirks
688  smirks_counter = self.engine.get_smirks_counter()
689  for smirks in smirks_counter:
690  if smirks_counter[smirks] > 0:
691  pidx_list = smirks_params_map[smirks]
692  # update the set of parameters present in this target
693  pgrads_set.update(pidx_list)
694  # this ensure we do not add any new items into self.pgrad
695  pgrads_set.intersection_update(orig_pgrad_set)
696  self.pgrad = sorted(list(pgrads_set))
698 # class BindingEnergy_SMIRNOFF(BindingEnergy):
699 # """ Binding energy matching using OpenMM. """
700 
701 # def __init__(self,options,tgt_opts,forcefield):
702 # self.engine_ = OpenMM
703 # self.set_option(tgt_opts,'openmm_precision','precision',default="double", forceprint=True)
704 # self.set_option(tgt_opts,'openmm_platform','platname',default="Reference", forceprint=True)
705 # ## Initialize base class.
706 # super(BindingEnergy_OpenMM,self).__init__(options,tgt_opts,forcefield)
707 
708 # class Interaction_SMIRNOFF(Interaction):
709 # """ Interaction matching using OpenMM. """
710 # def __init__(self,options,tgt_opts,forcefield):
711 # ## Default file names for coordinates and key file.
712 # self.set_option(tgt_opts,'coords',default="all.pdb")
713 # self.set_option(tgt_opts,'openmm_precision','precision',default="double", forceprint=True)
714 # self.set_option(tgt_opts,'openmm_platform','platname',default="Reference", forceprint=True)
715 # self.engine_ = OpenMM
716 # ## Initialize base class.
717 # super(Interaction_OpenMM,self).__init__(options,tgt_opts,forcefield)
718 
719 # class Moments_SMIRNOFF(Moments):
720 # """ Multipole moment matching using OpenMM. """
721 # def __init__(self,options,tgt_opts,forcefield):
722 # ## Default file names for coordinates and key file.
723 # self.set_option(tgt_opts,'coords',default="input.pdb")
724 # self.set_option(tgt_opts,'openmm_precision','precision',default="double", forceprint=True)
725 # self.set_option(tgt_opts,'openmm_platform','platname',default="Reference", forceprint=True)
726 # self.engine_ = OpenMM
727 # ## Initialize base class.
728 # super(Moments_OpenMM,self).__init__(options,tgt_opts,forcefield)
729 
730 # class Hydration_SMIRNOFF(Hydration):
731 # """ Single point hydration free energies using OpenMM. """
732 
733 # def __init__(self,options,tgt_opts,forcefield):
734 # ## Default file names for coordinates and key file.
735 # # self.set_option(tgt_opts,'coords',default="input.pdb")
736 # self.set_option(tgt_opts,'openmm_precision','precision',default="double", forceprint=True)
737 # self.set_option(tgt_opts,'openmm_platform','platname',default="CUDA", forceprint=True)
738 # self.engine_ = SMIRNOFF
739 # self.engname = "smirnoff"
740 # ## Scripts to be copied from the ForceBalance installation directory.
741 # self.scripts = ['runcuda.sh']
742 # ## Suffix for coordinate files.
743 # self.crdsfx = '.pdb'
744 # ## Command prefix.
745 # self.prefix = "bash runcuda.sh"
746 # if tgt_opts['remote_backup']:
747 # self.prefix += " -b"
748 # ## Initialize base class.
749 # super(Hydration_OpenMM,self).__init__(options,tgt_opts,forcefield)
750 # ## Send back the trajectory file.
751 # if self.save_traj > 0:
752 # self.extra_output = ['openmm-md.dcd']
def optimize(self, shot=0, align=True, crit=1e-4)
Definition: smirnoffio.py:395
def interaction_energy(self, fraga, fragb)
Calculate the interaction energy for two fragments.
Definition: smirnoffio.py:404
Class for parsing OpenMM force field files.
Definition: smirnoffio.py:117
Optimized Geometry fitting module.
off_topology
Load mol2 files for smirnoff topology.
Definition: smirnoffio.py:291
Force and energy matching using OpenMM.
Definition: smirnoffio.py:495
restrain_k
Specify frozen atoms and restraint force constant.
Definition: smirnoffio.py:301
Vibrational mode fitting module.
def __init__(self, options, tgt_opts, forcefield)
Definition: smirnoffio.py:548
engine_
Default file names for coordinates and key file.
Definition: smirnoffio.py:555
Nifty functions, intended to be imported by any module within ForceBalance.
Ab-initio fitting module (energies, forces, resp).
mmopts
OpenMM options for setting up the System.
Definition: smirnoffio.py:297
def smirnoff_analyze_parameter_coverage(forcefield, tgt_opts)
Definition: smirnoffio.py:66
def __init__(self, options, tgt_opts, forcefield)
Definition: smirnoffio.py:598
def readsrc(self, kwargs)
SMIRNOFF simulations always require the following passed in via kwargs:
Definition: smirnoffio.py:217
Vibrational frequency matching using TINKER.
Definition: smirnoffio.py:547
Torsion profile fitting module.
Binding energy fitting module.
Force and energy matching using SMIRKS native Open Force Field (SMIRNOFF).
Definition: smirnoffio.py:669
pbc
Set system options from ForceBalance force field options.
Definition: smirnoffio.py:316
def build_system_mval_masks(self)
Build a mask of mvals for each system, to speed up finite difference gradients.
Definition: smirnoffio.py:635
Optimized geometry fitting using SMIRNOFF format powered by OpenMM.
Definition: smirnoffio.py:597
AtomLists
Build a topology and atom lists.
Definition: smirnoffio.py:348
engine_
Default file names for coordinates and key file.
Definition: smirnoffio.py:504
def assign_openff_parameter(ff, new_value, pid)
Assign a SMIRNOFF parameter given the openforcefield.ForceField object, the desired parameter value...
Definition: smirnoffio.py:142
def UpdateSimulationParameters(src_system, dest_simulation)
Definition: openmmio.py:335
def submit_jobs(self, mvals, AGrad=False, AHess=False)
Definition: smirnoffio.py:508
xyz_omms
print warning for &#39;nonbonded_cutoff&#39; keywords
Definition: smirnoffio.py:322
def __init__(self, options, tgt_opts, forcefield)
Definition: smirnoffio.py:449
Hydration free energy fitting module.
def get_smirks_counter(self)
Get a counter for the time of appreance of each SMIRKS.
Definition: smirnoffio.py:436
def printcool(text, sym="#", bold=False, color=2, ansi=None, bottom='-', minwidth=50, center=True, sym2="=")
Cool-looking printout for slick formatting of output.
Definition: nifty.py:321
def __init__(self, options, tgt_opts, forcefield)
Definition: smirnoffio.py:496
pdict
Initialize the superclass.
Definition: smirnoffio.py:122
def __init__(self, name="openmm", kwargs)
Definition: smirnoffio.py:195
Derived from Engine object for carrying out OpenMM calculations that use the SMIRNOFF force field...
Definition: smirnoffio.py:192
Interaction energy fitting module.
def listfiles(fnms=None, ext=None, err=False, dnm=None)
Definition: nifty.py:1173
def smirnoff_update_pgrads(self)
Update self.pgrads based on smirks present in mol2 files.
Definition: smirnoffio.py:574
Multipole moment fitting module.
def smirnoff_update_pgrads(self)
Update self.pgrads based on smirks present in mol2 files.
Definition: smirnoffio.py:523
Condensed phase property matching using OpenMM.
Definition: smirnoffio.py:448
def Counter()
Definition: optimizer.py:35
def prepare(self, pbc=False, mmopts={}, kwargs)
Prepare the calculation.
Definition: smirnoffio.py:270
def update_simulation(self, kwargs)
Create the simulation object, or update the force field parameters in the existing simulation object...
Definition: smirnoffio.py:364
Matching of liquid bulk properties.
OpenMM input/output.
def build_pid(self, element, parameter)
Build the parameter identifier (see link for an example)
Definition: smirnoffio.py:127