1 """ @package forcebalance.smirnoff SMIRNOFF force field support. 6 from __future__
import division
8 from builtins
import zip
9 from builtins
import range
11 from forcebalance
import BaseReader
27 from copy
import deepcopy
28 from forcebalance.engine
import Engine
33 from collections
import OrderedDict, defaultdict, Counter
34 from forcebalance.output
import getLogger
38 logger = getLogger(__name__)
43 import simtk.openmm._openmm
as _openmm
49 from forcebalance
import smirnoff_hack
51 from openforcefield.typing.engines.smirnoff
import ForceField
as OpenFF_ForceField
53 from openforcefield.topology
import Molecule
as OffMolecule
54 from openforcefield.topology
import Topology
as OffTopology
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')
63 pdict =
"XML_Override" 67 assert hasattr(forcefield,
'offxml'),
"Only SMIRNOFF Force Field is supported" 68 parameter_assignment_data = defaultdict(list)
71 ff = forcefield.openff_forcefield
73 for tgt_option
in tgt_opts:
74 target_path = os.path.join(
'targets', tgt_option[
'name'])
77 if tgt_option[
'type'] ==
'OPTGEOTARGET_SMIRNOFF':
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']]
85 for mol_fnm
in mol2_paths:
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():
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
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)
103 logger.info(
"%4s %-100s %10s\n" % (
"idx",
"Parameter",
"Count"))
104 logger.info(
"-"*118 +
'\n')
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:
111 logger.info(
"SNIRNOFF Parameter Coverage Analysis result: %d/%d parameters are covered.\n" % (n_covered, len(forcefield.plist)))
112 logger.info(
"-"*118 +
'\n')
115 """ Class for parsing OpenMM force field files. """ 116 def __init__(self,fnm):
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
128 Involved = element.attrib[
"smirks"]
129 return "/".join([ParentType, InteractionType, parameter, Involved])
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])
136 Assign a SMIRNOFF parameter given the openforcefield.ForceField object, the desired parameter value, 137 and the parameter's unique ID. 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(
'/')
154 parameter = ff.get_parameter_handler(handler_name).parameters[smirks]
155 if hasattr(parameter, value_name):
157 unit = getattr(parameter, value_name).unit
159 param_quantity = getattr(parameter, value_name)
160 elif value_name
in parameter._cosmetic_attribs:
161 param_quantity =
None 166 attribute_split = re.split(
r'(\d+)', value_name)
169 assert hasattr(parameter, attribute_split[0]),
"%s.%s not exist" % (parameter, attribute_split[0])
171 value_name = attribute_split[0]
173 parameter_index = int(attribute_split[1]) - 1
175 value_list = getattr(parameter, value_name)
177 param_quantity = value_list[parameter_index]
179 ff._forcebalance_assign_parameter_map[pid] = param_quantity
181 param_quantity = ff._forcebalance_assign_parameter_map[pid]
183 if param_quantity
is not None:
184 param_quantity._value = new_value
188 """ Derived from Engine object for carrying out OpenMM calculations that use the SMIRNOFF force field. """ 190 def __init__(self, name="openmm", **kwargs):
191 self.valkwd = [
'ffxml',
'pdb',
'mol2',
'platname',
'precision',
'mmopts',
'vsite_bonds',
'implicit_solvent',
'restrain_k',
'freeze_atoms']
196 SMIRNOFF simulations always require the following passed in via kwargs: 201 Name of a .pdb file containing the topology of the system 203 A list of .mol2 file names containing the molecule/residue templates of the system 205 Also provide 1 of the following, containing the coordinates to be used: 207 forcebalance.Molecule object 209 Name of a file (readable by forcebalance.Molecule) 210 This could be the same as the pdb argument from above. 213 pdbfnm = kwargs.get(
'pdb')
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)
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'])
227 self.
mol = Molecule(kwargs[
'coords'])
229 logger.error(
'Must provide either a molecule object or coordinate file.\n')
236 self.
mol2 = kwargs.get(
'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)
244 logger.error(
"Must provide a list of .mol2 files.\n")
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]
254 def prepare(self, pbc=False, mmopts={}, **kwargs):
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. 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 268 if hasattr(self,
'FF'):
269 self.
offxml = [self.FF.offxml]
279 mol = OffMolecule.from_file(fnm)
280 except Exception
as e:
281 logger.error(
"Error when loading %s" % fnm)
283 openff_mols.append(mol)
284 self.
off_topology = OffTopology.from_openmm(self.
pdb.topology, unique_molecules=openff_mols)
287 self.
mod = Modeller(self.
pdb.topology, self.
pdb.positions)
290 self.
mmopts = dict(mmopts)
293 if 'restrain_k' in kwargs:
295 if 'freeze_atoms' in kwargs:
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]):
306 self.FF.make(np.zeros(self.FF.np))
311 if 'nonbonded_cutoff' in kwargs:
312 logger.warning(
"nonbonded_cutoff keyword ignored because it's set in the offxml file\n")
316 for I
in range(len(self.
mol)):
317 position = self.
mol.xyzs[I] * angstrom
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')
328 box_omm = np.diag([self.
mol.boxes[I].a, self.
mol.boxes[I].b, self.
mol.boxes[I].c]) * angstrom
332 self.
xyz_omms.append((position, box_omm))
335 Top = self.
pdb.topology
336 Atoms = list(Top.atoms())
337 Bonds = [(a.index, b.index)
for a, b
in list(Top.bonds())]
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]
347 if hasattr(self,
'FF')
and fftmp:
348 for f
in self.FF.fnms:
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. 365 except Exception
as error:
366 logger.error(
"Error when creating system for %s" % self.
mol2)
382 if hasattr(self,
'simulation'):
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)
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. 400 if self.
name ==
'A' or self.
name ==
'B':
401 logger.error(
"Don't name the engine A or B!\n")
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)
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, \
423 return (D - A - B) / 4.184
426 """Get a counter for the time of appreance of each SMIRKS""" 429 for mol_idx, mol_forces
in enumerate(molecule_force_list):
430 for force_tag, force_dict
in mol_forces.items():
432 for parameter
in force_dict.values():
433 smirks_counter[parameter.smirks] += 1
434 return smirks_counter
437 """ Condensed phase property matching using OpenMM. """ 438 def __init__(self,options,tgt_opts,forcefield):
440 self.set_option(tgt_opts,
'force_cuda',forceprint=
True)
442 self.set_option(tgt_opts,
'mts_integrator',forceprint=
True)
444 self.set_option(options,
'rpmd_beads',forceprint=
True)
446 self.set_option(tgt_opts,
'mol2',forceprint=
True)
448 self.set_option(tgt_opts,
'openmm_precision',
'precision',default=
"mixed")
450 self.set_option(tgt_opts,
'openmm_platform',
'platname',default=
"CUDA")
452 self.set_option(tgt_opts,
'liquid_coords',default=
'liquid.pdb',forceprint=
True)
454 self.set_option(tgt_opts,
'gas_coords',default=
'gas.pdb',forceprint=
True)
456 self.set_option(tgt_opts,
'nvt_coords',default=
'surf.pdb',forceprint=
True)
458 self.set_option(tgt_opts,
'mc_nbarostat')
464 self.
nptpfx =
"bash runcuda.sh" 465 if tgt_opts[
'remote_backup']:
475 super(Liquid_SMIRNOFF,self).
__init__(options,tgt_opts,forcefield)
477 if self.save_traj > 0:
480 self.post_init(options)
483 """ Force and energy matching using OpenMM. """ 486 self.set_option(tgt_opts,
'pdb',default=
"conf.pdb")
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)
494 super(AbInitio_SMIRNOFF,self).
__init__(options,tgt_opts,forcefield)
502 Update self.pgrads based on smirks present in mol2 files 504 This can greatly improve gradients evaluation in big optimizations 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. 511 orig_pgrad_set = set(self.
pgrad)
513 smirks_params_map = defaultdict(list)
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)
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]
526 pgrads_set.update(pidx_list)
528 pgrads_set.intersection_update(orig_pgrad_set)
529 self.
pgrad = sorted(list(pgrads_set))
533 """ Vibrational frequency matching using TINKER. """ 534 def __init__(self,options,tgt_opts,forcefield):
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)
543 super(Vibration_SMIRNOFF,self).
__init__(options,tgt_opts,forcefield)
545 def submit_jobs(self, mvals, AGrad=False, AHess=False):
551 Update self.pgrads based on smirks present in mol2 files 553 This can greatly improve gradients evaluation in big optimizations 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. 560 orig_pgrad_set = set(self.
pgrad)
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)
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]
574 pgrads_set.update(pidx_list)
576 pgrads_set.intersection_update(orig_pgrad_set)
577 self.
pgrad = sorted(list(pgrads_set))
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)
587 super(OptGeoTarget_SMIRNOFF,self).
__init__(options,tgt_opts,forcefield)
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():
596 pdbpath = os.path.join(self.root, self.tgtdir, sysopt[
'topology'])
598 mol2path = [os.path.join(self.root, self.tgtdir, f)
for f
in sysopt[
'mol2']]
600 M = Molecule(os.path.join(self.root, self.tgtdir, sysopt[
'topology']))
602 M0 = Molecule(os.path.join(self.root, self.tgtdir, sysopt[
'geometry']))
606 self.
engines[sysname] = self.
engine_(target=self, mol=M, name=sysname, pdb=pdbpath, mol2=mol2path, **engine_args)
611 Build a mask of mvals for each system, to speed up finite difference gradients 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. 619 if hasattr(self,
'system_mval_masks'):
return 620 n_params = len(self.FF.map)
622 system_mval_masks = {sysname: np.zeros(n_params, dtype=bool)
for sysname
in self.sys_opts}
623 orig_pgrad_set = set(self.pgrad)
625 smirks_params_map = defaultdict(list)
627 for pname
in self.FF.pTree:
628 smirks = pname.rsplit(
'/',maxsplit=1)[-1]
630 for pidx
in self.FF.get_mathid(pname):
631 smirks_params_map[smirks].append(pidx)
637 for sysname
in self.sys_opts:
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]
644 system_mval_masks[sysname][pidx_list] =
True 646 logger.info(
"system_mval_masks is built for faster gradient evaluations")
650 """ Force and energy matching using SMIRKS native Open Force Field (SMIRNOFF). """ 651 def __init__(self,options,tgt_opts,forcefield):
653 self.set_option(tgt_opts,
'pdb',default=
"conf.pdb")
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)
661 super(TorsionProfileTarget_SMIRNOFF,self).
__init__(options,tgt_opts,forcefield)
663 def submit_jobs(self, mvals, AGrad=False, AHess=False):
665 self.smirnoff_update_pgrads()
667 def smirnoff_update_pgrads(self):
669 Update self.pgrads based on smirks present in mol2 files 671 This can greatly improve gradients evaluation in big optimizations 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. 680 smirks_params_map = defaultdict(list)
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)
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]
693 pgrads_set.update(pidx_list)
695 pgrads_set.intersection_update(orig_pgrad_set)
696 self.
pgrad = sorted(list(pgrads_set))
def optimize(self, shot=0, align=True, crit=1e-4)
def interaction_energy(self, fraga, fragb)
Calculate the interaction energy for two fragments.
Class for parsing OpenMM force field files.
Optimized Geometry fitting module.
off_topology
Load mol2 files for smirnoff topology.
Force and energy matching using OpenMM.
restrain_k
Specify frozen atoms and restraint force constant.
Vibrational mode fitting module.
def __init__(self, options, tgt_opts, forcefield)
engine_
Default file names for coordinates and key file.
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.
def smirnoff_analyze_parameter_coverage(forcefield, tgt_opts)
def __init__(self, options, tgt_opts, forcefield)
def readsrc(self, kwargs)
SMIRNOFF simulations always require the following passed in via kwargs:
Vibrational frequency matching using TINKER.
Torsion profile fitting module.
Binding energy fitting module.
Force and energy matching using SMIRKS native Open Force Field (SMIRNOFF).
pbc
Set system options from ForceBalance force field options.
def build_system_mval_masks(self)
Build a mask of mvals for each system, to speed up finite difference gradients.
Optimized geometry fitting using SMIRNOFF format powered by OpenMM.
AtomLists
Build a topology and atom lists.
engine_
Default file names for coordinates and key file.
def assign_openff_parameter(ff, new_value, pid)
Assign a SMIRNOFF parameter given the openforcefield.ForceField object, the desired parameter value...
def UpdateSimulationParameters(src_system, dest_simulation)
def submit_jobs(self, mvals, AGrad=False, AHess=False)
xyz_omms
print warning for 'nonbonded_cutoff' keywords
def __init__(self, options, tgt_opts, forcefield)
Hydration free energy fitting module.
def get_smirks_counter(self)
Get a counter for the time of appreance of each SMIRKS.
def printcool(text, sym="#", bold=False, color=2, ansi=None, bottom='-', minwidth=50, center=True, sym2="=")
Cool-looking printout for slick formatting of output.
def __init__(self, options, tgt_opts, forcefield)
pdict
Initialize the superclass.
def __init__(self, name="openmm", kwargs)
Derived from Engine object for carrying out OpenMM calculations that use the SMIRNOFF force field...
Interaction energy fitting module.
def listfiles(fnms=None, ext=None, err=False, dnm=None)
def smirnoff_update_pgrads(self)
Update self.pgrads based on smirks present in mol2 files.
Multipole moment fitting module.
def smirnoff_update_pgrads(self)
Update self.pgrads based on smirks present in mol2 files.
Condensed phase property matching using OpenMM.
def prepare(self, pbc=False, mmopts={}, kwargs)
Prepare the calculation.
def update_simulation(self, kwargs)
Create the simulation object, or update the force field parameters in the existing simulation object...
Matching of liquid bulk properties.
def build_pid(self, element, parameter)
Build the parameter identifier (see link for an example)