View Javadoc
1   // ******************************************************************************
2   //
3   // Title:       Force Field X.
4   // Description: Force Field X - Software for Molecular Biophysics.
5   // Copyright:   Copyright (c) Michael J. Schnieders 2001-2024.
6   //
7   // This file is part of Force Field X.
8   //
9   // Force Field X is free software; you can redistribute it and/or modify it
10  // under the terms of the GNU General Public License version 3 as published by
11  // the Free Software Foundation.
12  //
13  // Force Field X is distributed in the hope that it will be useful, but WITHOUT
14  // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15  // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16  // details.
17  //
18  // You should have received a copy of the GNU General Public License along with
19  // Force Field X; if not, write to the Free Software Foundation, Inc., 59 Temple
20  // Place, Suite 330, Boston, MA 02111-1307 USA
21  //
22  // Linking this library statically or dynamically with other modules is making a
23  // combined work based on this library. Thus, the terms and conditions of the
24  // GNU General Public License cover the whole combination.
25  //
26  // As a special exception, the copyright holders of this library give you
27  // permission to link this library with independent modules to produce an
28  // executable, regardless of the license terms of these independent modules, and
29  // to copy and distribute the resulting executable under terms of your choice,
30  // provided that you also meet, for each linked independent module, the terms
31  // and conditions of the license of that module. An independent module is a
32  // module which is not derived from or based on this library. If you modify this
33  // library, you may extend this exception to your version of the library, but
34  // you are not obligated to do so. If you do not wish to do so, delete this
35  // exception statement from your version.
36  //
37  // ******************************************************************************
38  package ffx.potential.nonbonded.implicit;
39  
40  import static java.lang.String.format;
41  import static org.apache.commons.math3.util.FastMath.PI;
42  import static org.apache.commons.math3.util.FastMath.cbrt;
43  import static org.apache.commons.math3.util.FastMath.pow;
44  import static org.apache.commons.math3.util.FastMath.sqrt;
45  
46  import ffx.numerics.atomic.AtomicDoubleArray3D;
47  import ffx.numerics.switching.MultiplicativeSwitch;
48  import ffx.potential.bonded.Atom;
49  import ffx.potential.nonbonded.GeneralizedKirkwood;
50  import ffx.potential.parameters.ForceField;
51  import java.util.logging.Level;
52  import java.util.logging.Logger;
53  
54  /**
55   * The ChandlerCavitation class smoothly switches between a volume based dependence for small
56   * solutes to a surface area dependence for large solutes.
57   *
58   * @author Michael J. Schnieders
59   * @since 1.0
60   */
61  public class ChandlerCavitation {
62  
63    private static final Logger logger = Logger.getLogger(ChandlerCavitation.class.getName());
64    /** Volume / Surface Area are switched on / off over 7.0 Angstroms; half is 3.5 A. */
65    private final double HALF_SWITCH_RANGE = 3.5;
66    /**
67     * A value of 0.2 A smoothly moves from Volume dependence to Surface Area dependence.
68     *
69     * <p>However, a smaller offset of 0.0 to ~0.1 A overshoots the limiting surface tension.
70     */
71    private final double SA_SWITCH_OFFSET = 0.2;
72  
73    private final boolean doVolume;
74    private final boolean doSurfaceArea;
75  
76    private final GaussVol gaussVol;
77    private final ConnollyRegion connollyRegion;
78    /** Surface area (Ang^2). */
79    private double surfaceArea;
80    /** Surface area energy (kcal/mol). */
81    private double surfaceAreaEnergy;
82    /** Volume (Ang^3). */
83    private double volume;
84    /** Volume energy (kcal/mol). */
85    private double volumeEnergy;
86    /** Cavitation energy, which is a function of volume and/or surface area. */
87    private double cavitationEnergy;
88    /**
89     * Effective radius probe.
90     *
91     * <p>In cavitation volume scaling regime, approximate solvent excluded volume and effective
92     * radius are computed as follow.
93     *
94     * <p>1) GaussVol vdW volume is computed from defined radii. 2) Effective radius is computed as
95     * Reff = cbrt(3.0 * volume / (4.0 * PI)) + effectiveRadiusProbe. 3) Solvent Excluded Volume = 4/3
96     * * Pi * Reff^3
97     */
98    private double effectiveRadius;
99    /**
100    * Solvent pressure in kcal/mol/Ang^3. Original value from Schnieders thesis work: 0.0327 Value
101    * based on testing with Schnieders thesis test set, Sept 2019: 0.11337
102    */
103   private double solventPressure = GeneralizedKirkwood.DEFAULT_SOLVENT_PRESSURE;
104   /** Surface tension in kcal/mol/Ang^2. */
105   private double surfaceTension = GeneralizedKirkwood.DEFAULT_CAVDISP_SURFACE_TENSION;
106   /**
107    * Radius where volume dependence crosses over to surface area dependence (approximately at 1 nm).
108    */
109   private double crossOver = GeneralizedKirkwood.DEFAULT_CROSSOVER;
110   /** Begin turning off the Volume term. */
111   private double beginVolumeOff = crossOver - HALF_SWITCH_RANGE;
112   /** Volume term is zero at the cut-off. */
113   private double endVolumeOff = beginVolumeOff + 2.0 * HALF_SWITCH_RANGE;
114   /** Begin turning off the SA term. */
115   private double beginSurfaceAreaOff = crossOver + SA_SWITCH_OFFSET + HALF_SWITCH_RANGE;
116   /** SA term is zero at the cut-off. */
117   private double endSurfaceAreaOff = beginSurfaceAreaOff - 2.0 * HALF_SWITCH_RANGE;
118   /** Volume multiplicative switch. */
119   private MultiplicativeSwitch volumeSwitch =
120       new MultiplicativeSwitch(beginVolumeOff, endVolumeOff);
121   /** Surface area multiplicative switch. */
122   private MultiplicativeSwitch surfaceAreaSwitch =
123       new MultiplicativeSwitch(beginSurfaceAreaOff, endSurfaceAreaOff);
124 
125   private final int nAtoms;
126   private final Atom[] atoms;
127 
128   public ChandlerCavitation(Atom[] atoms, GaussVol gaussVol, ForceField forceField) {
129     this.atoms = atoms;
130     this.nAtoms = atoms.length;
131     this.gaussVol = gaussVol;
132     this.connollyRegion = null;
133 
134     doVolume = forceField.getBoolean("VOLUMETERM", true);
135     doSurfaceArea = forceField.getBoolean("AREATERM", true);
136   }
137 
138   public ChandlerCavitation(Atom[] atoms, ConnollyRegion connollyRegion, ForceField forceField) {
139     this.atoms = atoms;
140     this.nAtoms = atoms.length;
141     this.connollyRegion = connollyRegion;
142     this.gaussVol = null;
143 
144     doVolume = forceField.getBoolean("VOLUMETERM", true);
145     doSurfaceArea = forceField.getBoolean("AREATERM", true);
146   }
147 
148   /**
149    * Compute molecular volume and surface area.
150    *
151    * @param positions Atomic positions to use.
152    * @param gradient Atomic coordinate gradient.
153    * @return The cavitation energy.
154    */
155   public double energyAndGradient(double[][] positions, AtomicDoubleArray3D gradient) {
156     if (gaussVol != null) {
157       return energyAndGradientGausVol(positions, gradient);
158     } else {
159       return energyAndGradientConnolly(gradient);
160     }
161   }
162 
163   /**
164    * Compute the cavitation energy.
165    *
166    * @param gradient Add the gradient to this AtomicDoubleArray3D.
167    * @return Returns the cavitation energy.
168    */
169   public double energyAndGradientConnolly(AtomicDoubleArray3D gradient) {
170 
171     // Zero out cavitation energy.
172     cavitationEnergy = 0.0;
173 
174     connollyRegion.init(atoms, true);
175     connollyRegion.runVolume();
176 
177     // Calculate a purely surface area based cavitation energy.
178     surfaceArea = connollyRegion.getSurfaceArea();
179     surfaceAreaEnergy = surfaceArea * surfaceTension;
180 
181     // Calculate a purely volume based cavitation energy.
182     volume = connollyRegion.getVolume();
183     volumeEnergy = volume * solventPressure;
184 
185     // effectiveRadius = 0.5 * sqrt(surfaceArea / PI);
186     // double reff = effectiveRadius;
187     // double reff2 = reff * reff;
188     // double reff3 = reff2 * reff;
189     // double reff4 = reff3 * reff;
190     // double reff5 = reff4 * reff;
191     // double dReffdvdW = reff / (2.0 * surfaceArea);
192 
193     // Use Volume to find an effective cavity radius.
194     effectiveRadius = cbrt(3.0 * volume / (4.0 * PI));
195     double reff = effectiveRadius;
196     double reff2 = reff * reff;
197     double reff3 = reff2 * reff;
198     double reff4 = reff3 * reff;
199     double reff5 = reff4 * reff;
200     double vdWVolPI23 = pow(volume / PI, 2.0 / 3.0);
201     double dReffdvdW = 1.0 / (pow(6.0, 2.0 / 3.0) * PI * vdWVolPI23);
202 
203     // Find the cavitation energy using a combination of volume and surface area dependence.
204     if (doVolume) {
205       if (!doSurfaceArea || reff < beginVolumeOff) {
206         // Find cavity energy from only the molecular volume.
207         cavitationEnergy = volumeEnergy;
208         double[][] volumeGradient = connollyRegion.getVolumeGradient();
209         for (int i = 0; i < nAtoms; i++) {
210           double dx = solventPressure * volumeGradient[0][i];
211           double dy = solventPressure * volumeGradient[1][i];
212           double dz = solventPressure * volumeGradient[2][i];
213           gradient.add(0, i, dx, dy, dz);
214         }
215       } else if (reff <= endVolumeOff) {
216         // Include a tapered molecular volume.
217         double taper = volumeSwitch.taper(reff, reff2, reff3, reff4, reff5);
218         cavitationEnergy = taper * volumeEnergy;
219         double dtaper = volumeSwitch.dtaper(reff, reff2, reff3, reff4) * dReffdvdW;
220         double factor = dtaper * volumeEnergy + taper * solventPressure;
221         double[][] volumeGradient = connollyRegion.getVolumeGradient();
222         for (int i = 0; i < nAtoms; i++) {
223           double dx = factor * volumeGradient[0][i];
224           double dy = factor * volumeGradient[1][i];
225           double dz = factor * volumeGradient[2][i];
226           gradient.add(0, i, dx, dy, dz);
227         }
228       }
229     }
230 
231     // TODO: We need to include the surface area contribution to the gradient.
232     if (doSurfaceArea) {
233       if (!doVolume || reff > beginSurfaceAreaOff) {
234         // Find cavity energy from only SA.
235         cavitationEnergy = surfaceAreaEnergy;
236         // addSurfaceAreaGradient(0.0, surfaceTension, gradient);
237       } else if (reff >= endSurfaceAreaOff) {
238         // Include a tapered surface area term.
239         double taperSA = surfaceAreaSwitch.taper(reff, reff2, reff3, reff4, reff5);
240         cavitationEnergy += taperSA * surfaceAreaEnergy;
241         // double dtaperSA = surfaceAreaSwitch.dtaper(reff, reff2, reff3, reff4) * dReffdvdW;
242         // addSurfaceAreaGradient(dtaperSA * surfaceAreaEnergy, taperSA * surfaceTension, gradient);
243       }
244     }
245 
246     if (logger.isLoggable(Level.FINE)) {
247       logger.fine("\n Connolly");
248       logger.fine(format(" Volume:              %8.3f (Ang^3)", volume));
249       logger.fine(format(" Volume Energy:       %8.3f (kcal/mol)", volumeEnergy));
250       logger.fine(format(" Surface Area:        %8.3f (Ang^2)", surfaceArea));
251       logger.fine(format(" Surface Area Energy: %8.3f (kcal/mol)", surfaceAreaEnergy));
252       logger.fine(format(" Volume + SA Energy:  %8.3f (kcal/mol)", cavitationEnergy));
253       logger.fine(format(" Effective Radius:    %8.3f (Ang)", reff));
254     }
255 
256     return cavitationEnergy;
257   }
258 
259   /**
260    * Compute molecular volume and surface area.
261    *
262    * @param positions Atomic positions to use.
263    * @param gradient Atomic coordinate gradient.
264    * @return The cavitation energy.
265    */
266   public double energyAndGradientGausVol(double[][] positions, AtomicDoubleArray3D gradient) {
267 
268     // Zero out cavitation energy.
269     cavitationEnergy = 0.0;
270 
271     gaussVol.computeVolumeAndSA(positions);
272 
273     // Save the total molecular volume.
274     volume = gaussVol.getVolume();
275     double[] volumeGradient = gaussVol.getVolumeGradient();
276 
277     // Calculate a purely volume based cavitation energy.
278     volumeEnergy = volume * solventPressure;
279 
280     // Calculate the surface area.
281     surfaceArea = gaussVol.getSurfaceArea();
282     double[] surfaceAreaGradient = gaussVol.getSurfaceAreaGradient();
283 
284     // Calculate a purely surface area based cavitation energy.
285     surfaceAreaEnergy = surfaceArea * surfaceTension;
286 
287     // Use SA to find an effective cavity radius.
288     effectiveRadius = 0.5 * sqrt(surfaceArea / PI);
289     double reff = effectiveRadius;
290     double reff2 = reff * reff;
291     double reff3 = reff2 * reff;
292     double reff4 = reff3 * reff;
293     double reff5 = reff4 * reff;
294     double dRdSA = reff / (2.0 * surfaceArea);
295 
296     // Use Volume to find an effective cavity radius.
297     //        effectiveRadius = cbrt(3.0 * volume / (4.0 * PI));
298     //        double reff = effectiveRadius;
299     //        double reff2 = reff * reff;
300     //        double reff3 = reff2 * reff;
301     //        double reff4 = reff3 * reff;
302     //        double reff5 = reff4 * reff;
303     //        double volPI23 = pow(volume / PI, 2.0 / 3.0);
304     //        double dReffdvdW = 1.0 / (pow(6.0, 2.0 / 3.0) * PI * volPI23);
305 
306     // Find the cavitation energy using a combination of volume and surface area dependence.
307     if (doVolume) {
308       if (!doSurfaceArea || reff < beginVolumeOff) {
309         // Find cavity energy from only the molecular volume.
310         cavitationEnergy = volumeEnergy;
311         addVolumeGradient(0.0, solventPressure, surfaceAreaGradient, volumeGradient, gradient);
312       } else if (reff <= endVolumeOff) {
313         // Include a tapered molecular volume.
314         double taper = volumeSwitch.taper(reff, reff2, reff3, reff4, reff5);
315         double dtaper = volumeSwitch.dtaper(reff, reff2, reff3, reff4) * dRdSA;
316         cavitationEnergy = taper * volumeEnergy;
317         double factorSA = dtaper * volumeEnergy;
318         double factorVol = taper * solventPressure;
319         addVolumeGradient(factorSA, factorVol, surfaceAreaGradient, volumeGradient, gradient);
320       }
321     }
322 
323     if (doSurfaceArea) {
324       if (!doVolume || reff > beginSurfaceAreaOff) {
325         // Find cavity energy from only SA.
326         cavitationEnergy = surfaceAreaEnergy;
327         addSurfaceAreaGradient(surfaceTension, surfaceAreaGradient, gradient);
328       } else if (reff >= endSurfaceAreaOff) {
329         // Include a tapered surface area term.
330         double taperSA = surfaceAreaSwitch.taper(reff, reff2, reff3, reff4, reff5);
331         double dtaperSA = surfaceAreaSwitch.dtaper(reff, reff2, reff3, reff4) * dRdSA;
332         cavitationEnergy += taperSA * surfaceAreaEnergy;
333         double factor = dtaperSA * surfaceAreaEnergy + taperSA * surfaceTension;
334         addSurfaceAreaGradient(factor, surfaceAreaGradient, gradient);
335       }
336     }
337 
338     if (logger.isLoggable(Level.FINE)) {
339       logger.fine("\n GaussVol");
340       logger.fine(format(" Volume:              %8.3f (Ang^3)", volume));
341       logger.fine(format(" Volume Energy:       %8.3f (kcal/mol)", volumeEnergy));
342       logger.fine(format(" Surface Area:        %8.3f (Ang^2)", surfaceArea));
343       logger.fine(format(" Surface Area Energy: %8.3f (kcal/mol)", surfaceAreaEnergy));
344       logger.fine(format(" Volume + SA Energy:  %8.3f (kcal/mol)", cavitationEnergy));
345       logger.fine(format(" Effective Radius:    %8.3f (Ang)", reff));
346     }
347 
348     return cavitationEnergy;
349   }
350 
351   public ConnollyRegion getConnollyRegion() {
352     return connollyRegion;
353   }
354 
355   public double getCrossOver() {
356     return crossOver;
357   }
358 
359   public void setCrossOver(double crossOver) {
360     if (crossOver < 3.5) {
361       logger.severe(
362           format(" The cross-over point (%8.6f A) must be greater than 3.5 A", crossOver));
363       return;
364     }
365     this.crossOver = crossOver;
366     beginVolumeOff = crossOver - HALF_SWITCH_RANGE;
367     endVolumeOff = beginVolumeOff + 2.0 * HALF_SWITCH_RANGE;
368     beginSurfaceAreaOff = crossOver + SA_SWITCH_OFFSET + HALF_SWITCH_RANGE;
369     endSurfaceAreaOff = beginSurfaceAreaOff - 2.0 * HALF_SWITCH_RANGE;
370     volumeSwitch = new MultiplicativeSwitch(beginVolumeOff, endVolumeOff);
371     surfaceAreaSwitch = new MultiplicativeSwitch(beginSurfaceAreaOff, endSurfaceAreaOff);
372   }
373 
374   public double getEffectiveRadius() {
375     return effectiveRadius;
376   }
377 
378   public double getEnergy() {
379     return cavitationEnergy;
380   }
381 
382   public GaussVol getGaussVol() {
383     return gaussVol;
384   }
385 
386   public double getSolventPressure() {
387     return solventPressure;
388   }
389 
390   public void setSolventPressure(double solventPressure) {
391     double newCrossOver = 3.0 * surfaceTension / solventPressure;
392     if (newCrossOver < 3.5) {
393       logger.severe(
394           format(
395               " The solvent pressure (%8.6f kcal/mol/A^3)"
396                   + " and surface tension (%8.6f kcal/mol/A^2) combination is not supported.",
397               solventPressure, surfaceTension));
398       return;
399     }
400     this.solventPressure = solventPressure;
401     this.setCrossOver(newCrossOver);
402   }
403 
404   /**
405    * Return Surface Area (A^2).
406    *
407    * @return Surface Area (A^2).
408    */
409   public double getSurfaceArea() {
410     return surfaceArea;
411   }
412 
413   /**
414    * Return Surface Area based cavitation energy.
415    *
416    * @return Surface Area based cavitation energy.
417    */
418   public double getSurfaceAreaEnergy() {
419     return surfaceAreaEnergy;
420   }
421 
422   public double getSurfaceTension() {
423     return surfaceTension;
424   }
425 
426   public void setSurfaceTension(double surfaceTension) {
427     double newCrossOver = 3.0 * surfaceTension / solventPressure;
428     if (newCrossOver < 3.5) {
429       logger.severe(
430           format(
431               " The solvent pressure (%8.6f kcal/mol/A^3)"
432                   + " and surface tension (%8.6f kcal/mol/A^2) combination is not supported.",
433               solventPressure, surfaceTension));
434       return;
435     }
436     this.surfaceTension = surfaceTension;
437     this.setCrossOver(newCrossOver);
438   }
439 
440   /**
441    * Return Volume (A^3).
442    *
443    * @return Volume (A^3).
444    */
445   public double getVolume() {
446     return volume;
447   }
448 
449   /**
450    * Return Volume based cavitation energy.
451    *
452    * @return Volume based cavitation energy.
453    */
454   public double getVolumeEnergy() {
455     return volumeEnergy;
456   }
457 
458   /**
459    * Collect the volume base cavitation energy and gradient contributions.
460    *
461    * @param factorSA Factor to multiply surface area gradient by.
462    * @param factorVol Factor to multiply surface area gradient by.
463    * @param surfaceAreaGradient Surface area gradient.
464    * @param volumeGradient Volume gradient.
465    * @param gradient Array to accumulate derivatives.
466    */
467   private void addVolumeGradient(
468       double factorSA,
469       double factorVol,
470       double[] surfaceAreaGradient,
471       double[] volumeGradient,
472       AtomicDoubleArray3D gradient) {
473     for (int i = 0; i < nAtoms; i++) {
474       int index = i * 3;
475       double gx = factorSA * surfaceAreaGradient[index] + factorVol * volumeGradient[index++];
476       double gy = factorSA * surfaceAreaGradient[index] + factorVol * volumeGradient[index++];
477       double gz = factorSA * surfaceAreaGradient[index] + factorVol * volumeGradient[index];
478       gradient.add(0, i, gx, gy, gz);
479     }
480   }
481 
482   /**
483    * Collect the surface area based cavitation energy and gradient contributions.
484    *
485    * @param factor Factor to multiply surface area gradient by.
486    * @param surfaceAreaGradient Surface area gradient.
487    * @param gradient Array to accumulate derivatives.
488    */
489   private void addSurfaceAreaGradient(
490       double factor, double[] surfaceAreaGradient, AtomicDoubleArray3D gradient) {
491     for (int i = 0; i < nAtoms; i++) {
492       int index = i * 3;
493       double gx = factor * surfaceAreaGradient[index++];
494       double gy = factor * surfaceAreaGradient[index++];
495       double gz = factor * surfaceAreaGradient[index];
496       gradient.add(0, i, gx, gy, gz);
497     }
498   }
499 }