ForceBalance API  1.3
Automated optimization of force fields and empirical potentials
forcefield.py
Go to the documentation of this file.
1 """ @package forcebalance.forcefield
2 
3 Force field module.
4 
5 In ForceBalance a 'force field' is built from a set of files containing
6 physical parameters. These files can be anything that enter into any
7 computation - our original program was quite dependent on the GROMACS
8 force field format, but this program is set up to allow very general
9 input formats.
10 
11 We introduce several important concepts:
12 
13 1) Adjustable parameters are allocated into a vector.
14 
15 To cast the force field optimization as a math problem, we treat all
16 of the parameters on equal footing and write them as indices in a parameter vector.
17 
18 2) A mapping from interaction type to parameter number.
19 
20 Each element in the parameter vector corresponds to one or more
21 interaction types. Whenever we change the parameter vector and
22 recompute the objective function, this amounts to changing the
23 physical parameters in the simulations, so we print out new
24 force field files for external programs. In addition, when
25 these programs are computing the objective function we are often
26 in low-level subroutines that compute terms in the energy and
27 force. If we need an analytic derivative of the objective
28 function, then these subroutines need to know which index of the
29 parameter vector needs to be modified.
30 
31 This is done by way of a hash table: for example, when we are
32 computing a Coulomb interaction between atom 4 and atom 5, we
33 can build the words 'COUL4' and 'COUL5' and look it up in the
34 parameter map; this gives us two numbers (say, 10 and 11)
35 corresponding to the eleventh and twelfth element of the
36 parameter vector. Then we can compute the derivatives of
37 the energy w/r.t. these parameters (in this case, COUL5/rij
38 and COUL4/rij) and increment these values in the objective function
39 gradient.
40 
41 In custom-implemented force fields (see counterpoisematch.py)
42 the hash table can also be used to look up parameter values for
43 computation of interactions. This is probably not the fastest way
44 to do things, however.
45 
46 3) Distinction between physical and mathematical parameters.
47 
48 The optimization algorithm works in a space that is related to, but
49 not exactly the same as the physical parameter space. The reasons
50 for why we do this are:
51 
52 a) Each parameter has its own physical units. On the one hand
53 it's not right to treat different physical units all on the same
54 footing, so nondimensionalization is desirable. To make matters
55 worse, the force field parameters can be small as 1e-8 or as
56 large as 1e+6 depending on the parameter type. This means the
57 elements of the objective function gradient / Hessian have
58 elements that differ from each other in size by 10+ orders of
59 magnitude, leading to mathematical instabilities in the optimizer.
60 
61 b) The parameter space can be constrained, most notably for atomic
62 partial charges where we don't want to change the overall charge
63 on a molecule. Thus we wish to project out certain movements
64 in the mathematical parameters such that they don't change the physical
65 parameters.
66 
67 c) We wish to regularize our optimization so as to avoid changing
68 our parameters in very insensitive directions (linear dependencies).
69 However, the sensitivity of the objective function to changes in the
70 force field depends on the physical units!
71 
72 For all of these reasons, we introduce a 'transformation matrix'
73 which maps mathematical parameters onto physical parameters. The
74 diagonal elements in this matrix are rescaling factors; they take the
75 mathematical parameter and magnify it by this constant factor. The
76 off-diagonal elements correspond to rotations and other linear
77 transformations, and currently I just use them to project out the
78 'increase the net charge' direction in the physical parameter space.
79 
80 Note that with regularization, these rescaling factors are equivalent
81 to the widths of prior distributions in a maximum likelihood framework.
82 Because there is such a correspondence between rescaling factors and
83 choosing a prior, they need to be chosen carefully. This is work in
84 progress. Another possibility is to sample the width of the priors from
85 a noninformative distribution -- the hyperprior (we can choose the
86 Jeffreys prior or something). This is work in progress.
87 
88 Right now only GROMACS parameters are supported, but this class is extensible,
89 we need more modules!
90 
91 @author Lee-Ping Wang
92 @date 04/2012
93 
94 """
95 from __future__ import division
96 
97 from builtins import zip
98 from builtins import str
99 from builtins import range
100 import os
101 import sys
102 import numpy as np
103 import networkx as nx
104 from numpy import sin, cos, tan, sinh, cosh, tanh, exp, log, sqrt, pi
105 from re import match, sub, split
106 import forcebalance
107 from forcebalance import gmxio, qchemio, tinkerio, custom_io, openmmio, amberio, psi4io, smirnoffio
108 from forcebalance.finite_difference import in_fd
109 from forcebalance.smirnoffio import assign_openff_parameter
110 from forcebalance.nifty import *
111 # from string import count
112 from copy import deepcopy
113 import traceback
114 import itertools
115 from collections import OrderedDict, defaultdict
116 from forcebalance.output import getLogger
117 logger = getLogger(__name__)
119 try:
120  from lxml import etree
121 except:
122  logger.warning("Failed to import lxml module, needed by OpenMM engine")
123 
124 FF_Extensions = {"itp" : "gmx",
125  "top" : "gmx",
126  "in" : "qchem",
127  "prm" : "tinker",
128  "gen" : "custom",
129  "xml" : "openmm",
130  "offxml" : "smirnoff",
131  "frcmod" : "frcmod",
132  "mol2" : "mol2",
133  "gbs" : "gbs",
134  "grid" : "grid"
135  }
136 
137 """ Recognized force field formats. """
138 FF_IOModules = {"gmx": gmxio.ITP_Reader ,
140  "tinker": tinkerio.Tinker_Reader ,
141  "custom": custom_io.Gen_Reader ,
142  "openmm" : openmmio.OpenMM_Reader,
143  "smirnoff" : smirnoffio.SMIRNOFF_Reader,
144  "frcmod" : amberio.FrcMod_Reader,
145  "mol2" : amberio.Mol2_Reader,
146  "gbs" : psi4io.GBS_Reader,
147  "grid" : psi4io.Grid_Reader
148  }
149 
150 def determine_fftype(ffname,verbose=False):
151  """ Determine the type of a force field file. It is possible to
152  specify the file type explicitly in the input file using the
153  syntax 'force_field.ext:type'. Otherwise this function will try
154  to determine the force field type by extension. """
155 
156  fsplit = ffname.split('/')[-1].split(':')
157  fftype = None
158  if verbose: logger.info("Determining file type of %s ..." % fsplit[0])
159  if len(fsplit) == 2:
160  if fsplit[1] in FF_IOModules:
161  if verbose: logger.info("We're golden! (%s)\n" % fsplit[1])
162  fftype = fsplit[1]
163  else:
164  if verbose: logger.info("\x1b[91m Warning: \x1b[0m %s not in supported types (%s)!\n" % (fsplit[1],', '.join(FF_IOModules.keys())))
165  elif len(fsplit) == 1:
166  if verbose: logger.info("Guessing from extension (you may specify type with filename:type) ...")
167  ffname = fsplit[0]
168  ffext = ffname.split('.')[-1]
169  if ffext in FF_Extensions:
170  guesstype = FF_Extensions[ffext]
171  if guesstype in FF_IOModules:
172  if verbose: logger.info("guessing %s -> %s!\n" % (ffext, guesstype))
173  fftype = guesstype
174  else:
175  if verbose: logger.info("\x1b[91m Warning: \x1b[0m %s not in supported types (%s)!\n" % (fsplit[0],', '.join(FF_IOModules.keys())))
176  else:
177  if verbose: logger.info("\x1b[91m Warning: \x1b[0m %s not in supported extensions (%s)!\n" % (ffext,', '.join(FF_Extensions.keys())))
178  if fftype is None:
179  if verbose: logger.warning("Force field type not determined!\n")
180  #sys.exit(1)
181  return fftype
182 
183 # Thanks to tos9 from #python on freenode. :)
184 class BackedUpDict(dict):
185  def __init__(self, backup_dict):
186  super(BackedUpDict, self).__init__()
187  self.backup_dict = backup_dict
188  def __missing__(self, key):
189  try:
190  return self.backup_dict[self['AtomType']][key]
191  except:
192  logger.error('The key %s does not exist as an atom attribute or as an atom type attribute!\n' % key)
193  raise KeyError
194 
195 class FF(forcebalance.BaseClass):
196  """ Force field class.
197 
198  This class contains all methods for force field manipulation.
199  To create an instance of this class, an input file is required
200  containing the list of force field file names. Everything else
201  inside this class pertaining to force field generation is self-contained.
202 
203  For details on force field parsing, see the detailed documentation for addff.
204 
205  """
206  def __init__(self, options, verbose=True, printopt=True):
207 
208  """Instantiation of force field class.
209 
210  Many variables here are initialized to zero, but they are filled out by
211  methods like addff, rsmake, and mktransmat.
212 
213  """
214  super(FF, self).__init__(options)
215  #======================================#
216  # Options that are given by the parser #
217  #======================================#
218 
221  self.set_option(None, None, 'root', os.getcwd())
222 
223  self.set_option(options,'forcefield','fnms')
224 
225  self.set_option(options,'ffdir')
226 
227  self.set_option(options,'priors')
228 
229  self.set_option(options,'constrain_charge')
230 
231  self.set_option(options,'logarithmic_map')
232 
233  self.set_option(options, 'amoeba_pol')
234 
235  self.set_option(options, 'amoeba_eps')
236 
237  self.set_option(options, 'rigid_water')
238 
239  self.set_option(options, 'constrain_h')
240 
241  self.set_option(options, 'use_pvals')
242 
243  self.set_option(options, 'duplicate_pnames')
244 
245  #======================================#
246  # Variables which are set here #
247  #======================================#
248  # A lot of these variables are filled out by calling the methods.
249 
250 
251  self.ffdata = {}
252  self.ffdata_isxml = {}
253 
254  self.map = {}
255 
256  self.plist = []
257 
258  self.patoms = []
259 
263  self.pfields = []
264 
265  self.pTree = nx.DiGraph()
266  # unit strings that might appear in offxml file
267  self.offxml_unit_strs = defaultdict(str)
268 
269  self.rs = []
270 
271  self.tm = None
272 
273  self.tmI = None
274 
275  self.excision = None
276 
277  self.np = 0
278 
279  self.pvals0 = []
280 
281  self.Readers = OrderedDict()
282 
283  self.atomnames = []
284 
285  # Read the force fields into memory.
286  for fnm in self.fnms:
287  if verbose:
288  logger.info("Reading force field from file: %s\n" % fnm)
289  self.addff(fnm)
290 
291 
293  self.FFAtomTypes = OrderedDict()
294  for Reader in self.Readers.values():
295  for k, v in Reader.AtomTypes.items():
296  if k in self.FFAtomTypes:
297  warn_press_key('Trying to insert atomtype %s into the force field, but it is already there' % k)
298  self.FFAtomTypes[k] = v
299  # This is an ordered dictionary of {'Molecule' : [{'AtomType' : string, 'ResidueNumber' : int, 'ResidueName' : string,
300  # 'AtomName' : string, 'ChargeGroup' : int, 'Charge' : float}]}
301  # Each value in the dictionary is a list of BackedUpDictionaries.
302  # If we query a BackedUpDictionary and the value does not exist,
303  # then it will query the backup dictionary using the 'AtomType' value.
304  # Thus, if we look up the mass of 'HW1' or 'HW2' in the dictionary, it will
305  # return the mass for 'HW' in the AtomTypes dictionary.
306  self.FFMolecules = OrderedDict()
307  for Reader in self.Readers.values():
308  for Molecule, AtomList in Reader.Molecules.items():
309  for FFAtom in AtomList:
310  FFAtomWithDefaults = BackedUpDict(self.FFAtomTypes)
311  for k, v in FFAtom.items():
312  FFAtomWithDefaults[k] = v
313  self.FFMolecules.setdefault(Molecule, []).append(FFAtomWithDefaults)
314 
315  # Set the initial values of parameter arrays.
316 
317  self.pvals0 = np.array(self.pvals0)
318 
319  # Prepare various components of the class for first use.
320 
321  self.list_map()
322  if verbose:
323 
324  bar = printcool("Starting parameter indices, physical values and IDs")
325  self.print_map()
326  logger.info(bar)
327 
328  self.rsmake(printfacs=verbose)
329 
330  self.mktransmat()
331 
332  self.redirect = {}
333 
334  self.linedestroy_save = []
335  self.prmdestroy_save = []
337  self.prmdestroy_this = []
338 
339  if printopt: printcool_dictionary(self.PrintOptionDict, title="Setup for force field")
341  @classmethod
342  def fromfile(cls, fnm):
343  ffdir = os.path.split(fnm)[0]
344  fnm = os.path.split(fnm)[1]
345  options = {'forcefield' : [fnm], 'ffdir' : ffdir, 'duplicate_pnames' : True}
346  return cls(options, verbose=False, printopt=False)
347 
348  def __getstate__(self):
349  state = deepcopy(self.__dict__)
350  for ffname in self.ffdata:
351  if self.ffdata_isxml[ffname]:
352  temp = etree.tostring(self.ffdata[ffname])
353  del state['ffdata'][ffname]
354  state['ffdata'][ffname] = temp
355  return state
356 
357  def __setstate__(self, state):
358  self.__dict__.update(state)
359  for ffname in self.ffdata:
360  if self.ffdata_isxml[ffname]:
361  temp = etree.ElementTree(etree.fromstring(self.ffdata[ffname]))
362  self.ffdata[ffname] = temp
363 
364  def addff(self,ffname,xmlScript=False):
365  """ Parse a force field file and add it to the class.
366 
367  First, figure out the type of force field file. This is done
368  either by explicitly specifying the type using for example,
369  <tt> ffname force_field.xml:openmm </tt> or we figure it out
370  by looking at the file extension.
371 
372  Next, parse the file. Currently we support two classes of
373  files - text and XML. The two types are treated very
374  differently; for XML we use the parsers in libxml (via the
375  python lxml module), and for text files we have our own
376  in-house parsing class. Within text files, there is also a
377  specialized GROMACS and TINKER parser as well as a generic
378  text parser.
379 
380  The job of the parser is to determine the following things:
381  1) Read the user-specified selection of parameters being fitted
382  2) Build a mapping (dictionary) of <tt> parameter identifier -> index in parameter vector </tt>
383  3) Build a list of physical parameter values
384  4) Figure out where to replace the parameter values in the force field file when the values are changed
385  5) Figure out which parameters need to be repeated or sign-flipped
386 
387  Generally speaking, each parameter value in the force field
388  file has a <tt> unique parameter identifier <tt>. The
389  identifier consists of three parts - the interaction type, the
390  parameter subtype (within that interaction type), and the
391  atoms involved.
392 
393  --- If XML: ---
394 
395  The force field file is read in using the lxml Python module. Specify
396  which parameter you want to fit using by adding a 'parameterize' element
397  to the end of the force field XML file, like so.
398 
399  @code
400  <AmoebaVdwForce type="BUFFERED-14-7">
401  <Vdw class="74" sigma="0.2655" epsilon="0.056484" reduction="0.910" parameterize="sigma, epsilon, reduction" />
402  @endcode
403 
404  In this example, the parameter identifier would look like <tt> Vdw/74/epsilon </tt>.
405 
406  --- If GROMACS (.itp) or TINKER (.prm) : ---
407 
408  Follow the rules in the ITP_Reader or Tinker_Reader derived
409  class. Read the documentation in the class documentation or
410  the 'feed' method to learn more. In all cases the parameter
411  is tagged using <tt> # PRM 3 </tt> (where # denotes a comment,
412  the word PRM stays the same, and 3 is the field number starting
413  from zero.)
414 
415  --- If normal text : ---
416 
417  The parameter identifier is simply built using the file name,
418  line number, and field. Thus, the identifier is unique but
419  completely noninformative (which is not ideal for our
420  purposes, but it works.)
421 
422  --- Endif ---
423 
424  @warning My program currently assumes that we are only using one MM program per job.
425  If we use CHARMM and GROMACS to perform simulations as part of the same TARGET, we will
426  get messed up. Maybe this needs to be fixed in the future, with program prefixes to
427  parameters like C_ , G_ .. or simply unit conversions, you get the idea.
428 
429  @warning I don't think the multiplier actually works for analytic derivatives unless
430  the interaction calculator knows the multiplier as well. I'm sure I can make this
431  work in the future if necessary.
432 
433  @param[in] ffname Name of the force field file
434 
435  """
436  fftype = determine_fftype(ffname)
437  ffname = ffname.split(':')[0]
438 
439  # Set the Tinker PRM file, which will help for running programs like "analyze".
440  if fftype == "tinker":
441  if hasattr(self, "tinkerprm"):
442  warn_press_key("There should only be one TINKER parameter file")
443  else:
444  self.tinkerprm = ffname
445 
446  # Set the OpenMM XML file, which will help for running OpenMM.
447  if fftype == "openmm":
448  if hasattr(self, "openmmxml"):
449  warn_press_key("There should only be one OpenMM XML file - confused!!")
450  else:
451  self.openmmxml = ffname
452 
453  if fftype == "smirnoff":
454  if hasattr(self, "offxml"):
455  warn_press_key("There should only be one SMIRNOFF XML file - confused!!")
456  else:
457  # OpenFF force field wants parameters to be modified using its API, so we enable that here
458  from openforcefield.typing.engines.smirnoff import ForceField as OpenFF_ForceField
459  self.offxml = ffname
460  self.openff_forcefield = OpenFF_ForceField(os.path.join(self.root, self.ffdir, self.offxml),
461  allow_cosmetic_attributes=True)
462 
463  self.amber_mol2 = []
464  if fftype == "mol2":
465  self.amber_mol2.append(ffname)
466 
467  self.amber_frcmod = []
468  if fftype == "frcmod":
469  self.amber_frcmod.append(ffname)
470 
471  # Determine the appropriate parser from the FF_IOModules dictionary.
472  # If we can't figure it out, then use the base reader, it ain't so bad. :)
473  Reader = FF_IOModules.get(fftype, forcebalance.BaseReader)
474 
475  # Open the force field using an absolute path and read its contents into memory.
476  absff = os.path.join(self.root,self.ffdir,ffname)
477 
478  # Create an instance of the Reader.
479  # The reader is essentially a finite state machine that allows us to
480  # build the pid.
481  self.Readers[ffname] = Reader(ffname)
482  if fftype in ["openmm", "smirnoff"]:
483 
484  try:
485  self.ffdata[ffname] = etree.parse(absff)
486  except NameError:
487  logger.error("If etree not defined, please check if lxml module has been installed")
488  raise
489  self.ffdata_isxml[ffname] = True
490  # Process the file
491  self.addff_xml(ffname)
492  else:
493 
494  self.ffdata[ffname] = [line.expandtabs() for line in open(absff).readlines()]
495  self.ffdata_isxml[ffname] = False
496  # Process the file
497  self.addff_txt(ffname, fftype,xmlScript)
498  if hasattr(self.Readers[ffname], 'atomnames'):
499  if len(self.atomnames) > 0:
500  sys.stderr.write('Found more than one force field containing atom names; skipping the second one (%s)\n' % ffname)
501  else:
502  self.atomnames += self.Readers[ffname].atomnames
503 
504  def check_dupes(self, pid, ffname, ln, pfld):
505  """ Check to see whether a given parameter ID already exists, and provide an alternate if needed. """
506  pid_ = pid
507 
508  have_pids = [f[0] for f in self.pfields]
509 
510  if pid in have_pids:
511  pid0 = pid
512  extranum = 0
513  dupfnms = [i[1] for i in self.pfields if pid == i[0]]
514  duplns = [i[2] for i in self.pfields if pid == i[0]]
515  dupflds = [i[3] for i in self.pfields if pid == i[0]]
516  while pid in have_pids:
517  pid = "%s%i" % (pid0, extranum)
518  extranum += 1
519  def warn_or_err(*args):
520  if self.duplicate_pnames:
521  logger.warn(*args)
522  else:
523  logger.error(*args)
524  warn_or_err("Encountered an duplicate parameter ID (%s)\n" % pid_)
525  warn_or_err("file %s line %i field %i duplicates:\n"
526  % (os.path.basename(ffname), ln+1, pfld))
527  for dupfnm, dupln, dupfld in zip(dupfnms, duplns, dupflds):
528  warn_or_err("file %s line %i field %i\n" % (dupfnm, dupln+1, dupfld))
529  if self.duplicate_pnames:
530  logger.warn("Parameter name has been changed to %s\n" % pid)
531  else:
532  raise RuntimeError
533  return pid
534 
535  def addff_txt(self, ffname, fftype, xmlScript):
536  """ Parse a text force field and create several important instance variables.
537 
538  Each line is processed using the 'feed' method as implemented
539  in the reader class. This essentially allows us to create the
540  correct parameter identifier (pid), because the pid comes from
541  more than the current line, it also depends on the section that
542  we're in.
543 
544  When 'PRM' or 'RPT' is encountered, we do several things:
545  - Build the parameter identifier and insert it into the map
546  - Point to the file name, line number, and field where the parameter may be modified
547 
548  Additionally, when 'PRM' is encountered:
549  - Store the physical parameter value (this is permanent; it's the original value)
550  - Increment the total number of parameters
551 
552  When 'RPT' is encountered we don't expand the parameter vector
553  because this parameter is a copy of an existing one. If the
554  parameter identifier is preceded by MINUS_, we chop off the
555  prefix but remember that the sign needs to be flipped.
556 
557  """
558 
559  for ln, line in enumerate(self.ffdata[ffname]):
560  try:
561  self.Readers[ffname].feed(line)
562  except:
563  logger.warning(traceback.format_exc() + '\n')
564  logger.warning("The force field reader crashed when trying to read the following line:\n")
565  logger.warning(line + '\n')
566  traceback.print_exc()
567  warn_press_key("The force field parser got confused! The traceback and line in question are printed above.")
568  sline = self.Readers[ffname].Split(line)
569 
570  kwds = list(itertools.chain(*[[i, "/%s" % i] for i in ['PRM', 'PARM', 'RPT', 'EVAL']]))
571 
572  marks = OrderedDict()
573  for k in kwds:
574  if sline.count(k) > 1:
575  logger.error(line)
576  logger.error("The above line contains multiple occurrences of the keyword %s\n" % k)
577  raise RuntimeError
578  elif sline.count(k) == 1:
579  marks[k] = (np.array(sline) == k).argmax()
580  marks['END'] = len(sline)
581 
582  pmark = marks.get('PRM',None)
583  if pmark is None: pmark = marks.get('PARM',None)
584  rmark = marks.get('RPT',None)
585  emark = marks.get('EVAL',None)
586 
587  if pmark is not None:
588  pstop = min([i for i in marks.values() if i > pmark])
589  pflds = [int(i) for i in sline[pmark+1:pstop]] # The integers that specify the parameter word positions
590  for pfld in pflds:
591  # For each of the fields that are to be parameterized (indicated by PRM #),
592  # assign a parameter type to it according to the Interaction Type -> Parameter Dictionary.
593  pid = self.Readers[ffname].build_pid(pfld)
594  if xmlScript:
595  pid = 'Script/'+sline[pfld-2]+'/'
596  pid = self.check_dupes(pid, ffname, ln, pfld)
597  self.map[pid] = self.np
598  # This parameter ID has these atoms involved.
599  self.patoms.append([self.Readers[ffname].molatom])
600  # Also append pid to the parameter list
601  self.assign_p0(self.np,float(sline[pfld]))
602  self.assign_field(self.np,pid,ffname,ln,pfld,1)
603  self.np += 1
604  if rmark is not None:
605  parse = rmark + 1
606  stopparse = min([i for i in marks.values() if i > rmark])
607  while parse < stopparse:
608  # Between RPT and /RPT, the words occur in pairs.
609  # First is a number corresponding to the field that contains the dependent parameter.
610  # Second is a string corresponding to the 'pid' that this parameter depends on.
611  pfld = int(sline[parse])
612  try:
613  prep = self.map[sline[parse+1].replace('MINUS_','')]
614  except:
615  sys.stderr.write("Valid parameter IDs:\n")
616  count = 0
617  for i in self.map:
618  sys.stderr.write("%25s" % i)
619  if count%3 == 2:
620  sys.stderr.write("\n")
621  count += 1
622  sys.stderr.write("\nOffending ID: %s\n" % sline[parse+1])
623 
624  logger.error('Parameter repetition entry in force field file is incorrect (see above)\n')
625  raise RuntimeError
626  pid = self.Readers[ffname].build_pid(pfld)
627  pid = self.check_dupes(pid, ffname, ln, pfld)
628  self.map[pid] = prep
629  # This repeated parameter ID also has these atoms involved.
630  self.patoms[prep].append(self.Readers[ffname].molatom)
631  self.assign_field(prep,pid,ffname,ln,pfld,"MINUS_" in sline[parse+1] and -1 or 1)
632  parse += 2
633  if emark is not None:
634  parse = emark + 1
635  stopparse = min([i for i in marks.values() if i > emark])
636  while parse < stopparse:
637  # Between EVAL and /EVAL, the words occur in pairs.
638  # First is a number corresponding to the field that contains the dependent parameter.
639  # Second is a Python command that determines how to calculate the parameter.
640  pfld = int(sline[parse])
641  evalcmd = sline[parse+1] # This string is actually Python code!!
642  pid = self.Readers[ffname].build_pid(pfld)
643  pid = self.check_dupes(pid, ffname, ln, pfld)
644  # EVAL parameters have no corresponding parameter index
645  #self.map[pid] = None
646  #self.map[pid] = prep
647  # This repeated parameter ID also has these atoms involved.
648  #self.patoms[prep].append(self.Readers[ffname].molatom)
649  self.assign_field(None,pid,ffname,ln,pfld,None,evalcmd)
650  parse += 2
651 
652  def addff_xml(self, ffname):
653  """ Parse an XML force field file and create important instance variables.
654 
655  This was modeled after addff_txt, but XML and text files are
656  fundamentally different, necessitating two different methods.
657 
658  We begin with an _ElementTree object. We search through the tree
659  for the 'parameterize' and 'parameter_repeat' keywords. Each time
660  the keyword is encountered, we do the same four actions that
661  I describe in addff_txt.
662 
663  It's hard to specify precisely the location in an XML file to
664  change a force field parameter. I can create a list of tree
665  elements (essentially pointers to elements within a tree), but
666  this method breaks down when I copy the tree because I have no
667  way to refer to the copied tree elements. Fortunately, lxml
668  gives me a way to represent a tree using a flat list, and my
669  XML file 'locations' are represented using the positions in
670  the list.
671 
672  @warning The sign-flip hasn't been implemented yet. This
673  shouldn't matter unless your calculation contains repeated
674  parameters with opposite sign.
675 
676  """
677 
678  #check if xml file contains a script
679  #throw error if more than one script
680  #write script into .txt file and parse as text
681  fflist = list(self.ffdata[ffname].iter())
682  scriptElements = [elem for elem in fflist if elem.tag=='Script']
683  if len(scriptElements) > 1:
684  logger.error('XML file'+ffname+'contains more than one script! Consolidate your scripts into one script!\n')
685  raise RuntimeError
686  elif len(scriptElements)==1:
687  Script = scriptElements[0].text
688  ffnameList = ffname.split('.')
689  ffnameScript = ffnameList[0]+'Script.txt'
690  absScript = os.path.join(self.root, self.ffdir, ffnameScript)
691  if os.path.exists(absScript):
692  logger.error('XML file '+absScript+' already exists on disk! Please delete it\n')
693  raise RuntimeError
694  wfile = forcebalance.nifty.wopen(absScript)
695  wfile.write(Script)
696  wfile.close()
697  self.addff(ffnameScript, xmlScript=True)
698  os.unlink(absScript)
699 
700  for e in self.ffdata[ffname].getroot().xpath('//@parameterize/..'):
701  parameters_to_optimize = [i.strip() for i in e.get('parameterize').split(',')]
702  for p in parameters_to_optimize:
703  if p not in e.attrib:
704  logger.error("Parameter \'%s\' is not found for \'%s\', please check %s" % (p, e.get('type'), ffname) )
705  raise RuntimeError
706  pid = self.Readers[ffname].build_pid(e, p)
707  self.map[pid] = self.np
708  # offxml file later than v0.3 may have unit strings in the field
709  quantity_str = e.get(p)
710  res = re.search(r'^[-+]?[0-9]*\.?[0-9]*([eEdD][-+]?[0-9]+)?', quantity_str)
711  value_str, unit_str = quantity_str[:res.end()], quantity_str[res.end():]
712  self.assign_p0(self.np, float(value_str))
713  self.offxml_unit_strs[pid] = unit_str
714  self.assign_field(self.np,pid,ffname,fflist.index(e),p,1)
715  self.np += 1
716  self.patoms.append([])
717 
718  for e in self.ffdata[ffname].getroot().xpath('//@parameter_repeat/..'):
719  for field in e.get('parameter_repeat').split(','):
720  parameter_name = field.strip().split('=', 1)[0]
721  if parameter_name not in e.attrib:
722  logger.error("Parameter \'%s\' is not found for \'%s\', please check %s" % (parameter_name, e.get('type'), ffname) )
723  raise RuntimeError
724  dest = self.Readers[ffname].build_pid(e, parameter_name)
725  src = field.strip().split('=', 1)[1]
726  if src in self.map:
727  self.map[dest] = self.map[src]
728  else:
729  warn_press_key("Warning: You wanted to copy parameter from %s to %s, but the source parameter does not seem to exist!" % (src, dest))
730  self.assign_field(self.map[dest],dest,ffname,fflist.index(e),parameter_name,1)
731  quantity_str = e.get(parameter_name)
732  res = re.search(r'^[-+]?[0-9]*\.?[0-9]*([eEdD][-+]?[0-9]+)?', quantity_str)
733  value_str, unit_str = quantity_str[:res.end()], quantity_str[res.end():]
734  quantity_str = e.get(parameter_name)
735  self.offxml_unit_strs[dest] = unit_str
736 
737  for e in self.ffdata[ffname].getroot().xpath('//@parameter_eval/..'):
738  for field in e.get('parameter_eval').split(','):
739  parameter_name = field.strip().split('=', 1)[0]
740  if parameter_name not in e.attrib:
741  logger.error("Parameter \'%s\' is not found for \'%s\', please check %s" % (parameter_name, e.get('type'), ffname) )
742  raise RuntimeError
743  dest = self.Readers[ffname].build_pid(e, parameter_name)
744  evalcmd = field.strip().split('=', 1)[1]
745  self.assign_field(None,dest,ffname,fflist.index(e),parameter_name,None,evalcmd)
746  quantity_str = e.get(parameter_name)
747  res = re.search(r'^[-+]?[0-9]*\.?[0-9]*([eEdD][-+]?[0-9]+)?', quantity_str)
748  value_str, unit_str = quantity_str[:res.end()], quantity_str[res.end():]
749  quantity_str = e.get(parameter_name)
750  self.offxml_unit_strs[dest] = unit_str
751 
752  def make(self,vals=None,use_pvals=False,printdir=None,precision=12):
753  """ Create a new force field using provided parameter values.
754 
755  This big kahuna does a number of things:
756  1) Creates the physical parameters from the mathematical parameters
757  2) Creates force fields with physical parameters substituted in
758  3) Prints the force fields to the specified file.
759 
760  It does NOT store the mathematical parameters in the class state
761  (since we can only hold one set of parameters).
762 
763  @param[in] printdir The directory that the force fields are printed to; as usual
764  this is relative to the project root directory.
765  @param[in] vals Input parameters. I previously had an option where it uses
766  stored values in the class state, but I don't think that's a good idea anymore.
767  @param[in] use_pvals Switch for whether to bypass the coordinate transformation
768  and use physical parameters directly.
769  @param[in] precision Number of decimal points to print out
770 
771  """
772  if type(vals)==np.ndarray and vals.ndim != 1:
773  logger.error('Please only pass 1-D arrays\n')
774  raise RuntimeError
775  if len(vals) != self.np:
776  logger.error('Input parameter np.array (%i) not the required size (%i)\n' % (len(vals), self.np))
777  raise RuntimeError
778  if use_pvals or self.use_pvals:
779  logger.info("Using physical parameters directly!\r")
780  pvals = vals.copy().flatten()
781  else:
782  pvals = self.create_pvals(vals)
783 
784  OMMFormat = "%%.%ie" % precision
785  def TXTFormat(number, precision):
786  SciNot = "%% .%ie" % precision
787  if abs(number) < 1000 and abs(number) > 0.001:
788  Decimal = "%% .%if" % precision
789  Num = Decimal % number
790  Mum = Decimal % (-1 * number)
791  if (float(Num) == float(Mum)):
792  return Decimal % abs(number)
793  else:
794  return Decimal % number
795  else:
796  Num = SciNot % number
797  Mum = SciNot % (-1 * number)
798  if (float(Num) == float(Mum)):
799  return SciNot % abs(number)
800  else:
801  return SciNot % number
802 
803  pvals = list(pvals)
804  # pvec1d(vals, precision=4)
805  newffdata = deepcopy(self.ffdata)
806 
807  # The dictionary that takes parameter names to physical values.
808  PRM = {i:pvals[self.map[i]] for i in self.map}
809 
810  #======================================#
811  # Print the new force field. #
812  #======================================#
813 
814  xml_lines = OrderedDict([(fnm, list(newffdata[fnm].iter())) for fnm in self.fnms if self.ffdata_isxml[fnm]])
815 
816  for i in range(len(self.pfields)):
817  pfield = self.pfields[i]
818  pid,fnm,ln,fld,mult,cmd = pfield
819  # XML force fields are easy to print.
820  # Our 'pointer' to where to replace the value
821  # is given by the position of this line in the
822  # iterable representation of the tree and the
823  # field number.
824  # if type(newffdata[fnm]) is etree._ElementTree:
825  if cmd is not None:
826  try:
827  # Bobby Tables, anyone?
828  if any([x in cmd for x in ("system", "subprocess", "import")]):
829  warn_press_key("The command %s (written in the force field file) appears to be unsafe!" % cmd)
830  wval = eval(cmd.replace("PARM","PRM"))
831  # Attempt to allow evaluated parameters to be functions of each other.
832  PRM[pid] = wval
833  except:
834  logger.error(traceback.format_exc() + '\n')
835  logger.error("The command %s (written in the force field file) cannot be evaluated!\n" % cmd)
836  raise RuntimeError
837  else:
838  wval = mult*pvals[self.map[pid]]
839  if self.ffdata_isxml[fnm]:
840  # offxml files with version higher than 0.3 may have unit strings in the field
841  xml_lines[fnm][ln].attrib[fld] = OMMFormat % (wval) + self.offxml_unit_strs[pid]
842  # If this is a SMIRNOFF parameter, assign it directly in the openforcefield.ForceField object
843  # as well as writing out the file.
844  if hasattr(self, 'offxml') and fnm == self.offxml:
846  # list(newffdata[fnm].iter())[ln].attrib[fld] = OMMFormat % (wval)
847  # Text force fields are a bit harder.
848  # Our pointer is given by the line and field number.
849  # We take care to preserve whitespace in the printout
850  # so that the new force field still has nicely formated
851  # columns.
852  else:
853  # Split the string into whitespace and data fields.
854  sline = self.Readers[fnm].Split(newffdata[fnm][ln])
855  whites = self.Readers[fnm].Whites(newffdata[fnm][ln])
856  # Align whitespaces and fields (it should go white, field, white, field)
857  if newffdata[fnm][ln][0] != ' ':
858  whites = [''] + whites
859  # Subtract one whitespace, unless the line begins with a minus sign.
860  if not match('^-',sline[fld]) and len(whites[fld]) > 1:
861  whites[fld] = whites[fld][:-1]
862  # Actually replace the field with the physical parameter value.
863  if precision == 12:
864  newrd = "% 17.12e" % (wval)
865  else:
866  newrd = TXTFormat(wval, precision)
867  # The new word might be longer than the old word.
868  # If this is the case, we can try to shave off some whitespace.
869  Lold = len(sline[fld])
870  if not match('^-',sline[fld]):
871  Lold += 1
872  Lnew = len(newrd)
873  if Lnew > Lold:
874  Shave = Lnew - Lold
875  if Shave < (len(whites[fld+1])+2):
876  whites[fld+1] = whites[fld+1][:-Shave]
877  sline[fld] = newrd
878  # Replace the line in the new force field.
879  newffdata[fnm][ln] = ''.join([(whites[j] if (len(whites[j]) > 0 or j == 0) else ' ')+sline[j] for j in range(len(sline))])+'\n'
880 
881  if printdir is not None:
882  absprintdir = os.path.join(self.root,printdir)
883  else:
884  absprintdir = os.getcwd()
885 
886  if not os.path.exists(absprintdir):
887  logger.info('Creating the directory %s to print the force field\n' % absprintdir)
888  os.makedirs(absprintdir)
889 
890  for fnm in newffdata:
891  if self.ffdata_isxml[fnm]:
892  with wopen(os.path.join(absprintdir,fnm), binary=True) as f: newffdata[fnm].write(f)
893  elif 'Script.txt' in fnm:
894  # if the xml file contains a script, ForceBalance will generate
895  # a temporary .txt file containing the script and any updates.
896  # We copy the updates made in the .txt file into the xml file by:
897  # First, find xml file corresponding to this .txt file
898  # Second, copy context of the .txt file into the text attribute
899  # of the script element (assumed to be the last element)
900  # Third. open the updated xml file as in the if statement above
901  tempText = "".join(newffdata[fnm])
902  fnmXml = fnm.split('Script')[0]+'.xml'
903  Ntemp = len(list(newffdata[fnmXml].iter()))
904  list(newffdata[fnmXml].iter())[Ntemp-1].text = tempText
905  '''
906  scriptElements = [elem for elem in fflist if elem.tag=='Script']
907  if len(scriptElements) > 1:
908  logger.error('XML file'+ffname+'contains more than one script! Consolidate your scripts into one script!\n')
909  raise RuntimeError
910  else:
911  '''
912  with wopen(os.path.join(absprintdir,fnmXml), binary=True) as f: newffdata[fnmXml].write(f)
913  else:
914  with wopen(os.path.join(absprintdir,fnm)) as f: f.writelines(newffdata[fnm])
915 
916  return pvals
917 
918  def make_redirect(self,mvals):
919  Groups = defaultdict(list)
920  for p, pid in enumerate(self.plist):
921  if 'Exponent' not in pid or len(pid.split()) != 1:
922  warn_press_key("Fusion penalty currently implemented only for basis set optimizations, where parameters are like this: Exponent:Elem=H,AMom=D,Bas=0,Con=0")
923  Data = dict([(i.split('=')[0],i.split('=')[1]) for i in pid.split(':')[1].split(',')])
924  if 'Con' not in Data or Data['Con'] != '0':
925  warn_press_key("More than one contraction coefficient found! You should expect the unexpected")
926  key = Data['Elem']+'_'+Data['AMom']
927  Groups[key].append(p)
928  #pvals = self.FF.create_pvals(mvals)
929  #print "pvals: ", pvals
930 
931  pvals = self.create_pvals(mvals)
932  logger.info("pvals:\n")
933  logger.info(str(pvals) + '\n')
934 
935  Thresh = 1e-4
936 
937  for gnm, pidx in Groups.items():
938  # The group of parameters for a particular element / angular momentum.
939  pvals_grp = pvals[pidx]
940  # The order that the parameters come in.
941  Order = np.argsort(pvals_grp)
942  for p in range(len(Order) - 1):
943  # The pointers to the parameter indices.
944  pi = pidx[Order[p]]
945  pj = pidx[Order[p+1]]
946  # pvals[pi] is the SMALLER parameter.
947  # pvals[pj] is the LARGER parameter.
948  dp = np.log(pvals[pj]) - np.log(pvals[pi])
949  if dp < Thresh:
950  if pi in self.redirect:
951  pk = self.redirect[pi]
952  else:
953  pk = pi
954  #if pj not in self.redirect:
955  #print "Redirecting parameter %i to %i" % (pj, pk)
956  #self.redirect[pj] = pk
957  #print self.redirect
958 
959  def find_spacings(self):
960  Groups = defaultdict(list)
961  for p, pid in enumerate(self.plist):
962  if 'Exponent' not in pid or len(pid.split()) != 1:
963  warn_press_key("Fusion penalty currently implemented only for basis set optimizations, where parameters are like this: Exponent:Elem=H,AMom=D,Bas=0,Con=0")
964  Data = dict([(i.split('=')[0],i.split('=')[1]) for i in pid.split(':')[1].split(',')])
965  if 'Con' not in Data or Data['Con'] != '0':
966  warn_press_key("More than one contraction coefficient found! You should expect the unexpected")
967  key = Data['Elem']+'_'+Data['AMom']
968  Groups[key].append(p)
969 
970  pvals = self.create_pvals(np.zeros(self.np))
971  logger.info("pvals:\n")
972  logger.info(str(pvals) + '\n')
973 
974  spacdict = {}
975  for gnm, pidx in Groups.items():
976  # The group of parameters for a particular element / angular momentum.
977  pvals_grp = pvals[pidx]
978  # The order that the parameters come in.
979  Order = np.argsort(pvals_grp)
980  spacs = []
981  for p in range(len(Order) - 1):
982  # The pointers to the parameter indices.
983  pi = pidx[Order[p]]
984  pj = pidx[Order[p+1]]
985  # pvals[pi] is the SMALLER parameter.
986  # pvals[pj] is the LARGER parameter.
987  dp = np.log(pvals[pj]) - np.log(pvals[pi])
988  spacs.append(dp)
989  if len(spacs) > 0:
990  spacdict[gnm] = np.mean(np.array(spacs))
991  return spacdict
992 
993  def create_pvals(self,mvals):
994  """Converts mathematical to physical parameters.
995 
996  First, mathematical parameters are rescaled and rotated by
997  multiplying by the transformation matrix, followed by adding
998  the original physical parameters.
999 
1000  @param[in] mvals The mathematical parameters
1001  @return pvals The physical parameters
1002 
1003  """
1004  if isinstance(mvals, list):
1005  mvals = np.array(mvals)
1006  for p in self.redirect:
1007  mvals[p] = 0.0
1008  if self.logarithmic_map:
1009  try:
1010  pvals = np.exp(mvals.flatten()) * self.pvals0
1011  except:
1012  logger.exception(str(mvals) + '\n')
1013  logger.error('What the hell did you do?\n')
1014  raise RuntimeError
1015  else:
1016  pvals = flat(np.dot(self.tmI,col(mvals))) + self.pvals0
1017  concern= ['polarizability','epsilon','VDWT']
1018  # Guard against certain types of parameters changing sign.
1019 
1020  for i in range(self.np):
1021  if any([j in self.plist[i] for j in concern]) and pvals[i] * self.pvals0[i] < 0:
1022  #print "Parameter %s has changed sign but it's not allowed to! Setting to zero." % self.plist[i]
1023  pvals[i] = 0.0
1024  # Redirect parameters (for the fusion penalty function.)
1025  for p in self.redirect:
1026  pvals[p] = pvals[self.redirect[p]]
1027  # if not in_fd():
1028  # print pvals
1029  #print "pvals = ", pvals
1030 
1031  return pvals
1032 
1033 
1034  def create_mvals(self,pvals):
1035  """Converts physical to mathematical parameters.
1036 
1037  We create the inverse transformation matrix using SVD.
1038 
1039  @param[in] pvals The physical parameters
1040  @return mvals The mathematical parameters
1041  """
1042 
1043  if self.logarithmic_map:
1044  logger.error('create_mvals has not been implemented for logarithmic_map\n')
1045  raise RuntimeError
1046  mvals = flat(np.dot(invert_svd(self.tmI), col(pvals-self.pvals0)))
1047 
1048  return mvals
1049 
1050  def rsmake(self,printfacs=True):
1051  """Create the rescaling factors for the coordinate transformation in parameter space.
1053  The proper choice of rescaling factors (read: prior widths in maximum likelihood analysis)
1054  is still a black art. This is a topic of current research.
1055 
1056  @todo Pass in rsfactors through the input file
1057 
1058  @param[in] printfacs List for printing out the resecaling factors
1059 
1060  """
1061  typevals = OrderedDict()
1062  rsfactors = OrderedDict()
1063  rsfac_list = []
1064 
1066  termtypelist = []
1067  for f in self.fnms:
1068  if self.Readers[f].pdict == "XML_Override":
1069  for i, pName in enumerate(self.map):
1070  for p in self.pfields:
1071  if p[0] == pName:
1072  fnm = p[1]
1073  break
1074  ffnameScript = f.split('.')[0]+'Script.txt'
1075  if fnm == f or fnm == ffnameScript:
1076  if fnm.endswith('offxml'):
1077  ttstr = '/'.join([pName.split('/')[0],pName.split('/')[1],pName.split('/')[2]])
1078  else:
1079  ttstr = '/'.join([pName.split('/')[0],pName.split('/')[1]])
1080  if ttstr not in termtypelist:
1081  termtypelist.append(ttstr)
1082  else:
1083  for i in self.Readers[f].pdict:
1084  for j in self.Readers[f].pdict[i]:
1085  if isint(str(j)):
1086  ttstr = i+self.Readers[f].pdict[i][j]
1087  if ttstr not in termtypelist:
1088  termtypelist.append(ttstr)
1089  for termtype in termtypelist:
1090  for pid in self.map:
1091  if termtype in pid:
1092  typevals.setdefault(termtype, []).append(self.pvals0[self.map[pid]])
1093  for termtype in typevals:
1094  # The old, horrendously complicated rule
1095  # rsfactors[termtype] = exp(mean(log(abs(array(typevals[termtype]))+(abs(array(typevals[termtype]))==0))))
1096  # The newer, maximum rule (thanks Baba)
1097  maxval = max(abs(np.array(typevals[termtype])))
1098  # When all initial parameter values are zero, it could be a problem...
1099  if maxval == 0:
1100  maxval += 1
1101  rsfactors[termtype] = maxval
1102  rsfac_list.append(termtype)
1103  # Physically motivated overrides
1104  rs_override(rsfactors,termtype)
1105  # Overrides from input file
1106  for termtype in self.priors:
1107  while termtype in rsfac_list:
1108  rsfac_list.remove(termtype)
1109  rsfac_list.append(termtype)
1110  rsfactors[termtype] = self.priors[termtype]
1111 
1112  # for line in os.popen("awk '/rsfactor/ {print $2,$3}' %s" % pkg.options).readlines():
1113  # rsfactors[line.split()[0]] = float(line.split()[1])
1114  if printfacs:
1115  bar = printcool("Rescaling Factors by Type (Lower Takes Precedence):",color=1)
1116  logger.info(''.join([" %-35s : %.5e\n" % (i, rsfactors[i]) for i in rsfac_list]))
1117  logger.info(bar)
1118  self.rs_ord = OrderedDict([(i, rsfactors[i]) for i in rsfac_list])
1119 
1120  self.rs = np.ones(len(self.pvals0))
1121  self.rs_type = OrderedDict()
1122  have_rs = []
1123  for pnum in range(len(self.pvals0)):
1124  for termtype in rsfac_list:
1125  if termtype in self.plist[pnum]:
1126  if pnum not in have_rs:
1127  self.rs[pnum] = rsfactors[termtype]
1128  self.rs_type[pnum] = termtype
1129  elif self.rs[pnum] != rsfactors[termtype]:
1130  self.rs[pnum] = rsfactors[termtype]
1131  self.rs_type[pnum] = termtype
1132  have_rs.append(pnum)
1134  for pnum in range(len(self.pvals0)):
1135  if pnum not in have_rs:
1136  self.rs_type[pnum] = self.plist[pnum][0]
1137  if printfacs:
1138  bar = printcool("Rescaling Types / Factors by Parameter Number:",color=1)
1139  self.print_map(vals=[" %-28s : %.5e" % (self.rs_type[pnum], self.rs[pnum]) for pnum in range(len(self.pvals0))])
1140  logger.info(bar)
1141 
1142  def make_rescale(self, scales, mvals=None, G=None, H=None, multiply=True, verbose=False):
1143  """ Obtain rescaled versions of the inputs according to dictionary values
1144  in "scales" (i.e. a replacement or multiplicative factor on self.rs_ord).
1145  Note that self.rs and self.rs_ord are not updated in this function. You
1146  need to do that outside.
1147 
1148  The purpose of this function is to simulate the effect of changing the
1149  parameter scale factors in the force field. If the scale factor is N,
1150  then a unit change in the mathematical parameters produces a change of
1151  N in the physical parameters. Thus, for a given point in the physical
1152  parameter space, the mathematical parameters are proportional to 1/N,
1153  the gradient is proportional to N, and the Hessian is proportional to N^2.
1154 
1155  Parameters
1156  ----------
1157  mvals : numpy.ndarray
1158  Parameters to be transformed, if desired. Must be same length as number of parameters.
1159  G : numpy.ndarray
1160  Gradient to be transformed, if desired. Must be same length as number of parameters.
1161  H : numpy.ndarray
1162  Hessian to be transformed, if desired. Must be square matrix with side-length = number of parameters.
1163  scales : OrderedDict
1164  A dictionary with the same keys as self.rs_ord and floating point values.
1165  These represent the values with which to multiply the existing scale factors
1166  (if multiply == True) or replace them (if multiply == False).
1167  Pro Tip: Create this variable from a copy of self.rs_ord
1168  multiply : bool
1169  When set to True, the new scale factors are the existing scale factors
1170  verbose : bool
1171  Loud and noisy
1172 
1173  Returns
1174  -------
1175  answer : OrderedDict
1176  Output dictionary containing :
1177  'rs' : New parameter scale factors (multiplied by scales if multiply=True, or replaced if multiply=False)
1178  'rs_ord' : New parameter scale factor dictionary
1179  'mvals' : New parameter values (if mvals is provided)
1180  'G' : New gradient (if G is provided)
1181  'H' : New Hessian (if H is provided)
1182  """
1183  if type(scales) != OrderedDict:
1184  raise RuntimeError('scales must have type OrderedDict')
1185  if scales.keys() != self.rs_ord.keys():
1186  raise RuntimeError('scales should have same keys as self.rs_ord')
1187  # Make the new dictionary of rescaling factors
1188  if multiply == False:
1189  rsord_out = deepcopy(scales)
1190  else:
1191  rsord_out = OrderedDict([(k, scales[k]*self.rs_ord[k]) for k in scales.keys()])
1192  answer = OrderedDict()
1193  answer['rs_ord'] = rsord_out
1194  # Make the new array of rescaling factors
1195  rs_out = np.array([rsord_out[self.rs_type[p]] for p in range(self.np)])
1196  answer['rs'] = rs_out
1197  if mvals is not None:
1198  if mvals.shape != (self.np,):
1199  raise RuntimeError('mvals has the wrong shape')
1200  mvals_out = deepcopy(mvals)
1201  for p in range(self.np):
1202  # Remember that for the same physical parameters, the mathematical
1203  # parameters are inversely proportional to scale factors
1204  mvals_out[p] *= (float(self.rs[p])/float(rs_out[p]))
1205  answer['mvals'] = mvals_out
1206  if G is not None:
1207  if G.shape != (self.np,):
1208  raise RuntimeError('G has the wrong shape')
1209  G_out = deepcopy(G)
1210  for p in range(self.np):
1211  # The gradient should be proportional to the scale factors
1212  G_out[p] *= (float(rs_out[p])/float(self.rs[p]))
1213  answer['G'] = G_out
1214  if H is not None:
1215  if H.shape != (self.np,self.np):
1216  raise RuntimeError('H has the wrong shape')
1217  H_out = deepcopy(H)
1218  for p in range(self.np):
1219  # The Hessian should be proportional to the product of two scale factors
1220  H_out[p, :] *= (float(rs_out[p])/float(self.rs[p]))
1221  for p in range(self.np):
1222  H_out[:, p] *= (float(rs_out[p])/float(self.rs[p]))
1223  answer['H'] = H_out
1224  # The final parameters, gradient and Hessian should be consistent with the
1225  # returned scale factors.
1226  return answer
1227 
1228  def mktransmat(self):
1229  """ Create the transformation matrix to rescale and rotate the mathematical parameters.
1230 
1231  For point charge parameters, project out perturbations that
1232  change the total charge.
1233 
1234  First build these:
1235 
1236  'qmap' : Just a list of parameter indices that point to charges.
1237 
1238  'qid' : For each parameter in the qmap, a list of the affected atoms :)
1239  A potential target for the molecule-specific thang.
1240 
1241  Then make this:
1242 
1243  'qtrans2' : A transformation matrix that rotates the charge parameters.
1244  The first row is all zeros (because it corresponds to increasing the charge on all atoms)
1245  The other rows correspond to changing one of the parameters and decreasing all of the others
1246  equally such that the overall charge is preserved.
1247 
1248  'qmat2' : An identity matrix with 'qtrans2' pasted into the right place
1249 
1250  'transmat': 'qmat2' with rows and columns scaled using self.rs
1251 
1252  'excision': Parameter indices that need to be 'cut out' because they are irrelevant and
1253  mess with the matrix diagonalization
1254 
1255  @todo Only project out changes in total charge of a molecule, and perhaps generalize to
1256  fragments of molecules or other types of parameters.
1257  @todo The AMOEBA selection of charge depends not only on the atom type, but what that atom is bonded to.
1258  """
1259  self.qmap = []
1260  self.qid = []
1261  self.qid2 = []
1262  qnr = 1
1263  concern= ['COUL','c0','charge']
1264  qmat2 = np.eye(self.np)
1265 
1266  def insert_mat(qtrans2, qmap):
1267  # Write the qtrans2 block into qmat2.
1268  x = 0
1269  for i in range(self.np):
1270  if i in qmap:
1271  y = 0
1272  for j in qmap:
1273  qmat2[i, j] = qtrans2[x, y]
1274  y += 1
1275  x += 1
1276 
1277  def build_qtrans2(tq, qid, qmap):
1278  """ Build the matrix that ensures the net charge does not change. """
1279  nq = len(qmap)
1280  # tq = Total number of atomic charges that are being optimized on the molecule
1281  # NOTE: This may be greater than the number of charge parameters (nq)
1282  # The reason for the "one" here is because LP wanted to have multiple charge constraints
1283  # at some point in the future
1284  cons0 = np.ones((1,tq))
1285  cons = np.zeros((cons0.shape[0], nq))
1286  # Identity matrix equal to the number of charge parameters
1287  qtrans2 = np.eye(nq)
1288  # This is just one
1289  for i in range(cons.shape[0]):
1290  # Loop over the number of charge parameters
1291  for j in range(cons.shape[1]):
1292  # Each element of qid is a list that points to atom indices.
1293  # LPW: This code is breaking when we're not optimizing ALL the charges
1294  # Replace cons0[i][k-1] with all ones
1295  # cons[i][j] = sum([cons0[i][k-1] for k in qid[j]])
1296  cons[i][j] = float(len(qid[j]))
1297  cons[i] /= np.linalg.norm(cons[i])
1298  for j in range(i):
1299  cons[i] = orthogonalize(cons[i], cons[j])
1300  qtrans2[i,:] = 0
1301  for j in range(nq-i-1):
1302  qtrans2[i+j+1, :] = orthogonalize(qtrans2[i+j+1, :], cons[i])
1303  return qtrans2
1304  # Here we build a charge constraint for each molecule.
1305  if any(len(r.adict) > 0 for r in self.Readers.values()):
1306  logger.info("Building charge constraints...\n")
1307  # Build a concatenated dictionary
1308  Adict = OrderedDict()
1309  # This is a loop over files
1310  for r in self.Readers.values():
1311  # This is a loop over molecules
1312  for k, v in r.adict.items():
1313  Adict[k] = v
1314  nmol = 0
1315  for molname, molatoms in Adict.items():
1316  mol_charge_count = np.zeros(self.np)
1317  tq = 0
1318  qmap = []
1319  qid = []
1320  for i in range(self.np):
1321  qct = 0
1322  qidx = []
1323  for imol, iatoms in self.patoms[i]:
1324  for iatom in iatoms:
1325  if imol == molname and iatom in molatoms:
1326  qct += 1
1327  tq += 1
1328  qidx.append(molatoms.index(iatom))
1329  if any([j in self.plist[i] for j in concern]) and qct > 0:
1330  qmap.append(i)
1331  qid.append(qidx)
1332  logger.info("Parameter %i occurs %i times in molecule %s in locations %s (%s)\n" % (i, qct, molname, str(qidx), self.plist[i]))
1333  #Here is where we build the qtrans2 matrix.
1334  if len(qmap) > 0:
1335  qtrans2 = build_qtrans2(tq, qid, qmap)
1336  if self.constrain_charge:
1337  insert_mat(qtrans2, qmap)
1338  if nmol == 0:
1339  self.qid = qid
1340  self.qmap = qmap
1341  # The warning about ESP fitting is not very helpful
1342  # else:
1343  # logger.info("Note: ESP fitting will be performed assuming that molecule id %s is the FIRST molecule and the only one being fitted.\n" % molname)
1344  nmol += 1
1345  elif self.constrain_charge:
1346  warn_press_key("'adict' {molecule:atomnames} was not found.\n This isn't a big deal if we only have one molecule, but might cause problems if we want multiple charge neutrality constraints.")
1347  qnr = 0
1348  if any([self.Readers[i].pdict == "XML_Override" for i in self.fnms]):
1349  # Hack to count the number of atoms for each atomic charge parameter, when the force field is an XML file.
1350  # This needs to be changed to Chain or Molecule
1351  logger.info(str([determine_fftype(k) for k in self.ffdata]))
1352  ListOfAtoms = list(itertools.chain(*[[e.get('type') for e in self.ffdata[k].getroot().xpath('//Residue/Atom')] for k in self.ffdata if determine_fftype(k) == "openmm"]))
1353  for i in range(self.np):
1354  if any([j in self.plist[i] for j in concern]):
1355  self.qmap.append(i)
1356  if 'Multipole/c0' in self.plist[i] or 'Atom/charge' in self.plist[i]:
1357  AType = self.plist[i].split('/')[-1].split('.')[0]
1358  nq = ListOfAtoms.count(AType)
1359  else:
1360  thisq = []
1361  for k in self.plist[i].split():
1362  for j in concern:
1363  if j in k:
1364  thisq.append(k.split('-')[-1])
1365  break
1366  try:
1367  self.qid2.append(np.array([self.atomnames.index(k) for k in thisq]))
1368  except: pass
1369  nq = sum(np.array([self.plist[i].count(j) for j in concern]))
1370  self.qid.append(qnr+np.arange(nq))
1371  qnr += nq
1372  if len(self.qid2) == 0:
1373  sys.stderr.write('Unable to match atom numbers up with atom names (minor issue, unless doing ESP fitting). \nAre atom names implemented in the force field parser?\n')
1374  else:
1375  self.qid = self.qid2
1376  tq = qnr - 1
1377  #Here is where we build the qtrans2 matrix.
1378  if len(self.qmap) > 0:
1379  cons0 = np.ones((1,tq))
1380  qtrans2 = build_qtrans2(tq, self.qid, self.qmap)
1381  # Insert qtrans2 into qmat2.
1382  if self.constrain_charge:
1383  insert_mat(qtrans2, self.qmap)
1384 
1385 
1387  if self.constrain_charge:
1388  MultipoleAtoms = set([p.split('/')[-1] for p in self.plist if 'Multipole' in p])
1389  QuadrupoleGrps = [[i for i, p in enumerate(self.plist) if 'Multipole' in p and p.split('/')[-1] == A and p.split('/')[1] in ['q11','q22','q33']] for A in MultipoleAtoms]
1390  for Grp in QuadrupoleGrps:
1391  qid = [np.array([i]) for i in range(3)]
1392  tq = 3
1393  qtrans2 = build_qtrans2(tq, qid, Grp)
1394  logger.info("Making sure that quadrupoles are traceless (for parameter IDs %s)\n" % str(Grp))
1395  insert_mat(qtrans2, Grp)
1396 
1397  #ListOfAtoms = list(itertools.chain(*[[e.get('type') for e in self.ffdata[k].getroot().xpath('//Multipole')] for k in self.ffdata]))
1398 
1399  # print "Charge parameter constraint matrix - feel free to check it"
1400  # for i in qmat2:
1401  # for j in i:
1402  # print "% .3f" % j,
1403  # print
1404  # print
1405 
1406  # There is a bad bug here .. this matrix multiplication operation doesn't work!!
1407  # I will proceed using loops. This is unsettling.
1408  # Input matrices are qmat2 and self.rs (diagonal)
1409  transmat = np.dot(qmat2, np.diag(self.rs))
1410  transmat1 = np.zeros((self.np, self.np))
1411  for i in range(self.np):
1412  for k in range(self.np):
1413  transmat1[i,k] = qmat2[i,k] * self.rs[k]
1414 
1415  # This prints out the difference between the result of matrix multiplication
1416  # and the manual multiplication.
1417  #print transmat1
1418  #print transmat
1419  if len(transmat) > 0 and np.max(np.abs(transmat1 - transmat)) > 0.0:
1420  logger.warning('The difference between the numpy multiplication and the manual multiplication is \x1b[1;91m%f\x1b[0m, '
1421  'but it should be zero.\n' % np.max(np.abs(transmat1 - transmat)))
1422 
1423  transmat = np.array(transmat1, copy=True)
1424  transmatNS = np.array(transmat,copy=True)
1425  self.excision = []
1426  for i in range(self.np):
1427  if abs(transmatNS[i, i]) < 1e-8:
1428  self.excision.append(i)
1429  transmatNS[i, i] += 1
1430  self.excision = list(set(self.excision))
1431  for i in self.excision:
1432  transmat[i, :] = np.zeros(self.np)
1433  self.tm = transmat
1434  self.tmI = transmat.T
1435 
1436  def list_map(self):
1437  """ Create the plist, which is like a reversed version of the parameter map. More convenient for printing. """
1438  if len(self.map) == 0:
1439  warn_press_key('The parameter map has no elements (Okay if we are not actually tuning any parameters.)')
1440  else:
1441  self.plist = [[] for j in range(max([self.map[i] for i in self.map])+1)]
1442  for i in self.map:
1443  self.plist[self.map[i]].append(i)
1444  for i in range(self.np):
1445  self.plist[i] = ' '.join(natural_sort(self.plist[i]))
1446 
1447  def print_map(self,vals = None,precision=4):
1448  """Prints out the (physical or mathematical) parameter indices, IDs and values in a visually appealing way."""
1449  if vals is None:
1450  vals = self.pvals0
1451  logger.info('\n'.join(["%4i [ %s ]" % (self.plist.index(i), "%% .%ie" % precision % float(vals[self.plist.index(i)]) if isfloat(str(vals[self.plist.index(i)])) else (str(vals[self.plist.index(i)]))) + " : " + "%s" % i.split()[0] for i in self.plist]))
1452  logger.info('\n')
1454  def sprint_map(self,vals = None,precision=4):
1455  """Prints out the (physical or mathematical) parameter indices, IDs and values to a string."""
1456  if vals is None:
1457  vals = self.pvals0
1458  out = '\n'.join(["%4i [ %s ]" % (self.plist.index(i), "%% .%ie" % precision % float(vals[self.plist.index(i)]) if isfloat(str(vals[self.plist.index(i)])) else (str(vals[self.plist.index(i)]))) + " : " + "%s" % i.split()[0] for i in self.plist])
1459  return out
1460 
1461  def assign_p0(self,idx,val):
1462  """ Assign physical parameter values to the 'pvals0' array.
1463 
1464  @param[in] idx The index to which we assign the parameter value.
1465  @param[in] val The parameter value to be inserted.
1466  """
1467  if idx == len(self.pvals0):
1468  self.pvals0.append(val)
1469  else:
1470  self.pvals0[idx] = val
1471 
1472  def assign_field(self,idx,pid,fnm,ln,pfld,mult,cmd=None):
1473  """ Record the locations of a parameter in a txt file; [[file name, line number, field number, and multiplier]].
1474 
1475  Note that parameters can have multiple locations because of the repetition functionality.
1476 
1477  LPW 2019-07-08: Build a graph representation of the parameters where each node is a specific parameter
1478  in the force field file, and contains attributes such as the mathematical ID, file name, etc.
1479  This is an improved representation of the data contained in self.pfields.
1480 
1481  @param[in] idx The (not necessarily unique) index of the parameter.
1482  @param[in] pid The unique parameter name.
1483  @param[in] fnm The file name of the parameter field.
1484  @param[in] ln The line number within the file (or the node index in the flattened xml)
1485  @param[in] pfld The field within the line (or the name of the attribute in the xml)
1486  @param[in] mult The multiplier (this is usually 1.0)
1487 
1488  """
1489  self.pTree.add_node(pid, param_mathid=idx, file_name=fnm, line_number=ln, field_number=pfld, multiplier=mult, evalcmd=cmd)
1490 
1491  if cmd:
1492  # Matches dictionary keys in a string like this:
1493  # Given string: PRM['Bonds/Bond/k/[#6X3:1]-[#6X3:2]']*np.arccos(PRM["[*:1]~[#7X3$(*~[#6X3]):2](~[*:3])~[*:4]"])
1494  # Returns: (['Bonds/Bond/k/[#6X3:1]-[#6X3:2]'], ["[*:1]~[#7X3$(*~[#6X3]):2](~[*:3])~[*:4]"])
1495  matches = re.findall("\[['\"][^'\"]*['\"]\]", cmd)
1496  for word in matches:
1497  src_param_name = re.sub("\[['\"]|['\"]\]", "", word)
1498  src_nodes = [x for x in self.pTree.nodes() if x==src_param_name]
1499  if len(src_nodes) > 1:
1500  raise RuntimeError('Detected multiple nodes in pTree with the same name; this should not happen')
1501  elif src_nodes:
1502  if src_nodes[0] == pid:
1503  raise RuntimeError('Evaluated parameter depends on itself')
1504  # This makes a graph where the evaluated parameter is called the "successor"
1505  # and the original parameter is called the "predecessor"
1506  # Arrows point from successors to predecessors in the plot representation
1507  self.pTree.add_edge(pid, src_nodes[0])
1508  else:
1509  raise RuntimeError('Evaluated parameter refers to a source parameter that does not exist:\n%s'%cmd)
1510 
1511  self.pfields.append([pid,fnm,ln,pfld,mult,cmd])
1512 
1513  def plot_parameter_tree(fnm):
1514 
1515  import matplotlib.pyplot as plt
1516  plt.switch_backend('agg')
1517  fig, ax = plt.subplots()
1518  fig.set_size_inches(12, 8)
1519  plt.sca(ax)
1520  nx.draw(self.pTree, with_labels=True)
1521  fig.savefig(fnm)
1522 
1523  def get_mathid(self, pid):
1524  """
1525  For a given unique parameter identifier, return all of the mvals that it is connected to.
1526  """
1527  mathids = []
1528  if nx.get_node_attributes(self.pTree, 'param_mathid')[pid] is not None:
1529  mathids.append(nx.get_node_attributes(self.pTree, 'param_mathid')[pid])
1530 
1531  for node in nx.dfs_predecessors(self.pTree, pid).keys():
1532  if nx.get_node_attributes(self.pTree, 'param_mathid')[node] is not None:
1533  mathids.append(nx.get_node_attributes(self.pTree, 'param_mathid')[node])
1534  return mathids
1535 
1536  def __eq__(self, other):
1537  # check equality of forcefields using comparison of pfields and map
1538  if isinstance(other, FF):
1539  # list comprehension removes pid/filename element of pfields since we don't care about filename uniqueness
1540  self_pfields = [p[2:] for p in self.pfields]
1541  other_pfields= [p[2:] for p in other.pfields]
1542 
1543  return self_pfields == other_pfields and\
1544  self.map == other.map and\
1545  (self.pvals0 == other.pvals0).all()
1546 
1547  # we only compare two forcefield objects
1548  else: return NotImplemented
1549 
1550 def rs_override(rsfactors,termtype,Temperature=298.15):
1551  """ This function takes in a dictionary (rsfactors) and a string (termtype).
1552 
1553  If termtype matches any of the strings below, rsfactors[termtype] is assigned
1554  to one of the numbers below.
1555 
1556  This is LPW's attempt to simplify the rescaling factors.
1558  @param[out] rsfactors The computed rescaling factor.
1559  @param[in] termtype The interaction type (corresponding to a physical unit)
1560  @param[in] Temperature The temperature for computing the kT energy scale
1561 
1562  """
1563  if match('PIMPDIHS[1-6]K|PDIHMULS[1-6]K|PDIHS[1-6]K|RBDIHSK[1-5]|MORSEC',termtype):
1564  # eV or eV rad^-2
1565  rsfactors[termtype] = 96.4853
1566  elif match('UREY_BRADLEYK1|ANGLESK',termtype):
1567  rsfactors[termtype] = 96.4853 * 6.28
1568  elif match('COUL|VPAIR_BHAMC|QTPIEA',termtype):
1569  # elementary charge, or unitless, or already in atomic unit
1570  rsfactors[termtype] = 1.0
1571  elif match('QTPIEC|QTPIEH',termtype):
1572  # eV to atomic unit
1573  rsfactors[termtype] = 27.2114
1574  elif match('BONDSB|UREY_BRADLEYB|MORSEB|VDWS|VPAIRS|VSITE|VDW_BHAMA|VPAIR_BHAMA',termtype):
1575  # nm to atomic unit
1576  rsfactors[termtype] = 0.05291772
1577  elif match('BONDSK|UREY_BRADLEYK2',termtype):
1578  # au bohr^-2
1579  rsfactors[termtype] = 34455.5275 * 27.2114
1580  elif match('PDIHS[1-6]B|ANGLESB|UREY_BRADLEYB',termtype):
1581  # radian
1582  rsfactors[termtype] = 57.295779513
1583  elif match('VDWT|VDW_BHAMB|VPAIR_BHAMB',termtype):
1584  # VdW well depth; using kT. This was a tough one because the energy scale is so darn small.
1585  rsfactors[termtype] = kb*Temperature
1586  elif match('MORSEE',termtype):
1587  rsfactors[termtype] = 18.897261
def make(self, vals=None, use_pvals=False, printdir=None, precision=12)
Create a new force field using provided parameter values.
Definition: forcefield.py:780
def find_spacings(self)
Definition: forcefield.py:968
Class for parsing OpenMM force field files.
Definition: smirnoffio.py:117
def assign_p0(self, idx, val)
Assign physical parameter values to the &#39;pvals0&#39; array.
Definition: forcefield.py:1485
excision
Indices to exclude from optimization / Hessian inversion.
Definition: forcefield.py:279
def create_mvals(self, pvals)
Converts physical to mathematical parameters.
Definition: forcefield.py:1052
np
The total number of parameters.
Definition: forcefield.py:281
Nifty functions, intended to be imported by any module within ForceBalance.
def determine_fftype(ffname, verbose=False)
Determine the type of a force field file.
Definition: forcefield.py:156
plist
The listing of parameter number -> interaction types.
Definition: forcefield.py:260
tm
The transformation matrix for mathematical -> physical parameters.
Definition: forcefield.py:275
Finite state machine for parsing DVR grid files.
Definition: psi4io.py:254
def print_map(self, vals=None, precision=4)
Prints out the (physical or mathematical) parameter indices, IDs and values in a visually appealing w...
Definition: forcefield.py:1465
def __getstate__(self)
Definition: forcefield.py:352
pfields
A list where pfields[i] = [pid,&#39;file&#39;,line,field,mult,cmd], basically a new way to modify force field...
Definition: forcefield.py:267
def invert_svd(X, thresh=1e-12)
Invert a matrix using singular value decomposition.
Definition: nifty.py:621
def isint(word)
ONLY matches integers! If you have a decimal point? None shall pass!
Definition: molecule.py:401
def flat(vec)
Given any list, array, or matrix, return a single-index array.
Definition: nifty.py:467
def make_redirect(self, mvals)
Definition: forcefield.py:927
def sprint_map(self, vals=None, precision=4)
Prints out the (physical or mathematical) parameter indices, IDs and values to a string.
Definition: forcefield.py:1473
Interaction type -> Parameter Dictionary.
Definition: psi4io.py:39
def rs_override(rsfactors, termtype, Temperature=298.15)
This function takes in a dictionary (rsfactors) and a string (termtype).
Definition: forcefield.py:1584
def natural_sort(l)
Return a natural sorted list.
Definition: nifty.py:285
def check_dupes(self, pid, ffname, ln, pfld)
Check to see whether a given parameter ID already exists, and provide an alternate if needed...
Definition: forcefield.py:511
map
The mapping of interaction type -> parameter number.
Definition: forcefield.py:258
FFAtomTypes
WORK IN PROGRESS ## This is a dictionary of {&#39;AtomType&#39;:{&#39;Mass&#39; : float, &#39;Charge&#39; : float...
Definition: forcefield.py:297
Finite state machine for parsing custom GROMACS force field files.
Definition: custom_io.py:41
atomnames
A list of atom names (this is new, for ESP fitting)
Definition: forcefield.py:287
Finite state machine for parsing Mol2 force field file.
Definition: amberio.py:500
ffdata
As these options proliferate, the force field class becomes less standalone.
Definition: forcefield.py:255
def assign_field(self, idx, pid, fnm, ln, pfld, mult, cmd=None)
Record the locations of a parameter in a txt file; [[file name, line number, field number...
Definition: forcefield.py:1508
def addff_xml(self, ffname)
Parse an XML force field file and create important instance variables.
Definition: forcefield.py:684
def col(vec)
Given any list, array, or matrix, return a 1-column 2D array.
Definition: nifty.py:448
def plot_parameter_tree(fnm)
Definition: forcefield.py:1533
pvals0
Initial value of physical parameters.
Definition: forcefield.py:283
linedestroy_save
Destruction dictionary (experimental).
Definition: forcefield.py:338
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 rsmake(self, printfacs=True)
Create the rescaling factors for the coordinate transformation in parameter space.
Definition: forcefield.py:1072
redirect
Creates plist from map.
Definition: forcefield.py:336
def orthogonalize(vec1, vec2)
Given two vectors vec1 and vec2, project out the component of vec1 that is along the vec2-direction...
Definition: nifty.py:608
tmI
The transpose of the transformation matrix.
Definition: forcefield.py:277
Readers
A dictionary of force field reader classes.
Definition: forcefield.py:285
def __init__(self, backup_dict)
Definition: forcefield.py:187
def warn_press_key(warning, timeout=10)
Definition: nifty.py:1599
def printcool_dictionary(Dict, title="Dictionary Keys : Values", bold=False, color=2, keywidth=25, topwidth=50, center=True, leftpad=0)
See documentation for printcool; this is a nice way to print out keys/values in a dictionary...
Definition: nifty.py:366
def get_mathid(self, pid)
For a given unique parameter identifier, return all of the mvals that it is connected to...
Definition: forcefield.py:1547
rs_ord
Takes the dictionary &#39;BONDS&#39;:{3:&#39;B&#39;, 4:&#39;K&#39;}, &#39;VDW&#39;:{4:&#39;S&#39;, 5:&#39;T&#39;}, and turns it into a list of term t...
Definition: forcefield.py:1130
def addff_txt(self, ffname, fftype, xmlScript)
Parse a text force field and create several important instance variables.
Definition: forcefield.py:564
Finite state machine for parsing GROMACS force field files.
Definition: gmxio.py:362
def make_rescale(self, scales, mvals=None, G=None, H=None, multiply=True, verbose=False)
Obtain rescaled versions of the inputs according to dictionary values in "scales" (i...
Definition: forcefield.py:1195
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
pTree
Improved representation of pfields as a networkx graph.
Definition: forcefield.py:269
def __missing__(self, key)
Definition: forcefield.py:190
def addff(self, ffname, xmlScript=False)
Parse a force field file and add it to the class.
Definition: forcefield.py:440
def fromfile(cls, fnm)
Definition: forcefield.py:346
Finite state machine for parsing FrcMod force field file.
Definition: amberio.py:556
Finite state machine for parsing TINKER force field files.
Definition: tinkerio.py:97
Finite state machine for parsing Q-Chem input files.
Definition: qchemio.py:33
def create_pvals(self, mvals)
Converts mathematical to physical parameters.
Definition: forcefield.py:1013
rs
List of rescaling factors.
Definition: forcefield.py:273
def isfloat(word)
Matches ANY number; it can be a decimal, scientific notation, integer, or what have you...
Definition: molecule.py:406
def mktransmat(self)
Create the transformation matrix to rescale and rotate the mathematical parameters.
Definition: forcefield.py:1272
patoms
A listing of parameter number -> atoms involved.
Definition: forcefield.py:262
def wopen(dest, binary=False)
If trying to write to a symbolic link, remove it first.
Definition: nifty.py:1304
def __eq__(self, other)
Definition: forcefield.py:1557
Force field class.
Definition: forcefield.py:208
def __setstate__(self, state)
Definition: forcefield.py:361
def list_map(self)
Create the plist, which is like a reversed version of the parameter map.
Definition: forcefield.py:1453
def __init__(self, options, verbose=True, printopt=True)
Instantiation of force field class.
Definition: forcefield.py:216