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 edu.rit.pj.IntegerForLoop;
41  import edu.rit.pj.ParallelRegion;
42  import edu.rit.pj.reduction.DoubleOp;
43  import edu.rit.pj.reduction.SharedDoubleArray;
44  import ffx.crystal.Crystal;
45  import ffx.potential.bonded.Atom;
46  import ffx.potential.parameters.ForceField;
47  import org.apache.commons.configuration2.CompositeConfiguration;
48  
49  import java.util.logging.Level;
50  import java.util.logging.Logger;
51  
52  import static ffx.potential.nonbonded.implicit.BornTanhRescaling.MAX_BORN_RADIUS;
53  import static ffx.potential.nonbonded.implicit.BornTanhRescaling.tanhRescaling;
54  import static ffx.potential.nonbonded.implicit.NeckIntegral.getNeckConstants;
55  import static java.lang.Double.isInfinite;
56  import static java.lang.Double.isNaN;
57  import static java.lang.String.format;
58  import static java.lang.System.arraycopy;
59  import static java.util.Arrays.fill;
60  import static org.apache.commons.math3.util.FastMath.PI;
61  import static org.apache.commons.math3.util.FastMath.max;
62  import static org.apache.commons.math3.util.FastMath.pow;
63  import static org.apache.commons.math3.util.FastMath.sqrt;
64  
65  /**
66   * Parallel computation of Born radii via the Grycuk method.
67   *
68   * @author Michael J. Schnieders
69   * @since 1.0
70   */
71  public class BornRadiiRegion extends ParallelRegion {
72  
73    private static final Logger logger = Logger.getLogger(BornRadiiRegion.class.getName());
74    private static final double oneThird = 1.0 / 3.0;
75    private static final double PI4_3 = 4.0 / 3.0 * PI;
76    private static final double INVERSE_PI4_3 = 1.0 / PI4_3;
77    private static final double PI_12 = PI / 12.0;
78    private final BornRadiiLoop[] bornRadiiLoop;
79    /** An ordered array of atoms in the system. */
80    protected Atom[] atoms;
81    /** Periodic boundary conditions and symmetry. */
82    private Crystal crystal;
83    /** Atomic coordinates for each symmetry operator. */
84    private double[][][] sXYZ;
85    /** Neighbor lists for each atom and symmetry operator. */
86    private int[][][] neighborLists;
87    /** Base radius of each atom. */
88    private double[] baseRadius;
89    /** Descreen radius of each atom. */
90    private double[] descreenRadius;
91    /**
92     * Overlap scale factor for each atom, when using the Hawkins, Cramer & Truhlar pairwise
93     * descreening algorithm.
94     *
95     * <p>G. D. Hawkins, C. J. Cramer and D. G. Truhlar, "Parametrized Models of Aqueous Free Energies
96     * of Solvation Based on Pairwise Descreening of Solute Atomic Charges from a Dielectric Medium",
97     * J. Phys. Chem., 100, 19824-19839 (1996).
98     */
99    private double[] overlapScale;
100   /**
101    * Sneck scaling parameter for each atom. Set based on maximum Sneck scaling parameter and number
102    * of bound non-hydrogen atoms
103    */
104   private double[] neckScale;
105   /**
106    * Perfect born radius values
107    */
108   private final double[] perfectRadii;
109   /**
110    * Flag to turn on use of perfect Born radii.
111    */
112   private final boolean usePerfectRadii;
113   /** Born radius of each atom. */
114   private double[] born;
115   /**
116    * Flag to indicate if an atom should be included.
117    */
118   private boolean[] use;
119   /** GK cut-off distance squared. */
120   private double cut2;
121   /**
122    * Forces all atoms to be considered during Born radius updates.
123    */
124   private boolean nativeEnvironmentApproximation;
125   /** If true, the descreening integral includes overlaps with the volume of the descreened atom */
126   private final boolean perfectHCTScale;
127   private double descreenOffset = 0.0;
128   /**
129    * The Born radius for each atom.
130    */
131   private SharedDoubleArray sharedBorn;
132   /**
133    * Boolean indicating whether or not to print all Born radii for an input molecular system. This is
134    * turned off after the first round of printing.
135    */
136   private boolean verboseRadii;
137   /**
138    * Boolean indicating whether or not to use the neck volume correction for the implicit solvent
139    */
140   private final boolean neckCorrection;
141   /**
142    * Boolean indicating whether or not to use the tanh volume correction for the implicit solvent
143    */
144   private final boolean tanhCorrection;
145   /**
146    * This is the Born Integral prior to rescaling with a tanh function, which is a quantity needed
147    * for the computing the derivative of the energy with respect to atomic coordinates.
148    */
149   private double[] unscaledBornIntegral;
150 
151   /**
152    * BornRadiiRegion Constructor.
153    *
154    * @param nt Number of threads.
155    * @param nAtoms Number of atoms.
156    * @param forceField The ForceField in use.
157    * @param neckCorrection Perform a neck correction.
158    * @param tanhCorrection Perform a tanh correction.
159    * @param perfectHCTScale Use "perfect" HCT scale factors.
160    */
161   public BornRadiiRegion(int nt, int nAtoms, ForceField forceField, boolean neckCorrection,
162       boolean tanhCorrection, boolean perfectHCTScale) {
163     bornRadiiLoop = new BornRadiiLoop[nt];
164     for (int i = 0; i < nt; i++) {
165       bornRadiiLoop[i] = new BornRadiiLoop();
166     }
167     verboseRadii = forceField.getBoolean("VERBOSE_BORN_RADII", false);
168     this.perfectHCTScale = perfectHCTScale;
169     this.neckCorrection = neckCorrection;
170     this.tanhCorrection = tanhCorrection;
171     if (verboseRadii && logger.isLoggable(Level.FINER)) {
172       logger.finer(" Verbose Born radii.");
173     }
174 
175     if (tanhCorrection) {
176       unscaledBornIntegral = new double[nAtoms];
177     }
178 
179     CompositeConfiguration compositeConfiguration = forceField.getProperties();
180     usePerfectRadii = forceField.getBoolean("PERFECT_RADII", false);
181     String[] radii = compositeConfiguration.getStringArray("perfect-radius");
182     if (usePerfectRadii) {
183       perfectRadii = new double[nAtoms];
184       if (radii != null && radii.length > 0) {
185         if (logger.isLoggable(Level.FINER)) {
186           logger.finer(format(" Reading %d perfect-radius records .", radii.length));
187         }
188         for (String radius : radii) {
189           String[] tokens = radius.trim().split(" +");
190           if (tokens.length == 2) {
191             // Input records should be from 1 to the number of atoms (subtract 1 to index from 0).
192             int index = Integer.parseInt(tokens[0]) - 1;
193             double value = Double.parseDouble(tokens[1]);
194             if (logger.isLoggable(Level.FINER)) {
195               logger.finer(format(" perfect-radius %d %16.8f", index, value));
196             }
197             if (index >= 0 && index < nAtoms && value > 0.0) {
198               perfectRadii[index] = value;
199             }
200           } else {
201             logger.warning(format(" Could not parse perfect-radius line %s", radius));
202           }
203         }
204       }
205     } else {
206       perfectRadii = null;
207     }
208   }
209 
210   /**
211    * Return perfect Born radii read in as keywords, or base radii if perfect radii are not available.
212    *
213    * @return Array of perfect Born radii.
214    */
215   public double[] getPerfectRadii() {
216     int nAtoms = atoms.length;
217 
218     // Start with base radii.
219     double[] radii = new double[nAtoms];
220     arraycopy(baseRadius, 0, radii, 0, nAtoms);
221 
222     // Load perfect radii and return.
223     if (usePerfectRadii) {
224       for (int i = 0; i < nAtoms; i++) {
225         double perfectRadius = perfectRadii[i];
226         if (perfectRadius > 0.0) {
227           radii[i] = perfectRadius;
228         }
229       }
230     }
231 
232     return radii;
233   }
234 
235   @Override
236   public void finish() {
237     int nAtoms = atoms.length;
238 
239     // Load perfect radii and return.
240     if (usePerfectRadii) {
241       for (int i = 0; i < nAtoms; i++) {
242         double perfectRadius = perfectRadii[i];
243         if (!use[i] || perfectRadius <= 0.0) {
244           born[i] = baseRadius[i];
245         } else {
246           born[i] = perfectRadius;
247         }
248       }
249       return;
250     }
251 
252     for (int i = 0; i < nAtoms; i++) {
253       final double baseRi = baseRadius[i];
254       if (!use[i]) {
255         born[i] = baseRi;
256       } else {
257         // A positive integral of 1/r^6 over the solute outside atom i.
258         double soluteIntegral = -sharedBorn.get(i);
259         if (tanhCorrection) {
260           // Scale up the integral to account for interstitial spaces.
261           unscaledBornIntegral[i] = soluteIntegral;
262           soluteIntegral = tanhRescaling(soluteIntegral, baseRi);
263         }
264         // The total integral assumes no solute outside atom i, then subtracts away solute descreening.
265         double sum = PI4_3 / (baseRi * baseRi * baseRi) - soluteIntegral;
266         // Due to solute atomic overlaps, in rare cases the sum can be less than zero.
267         if (sum <= 0.0) {
268           born[i] = MAX_BORN_RADIUS;
269           if (verboseRadii) {
270             logger.info(format(" Born Integral < 0 for atom %d; set Born radius to %12.6f (Base Radius: %12.6f)",
271                 i + 1, born[i], baseRadius[i]));
272           }
273         } else {
274           born[i] = pow(INVERSE_PI4_3 * sum, -oneThird);
275           if (born[i] < baseRi) {
276             born[i] = baseRi;
277             if (verboseRadii) {
278               logger.info(format(" Born radius < Base Radius for atom %d: set Born radius to %12.6f", i + 1, baseRi));
279             }
280           } else if (born[i] > MAX_BORN_RADIUS) {
281             born[i] = MAX_BORN_RADIUS;
282             if (verboseRadii) {
283               logger.info(format(" Born radius > %12.6f Angstroms for atom %d: set Born radius to %12.6f",
284                      MAX_BORN_RADIUS, i + 1, baseRi));
285             }
286           } else if (isInfinite(born[i]) || isNaN(born[i])) {
287             born[i] = baseRi;
288             if (verboseRadii) {
289               logger.info(format(" Born radius NaN / Infinite for atom %d; set Born radius to %12.6f", i + 1, baseRi));
290             }
291           } else {
292             if (verboseRadii) {
293               logger.info(format(" Born radius for atom %d: %12.6f (Base Radius: %2.6f)", i + 1, born[i], baseRi));
294             }
295           }
296         }
297       }
298     }
299 
300     if (verboseRadii) {
301       // Only log the Born radii once.
302       logger.info(" Disabling verbose radii printing.");
303       verboseRadii = false;
304     }
305 
306   }
307 
308   public void init(
309       Atom[] atoms,
310       Crystal crystal,
311       double[][][] sXYZ,
312       int[][][] neighborLists,
313       double[] baseRadius,
314       double[] descreenRadius,
315       double[] overlapScale,
316       double[] neckScale,
317       double descreenOffset,
318       boolean[] use,
319       double cut2,
320       boolean nativeEnvironmentApproximation,
321       double[] born) {
322     this.atoms = atoms;
323     this.crystal = crystal;
324     this.sXYZ = sXYZ;
325     this.neighborLists = neighborLists;
326     this.baseRadius = baseRadius;
327     this.descreenRadius = descreenRadius;
328     this.overlapScale = overlapScale;
329     this.neckScale = neckScale;
330     this.descreenOffset = descreenOffset;
331     this.use = use;
332     this.cut2 = cut2;
333     this.nativeEnvironmentApproximation = nativeEnvironmentApproximation;
334     this.born = born;
335   }
336 
337   @Override
338   public void run() {
339     if (!usePerfectRadii) {
340       try {
341         int nAtoms = atoms.length;
342         execute(0, nAtoms - 1, bornRadiiLoop[getThreadIndex()]);
343       } catch (Exception e) {
344         String message = "Fatal exception computing Born radii in thread " + getThreadIndex() + "\n";
345         logger.log(Level.SEVERE, message, e);
346       }
347     }
348   }
349 
350   @Override
351   public void start() {
352     int nAtoms = atoms.length;
353     if (sharedBorn == null || sharedBorn.length() < nAtoms) {
354       sharedBorn = new SharedDoubleArray(nAtoms);
355     }
356     for (int i = 0; i < nAtoms; i++) {
357       sharedBorn.set(i, 0.0);
358     }
359   }
360 
361   public double[] getBorn() {
362     return born;
363   }
364 
365   public double[] getUnscaledBornIntegral() {
366     return unscaledBornIntegral;
367   }
368 
369   /**
370    * Compute Born radii for a range of atoms via the Grycuk method.
371    *
372    * @since 1.0
373    */
374   private class BornRadiiLoop extends IntegerForLoop {
375 
376     private double[] localBorn;
377 
378     @Override
379     public void finish() {
380       sharedBorn.reduce(localBorn, DoubleOp.SUM);
381     }
382 
383     @Override
384     public void run(int lb, int ub) {
385       int nSymm = crystal.spaceGroup.symOps.size();
386       if (nSymm == 0) {
387         nSymm = 1;
388       }
389       double[] x = sXYZ[0][0];
390       double[] y = sXYZ[0][1];
391       double[] z = sXYZ[0][2];
392       for (int iSymOp = 0; iSymOp < nSymm; iSymOp++) {
393         double[][] xyz = sXYZ[iSymOp];
394         for (int i = lb; i <= ub; i++) {
395           if (!nativeEnvironmentApproximation && !use[i]) {
396             continue;
397           }
398           final double integralStartI = max(baseRadius[i], descreenRadius[i]) + descreenOffset;
399           final double descreenRi = descreenRadius[i];
400           final double xi = x[i];
401           final double yi = y[i];
402           final double zi = z[i];
403           int[] list = neighborLists[iSymOp][i];
404           for (int k : list) {
405             final double integralStartK = max(baseRadius[k], descreenRadius[k]) + descreenOffset;
406             final double descreenRk = descreenRadius[k];
407             if (!nativeEnvironmentApproximation && !use[k]) {
408               continue;
409             }
410 
411             // No necks will be computed unless the overlapScale is greater than 0.0 (e.g., for hydrogen).
412             double mixedNeckScale = 0.5 * (neckScale[i] + neckScale[k]);
413 
414             if (i != k) {
415               final double xr = xyz[0][k] - xi;
416               final double yr = xyz[1][k] - yi;
417               final double zr = xyz[2][k] - zi;
418               final double r2 = crystal.image(xr, yr, zr);
419               if (r2 > cut2) {
420                 continue;
421               }
422               final double r = sqrt(r2);
423               // Atom i being descreeened by atom k.
424               double sk = overlapScale[k];
425               // Non-descreening atoms (such as hydrogen) will have an sk of 0.0
426               if (sk > 0.0) {
427                 double descreenIK = descreen(r, r2, integralStartI, descreenRk, sk);
428                 localBorn[i] += descreenIK;
429                 if (neckCorrection) {
430                   localBorn[i] += neckDescreen(r, integralStartI, descreenRk, mixedNeckScale);
431                 }
432               }
433 
434               // Atom k being descreeened by atom i.
435               double si = overlapScale[i];
436               if (si > 0.0) {
437                 double descreenKI = descreen(r, r2, integralStartK, descreenRi, si);
438                 localBorn[k] += descreenKI;
439                 if (neckCorrection) {
440                   localBorn[k] += neckDescreen(r, integralStartK, descreenRi, mixedNeckScale);
441                 }
442               }
443 
444             } else if (iSymOp > 0) {
445               final double xr = xyz[0][k] - xi;
446               final double yr = xyz[1][k] - yi;
447               final double zr = xyz[2][k] - zi;
448               final double r2 = crystal.image(xr, yr, zr);
449               if (r2 > cut2) {
450                 continue;
451               }
452               final double r = sqrt(r2);
453               // Atom i being descreeened by atom k.
454               double sk = overlapScale[k];
455               if (sk > 0.0) {
456                 localBorn[i] += descreen(r, r2, integralStartI, descreenRk, sk);
457                 if (neckCorrection) {
458                   localBorn[i] += neckDescreen(r, integralStartI, descreenRk, mixedNeckScale);
459                 }
460               }
461               // For symmetry mates, atom k is not descreeened by atom i.
462             }
463           }
464         }
465       }
466     }
467 
468     @Override
469     public void start() {
470       int nAtoms = atoms.length;
471       if (localBorn == null || localBorn.length < nAtoms) {
472         localBorn = new double[nAtoms];
473       }
474       fill(localBorn, 0.0);
475     }
476 
477     /**
478      * Compute the integral of 1/r^6 over a neck region.
479      *
480      * @param r atomic separation.
481      * @param radius base radius of the atom being descreened.
482      * @param radiusK radius of the atom doing the descreening.
483      * @param sneck Sneck scaling factor, scaled based on number of bound non-hydrogen atoms.
484      * @return this contribution to the descreening integral.
485      */
486     private double neckDescreen(double r, double radius, double radiusK, double sneck) {
487       double radiusWater = 1.4;
488 
489       // If atoms are too widely separated there is no neck formed.
490       if (r > radius + radiusK + 2.0 * radiusWater) {
491         return 0.0;
492       }
493 
494       // Get Aij and Bij based on parameterization by Corrigan et al.
495       double[] constants = getNeckConstants(radius, radiusK);
496 
497       double Aij = constants[0];
498       double Bij = constants[1];
499       //logger.info(format("rhoi %2.4f rhoj %2.4f separation %2.4f Aij %2.10f Bij %2.2f",radius,radiusK,r,Aij,Bij));
500 
501       double rMinusBij = r - Bij;
502       double radiiMinusr = radius + radiusK + 2.0 * radiusWater - r;
503       double power1 = rMinusBij * rMinusBij * rMinusBij * rMinusBij;
504       double power2 = radiiMinusr * radiiMinusr * radiiMinusr * radiiMinusr;
505 
506       // Use Aij and Bij to get neck integral using Equations 13 and 14 from Aguilar/Onufriev 2010 paper
507       // Sneck may be based on the number of heavy atoms bound to the atom being descreened.
508       double neckIntegral = PI4_3 * sneck * Aij * power1 * power2;
509 
510       return -neckIntegral;
511     }
512 
513     private double descreen(double r, double r2, double radius, double radiusK, double hctScale) {
514       if (perfectHCTScale) {
515         return perfectHCTIntegral(r, r2, radius, radiusK, hctScale);
516       } else {
517         return integral(r, r2, radius, radiusK * hctScale);
518       }
519     }
520 
521     /**
522      * Use pairwise descreening to compute integral of 1/r^6.
523      *
524      * @param r atomic separation.
525      * @param r2 atomic separation squared.
526      * @param radius base radius of the atom being descreened.
527      * @param scaledRadius scaled radius of the atom doing the descreening.
528      * @return this contribution to the descreening integral.
529      */
530     private double integral(double r, double r2, double radius, double scaledRadius) {
531       double integral = 0.0;
532       // Descreen only if the scaledRadius is greater than zero.
533       // and atom I does not engulf atom K.
534       if (scaledRadius > 0.0 && (radius < r + scaledRadius)) {
535         // Atom i is engulfed by atom k.
536         if (radius + r < scaledRadius) {
537           final double upper = scaledRadius - r;
538           integral = (PI4_3 * (1.0 / (upper * upper * upper) - 1.0 / (radius * radius * radius)));
539         }
540 
541         // Upper integration bound is always the same.
542         double upper = r + scaledRadius;
543 
544         // Lower integration bound depends on atoms sizes and separation.
545         double lower;
546         if (radius + r < scaledRadius) {
547           // Atom i is engulfed by atom k.
548           lower = scaledRadius - r;
549         } else if (r < radius + scaledRadius) {
550           // Atoms are overlapped, begin integration from ri.
551           lower = radius;
552         } else {
553           // No overlap between atoms.
554           lower = r - scaledRadius;
555         }
556 
557         double l2 = lower * lower;
558         double l4 = l2 * l2;
559         double lr = lower * r;
560         double l4r = l4 * r;
561         double u2 = upper * upper;
562         double u4 = u2 * u2;
563         double ur = upper * r;
564         double u4r = u4 * r;
565         double scaledRk2 = scaledRadius * scaledRadius;
566         double term =
567             (3.0 * (r2 - scaledRk2) + 6.0 * u2 - 8.0 * ur) / u4r
568                 - (3.0 * (r2 - scaledRk2) + 6.0 * l2 - 8.0 * lr) / l4r;
569         integral -= PI_12 * term;
570       }
571       return integral;
572     }
573 
574     private double perfectHCTIntegral(double r, double r2,
575         double radius, double radiusK, double perfectHCT) {
576       double integral = 0.0;
577       // Descreen only if the scaledRadius is greater than zero.
578       // and atom I does not engulf atom K.
579       if (radiusK > 0.0 && (radius < r + radiusK)) {
580         // Atom i is engulfed by atom k.
581         // TODO: fix double counting of overlaps
582         if (radius + r < radiusK) {
583           final double upper = radiusK - r;
584           integral = (PI4_3 * (1.0 / (upper * upper * upper) - 1.0 / (radius * radius * radius)));
585         }
586 
587         // Upper integration bound is always the same.
588         double upper = r + radiusK;
589 
590         // Lower integration bound depends on atoms sizes and separation.
591         double lower;
592         if (radius + r < radiusK) {
593           // Atom i is engulfed by atom k.
594           lower = radiusK - r;
595         } else if (r < radius + radiusK) {
596           // Atoms are overlapped, begin integration from ri.
597           // TODO: fix double counting of the overlap
598           lower = radius;
599         } else {
600           // No overlap between atoms.
601           lower = r - radiusK;
602         }
603 
604         double l2 = lower * lower;
605         double l4 = l2 * l2;
606         double lr = lower * r;
607         double l4r = l4 * r;
608         double u2 = upper * upper;
609         double u4 = u2 * u2;
610         double ur = upper * r;
611         double u4r = u4 * r;
612         double scaledK2 = radiusK * radiusK;
613         double term =
614             (3.0 * (r2 - scaledK2) + 6.0 * u2 - 8.0 * ur) / u4r
615                 - (3.0 * (r2 - scaledK2) + 6.0 * l2 - 8.0 * lr) / l4r;
616         integral -= PI_12 * term;
617       }
618 
619       return perfectHCT * integral;
620     }
621   }
622 }