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 edu.rit.pj.IntegerForLoop;
41  import edu.rit.pj.ParallelRegion;
42  import edu.rit.pj.ParallelTeam;
43  import ffx.crystal.Crystal;
44  import ffx.crystal.SymOp;
45  import ffx.numerics.atomic.AtomicDoubleArray;
46  import ffx.numerics.atomic.AtomicDoubleArray3D;
47  import ffx.potential.bonded.Atom;
48  import ffx.potential.utils.EnergyException;
49  
50  import java.util.logging.Level;
51  import java.util.logging.Logger;
52  
53  import static ffx.potential.nonbonded.implicit.BornTanhRescaling.tanhRescalingChainRule;
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 org.apache.commons.math3.util.FastMath.PI;
59  import static org.apache.commons.math3.util.FastMath.max;
60  import static org.apache.commons.math3.util.FastMath.pow;
61  import static org.apache.commons.math3.util.FastMath.sqrt;
62  
63  /**
64   * Parallel computation of Born radii chain rule terms via the Grycuk method.
65   *
66   * @author Michael J. Schnieders
67   * @since 1.0
68   */
69  public class BornGradRegion extends ParallelRegion {
70  
71    private static final Logger logger = Logger.getLogger(BornRadiiRegion.class.getName());
72    private static final double PI4_3 = 4.0 / 3.0 * PI;
73    /**
74     * Constant factor used with quadrupoles.
75     */
76    private static final double oneThird = 1.0 / 3.0;
77  
78    private final BornCRLoop[] bornCRLoop;
79    /**
80     * An ordered array of atoms in the system.
81     */
82    protected Atom[] atoms;
83    /**
84     * Periodic boundary conditions and symmetry.
85     */
86    private Crystal crystal;
87    /**
88     * Atomic coordinates for each symmetry operator.
89     */
90    private double[][][] sXYZ;
91    /**
92     * Neighbor lists for each atom and symmetry operator.
93     */
94    private int[][][] neighborLists;
95    /**
96     * Base radius of each atom.
97     */
98    private double[] baseRadius;
99    /**
100    * Descreen radius of each atom.
101    */
102   private double[] descreenRadius;
103   /**
104    * Overlap scale factor for each atom, when using the Hawkins, Cramer & Truhlar pairwise
105    * descreening algorithm.
106    *
107    * <p>G. D. Hawkins, C. J. Cramer and D. G. Truhlar, "Parametrized Models of Aqueous Free Energies
108    * of Solvation Based on Pairwise Descreening of Solute Atomic Charges from a Dielectric Medium",
109    * J. Phys. Chem., 100, 19824-19839 (1996).
110    */
111   private double[] overlapScale;
112   /**
113    * Sneck scaling parameter for each atom. Set based on maximum Sneck scaling parameter and number
114    * of bound non-hydrogen atoms
115    */
116   private double[] neckScale;
117   private double descreenOffset;
118   /**
119    * If true, the descreening integral includes overlaps with the volume of the descreened atom
120    */
121   private final boolean perfectHCTScale;
122   /**
123    * Flag to indicate if an atom should be included.
124    */
125   private boolean[] use;
126   /**
127    * GK cut-off distance squared.
128    */
129   private double cut2;
130   /**
131    * Forces all atoms to be considered during Born radius updates.
132    */
133   private boolean nativeEnvironmentApproximation;
134   /**
135    * Born radius of each atom.
136    */
137   private double[] born;
138   /**
139    * Gradient array for each thread.
140    */
141   private AtomicDoubleArray3D grad;
142   /**
143    * Shared array for computation of Born radii gradient.
144    */
145   private AtomicDoubleArray sharedBornGrad;
146   private final double factor = -pow(PI, oneThird) * pow(6.0, (2.0 * oneThird)) / 9.0;
147   private double[] term;
148   /**
149    * Flag for using neck correction, default to false for now.
150    */
151   private final boolean neckCorrection;
152   /**
153    * Flag for using tanh correction, default to false for now.
154    */
155   private final boolean tanhCorrection;
156   /**
157    * This is the Born Integral prior to rescaling with a tanh function, which is a quantity needed
158    * for the computing the derivative of the energy with respect to atomic coordinates.
159    */
160   private double[] unscaledBornIntegral;
161 
162   /**
163    * Compute the gradient due to changes in Born radii.
164    *
165    * @param nt Number of threads.
166    * @param neckCorrection Include a neck correction.
167    * @param tanhCorrection Include a tanh correction.
168    * @param perfectHCTScale Use "perfect" HCT scale factors.
169    */
170   public BornGradRegion(int nt, boolean neckCorrection,
171       boolean tanhCorrection, boolean perfectHCTScale) {
172     bornCRLoop = new BornCRLoop[nt];
173     for (int i = 0; i < nt; i++) {
174       bornCRLoop[i] = new BornCRLoop();
175     }
176     this.neckCorrection = neckCorrection;
177     this.tanhCorrection = tanhCorrection;
178     this.perfectHCTScale = perfectHCTScale;
179   }
180 
181   /**
182    * Execute the InitializationRegion with the passed ParallelTeam.
183    *
184    * @param parallelTeam The ParallelTeam instance to execute with.
185    */
186   public void executeWith(ParallelTeam parallelTeam) {
187     sharedBornGrad.reduce(parallelTeam, 0, atoms.length - 1);
188     try {
189       parallelTeam.execute(this);
190     } catch (Exception e) {
191       String message = " Exception evaluating Born radii chain rule gradient.\n";
192       logger.log(Level.SEVERE, message, e);
193     }
194   }
195 
196   public void init(
197       Atom[] atoms,
198       Crystal crystal,
199       double[][][] sXYZ,
200       int[][][] neighborLists,
201       double[] baseRadius,
202       double[] descreenRadius,
203       double[] overlapScale,
204       double[] neckScale,
205       double descreenOffset,
206       double[] unscaledBornIntegral,
207       boolean[] use,
208       double cut2,
209       boolean nativeEnvironmentApproximation,
210       double[] born,
211       AtomicDoubleArray3D grad,
212       AtomicDoubleArray sharedBornGrad) {
213     this.atoms = atoms;
214     this.crystal = crystal;
215     this.sXYZ = sXYZ;
216     this.neighborLists = neighborLists;
217     this.baseRadius = baseRadius;
218     this.descreenRadius = descreenRadius;
219     this.overlapScale = overlapScale;
220     this.neckScale = neckScale;
221     this.descreenOffset = descreenOffset;
222     this.unscaledBornIntegral = unscaledBornIntegral;
223     this.use = use;
224     this.cut2 = cut2;
225     this.nativeEnvironmentApproximation = nativeEnvironmentApproximation;
226     this.born = born;
227     this.grad = grad;
228     this.sharedBornGrad = sharedBornGrad;
229   }
230 
231   @Override
232   public void start() {
233     int nAtoms = atoms.length;
234     if (term == null || term.length < nAtoms) {
235       term = new double[nAtoms];
236     }
237 
238     // The "term" array, initialized here, provides the Grycuk method chain rule term for each atom in BornGradRegion
239     // The base Grycuk correction sets the Born radius equal to (3/(4*PI) * integral(1/r^6))^(-1/3)
240     for (int i = 0; i < nAtoms; i++) {
241       double rbi = born[i];
242       term[i] = PI4_3 / (rbi * rbi * rbi);
243       term[i] = factor / pow(term[i], (4.0 * oneThird));
244       if (tanhCorrection) {
245         term[i] =
246             term[i] * tanhRescalingChainRule(unscaledBornIntegral[i], baseRadius[i]);
247       }
248     }
249   }
250 
251   @Override
252   public void run() {
253     try {
254       int nAtoms = atoms.length;
255       execute(0, nAtoms - 1, bornCRLoop[getThreadIndex()]);
256     } catch (Exception e) {
257       String message = "Fatal exception computing Born radii chain rule term in thread "
258               + getThreadIndex() + "\n";
259       logger.log(Level.SEVERE, message, e);
260     }
261   }
262 
263   /**
264    * Compute Born radii chain rule terms for a range of atoms via the Grycuk method.
265    *
266    * @since 1.0
267    */
268   private class BornCRLoop extends IntegerForLoop {
269 
270     private final double[] dx_local;
271     private int threadID;
272 
273     BornCRLoop() {
274       dx_local = new double[3];
275     }
276 
277     @Override
278     public void run(int lb, int ub) {
279 
280       double[] x = sXYZ[0][0];
281       double[] y = sXYZ[0][1];
282       double[] z = sXYZ[0][2];
283 
284       int nSymm = crystal.spaceGroup.symOps.size();
285       for (int iSymOp = 0; iSymOp < nSymm; iSymOp++) {
286         SymOp symOp = crystal.spaceGroup.symOps.get(iSymOp);
287         double[][] transOp = new double[3][3];
288         double[][] xyz = sXYZ[iSymOp];
289         crystal.getTransformationOperator(symOp, transOp);
290         for (int i = lb; i <= ub; i++) {
291           if (!nativeEnvironmentApproximation && !use[i]) {
292             continue;
293           }
294 
295           // Check the value of the Born radii chain rule term.
296           double bornGrad = sharedBornGrad.get(i);
297           if (isInfinite(bornGrad) || isNaN(bornGrad)) {
298             throw new EnergyException(format(" %s\n Born radii CR %d %8.3f", atoms[i], i, bornGrad), true);
299           }
300           final double integralStartI = max(baseRadius[i], descreenRadius[i]) + descreenOffset;
301           final double descreenRi = descreenRadius[i];
302           final double xi = x[i];
303           final double yi = y[i];
304           final double zi = z[i];
305           final double rbi = born[i];
306           int[] list = neighborLists[iSymOp][i];
307           for (int k : list) {
308             if (!nativeEnvironmentApproximation && !use[k]) {
309               continue;
310             }
311             final double integralStartK = max(baseRadius[k], descreenRadius[k]) + descreenOffset;
312             final double descreenRk = descreenRadius[k];
313             double mixedNeckScale = 0.5 * (neckScale[i] + neckScale[k]);
314 
315             if (k != i) {
316               dx_local[0] = xyz[0][k] - xi;
317               dx_local[1] = xyz[1][k] - yi;
318               dx_local[2] = xyz[2][k] - zi;
319               double r2 = crystal.image(dx_local);
320               if (r2 > cut2) {
321                 continue;
322               }
323               final double xr = dx_local[0];
324               final double yr = dx_local[1];
325               final double zr = dx_local[2];
326               final double r = sqrt(r2);
327 
328               // Atom i being descreeened by atom k.
329               double sk = overlapScale[k];
330               if (sk > 0.0 && rbi < 50.0 && descreenRk > 0.0) {
331                 double de = descreenDerivative(r, r2, integralStartI, descreenRk, sk);
332                 if (neckCorrection) {
333                   de += neckDescreenDerivative(r, integralStartI, descreenRk, mixedNeckScale);
334                 }
335                 if (isInfinite(de) || isNaN(de)) {
336                   logger.warning(
337                       format(" Born radii chain rule term is unstable %d %d %16.8f", i, k, de));
338                 }
339                 double dbr = term[i] * de / r;
340                 de = dbr * sharedBornGrad.get(i);
341                 incrementGradient(i, k, de, xr, yr, zr, transOp);
342               }
343 
344               // Atom k being descreeened by atom i.
345               double rbk = born[k];
346               double si = overlapScale[i];
347               if (si > 0.0 && rbk < 50.0 && descreenRi > 0.0) {
348                 double de = descreenDerivative(r, r2, integralStartK, descreenRi, si);
349                 if (neckCorrection) {
350                   de += neckDescreenDerivative(r, integralStartK, descreenRi, mixedNeckScale);
351                 }
352                 if (isInfinite(de) || isNaN(de)) {
353                   logger.warning(
354                       format(" Born radii chain rule term is unstable %d %d %16.8f", k, i, de));
355                 }
356                 double dbr = term[k] * de / r;
357                 de = dbr * sharedBornGrad.get(k);
358                 incrementGradient(i, k, de, xr, yr, zr, transOp);
359               }
360             } else if (iSymOp > 0 && rbi < 50.0) {
361               dx_local[0] = xyz[0][k] - xi;
362               dx_local[1] = xyz[1][k] - yi;
363               dx_local[2] = xyz[2][k] - zi;
364               double r2 = crystal.image(dx_local);
365               double sk = overlapScale[k];
366               if (sk > 0.0 && r2 < cut2 && descreenRk > 0.0) {
367                 final double xr = dx_local[0];
368                 final double yr = dx_local[1];
369                 final double zr = dx_local[2];
370                 final double r = sqrt(r2);
371                 // Atom i being descreeened by atom k.
372                 double de = descreenDerivative(r, r2, integralStartI, descreenRk, sk);
373                 if (neckCorrection) {
374                   de += neckDescreenDerivative(r, integralStartI, descreenRk, mixedNeckScale);
375                 }
376                 if (isInfinite(de) || isNaN(de)) {
377                   logger.warning(
378                       format(" Born radii chain rule term is unstable %d %d %d %16.8f", iSymOp, i,
379                           k, de));
380                 }
381                 double dbr = term[i] * de / r;
382                 de = dbr * sharedBornGrad.get(i);
383                 incrementGradient(i, k, de, xr, yr, zr, transOp);
384                 // For symmetry mates, atom k is not descreeened by atom i.
385               }
386             }
387           }
388         }
389       }
390     }
391 
392     @Override
393     public void start() {
394       threadID = getThreadIndex();
395     }
396 
397     private double neckDescreenDerivative(double r, double radius, double radiusK, double sneck) {
398       double radiusWater = 1.4;
399 
400       if (r > radius + radiusK + 2 * radiusWater) {
401         return 0.0;
402       }
403 
404       // Get Aij and Bij
405       double[] constants = getNeckConstants(radius, radiusK);
406       //logger.info(format("Grad Inputs: Ri %2.4f Rk %2.4f\nGrad Outputs: Aij %2.4f Bij %2.4f",radius,radiusK,constants[0],constants[1]));
407 
408       double Aij = constants[0];
409       double Bij = constants[1];
410 
411       // Use Aij and Bij to get neck value using derivative of Equation 13 from Aguilar/Onufriev 2010 paper
412       double rMinusBij = r - Bij;
413       double rMinusBij3 = rMinusBij * rMinusBij * rMinusBij;
414       double rMinusBij4 = rMinusBij3 * rMinusBij;
415       double radiiMinusr = radius + radiusK + 2.0 * radiusWater - r;
416       double radiiMinusr3 = radiiMinusr * radiiMinusr * radiiMinusr;
417       double radiiMinusr4 = radiiMinusr3 * radiiMinusr;
418 
419       return 4.0 * PI4_3 * (sneck * Aij * rMinusBij3 * radiiMinusr4
420           - sneck * Aij * rMinusBij4 * radiiMinusr3);
421     }
422 
423     private double descreenDerivative(double r, double r2, double radius, double radiusK,
424         double hctScale) {
425       if (perfectHCTScale) {
426         return perfectHCTIntegralDerivative(r, r2, radius, radiusK, hctScale);
427       } else {
428         return integralDerivative(r, r2, radius, radiusK * hctScale);
429       }
430     }
431 
432     /**
433      * Use pairwise descreening to compute derivative of the integral of 1/r^6 with respect to r.
434      *
435      * @param r separation distance.
436      * @param r2 separation distance squared.
437      * @param radius base radius of descreened atom.
438      * @param scaledRadius scaled radius descreening atom.
439      * @return the derivative.
440      */
441     private double integralDerivative(double r, double r2, double radius, double scaledRadius) {
442       double de = 0.0;
443       // Descreen only if the scaledRadius is greater than zero.
444       // and atom I does not engulf atom K.
445       if (scaledRadius > 0.0 && (radius < r + scaledRadius)) {
446         // Atom i is engulfed by atom k.
447         if (radius + r < scaledRadius) {
448           double uik = scaledRadius - r;
449           double uik2 = uik * uik;
450           double uik4 = uik2 * uik2;
451           de = -4.0 * PI / uik4;
452         }
453 
454         // Lower integration bound depends on atoms sizes and separation.
455         double sk2 = scaledRadius * scaledRadius;
456         if (radius + r < scaledRadius) {
457           // Atom i is engulfed by atom k.
458           double lik = scaledRadius - r;
459           double lik2 = lik * lik;
460           double lik4 = lik2 * lik2;
461           de = de + 0.25 * PI * (sk2 - 4.0 * scaledRadius * r + 17.0 * r2) / (r2 * lik4);
462         } else if (r < radius + scaledRadius) {
463           // Atoms are overlapped, begin integration from ri.
464           double lik = radius;
465           double lik2 = lik * lik;
466           double lik4 = lik2 * lik2;
467           de = de + 0.25 * PI * (2.0 * radius * radius - sk2 - r2) / (r2 * lik4);
468         } else {
469           // No overlap between atoms.
470           double lik = r - scaledRadius;
471           double lik2 = lik * lik;
472           double lik4 = lik2 * lik2;
473           de = de + 0.25 * PI * (sk2 - 4.0 * scaledRadius * r + r2) / (r2 * lik4);
474         }
475 
476         // Upper integration bound is always the same.
477         double uik = r + scaledRadius;
478         double uik2 = uik * uik;
479         double uik4 = uik2 * uik2;
480         de = de - 0.25 * PI * (sk2 + 4.0 * scaledRadius * r + r2) / (r2 * uik4);
481       }
482       return de;
483     }
484 
485     /**
486      * Use pairwise descreening to compute derivative of the integral of 1/r^6 with respect to r.
487      *
488      * @param r separation distance.
489      * @param r2 separation distance squared.
490      * @param radius base radius of descreened atom.
491      * @param radiusK base radius of descreening atom.
492      * @param perfectHCT perfect HCT scale factor.
493      * @return the derivative.
494      */
495     private double perfectHCTIntegralDerivative(double r, double r2, double radius, double radiusK,
496         double perfectHCT) {
497       double de = 0.0;
498       // Descreen only if the scaledRadius is greater than zero.
499       // and atom I does not engulf atom K.
500       if (radiusK > 0.0 && (radius < r + radiusK)) {
501         // Atom i is engulfed by atom k.
502         if (radius + r < radiusK) {
503           double uik = radiusK - r;
504           double uik2 = uik * uik;
505           double uik4 = uik2 * uik2;
506           de = -4.0 * PI / uik4;
507         }
508 
509         // Lower integration bound depends on atoms sizes and separation.
510         double sk2 = radiusK * radiusK;
511         if (radius + r < radiusK) {
512           // Atom i is engulfed by atom k.
513           double lik = radiusK - r;
514           double lik2 = lik * lik;
515           double lik4 = lik2 * lik2;
516           de = de + 0.25 * PI * (sk2 - 4.0 * radiusK * r + 17.0 * r2) / (r2 * lik4);
517         } else if (r < radius + radiusK) {
518           // Atoms are overlapped, begin integration from ri.
519           double lik = radius;
520           double lik2 = lik * lik;
521           double lik4 = lik2 * lik2;
522           de = de + 0.25 * PI * (2.0 * radius * radius - sk2 - r2) / (r2 * lik4);
523         } else {
524           // No overlap between atoms.
525           double lik = r - radiusK;
526           double lik2 = lik * lik;
527           double lik4 = lik2 * lik2;
528           de = de + 0.25 * PI * (sk2 - 4.0 * radiusK * r + r2) / (r2 * lik4);
529         }
530 
531         // Upper integration bound is always the same.
532         double uik = r + radiusK;
533         double uik2 = uik * uik;
534         double uik4 = uik2 * uik2;
535         de = de - 0.25 * PI * (sk2 + 4.0 * radiusK * r + r2) / (r2 * uik4);
536       }
537       return perfectHCT * de;
538     }
539 
540     /**
541      * Accumulate a contribution to the gradient and dU/dX/dL.
542      *
543      * @param i index of atom i.
544      * @param k index of atom k.
545      * @param dE partial derivative of the energy with respect to R.
546      * @param xr x-component of the separation vector.
547      * @param yr y-component of the separation vector.
548      * @param zr z-component of the separation vector.
549      */
550     private void incrementGradient(
551         int i, int k, double dE, double xr, double yr, double zr, double[][] transOp) {
552       double dedx = dE * xr;
553       double dedy = dE * yr;
554       double dedz = dE * zr;
555       grad.add(threadID, i, dedx, dedy, dedz);
556       final double dedxk = dedx * transOp[0][0] + dedy * transOp[1][0] + dedz * transOp[2][0];
557       final double dedyk = dedx * transOp[0][1] + dedy * transOp[1][1] + dedz * transOp[2][1];
558       final double dedzk = dedx * transOp[0][2] + dedy * transOp[1][2] + dedz * transOp[2][2];
559       grad.sub(threadID, k, dedxk, dedyk, dedzk);
560     }
561   }
562 }