ForceBalance API  1.3
Automated optimization of force fields and empirical potentials
counterpoise.py
Go to the documentation of this file.
1 """@package forcebalance.counterpoise
2 
3 Match an empirical potential to the counterpoise correction for basis set superposition error (BSSE).
4 
5 Here we test two different functional forms: a three-parameter
6 Gaussian repulsive potential and a four-parameter Gaussian which
7 goes smoothly to an exponential. The latter can be written in two
8 different ways - one which gives us control over the exponential,
9 the switching distance and the Gaussian decay constant, and
10 another which gives us control over the Gaussian and the switching
11 distance. They are called 'CPGAUSS', 'CPEXPG', and 'CPGEXP'. I think
12 the third option is the best although our early tests have
13 indicated that none of the force fields perform particularly well
14 for the water dimer.
15 
16 This subclass of Target implements the 'get' method.
17 
18 @author Lee-Ping Wang
19 @date 12/2011
20 """
21 from __future__ import absolute_import
22 
23 from builtins import range
24 import os
25 import sys
26 from re import match
27 from forcebalance.target import Target
28 from .nifty import *
29 import numpy as np
30 from forcebalance.output import getLogger
31 logger = getLogger(__name__)
32 
33 class Counterpoise(Target):
34  """ Target subclass for matching the counterpoise correction."""
35 
36  def __init__(self,options,tgt_opts,forcefield):
37  """ To instantiate Counterpoise, we read the coordinates and counterpoise data."""
38  # Initialize the superclass. :)
39  super(Counterpoise,self).__init__(options,tgt_opts,forcefield)
40 
41  #======================================#
42  # Options that are given by the parser #
43  #======================================#
44 
45  self.set_option(tgt_opts,'shots','ns')
46 
47  #======================================#
48  # Variables which are set here #
49  #======================================#
50 
52  self.loop_over_snapshots = True
53 
54  self.elem, self.xyzs = self.loadxyz(os.path.join(self.root,self.tgtdir,'all.xyz'))
55 
56  self.cpqm = self.load_cp(os.path.join(self.root,self.tgtdir,'cp.dat'))
57 
58  def loadxyz(self,fnm):
59  """ Parse an XYZ file which contains several xyz coordinates, and return their elements.
60 
61  @param[in] fnm The input XYZ file name
62  @return elem A list of chemical elements in the XYZ file
63  @return xyzs A list of XYZ coordinates (number of snapshots times number of atoms)
64  @todo I should probably put this into a more general library for reading coordinates.
65  """
66 
67  logger.info("Loading XYZ file!\n")
68  xyz = []
69  xyzs = []
70  elem = []
71  an = 0
72  sn = 0
73  for line in open(fnm):
74  strip = line.strip()
75  sline = line.split()
76  if match('^[0-9]+$',strip):
77 
78  self.na = int(strip)
79  elif match('[A-Z][a-z]*( +[-+]?([0-9]*\.[0-9]+|[0-9]+)){3}$',strip):
80  xyz.append([float(i) for i in sline[1:]])
81  if len(elem) < self.na:
82  elem.append(sline[0])
83  an += 1
84  if an == self.na:
85  xyzs.append(np.array(xyz))
86  sn += 1
87  if sn == self.ns:
88  break
89  xyz = []
90  an = 0
91  self.ns = len(xyzs)
92  return elem, xyzs
93 
94  def load_cp(self,fnm):
95  """ Load in the counterpoise data, which is easy; the file
96  consists of floating point numbers separated by newlines. """
97  logger.info("Loading CP Data!\n")
98  return np.array([float(i.strip()) for i in open(fnm).readlines()])[:self.ns]
99 
100  def get(self,mvals,AGrad=False,AHess=False):
101  """Gets the objective function for fitting the counterpoise correction.
102 
103  As opposed to AbInitio_GMXX2, which calls an external program,
104  this script actually computes the empirical interaction given the
105  force field parameters.
106 
107  It loops through the snapshots and atom pairs, and computes pairwise
108  contributions to an energy term according to hard-coded functional forms.
109 
110  One potential issue is that we go through all atom pairs instead of
111  looking only at atom pairs between different fragments. This means that
112  even for two infinitely separated fragments it will predict a finite
113  CP correction. While it might be okay to apply such a potential in practice,
114  there will be some issues for the fitting. Thus, we assume the last snapshot
115  to be CP-free and subtract that value of the potential back out.
116 
117  Note that forces and parametric derivatives are not implemented.
118 
119  @param[in] mvals Mathematical parameter values
120  @param[in] AGrad Switch to turn on analytic gradient (not implemented)
121  @param[in] AHess Switch to turn on analytic Hessian (not implemented)
122  @return Answer Contribution to the objective function
123 
124  """
125 
126  # Create the force field physical values from the mathematical values
127  pvals = self.FF.create_pvals(mvals)
128  cpmm = []
129  logger.info("CPMM: %s \r" % self.name)
130  # Loop through the snapshots
131  for s in range(self.ns):
132  xyz = self.xyzs[s] # Harvest the xyz. :)
133  cpmm_i = 0.0
134  # Loop through atom pairs
135  for i in range(self.na):
136  for j in range(i+1,self.na):
137  # Compute the distance between 2 atoms
138  # and the names of the elements involved
139  dx = np.linalg.norm(xyz[i,:]-xyz[j,:])
140  ai = self.elem[i]
141  aj = self.elem[j]
142  aiaj = ai < aj and "%s%s" % (ai,aj) or "%s%s" % (aj,ai)
143  pidlist = ['CPGAUSSA','CPGAUSSB','CPGAUSSC']
144  if all([k+aiaj in self.FF.map for k in pidlist]):
145  # Look up the parameter values. This might be something like 'CPGAUSSBLiCl'.
146  A, B, C = [self.FF.pvals[self.FF.map[k+aiaj]] for k in pidlist]
147  # Actually compute the interaction.
148  cpmm_i += A * np.exp (-B * (dx - C)**2)
149  # This is repeated for different interaction types...
150  pidlist = ['CPGEXPA', 'CPGEXPB', 'CPGEXPG', 'CPGEXPX']
151  if all([k+aiaj in self.FF.map for k in pidlist]):
152  A, B, G, X0 = [self.FF.pvals[self.FF.map[k+aiaj]] for k in pidlist]
153  a = 2*A*(X0-B)
154  b = A*(X0**2-B**2)+G
155  if dx < X0:
156  cpmm_i += np.exp(-a*dx+b)
157  else:
158  cpmm_i += np.exp(-A*(dx-B)**2+G)
159  pidlist = ['CPEXPGA1','CPEXPGB','CPEXPGX0','CPEXPGA2']
160  if all([k+aiaj in self.FF.map for k in pidlist]):
161  A1, B, X0, A2 = [self.FF.pvals[self.FF.map[k+aiaj]] for k in pidlist]
162  B2 = 2 * A2 * X0 - A1
163  G = B - A2 * X0**2
164  if dx < X0:
165  cpmm_i += np.exp(-A1*dx+B)
166  else:
167  cpmm_i += np.exp(-A2*dx**2 + B2*dx + G)
168  cpmm.append(cpmm_i)
169  cpmm = np.array(cpmm)
170  cpmm -= cpmm[-1] # This prevents craziness from happening
171  # Write the results to a file for plotting
172  with wopen('results') as f: f.writelines(["% .4f % .4f\n" % (cpmm[i],self.cpqm[i]) for i in range(len(cpmm))])
173  # Compute the difference between MM and QM counterpoise corrections
174  dcp = cpmm - self.cpqm
175  # Build the final answer and return it
176  Answer = {'X': np.dot(dcp,dcp),
177  'G': np.zeros(self.FF.np),
178  'H': np.zeros((self.FF.np,self.FF.np))
179  }
180  return Answer
def get(self, mvals, AGrad=False, AHess=False)
Gets the objective function for fitting the counterpoise correction.
loop_over_snapshots
Number of snapshots.
Definition: counterpoise.py:55
def loadxyz(self, fnm)
Parse an XYZ file which contains several xyz coordinates, and return their elements.
Definition: counterpoise.py:69
cpqm
Counterpoise correction data.
Definition: counterpoise.py:59
xyzs
XYZ elements and coordinates.
Definition: counterpoise.py:57
def load_cp(self, fnm)
Load in the counterpoise data, which is easy; the file consists of floating point numbers separated b...
Target subclass for matching the counterpoise correction.
Definition: counterpoise.py:36
def wopen(dest, binary=False)
If trying to write to a symbolic link, remove it first.
Definition: nifty.py:1304
def __init__(self, options, tgt_opts, forcefield)
To instantiate Counterpoise, we read the coordinates and counterpoise data.
Definition: counterpoise.py:40