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