1 """ @package forcebalance.forcefield 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 11 We introduce several important concepts: 13 1) Adjustable parameters are allocated into a vector. 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. 18 2) A mapping from interaction type to parameter number. 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. 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 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. 46 3) Distinction between physical and mathematical parameters. 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: 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. 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 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! 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. 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. 88 Right now only GROMACS parameters are supported, but this class is extensible, 95 from __future__
import division
97 from builtins
import zip
98 from builtins
import str
99 from builtins
import range
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
107 from forcebalance
import gmxio, qchemio, tinkerio, custom_io, openmmio, amberio, psi4io, smirnoffio
109 from forcebalance.smirnoffio
import assign_openff_parameter
112 from copy
import deepcopy
115 from collections
import OrderedDict, defaultdict
116 from forcebalance.output
import getLogger
117 logger = getLogger(__name__)
120 from lxml
import etree
122 logger.warning(
"Failed to import lxml module, needed by OpenMM engine")
124 FF_Extensions = {
"itp" :
"gmx",
130 "offxml" :
"smirnoff",
137 """ Recognized force field formats. """ 142 "openmm" : openmmio.OpenMM_Reader,
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. """ 156 fsplit = ffname.split(
'/')[-1].split(
':')
158 if verbose: logger.info(
"Determining file type of %s ..." % fsplit[0])
160 if fsplit[1]
in FF_IOModules:
161 if verbose: logger.info(
"We're golden! (%s)\n" % fsplit[1])
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) ...")
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))
175 if verbose: logger.info(
"\x1b[91m Warning: \x1b[0m %s not in supported types (%s)!\n" % (fsplit[0],
', '.join(FF_IOModules.keys())))
177 if verbose: logger.info(
"\x1b[91m Warning: \x1b[0m %s not in supported extensions (%s)!\n" % (ffext,
', '.join(FF_Extensions.keys())))
179 if verbose: logger.warning(
"Force field type not determined!\n")
185 def __init__(self, backup_dict):
192 logger.error(
'The key %s does not exist as an atom attribute or as an atom type attribute!\n' % key)
195 class FF(forcebalance.BaseClass):
196 """ Force field class. 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. 203 For details on force field parsing, see the detailed documentation for addff. 206 def __init__(self, options, verbose=True, printopt=True):
208 """Instantiation of force field class. 210 Many variables here are initialized to zero, but they are filled out by 211 methods like addff, rsmake, and mktransmat. 221 self.set_option(
None,
None,
'root', os.getcwd())
223 self.set_option(options,
'forcefield',
'fnms')
225 self.set_option(options,
'ffdir')
227 self.set_option(options,
'priors')
229 self.set_option(options,
'constrain_charge')
231 self.set_option(options,
'logarithmic_map')
233 self.set_option(options,
'amoeba_pol')
235 self.set_option(options,
'amoeba_eps')
237 self.set_option(options,
'rigid_water')
239 self.set_option(options,
'constrain_h')
241 self.set_option(options,
'use_pvals')
243 self.set_option(options,
'duplicate_pnames')
265 self.
pTree = nx.DiGraph()
286 for fnm
in self.fnms:
288 logger.info(
"Reading force field from file: %s\n" % fnm)
294 for Reader
in self.
Readers.values():
295 for k, v
in Reader.AtomTypes.items():
297 warn_press_key(
'Trying to insert atomtype %s into the force field, but it is already there' % k)
307 for Reader
in self.
Readers.values():
308 for Molecule, AtomList
in Reader.Molecules.items():
309 for FFAtom
in AtomList:
311 for k, v
in FFAtom.items():
312 FFAtomWithDefaults[k] = v
313 self.
FFMolecules.setdefault(Molecule, []).append(FFAtomWithDefaults)
324 bar =
printcool(
"Starting parameter indices, physical values and IDs")
328 self.
rsmake(printfacs=verbose)
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)
349 state = deepcopy(self.__dict__)
350 for ffname
in self.
ffdata:
352 temp = etree.tostring(self.
ffdata[ffname])
353 del state[
'ffdata'][ffname]
354 state[
'ffdata'][ffname] = temp
358 self.__dict__.update(state)
359 for ffname
in self.
ffdata:
361 temp = etree.ElementTree(etree.fromstring(self.
ffdata[ffname]))
362 self.
ffdata[ffname] = temp
364 def addff(self,ffname,xmlScript=False):
365 """ Parse a force field file and add it to the class. 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. 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 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 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 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. 400 <AmoebaVdwForce type="BUFFERED-14-7"> 401 <Vdw class="74" sigma="0.2655" epsilon="0.056484" reduction="0.910" parameterize="sigma, epsilon, reduction" /> 404 In this example, the parameter identifier would look like <tt> Vdw/74/epsilon </tt>. 406 --- If GROMACS (.itp) or TINKER (.prm) : --- 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 415 --- If normal text : --- 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.) 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. 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. 433 @param[in] ffname Name of the force field file 437 ffname = ffname.split(
':')[0]
440 if fftype ==
"tinker":
441 if hasattr(self,
"tinkerprm"):
447 if fftype ==
"openmm":
448 if hasattr(self,
"openmmxml"):
453 if fftype ==
"smirnoff":
454 if hasattr(self,
"offxml"):
455 warn_press_key(
"There should only be one SMIRNOFF XML file - confused!!")
458 from openforcefield.typing.engines.smirnoff
import ForceField
as OpenFF_ForceField
461 allow_cosmetic_attributes=
True)
468 if fftype ==
"frcmod":
473 Reader = FF_IOModules.get(fftype, forcebalance.BaseReader)
476 absff = os.path.join(self.root,self.ffdir,ffname)
481 self.
Readers[ffname] = Reader(ffname)
482 if fftype
in [
"openmm",
"smirnoff"]:
485 self.
ffdata[ffname] = etree.parse(absff)
487 logger.error(
"If etree not defined, please check if lxml module has been installed")
494 self.
ffdata[ffname] = [line.expandtabs()
for line
in open(absff).readlines()]
498 if hasattr(self.
Readers[ffname],
'atomnames'):
500 sys.stderr.write(
'Found more than one force field containing atom names; skipping the second one (%s)\n' % ffname)
505 """ Check to see whether a given parameter ID already exists, and provide an alternate if needed. """ 508 have_pids = [f[0]
for f
in self.
pfields]
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)
519 def warn_or_err(*args):
520 if self.duplicate_pnames:
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)
535 def addff_txt(self, ffname, fftype, xmlScript):
536 """ Parse a text force field and create several important instance variables. 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 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 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 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. 559 for ln, line
in enumerate(self.
ffdata[ffname]):
561 self.
Readers[ffname].feed(line)
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)
570 kwds = list(itertools.chain(*[[i,
"/%s" % i]
for i
in [
'PRM',
'PARM',
'RPT',
'EVAL']]))
572 marks = OrderedDict()
574 if sline.count(k) > 1:
576 logger.error(
"The above line contains multiple occurrences of the keyword %s\n" % k)
578 elif sline.count(k) == 1:
579 marks[k] = (np.array(sline) == k).argmax()
580 marks[
'END'] = len(sline)
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)
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]]
593 pid = self.
Readers[ffname].build_pid(pfld)
595 pid =
'Script/'+sline[pfld-2]+
'/' 597 self.
map[pid] = self.
np 604 if rmark
is not None:
606 stopparse = min([i
for i
in marks.values()
if i > rmark])
607 while parse < stopparse:
611 pfld = int(sline[parse])
613 prep = self.
map[sline[parse+1].replace(
'MINUS_',
'')]
615 sys.stderr.write(
"Valid parameter IDs:\n")
618 sys.stderr.write(
"%25s" % i)
620 sys.stderr.write(
"\n")
622 sys.stderr.write(
"\nOffending ID: %s\n" % sline[parse+1])
624 logger.error(
'Parameter repetition entry in force field file is incorrect (see above)\n')
626 pid = self.
Readers[ffname].build_pid(pfld)
631 self.
assign_field(prep,pid,ffname,ln,pfld,
"MINUS_" in sline[parse+1]
and -1
or 1)
633 if emark
is not None:
635 stopparse = min([i
for i
in marks.values()
if i > emark])
636 while parse < stopparse:
640 pfld = int(sline[parse])
641 evalcmd = sline[parse+1]
642 pid = self.
Readers[ffname].build_pid(pfld)
653 """ Parse an XML force field file and create important instance variables. 655 This was modeled after addff_txt, but XML and text files are 656 fundamentally different, necessitating two different methods. 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. 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 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. 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')
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')
694 wfile = forcebalance.nifty.wopen(absScript)
697 self.
addff(ffnameScript, xmlScript=
True)
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) )
706 pid = self.
Readers[ffname].build_pid(e, p)
707 self.
map[pid] = self.
np 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():]
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) )
724 dest = self.
Readers[ffname].build_pid(e, parameter_name)
725 src = field.strip().split(
'=', 1)[1]
727 self.
map[dest] = self.
map[src]
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)
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) )
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)
752 def make(self,vals=None,use_pvals=False,printdir=None,precision=12):
753 """ Create a new force field using provided parameter values. 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. 760 It does NOT store the mathematical parameters in the class state 761 (since we can only hold one set of parameters). 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 772 if type(vals)==np.ndarray
and vals.ndim != 1:
773 logger.error(
'Please only pass 1-D arrays\n')
775 if len(vals) != self.
np:
776 logger.error(
'Input parameter np.array (%i) not the required size (%i)\n' % (len(vals), self.
np))
778 if use_pvals
or self.use_pvals:
779 logger.info(
"Using physical parameters directly!\r")
780 pvals = vals.copy().flatten()
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)
794 return Decimal % number
796 Num = SciNot % number
797 Mum = SciNot % (-1 * number)
798 if (float(Num) == float(Mum)):
799 return SciNot % abs(number)
801 return SciNot % number
805 newffdata = deepcopy(self.
ffdata)
808 PRM = {i:pvals[self.
map[i]]
for i
in self.
map}
814 xml_lines = OrderedDict([(fnm, list(newffdata[fnm].iter()))
for fnm
in self.fnms
if self.
ffdata_isxml[fnm]])
816 for i
in range(len(self.
pfields)):
818 pid,fnm,ln,fld,mult,cmd = pfield
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"))
834 logger.error(traceback.format_exc() +
'\n')
835 logger.error(
"The command %s (written in the force field file) cannot be evaluated!\n" % cmd)
838 wval = mult*pvals[self.
map[pid]]
841 xml_lines[fnm][ln].attrib[fld] = OMMFormat % (wval) + self.
offxml_unit_strs[pid]
844 if hasattr(self,
'offxml')
and fnm == self.
offxml:
854 sline = self.
Readers[fnm].Split(newffdata[fnm][ln])
855 whites = self.
Readers[fnm].Whites(newffdata[fnm][ln])
857 if newffdata[fnm][ln][0] !=
' ':
858 whites = [
''] + whites
860 if not match(
'^-',sline[fld])
and len(whites[fld]) > 1:
861 whites[fld] = whites[fld][:-1]
864 newrd =
"% 17.12e" % (wval)
866 newrd = TXTFormat(wval, precision)
869 Lold = len(sline[fld])
870 if not match(
'^-',sline[fld]):
875 if Shave < (len(whites[fld+1])+2):
876 whites[fld+1] = whites[fld+1][:-Shave]
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' 881 if printdir
is not None:
882 absprintdir = os.path.join(self.root,printdir)
884 absprintdir = os.getcwd()
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)
890 for fnm
in newffdata:
892 with
wopen(os.path.join(absprintdir,fnm), binary=
True)
as f: newffdata[fnm].write(f)
893 elif 'Script.txt' in fnm:
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
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') 912 with
wopen(os.path.join(absprintdir,fnmXml), binary=
True)
as f: newffdata[fnmXml].write(f)
914 with
wopen(os.path.join(absprintdir,fnm))
as f: f.writelines(newffdata[fnm])
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)
932 logger.info(
"pvals:\n")
933 logger.info(str(pvals) +
'\n')
937 for gnm, pidx
in Groups.items():
939 pvals_grp = pvals[pidx]
941 Order = np.argsort(pvals_grp)
942 for p
in range(len(Order) - 1):
945 pj = pidx[Order[p+1]]
948 dp = np.log(pvals[pj]) - np.log(pvals[pi])
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)
971 logger.info(
"pvals:\n")
972 logger.info(str(pvals) +
'\n')
975 for gnm, pidx
in Groups.items():
977 pvals_grp = pvals[pidx]
979 Order = np.argsort(pvals_grp)
981 for p
in range(len(Order) - 1):
984 pj = pidx[Order[p+1]]
987 dp = np.log(pvals[pj]) - np.log(pvals[pi])
990 spacdict[gnm] = np.mean(np.array(spacs))
994 """Converts mathematical to physical parameters. 996 First, mathematical parameters are rescaled and rotated by 997 multiplying by the transformation matrix, followed by adding 998 the original physical parameters. 1000 @param[in] mvals The mathematical parameters 1001 @return pvals The physical parameters 1004 if isinstance(mvals, list):
1005 mvals = np.array(mvals)
1008 if self.logarithmic_map:
1010 pvals = np.exp(mvals.flatten()) * self.
pvals0 1012 logger.exception(str(mvals) +
'\n')
1013 logger.error(
'What the hell did you do?\n')
1017 concern= [
'polarizability',
'epsilon',
'VDWT']
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:
1035 """Converts physical to mathematical parameters. 1037 We create the inverse transformation matrix using SVD. 1039 @param[in] pvals The physical parameters 1040 @return mvals The mathematical parameters 1043 if self.logarithmic_map:
1044 logger.error(
'create_mvals has not been implemented for logarithmic_map\n')
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. 1056 @todo Pass in rsfactors through the input file 1058 @param[in] printfacs List for printing out the resecaling factors 1061 typevals = OrderedDict()
1062 rsfactors = OrderedDict()
1068 if self.
Readers[f].pdict ==
"XML_Override":
1069 for i, pName
in enumerate(self.
map):
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]])
1079 ttstr =
'/'.join([pName.split(
'/')[0],pName.split(
'/')[1]])
1080 if ttstr
not in termtypelist:
1081 termtypelist.append(ttstr)
1083 for i
in self.
Readers[f].pdict:
1084 for j
in self.
Readers[f].pdict[i]:
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:
1092 typevals.setdefault(termtype, []).append(self.
pvals0[self.
map[pid]])
1093 for termtype
in typevals:
1097 maxval = max(abs(np.array(typevals[termtype])))
1101 rsfactors[termtype] = maxval
1102 rsfac_list.append(termtype)
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]
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]))
1118 self.
rs_ord = OrderedDict([(i, rsfactors[i])
for i
in rsfac_list])
1120 self.
rs = np.ones(len(self.
pvals0))
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]
1129 elif self.
rs[pnum] != rsfactors[termtype]:
1130 self.
rs[pnum] = rsfactors[termtype]
1132 have_rs.append(pnum)
1134 for pnum
in range(len(self.
pvals0)):
1135 if pnum
not in have_rs:
1138 bar =
printcool(
"Rescaling Types / Factors by Parameter Number:",color=1)
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. 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. 1157 mvals : numpy.ndarray 1158 Parameters to be transformed, if desired. Must be same length as number of parameters. 1160 Gradient to be transformed, if desired. Must be same length as number of parameters. 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 1169 When set to True, the new scale factors are the existing scale factors 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) 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')
1188 if multiply ==
False:
1189 rsord_out = deepcopy(scales)
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
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):
1204 mvals_out[p] *= (float(self.
rs[p])/float(rs_out[p]))
1205 answer[
'mvals'] = mvals_out
1207 if G.shape != (self.
np,):
1208 raise RuntimeError(
'G has the wrong shape')
1210 for p
in range(self.
np):
1212 G_out[p] *= (float(rs_out[p])/float(self.
rs[p]))
1215 if H.shape != (self.
np,self.
np):
1216 raise RuntimeError(
'H has the wrong shape')
1218 for p
in range(self.
np):
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]))
1229 """ Create the transformation matrix to rescale and rotate the mathematical parameters. 1231 For point charge parameters, project out perturbations that 1232 change the total charge. 1236 'qmap' : Just a list of parameter indices that point to charges. 1238 'qid' : For each parameter in the qmap, a list of the affected atoms :) 1239 A potential target for the molecule-specific thang. 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. 1248 'qmat2' : An identity matrix with 'qtrans2' pasted into the right place 1250 'transmat': 'qmat2' with rows and columns scaled using self.rs 1252 'excision': Parameter indices that need to be 'cut out' because they are irrelevant and 1253 mess with the matrix diagonalization 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. 1263 concern= [
'COUL',
'c0',
'charge']
1264 qmat2 = np.eye(self.
np)
1266 def insert_mat(qtrans2, qmap):
1269 for i
in range(self.
np):
1273 qmat2[i, j] = qtrans2[x, y]
1277 def build_qtrans2(tq, qid, qmap):
1278 """ Build the matrix that ensures the net charge does not change. """ 1284 cons0 = np.ones((1,tq))
1285 cons = np.zeros((cons0.shape[0], nq))
1287 qtrans2 = np.eye(nq)
1289 for i
in range(cons.shape[0]):
1291 for j
in range(cons.shape[1]):
1296 cons[i][j] = float(len(qid[j]))
1297 cons[i] /= np.linalg.norm(cons[i])
1301 for j
in range(nq-i-1):
1302 qtrans2[i+j+1, :] =
orthogonalize(qtrans2[i+j+1, :], cons[i])
1305 if any(len(r.adict) > 0
for r
in self.
Readers.values()):
1306 logger.info(
"Building charge constraints...\n")
1308 Adict = OrderedDict()
1310 for r
in self.
Readers.values():
1312 for k, v
in r.adict.items():
1315 for molname, molatoms
in Adict.items():
1316 mol_charge_count = np.zeros(self.
np)
1320 for i
in range(self.
np):
1323 for imol, iatoms
in self.
patoms[i]:
1324 for iatom
in iatoms:
1325 if imol == molname
and iatom
in molatoms:
1328 qidx.append(molatoms.index(iatom))
1329 if any([j
in self.
plist[i]
for j
in concern])
and qct > 0:
1332 logger.info(
"Parameter %i occurs %i times in molecule %s in locations %s (%s)\n" % (i, qct, molname, str(qidx), self.
plist[i]))
1335 qtrans2 = build_qtrans2(tq, qid, qmap)
1336 if self.constrain_charge:
1337 insert_mat(qtrans2, qmap)
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.")
1348 if any([self.
Readers[i].pdict ==
"XML_Override" for i
in self.fnms]):
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]):
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)
1361 for k
in self.
plist[i].split():
1364 thisq.append(k.split(
'-')[-1])
1367 self.
qid2.append(np.array([self.
atomnames.index(k)
for k
in thisq]))
1369 nq = sum(np.array([self.
plist[i].count(j)
for j
in concern]))
1370 self.
qid.append(qnr+np.arange(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')
1378 if len(self.
qmap) > 0:
1379 cons0 = np.ones((1,tq))
1380 qtrans2 = build_qtrans2(tq, self.
qid, self.
qmap)
1382 if self.constrain_charge:
1383 insert_mat(qtrans2, self.
qmap)
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)]
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)
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]
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)))
1423 transmat = np.array(transmat1, copy=
True)
1424 transmatNS = np.array(transmat,copy=
True)
1426 for i
in range(self.
np):
1427 if abs(transmatNS[i, i]) < 1e-8:
1429 transmatNS[i, i] += 1
1432 transmat[i, :] = np.zeros(self.
np)
1434 self.
tmI = transmat.T
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.)')
1441 self.
plist = [[]
for j
in range(max([self.
map[i]
for i
in self.
map])+1)]
1444 for i
in range(self.
np):
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.""" 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]))
1454 def sprint_map(self,vals = None,precision=4):
1455 """Prints out the (physical or mathematical) parameter indices, IDs and values to a string.""" 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])
1462 """ Assign physical parameter values to the 'pvals0' array. 1464 @param[in] idx The index to which we assign the parameter value. 1465 @param[in] val The parameter value to be inserted. 1467 if idx == len(self.
pvals0):
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]]. 1475 Note that parameters can have multiple locations because of the repetition functionality. 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. 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) 1489 self.
pTree.add_node(pid, param_mathid=idx, file_name=fnm, line_number=ln, field_number=pfld, multiplier=mult, evalcmd=cmd)
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')
1502 if src_nodes[0] == pid:
1503 raise RuntimeError(
'Evaluated parameter depends on itself')
1507 self.
pTree.add_edge(pid, src_nodes[0])
1509 raise RuntimeError(
'Evaluated parameter refers to a source parameter that does not exist:\n%s'%cmd)
1511 self.
pfields.append([pid,fnm,ln,pfld,mult,cmd])
1515 import matplotlib.pyplot
as plt
1516 plt.switch_backend(
'agg')
1517 fig, ax = plt.subplots()
1518 fig.set_size_inches(12, 8)
1520 nx.draw(self.
pTree, with_labels=
True)
1525 For a given unique parameter identifier, return all of the mvals that it is connected to. 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])
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])
1538 if isinstance(other, FF):
1540 self_pfields = [p[2:]
for p
in self.
pfields]
1541 other_pfields= [p[2:]
for p
in other.pfields]
1543 return self_pfields == other_pfields
and\
1544 self.
map == other.map
and\
1548 else:
return NotImplemented
1550 def rs_override(rsfactors,termtype,Temperature=298.15):
1551 """ This function takes in a dictionary (rsfactors) and a string (termtype). 1553 If termtype matches any of the strings below, rsfactors[termtype] is assigned 1554 to one of the numbers below. 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 1563 if match(
'PIMPDIHS[1-6]K|PDIHMULS[1-6]K|PDIHS[1-6]K|RBDIHSK[1-5]|MORSEC',termtype):
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):
1570 rsfactors[termtype] = 1.0
1571 elif match(
'QTPIEC|QTPIEH',termtype):
1573 rsfactors[termtype] = 27.2114
1574 elif match(
'BONDSB|UREY_BRADLEYB|MORSEB|VDWS|VPAIRS|VSITE|VDW_BHAMA|VPAIR_BHAMA',termtype):
1576 rsfactors[termtype] = 0.05291772
1577 elif match(
'BONDSK|UREY_BRADLEYK2',termtype):
1579 rsfactors[termtype] = 34455.5275 * 27.2114
1580 elif match(
'PDIHS[1-6]B|ANGLESB|UREY_BRADLEYB',termtype):
1582 rsfactors[termtype] = 57.295779513
1583 elif match(
'VDWT|VDW_BHAMB|VPAIR_BHAMB',termtype):
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.
Class for parsing OpenMM force field files.
def assign_p0(self, idx, val)
Assign physical parameter values to the 'pvals0' array.
excision
Indices to exclude from optimization / Hessian inversion.
def create_mvals(self, pvals)
Converts physical to mathematical parameters.
np
The total number of parameters.
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.
plist
The listing of parameter number -> interaction types.
tm
The transformation matrix for mathematical -> physical parameters.
Finite state machine for parsing DVR grid files.
def print_map(self, vals=None, precision=4)
Prints out the (physical or mathematical) parameter indices, IDs and values in a visually appealing w...
pfields
A list where pfields[i] = [pid,'file',line,field,mult,cmd], basically a new way to modify force field...
def invert_svd(X, thresh=1e-12)
Invert a matrix using singular value decomposition.
def isint(word)
ONLY matches integers! If you have a decimal point? None shall pass!
def flat(vec)
Given any list, array, or matrix, return a single-index array.
def make_redirect(self, mvals)
def sprint_map(self, vals=None, precision=4)
Prints out the (physical or mathematical) parameter indices, IDs and values to a string.
Interaction type -> Parameter Dictionary.
def rs_override(rsfactors, termtype, Temperature=298.15)
This function takes in a dictionary (rsfactors) and a string (termtype).
def natural_sort(l)
Return a natural sorted list.
def check_dupes(self, pid, ffname, ln, pfld)
Check to see whether a given parameter ID already exists, and provide an alternate if needed...
map
The mapping of interaction type -> parameter number.
FFAtomTypes
WORK IN PROGRESS ## This is a dictionary of {'AtomType':{'Mass' : float, 'Charge' : float...
Finite state machine for parsing custom GROMACS force field files.
atomnames
A list of atom names (this is new, for ESP fitting)
Finite state machine for parsing Mol2 force field file.
ffdata
As these options proliferate, the force field class becomes less standalone.
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...
def addff_xml(self, ffname)
Parse an XML force field file and create important instance variables.
def col(vec)
Given any list, array, or matrix, return a 1-column 2D array.
def plot_parameter_tree(fnm)
pvals0
Initial value of physical parameters.
linedestroy_save
Destruction dictionary (experimental).
def assign_openff_parameter(ff, new_value, pid)
Assign a SMIRNOFF parameter given the openforcefield.ForceField object, the desired parameter value...
def rsmake(self, printfacs=True)
Create the rescaling factors for the coordinate transformation in parameter space.
redirect
Creates plist from map.
def orthogonalize(vec1, vec2)
Given two vectors vec1 and vec2, project out the component of vec1 that is along the vec2-direction...
tmI
The transpose of the transformation matrix.
Readers
A dictionary of force field reader classes.
def __init__(self, backup_dict)
def warn_press_key(warning, timeout=10)
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...
def get_mathid(self, pid)
For a given unique parameter identifier, return all of the mvals that it is connected to...
rs_ord
Takes the dictionary 'BONDS':{3:'B', 4:'K'}, 'VDW':{4:'S', 5:'T'}, and turns it into a list of term t...
def addff_txt(self, ffname, fftype, xmlScript)
Parse a text force field and create several important instance variables.
Finite state machine for parsing GROMACS force field files.
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...
def printcool(text, sym="#", bold=False, color=2, ansi=None, bottom='-', minwidth=50, center=True, sym2="=")
Cool-looking printout for slick formatting of output.
pTree
Improved representation of pfields as a networkx graph.
def __missing__(self, key)
def addff(self, ffname, xmlScript=False)
Parse a force field file and add it to the class.
Finite state machine for parsing FrcMod force field file.
Finite state machine for parsing TINKER force field files.
Finite state machine for parsing Q-Chem input files.
def create_pvals(self, mvals)
Converts mathematical to physical parameters.
rs
List of rescaling factors.
def isfloat(word)
Matches ANY number; it can be a decimal, scientific notation, integer, or what have you...
def mktransmat(self)
Create the transformation matrix to rescale and rotate the mathematical parameters.
patoms
A listing of parameter number -> atoms involved.
def wopen(dest, binary=False)
If trying to write to a symbolic link, remove it first.
def __setstate__(self, state)
def list_map(self)
Create the plist, which is like a reversed version of the parameter map.
def __init__(self, options, verbose=True, printopt=True)
Instantiation of force field class.