1 """ @package forcebalance.abinitio Ab-initio fitting module (energies, forces, resp). 6 from __future__
import division
7 from __future__
import print_function
9 from builtins
import zip
10 from builtins
import range
13 from forcebalance.nifty import col, eqcgmx, flat, floatornan, fqcgmx, invert_svd, kb, printcool, bohr2ang, warn_press_key, warn_once, pvec1d, commadash, uncommadash, isint
15 from forcebalance.target
import Target
17 from re
import match, sub
19 from subprocess
import PIPE
21 from collections
import defaultdict, OrderedDict
26 from forcebalance.output
import getLogger
27 logger = getLogger(__name__)
29 def norm2(arr, a=0, n=None, step=3):
31 Given a one-dimensional array, return the norm-squared of 32 every "step" elements, starting at 'a' and computing 'n' total 33 elements (so arr[a:a+step*n] must be valid). 38 One-dimensional array to be normed 42 The number of norms to calculate (in intervals of step) 44 The number of elements in each norm calculation (this is usually 3) 46 if len(arr.shape) != 1:
47 raise RuntimeError(
"Please only pass a one-dimensional array")
49 if arr.shape[0] < a+step*n:
50 raise RuntimeError(
"Please provide an array of length >= %i" % (a+step*n))
52 if ((arr.shape[0]-a)%step != 0):
53 raise RuntimeError(
"Please provide an array with (length-%i) divisible by %i" % (a, step))
54 n = int((arr.shape[0]-a)/step)
57 d = arr[a+step*j:a+step*j+step]
58 answer.append(np.dot(d,d))
59 return np.array(answer)
63 """ Subclass of Target for fitting force fields to ab initio data. 65 Currently Gromacs-X2, Gromacs, Tinker, OpenMM and AMBER are supported. 67 We introduce the following concepts: 68 - The number of snapshots 69 - The reference energies and forces (eqm, fqm) and the file they belong in (qdata.txt) 70 - The sampling simulation energies (emd0) 71 - The WHAM Boltzmann weights (these are computed externally and passed in) 72 - The QM Boltzmann weights (computed internally using the difference between eqm and emd0) 74 There are also these little details: 75 - Switches for whether to turn on certain Boltzmann weights (they stack) 76 - Temperature for the QM Boltzmann weights 77 - Whether to fit a subset of atoms 79 This subclass contains the 'get' method for building the objective 80 function from any simulation software (a driver to run the program and 81 read output is still required). The 'get' method can be overridden 82 by subclasses like AbInitio_GMX.""" 84 def __init__(self,options,tgt_opts,forcefield):
86 Initialization; define a few core concepts. 88 @todo Obtain the number of true atoms (or the particle -> atom mapping) 93 super(AbInitio,self).
__init__(options,tgt_opts,forcefield)
100 self.set_option(tgt_opts,
'shots',
'ns')
102 self.set_option(tgt_opts,
'absolute',
'absolute')
104 self.set_option(tgt_opts,
'fitatoms',
'fitatoms_in')
106 self.set_option(tgt_opts,
'energy',
'energy')
108 self.set_option(tgt_opts,
'force',
'force')
110 self.set_option(tgt_opts,
'resp',
'resp')
111 self.set_option(tgt_opts,
'resp_a',
'resp_a')
112 self.set_option(tgt_opts,
'resp_b',
'resp_b')
114 self.set_option(tgt_opts,
'w_energy',
'w_energy')
115 self.set_option(tgt_opts,
'w_force',
'w_force')
118 self.set_option(tgt_opts,
'energy_rms_override',
'energy_rms_override')
119 self.set_option(tgt_opts,
'force_rms_override',
'force_rms_override')
120 self.set_option(tgt_opts,
'force_map',
'force_map')
121 self.set_option(tgt_opts,
'w_netforce',
'w_netforce')
122 self.set_option(tgt_opts,
'w_torque',
'w_torque')
123 self.set_option(tgt_opts,
'w_resp',
'w_resp')
125 self.set_option(tgt_opts,
'w_normalize')
127 self.set_option(tgt_opts,
'writelevel',
'writelevel')
130 self.set_option(tgt_opts,
'all_at_once',
'all_at_once')
132 self.set_option(tgt_opts,
'run_internal',
'run_internal')
134 self.set_option(options,
'have_vsite',
'have_vsite')
136 self.set_option(tgt_opts,
'attenuate',
'attenuate')
138 self.set_option(tgt_opts,
'energy_denom',
'energy_denom')
140 self.set_option(tgt_opts,
'energy_upper',
'energy_upper')
142 self.set_option(tgt_opts,
'energy_asymmetry')
146 if not self.all_at_once:
147 logger.error(
"Asymmetric weights only work when all_at_once is enabled")
150 self.set_option(tgt_opts,
'force_denom',
'force_denom')
168 self.
qfnm = os.path.join(self.tgtdir,
"qdata.txt")
183 if hasattr(self,
'pdb')
and self.pdb
is not None:
184 self.
mol = Molecule(os.path.join(self.root,self.tgtdir,self.coords),
185 top=(os.path.join(self.root,self.tgtdir,self.pdb)))
187 self.
mol = Molecule(os.path.join(self.root,self.tgtdir,self.coords))
191 self.
ns = len(self.
mol)
195 engine_args = OrderedDict(list(self.OptionDict.items()) + list(options.items()))
196 engine_args.pop(
'name',
None)
198 self.
engine = self.engine_(target=self, mol=self.
mol, **engine_args)
209 self.set_option(
None,
'shots', val=self.
ns)
214 if 'VSITE' in self.FF.plist[i]:
219 if any([
'VSITE' in i
for i
in self.FF.map.keys()])
or self.have_vsite:
220 logger.info(
"\rGenerating virtual site positions.%s" % (
" "*30))
221 pvals = self.FF.make(mvals)
222 self.
mol.xyzs = self.
engine.generate_positions()
226 logger.info(
"\rPreparing the distance matrix... it will have %i * %i * %i = %i elements" % (self.
ns, self.
nesp, self.
nparticles, self.
ns * self.
nesp * self.
nparticles))
228 for espset, xyz
in zip(self.
espxyz, self.
mol.xyzs):
229 logger.info(
"\rGenerating ESP distances for snapshot %i%s\r" % (sn,
" "*50))
230 esparr = np.array(espset).reshape(-1,3)
232 DistMat = np.array([[np.linalg.norm(i - j)
for j
in xyz]
for i
in esparr])
233 invdists.append(1. / (DistMat / bohr2ang))
236 if 'VSITE' in self.FF.plist[i]:
239 return np.array(invdists)
247 kwds = {
"MoleculeNumber" :
"molecule",
248 "ResidueNumber" :
"residue",
249 "ChargeGroupNumber" :
"chargegroup"}
255 Block = self.
AtomLists[
'ChargeGroupNumber']
257 logger.error(
'force_map keyword "%s" is invalid. Please choose from: %s\n' % (self.
force_map,
', '.join([
'"%s"' % kwds[k]
for k
in self.
AtomLists.keys()
if k
in kwds])))
262 nfp = force.reshape(-1,3).shape[0]
270 mask = np.array([i
for i
in range(npr)
if self.
AtomMask[i]])
272 if nfp
not in [npr, nat]:
273 logger.error(
'Force contains %i particles but expected %i or %i\n' % (nfp, npr, nat))
276 frc1 = force.reshape(-1,3)[:nft].flatten()
278 frc1 = force.reshape(-1,3)[mask][:nft].flatten()
280 if nxp
not in [npr, nat]:
281 logger.error(
'Coordinates contains %i particles but expected %i or %i\n' % (nfp, npr, nat))
286 xyz1 = xyz[mask][:nft]
288 Block = list(np.array(Block)[mask])[:nft]
289 Mass = np.array(self.
AtomLists[
'Mass'])[mask][:nft]
293 for b
in sorted(set(Block)):
294 AtomBlock = np.array([i
for i
in range(len(Block))
if Block[i] == b])
295 CrdBlock = np.array(list(itertools.chain(*[list(range(3*i, 3*i+3))
for i
in AtomBlock])))
296 com = np.sum(xyz1[AtomBlock]*np.outer(Mass[AtomBlock],np.ones(3)), axis=0) / np.sum(Mass[AtomBlock])
297 frc = frc1[CrdBlock].reshape(-1,3)
298 NetForce = np.sum(frc, axis=0)
299 xyzb = xyz1[AtomBlock]
301 for a
in range(len(xyzb)):
305 Torque += np.cross(R, F) / 10
306 NetForces += [i
for i
in NetForce]
309 Torques += [i
for i
in Torque]
310 netfrc_torque = np.array(NetForces + Torques)
311 self.
nnf = int(len(NetForces)/3)
312 self.
ntq = int(len(Torques)/3)
317 """ Read the reference ab initio data from a file such as qdata.txt. 319 @todo Add an option for picking any slice out of 320 qdata.txt, helpful for cross-validation 322 After reading in the information from qdata.txt, it is converted 323 into the GROMACS energy units (kind of an arbitrary choice); 324 forces (kind of a misnomer in qdata.txt) are multipled by -1 325 to convert gradients to forces. 327 We typically subtract out the mean energies of all energy arrays 328 because energy/force matching does not account for zero-point 329 energy differences between MM and QM (i.e. energy of electrons 332 A 'hybrid' ensemble is possible where we use 50% MM and 50% QM 333 weights. Please read more in LPW and Troy Van Voorhis, JCP 334 Vol. 133, Pg. 231101 (2010), doi:10.1063/1.3519043. In the 335 updated version of the code (July 13 2016), this feature is 336 currently not implemented due to disuse, but it is easy to 337 re-implement if desired. 339 Finally, note that using non-Boltzmann weights degrades the 340 statistical information content of the snapshots. This 341 problem will generally become worse if the ensemble to which 342 we're reweighting is dramatically different from the one we're 343 sampling from. We end up with a set of Boltzmann weights like 344 [1e-9, 1e-9, 1.0, 1e-9, 1e-9 ... ] and this is essentially just 347 Here, we have a measure for the information content of our snapshots, 348 which comes easily from the definition of information entropy: 350 S = -1*Sum_i(P_i*log(P_i)) 351 InfoContent = exp(-S) 353 With uniform weights, InfoContent is equal to the number of snapshots; 354 with horrible weights, InfoContent is closer to one. 358 for line
in open(os.path.join(self.root,self.
qfnm)):
360 if len(sline) == 0:
continue 361 elif sline[0] ==
'ENERGY':
362 self.
eqm.append(float(sline[1]))
363 elif sline[0]
in [
'FORCES',
'GRADIENT']:
364 self.
fqm.append([float(i)
for i
in sline[1:]])
365 elif sline[0] ==
'ESPXYZ':
366 self.
espxyz.append([float(i)
for i
in sline[1:]])
367 elif sline[0] ==
'ESPVAL':
368 self.
espval.append([float(i)
for i
in sline[1:]])
377 self.
eqm = np.array(self.
eqm)
380 self.
eqm -= np.min(self.
eqm)
381 self.
smin = np.argmin(self.
eqm)
382 logger.info(
"Referencing all energies to the snapshot %i (minimum energy structure in QM)\n" % self.
smin)
384 logger.info(
"Fitting absolute energies. Make sure you know what you are doing!\n")
388 if len(self.
fqm) > 0:
389 self.
fqm = np.array(self.
fqm)
391 self.
qmatoms = list(range(int(self.
fqm.shape[1]/3)))
393 logger.info(
"QM forces are not present, only fitting energies.\n")
400 if isint(self.fitatoms_in):
401 if int(self.fitatoms_in) == 0:
404 warn_press_key(
"Provided an integer for fitatoms; will assume this means the first %i atoms" % int(self.fitatoms_in))
405 self.
fitatoms = list(range(int(self.fitatoms_in)))
411 warn_press_key(
"There are more fitting atoms than the total number of atoms in the QM calculation (something is probably wrong)")
415 logger.info(
"Fitting the forces on all atoms\n")
418 logger.info(
"Pruning the quantum force matrix...\n")
419 selct = list(itertools.chain(*[[3*i+j
for j
in range(3)]
for i
in self.
fitatoms]))
420 self.
fqm = self.
fqm[:, selct]
430 denom = self.energy_denom * 4.184
431 upper = self.energy_upper * 4.184
433 for i
in range(self.
ns):
436 elif eqm1[i] < denom:
439 self.
boltz_wts[i] = 1.0 / np.sqrt(denom**2 + (eqm1[i]-denom)**2)
447 for i
in range(len(self.
fqm)):
460 Headings = [
"Observable",
"Difference\n(Calc-Ref)",
"Denominator\n RMS (Ref)",
" Percent \nDifference",
"Term",
"x Wt =",
"Contrib."]
461 Data = OrderedDict([])
463 Data[
'Energy (kJ/mol)'] = [
"%8.4f" % self.
e_err,
466 "%8.4f" % self.
e_trm,
468 "%8.4f" % self.
e_ctr]
470 Data[
'Gradient (kJ/mol/A)'] = [
"%8.4f" % (self.
f_err/10),
471 "%8.4f" % (self.
f_ref/10),
473 "%8.4f" % self.
f_trm,
475 "%8.4f" % self.
f_ctr]
477 Data[
'Net Force (kJ/mol/A)'] = [
"%8.4f" % (self.
nf_err/10),
478 "%8.4f" % (self.
nf_ref/10),
483 Data[
'Torque (kJ/mol/rad)'] = [
"%8.4f" % self.
tq_err,
490 Data[
'Potential (a.u.'] = [
"%8.4f" % (self.
esp_err/10),
496 self.printcool_table(data=Data, headings=Headings, color=0)
501 if hasattr(self,
'engine'):
502 return self.
engine.energy().reshape(-1,1)
504 logger.error(
"Target must contain an engine object\n")
505 raise NotImplementedError
508 if hasattr(self,
'engine'):
509 return self.
engine.energy_force()
511 logger.error(
"Target must contain an engine object\n")
512 raise NotImplementedError
517 selct = [0] + list(itertools.chain(*[[1+3*i+j
for j
in range(3)]
for i
in self.
fitatoms]))
521 for i
in range(len(M)):
525 Nfts = np.array(Nfts)
526 return np.hstack((M, Nfts))
533 if hasattr(self,
'engine'):
536 logger.error(
"Target must contain an engine object\n")
537 raise NotImplementedError
540 if hasattr(self,
'engine'):
543 logger.error(
"Target must contain an engine object\n")
544 raise NotImplementedError
549 selct = [0] + list(itertools.chain(*[[1+3*i+j
for j
in range(3)]
for i
in self.
fitatoms]))
554 return np.hstack((M, Nft))
564 This code computes the least squares objective function for the energy and force. 565 The most recent revision simplified the code to make it easier to maintain, 566 and to remove the covariance matrix and dual-weight features. 568 The equations have also been simplified. Previously I normalizing each force 569 component (or triples of components belonging to an atom) separately. 570 I was also computing the objective function separately from the "indicators", 571 which led to confusion regarding why they resulted in different values. 572 The code was structured around computing the objective function by multiplying 573 an Applequist polytensor containing both energy and force with an inverse 574 covariance matrix, but it became apparent over time and experience that the 575 more complicated approach was not worth it, given the opacity that it introduced 576 into how things were computed. 578 In the new code, the objective function is computed in a simple way. 579 For the energies we compute a weighted sum of squared differences 580 between E_MM and E_QM, minus (optionally) the mean energy gap, for the numerator. 581 The weighted variance of the QM energies <E_QM^2>-<E_QM>^2 is the denominator. 582 The user-supplied w_energy option is a prefactor that multiplies this term. 583 If w_normalize is set to True (no longer the default), the prefactor is 584 further divided by the sum of all of the weights. 585 The indicators are set to the square roots of the numerator and denominator above. 587 For the forces we compute the same weighted sum, where each term in the sum 588 is the norm-squared of F_MM - F_QM, with no option to subtract the mean. 589 The denominator is computed in an analogous way using the norm-squared F_QM, 590 and the prefactor is w_force. The same approach is applied if the user asks 591 for net forces and/or torques. The indicators are computed from the square 592 roots of the numerator and denominator, divided by the number of 593 atoms (molecules) for forces (net forces / torques). 595 In equation form, the objective function is given by: 597 \[ = {W_E}\left[ {\frac{{\left( {\sum\limits_{i \in {N_s}} 598 {{w_i}{{\left( {E_i^{MM} - E_i^{QM}} \right)}^2}} } \right) - 599 {{\left( {{{\bar E}^{MM}} - {{\bar E}^{QM}}} \right)}^2}}} 600 {{\sum\limits_{i \in {N_s}} {{w_i}{{\left( 601 {E_i^{QM} - {{\bar E}^{QM}}} \right)}^2}} }}} \right] + 602 {W_F}\left[ {\frac{{\sum\limits_{i \in {N_s}} {{w_i}\sum\limits_{a \in {N_a}} 603 {{{\left| {{\bf{F}}_{i,a}^{MM} - {\bf{F}}_{i,a}^{QM}} \right|}^2}} } }} 604 {{\sum\limits_{i \in {N_s}} {{w_i}\sum\limits_{a \in {N_a}} 605 {{{\left| {{\bf{F}}_{i,a}^{QM}} \right|}^2}} } }}} \right]\] 607 In the previous code (ForTune, 2011 and previous) 608 this subroutine used analytic first derivatives of the 609 energy and force to build the derivatives of the objective function. 610 Here I will take a simplified approach, because building the analytic 611 derivatives require substantial modifications of the engine code, 612 which is unsustainable. We use a finite different calculation 613 of the first derivatives of the energies and forces to get the exact 614 first derivative and approximate second derivative of the objective function.. 616 @param[in] mvals Mathematical parameter values 617 @param[in] AGrad Switch to turn on analytic gradient 618 @param[in] AHess Switch to turn on analytic Hessian 619 @return Answer Contribution to the objective function 623 pvals = self.FF.make(mvals)
639 NCP1 += 3*(nnf + ntq)
661 M_p = np.zeros((NP,NCP1))
662 M_pp = np.zeros((NP,NCP1))
664 M0_p = np.zeros((NP,NCP1))
665 M0_pp = np.zeros((NP,NCP1))
669 SPX_p = np.zeros((NP,NCP1))
673 X2_Parts_p = np.zeros((NP,NParts))
675 SPX_pq = np.zeros((NP,NP,NCP1))
676 X2_Parts_pq = np.zeros((NP,NP,NParts))
680 M_all = np.zeros((NS,NCP1))
681 if AGrad
and self.all_at_once:
682 dM_all = np.zeros((NS,NP,NCP1))
683 ddM_all = np.zeros((NS,NP,NCP1))
688 logger.debug(
"\rExecuting\r")
691 M_all[:, 0] -= M_all[self.
smin, 0]
695 pvals = self.FF.make(mvals_)
698 dM_all[:,p,:], ddM_all[:,p,:] =
f12d3p(
fdwrap(callM, mvals, p), h = self.h, f0 = M_all)
700 dM_all[:, p, 0] -= dM_all[self.
smin, p, 0]
701 ddM_all[:, p, 0] -= ddM_all[self.
smin, p, 0]
708 logger.debug(
"\rIncrementing quantities for snapshot %i\r" % i)
715 Q[1:] = self.
fref[i,:].copy()
723 logger.debug(
"Shot %i\r" % i)
725 M_all[i,:] = M.copy()
730 if self.
asym and X[0] < 0.0:
735 dfrc2 =
norm2(M-Q, 1, nat)
736 if not in_fd()
and np.max(dfrc2) > self.
maxdf:
737 self.
maxdf = np.sqrt(np.max(dfrc2))
758 if not AGrad:
continue 760 M_p[p] = dM_all[i, p]
761 M_pp[p] = ddM_all[i, p]
766 pvals = self.FF.make(mvals_)
768 M_p[p],M_pp[p] =
f12d3p(
fdwrap(callM, mvals, p), h = self.h, f0 = M)
769 if all(M_p[p] == 0):
continue 770 M0_p[p][0] += P * M_p[p][0]
771 Xi_p = 2 * X * M_p[p]
774 if not AHess:
continue 776 M_pp[p] = ddM_all[i, p]
780 Xi_pq = 2 * (M_p[p] * M_p[p])
782 SPX_pq[p,p] += P * Xi_pq
784 if all(M_p[q] == 0):
continue 785 if q
not in self.pgrad:
continue 786 Xi_pq = 2 * M_p[p] * M_p[q]
788 SPX_pq[p,q] += P * Xi_pq
793 M_all_print = M_all.copy()
795 M_all_print[:,0] -= np.average(M_all_print[:,0], weights=self.
boltz_wts)
797 Q_all_print = np.hstack((
col(self.
eqm),self.
fref))
799 Q_all_print =
col(self.
eqm)
801 QEtmp = np.array(Q_all_print[:,0]).flatten()
802 Q_all_print[:,0] -= np.average(QEtmp, weights=self.
boltz_wts)
804 QEtmp = np.array(Q_all_print[:,0]).flatten()
805 Q_all_print[:,0] -= np.min(QEtmp)
806 MEtmp = np.array(M_all_print[:,0]).flatten()
807 M_all_print[:,0] -= np.min(MEtmp)
808 if self.writelevel > 1:
809 np.savetxt(
'M.txt',M_all_print)
810 np.savetxt(
'Q.txt',Q_all_print)
811 if self.writelevel > 0:
812 EnergyComparison = np.hstack((
col(Q_all_print[:,0]),
813 col(M_all_print[:,0]),
814 col(M_all_print[:,0])-
col(Q_all_print[:,0]),
816 np.savetxt(
"EnergyCompare.txt", EnergyComparison, header=
"%11s %12s %12s %12s" % (
"QMEnergy",
"MMEnergy",
"Delta(MM-QM)",
"Weight"), fmt=
"% 12.6e")
817 if self.writelevel > 1:
819 M_orig=self.
M_orig[:,0]
if self.
M_orig is not None else None,
820 title=
'Abinitio '+self.name)
822 self.
M_orig = M_all_print.copy()
823 if self.
force and self.writelevel > 1:
826 Mforce_obj = QMTraj[:]
827 Qforce_obj = QMTraj[:]
828 Mforce_print = np.array(M_all_print[:,1:3*nat+1])
829 Qforce_print = np.array(Q_all_print[:,1:3*nat+1])
830 MaxComp = np.max(np.abs(np.vstack((Mforce_print,Qforce_print)).flatten()))
831 Mforce_print /= MaxComp
832 Qforce_print /= MaxComp
834 Mforce_obj.xyzs[i] = Mforce_print[i, :].reshape(-1,3)
835 Qforce_obj.xyzs[i] = Qforce_print[i, :].reshape(-1,3)
840 if Mforce_obj.na != Mforce_obj.xyzs[0].shape[0]:
842 print(Mforce_obj.xyzs[0].shape[0])
843 warn_once(
'\x1b[91mThe printing of forces is not set up correctly. Not printing forces. Please report this issue.\x1b[0m')
845 if self.writelevel > 1:
846 QMTraj.write(
'coords.xyz')
847 Mforce_obj.elem = [
'H' for i
in range(Mforce_obj.na)]
848 Mforce_obj.write(
'MMforce.xyz')
849 Qforce_obj.elem = [
'H' for i
in range(Qforce_obj.na)]
850 Qforce_obj.write(
'QMforce.xyz')
856 logger.debug(
"Done with snapshots, building objective function now\r")
857 W_Components = np.zeros(NParts)
864 if np.sum(W_Components) > 0
and self.w_normalize:
865 W_Components /= np.sum(W_Components)
867 if self.energy_rms_override != 0.0:
868 QQ0[0] = self.energy_rms_override ** 2
871 if self.force_rms_override != 0.0:
874 QQ0[1:3*nat+1] = self.force_rms_override ** 2 * 100 / 3
877 def compute_objective(SPX_like,divide=1,L=None,R=None,L2=None,R2=None):
881 divide=divide,L=L,R=R,L2=L2,R2=R2)
897 return np.array(objs)
900 X2_Components = compute_objective(SPX,L=X0,R=X0)
902 X2_Physical = compute_objective(SPX,divide=0,L=X0,R=X0)
904 X2_Normalize = compute_objective(SPX,divide=2,L=X0,R=X0)
907 if not AGrad:
continue 908 X2_Parts_p[p,:] = compute_objective(SPX_p[p],L=2*X0,R=M0_p[p])
909 if not AHess:
continue 910 X2_Parts_pq[p,p,:] = compute_objective(SPX_pq[p,p],L=2*M0_p[p],R=M0_p[p],L2=2*X0,R2=M0_pp[p])
912 if q
not in self.pgrad:
continue 913 X2_Parts_pq[p,q,:] = compute_objective(SPX_pq[p,q],L=2*M0_p[p],R=M0_p[q])
915 X2_Parts_pq[q,p,:] = X2_Parts_pq[p,q,:]
917 X2 = np.dot(X2_Components, W_Components)
920 H = np.zeros((NP,NP))
922 if not AGrad:
continue 923 G[p] = np.dot(X2_Parts_p[p], W_Components)
924 if not AHess:
continue 926 H[p,q] = np.dot(X2_Parts_pq[p,q], W_Components)
935 self.
e_trm = X2_Components[0]
936 self.
e_ctr = X2_Components[0]*W_Components[0]
937 if self.w_normalize: self.
e_ctr /= tw
938 self.
e_ref = np.sqrt(X2_Normalize[0])
939 self.
e_err = np.sqrt(X2_Physical[0])
943 self.
f_ctr = X2_Components[1]*W_Components[1]
944 if self.w_normalize: self.
f_ctr /= tw
945 self.
f_ref = np.sqrt(X2_Normalize[1]/nat)
946 self.
f_err = np.sqrt(X2_Physical[1]/nat)
950 self.
nf_ctr = X2_Components[2]*W_Components[2]
951 self.
nf_ref = np.sqrt(X2_Normalize[2]/nnf)
952 self.
nf_err = np.sqrt(X2_Physical[2]/nnf)
954 self.
tq_trm = X2_Components[3]
955 self.
tq_ctr = X2_Components[3]*W_Components[3]
956 self.
tq_ref = np.sqrt(X2_Normalize[3]/ntq)
957 self.
tq_err = np.sqrt(X2_Physical[3]/ntq)
963 pvals = self.FF.make(mvals)
964 Answer = {
'X':X2,
'G':G,
'H':H}
967 def get_resp(self, mvals, AGrad=False, AHess=False):
968 """ Electrostatic potential fitting. Implements the RESP objective function. (In Python so obviously not optimized.) """ 973 pvals = self.FF.make(mvals)
982 def new_charges(mvals_):
983 """ Return the charges acting on the system. """ 985 pvals = self.FF.make(mvals_)
986 return self.
engine.get_charges()
998 charge0 = new_charges(mvals)
1005 dqPdqM = np.array([(
f12d3p(
fdwrap(new_charges,mvals,i), h = self.h, f0 = charge0)[0]
if i
in self.pgrad
else np.zeros_like(charge0))
for i
in range(NP)]).T
1006 xyzs = np.array(self.
mol.xyzs)
1007 espqvals = np.array(self.
espval)
1008 espxyz = np.array(self.
espxyz)
1014 for p
in self.pgrad:
1015 if 'VSITE' in self.FF.plist[p]:
1021 H = np.zeros((NP, NP))
1022 for i
in range(self.
ns):
1026 espqval = espqvals[i]
1027 espmval = np.dot(dVdqP,
col(new_charges(mvals)))
1028 desp =
flat(espmval) - espqval
1029 X += P * np.dot(desp, desp) / self.
nesp 1030 Q += P * np.dot(espqval, espqval) / self.
nesp 1031 D += P * (np.dot(espqval, espqval) / self.
nesp - (np.sum(espqval) / self.
nesp)**2)
1033 dVdqM = np.dot(dVdqP, dqPdqM).T
1034 for p, vsd
in ddVdqPdVS.items():
1035 dVdqM[p,:] +=
flat(np.dot(vsd[i],
col(new_charges(mvals))))
1036 G +=
flat(P * 2 * np.dot(dVdqM,
col(desp))) / self.
nesp 1038 d2VdqM2 = np.zeros(dVdqM.shape)
1039 for p, vsd
in dddVdqPdVS2.items():
1040 d2VdqM2[p,:] +=
flat(np.dot(vsd[i],
col(new_charges(mvals))))
1041 H += np.array(P * 2 * (np.dot(dVdqM, dVdqM.T) + np.dot(d2VdqM2,
col(desp)))) / self.
nesp 1062 q = new_charges(mvals)
1063 R = a*np.sum((q**2 + b**2)**0.5 - b)
1064 dR = a*q*(q**2 + b**2)**-0.5
1065 ddR = a*b**2*(q**2 + b**2)**-1.5
1069 G +=
flat(np.dot(dqPdqM.T,
col(dR)))
1071 H += np.diag(
flat(np.dot(dqPdqM.T,
col(ddR))))
1076 if self.w_normalize:
1079 Answer = {
'X':X,
'G':G,
'H':H}
1082 def get(self, mvals, AGrad=False, AHess=False):
1083 Answer = {
'X':0.0,
'G':np.zeros(self.FF.np),
'H':np.zeros((self.FF.np, self.FF.np))}
1085 if self.w_normalize:
1087 w_resp = self.
w_resp / tw
1091 if self.energy
or self.
force:
1094 Answer[i] += w_ef * Answer_EF[i]
1096 Answer_ESP = self.
get_resp(mvals, AGrad, AHess)
1097 for i
in Answer_ESP:
1098 Answer[i] += w_resp * Answer_ESP[i]
1099 if not any([self.energy, self.
force, self.resp]):
1100 logger.error(
"Ab initio fitting must have at least one of: Energy, Force, ESP\n")
1106 def compute_objective_part(SPX,QQ0,Q0,Z,a,n,energy=False,subtract_mean=False,divide=1,L=None,R=None,L2=None,R2=None):
1109 QQ0iZ = QQ0[a:a+n]/Z
1113 if L
is not None and R
is not None:
1117 elif L2
is not None and R2
is not None:
1122 raise RuntimeError(
"subtract_mean is set to True, must provide L/R or L2/R2")
1128 X2 = np.sum(XiZ)/np.sum(QQ0iZ)
1134 raise RuntimeError(
'Please pass either 0, 1, 2 to divide')
1138 import matplotlib.pyplot
as plt
1139 qm_min_dx = np.argmin(Q)
1140 e_qm = Q - Q[qm_min_dx]
1141 e_mm = M - M[qm_min_dx]
1142 if M_orig
is not None:
1143 e_mm_orig = M_orig - M_orig[qm_min_dx]
1144 plt.plot(e_qm, e_mm_orig,
'x', markersize=5, label=
'Orig.')
1145 plt.plot(e_qm, e_mm,
'.', markersize=5, label=
'Current')
1146 plt.xlabel(
'QM Energies (kJ/mol)')
1147 plt.ylabel(
'MM Energies (kJ/mol)')
1148 x1,x2,y1,y2 = plt.axis()
1155 plt.axis([x1-0.05*rng, x2+0.05*rng, y1-0.05*rng, y2+0.05*rng])
1156 plt.plot([x1,x2],[y1,y2],
'--' )
1157 plt.legend(loc=
'lower right')
1160 fig.set_size_inches(5,5)
1161 fig.savefig(
'e_qm_vs_mm.pdf')
fqm
Reference (QM) forces.
savg
Option for how much data to write to disk.
def compute_netforce_torque(self, xyz, force, QM=False)
def read_reference_data(self)
Read the reference ab initio data from a file such as qdata.txt.
save_vmvals
Save the mvals from the last time we updated the vsites.
buildx
Read in the reference data.
Nifty functions, intended to be imported by any module within ForceBalance.
use_nft
Whether to compute net forces and torques, or not.
AtomLists
Lists of atoms to do net force/torque fitting and excluding virtual sites.
def energy_force_transform(self)
f_err
Qualitative Indicator: average force error (fractional)
esp_err
Qualitative Indicator: "relative RMS" for electrostatic potential.
eqm
Reference (QM) energies.
def isint(word)
ONLY matches integers! If you have a decimal point? None shall pass!
w_force
Initialize the base class.
def flat(vec)
Given any list, array, or matrix, return a single-index array.
nparticles
The number of (atoms + drude particles + virtual sites)
def plot_qm_vs_mm(Q, M, M_orig=None, title='')
mol
Read in the trajectory file.
def in_fd()
Invoking this function from anywhere will tell us whether we're being called by a finite-difference f...
def compute_objective_part(SPX, QQ0, Q0, Z, a, n, energy=False, subtract_mean=False, divide=1, L=None, R=None, L2=None, R2=None)
def fdwrap(func, mvals0, pidx, key=None, kwargs)
A function wrapper for finite difference designed for differentiating 'get'-type functions.
def build_invdist(self, mvals)
def col(vec)
Given any list, array, or matrix, return a 1-column 2D array.
def norm2(arr, a=0, n=None, step=3)
Given a one-dimensional array, return the norm-squared of every "step" elements, starting at 'a' and ...
def __init__(self, options, tgt_opts, forcefield)
Initialization; define a few core concepts.
def warn_press_key(warning, timeout=10)
def energy_force_transform_one(self, i)
def energy_force_one(self, i)
engine
Build keyword dictionaries to pass to engine.
boltz_wts
Boltzmann weights.
def get_resp(self, mvals, AGrad=False, AHess=False)
Electrostatic potential fitting.
def get(self, mvals, AGrad=False, AHess=False)
def warn_once(warning, warnhash=None)
Prints a warning but will only do so once in a given run.
def get_energy_force(self, mvals, AGrad=False, AHess=False)
LPW 7-13-2016.
def f12d3p(f, h, f0=None)
A three-point finite difference stencil.
Subclass of Target for fitting force fields to ab initio data.
qfnm
The qdata.txt file that contains the QM energies and forces.
e_err
Qualitative Indicator: average energy error (in kJ/mol)
loop_over_snapshots
LPW 2018-02-11: This is set to True if the target calculates a single-point property over several exi...
def energy_force_all(self)