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.commands.test;
39  
40  import ffx.numerics.Potential;
41  import ffx.potential.ForceFieldEnergy;
42  import ffx.potential.MolecularAssembly;
43  import ffx.potential.bonded.LambdaInterface;
44  import ffx.potential.cli.AlchemicalOptions;
45  import ffx.potential.cli.GradientOptions;
46  import ffx.potential.cli.PotentialCommand;
47  import ffx.potential.cli.TopologyOptions;
48  import ffx.potential.nonbonded.ParticleMeshEwald;
49  import ffx.potential.nonbonded.pme.AlchemicalParameters;
50  import ffx.potential.openmm.OpenMMEnergy;
51  import ffx.utilities.FFXBinding;
52  import picocli.CommandLine.Command;
53  import picocli.CommandLine.Mixin;
54  import picocli.CommandLine.Option;
55  import picocli.CommandLine.Parameters;
56  
57  import java.util.ArrayList;
58  import java.util.Arrays;
59  import java.util.Collections;
60  import java.util.List;
61  
62  import static ffx.utilities.StringUtils.parseAtomRanges;
63  import static java.lang.String.format;
64  import static org.apache.commons.math3.util.FastMath.abs;
65  import static org.apache.commons.math3.util.FastMath.sqrt;
66  
67  /**
68   * The LambdaGradient script tests numeric gradients w.r.t. lambda against
69   * numeric, finite-difference gradients
70   * <br>
71   * Usage:
72   * <br>
73   * ffxc test.LambdaGradient [options] &lt;filename&gt; [file2...]
74   */
75  @Command(description = " Test potential energy derivatives with respect to Lambda.", name = "test.LambdaGradient")
76  public class LambdaGradient extends PotentialCommand {
77  
78    @Mixin
79    AlchemicalOptions alchemicalOptions;
80  
81    @Mixin
82    GradientOptions gradientOptions;
83  
84    @Mixin
85    TopologyOptions topologyOptions;
86  
87    /**
88     * --ls or --lambdaScan Scan lambda values.
89     */
90    @Option(names = {"--ls", "--lambdaScan"}, paramLabel = "false",
91        description = "Scan lambda values.")
92    boolean lambdaScan = false;
93  
94    /**
95     * --lm or --lambdaMoveSize Size of the lambda moves during the test.
96     */
97    @Option(names = {"--lm", "--lambdaMoveSize"}, paramLabel = "0.01",
98        description = "Size of the lambda moves during the test.")
99    double lambdaMoveSize = 0.01;
100 
101   /**
102    * --sk2 or --skip2 disables dU2/dL2, and dU2/dLdX tests.
103    */
104   @Option(names = {"--sk2", "--skip2"}, paramLabel = "false",
105       description = "Skip 2nd derivatives.")
106   boolean skipSecondDerivatives = false;
107 
108   /**
109    * -sdX or --skipdX disables the very long atomic gradient tests and
110    * only performs dU/dL, dU2/dL2, and dU2/dLdX tests. Useful if the
111    * script is being used to generate a dU/dL profile.
112    */
113   @Option(names = {"--sdX", "--skipdX"}, paramLabel = "false",
114       description = "Skip calculating per-atom dUdX values and only test lambda gradients.")
115   boolean skipAtomGradients = false;
116 
117   /**
118    * The final argument(s) should be one or more filenames.
119    */
120   @Parameters(arity = "1..*", paramLabel = "files", description = "The atomic coordinate files in PDB or XYZ format.")
121   List<String> filenames = null;
122 
123   MolecularAssembly[] topologies;
124   private Potential potential;
125 
126   public int ndEdLFailures = 0;
127   public int ndEdXFailures = 0;
128   public int nd2EdL2Failures = 0;
129   public int ndEdXdLFailures = 0;
130   public double e0 = 0.0;
131   public double e1 = 0.0;
132 
133   /**
134    * LambdaGradient Constructor
135    */
136   public LambdaGradient() {
137     super();
138   }
139 
140   /**
141    * LambdaGradient Constructor
142    * @param binding Binding to use.
143    */
144   public LambdaGradient(FFXBinding binding) {
145     super(binding);
146   }
147 
148   /**
149    * LambdaGradient constructor that sets the command line arguments.
150    * @param args Command line arguments.
151    */
152   public LambdaGradient(String[] args) {
153     super(args);
154   }
155 
156   /**
157    * Script run method.
158    */
159   @Override
160   public LambdaGradient run() {
161 
162     if (!init()) {
163       return this;
164     }
165 
166     // Determine the number of topologies to be read and allocate the array.
167     int numTopologies = topologyOptions.getNumberOfTopologies(filenames);
168     int threadsPerTopology = topologyOptions.getThreadsPerTopology(numTopologies);
169     topologies = new MolecularAssembly[numTopologies];
170 
171     // Turn on computation of lambda dependent properties.
172     alchemicalOptions.setAlchemicalProperties();
173     topologyOptions.setAlchemicalProperties(numTopologies);
174 
175     // Read in files.
176     if (filenames == null || filenames.isEmpty()) {
177       activeAssembly = getActiveAssembly(null);
178       if (activeAssembly == null) {
179         logger.info(helpString());
180         return this;
181       }
182       filenames = new ArrayList<>();
183       filenames.add(activeAssembly.getFile().getName());
184       topologies[0] = alchemicalOptions.processFile(topologyOptions, activeAssembly, 0);
185     } else {
186       logger.info(format(" Initializing %d topologies...", numTopologies));
187       for (int i = 0; i < numTopologies; i++) {
188         topologies[i] = alchemicalOptions.openFile(potentialFunctions, topologyOptions,
189             threadsPerTopology, filenames.get(i), i);
190       }
191     }
192 
193     // Configure the potential to test.
194     StringBuilder sb = new StringBuilder("\n Testing lambda derivatives for ");
195     potential = topologyOptions.assemblePotential(topologies, sb);
196     logger.info(sb.toString());
197 
198     // Detect the alchemical mode.
199     AlchemicalParameters.AlchemicalMode mode = AlchemicalParameters.AlchemicalMode.OST;
200     for (MolecularAssembly assembly : topologies) {
201       ForceFieldEnergy energy = assembly.getPotentialEnergy();
202       ParticleMeshEwald pme = energy.getPmeNode();
203       if (pme == null) {
204         continue;
205       }
206       AlchemicalParameters alchemicalParameters = pme.getAlchemicalParameters();
207       if (alchemicalParameters.mode != AlchemicalParameters.AlchemicalMode.OST) {
208         mode = AlchemicalParameters.AlchemicalMode.SCALE;
209       }
210     }
211 
212     boolean skipLambdaDerivatives = false;
213     if (mode == AlchemicalParameters.AlchemicalMode.SCALE) {
214       skipLambdaDerivatives = true;
215     }
216     if (potential instanceof OpenMMEnergy || mode == AlchemicalParameters.AlchemicalMode.SCALE) {
217       skipSecondDerivatives = true;
218     }
219 
220     LambdaInterface lambdaInterface = (LambdaInterface) potential;
221 
222     // Reset the number of variables for the case of dual topology.
223     int n = potential.getNumberOfVariables();
224     double[] x = new double[n];
225     double[] gradient = new double[n];
226     double[] lambdaGrad = new double[n];
227     double[][] lambdaGradFD = new double[2][n];
228 
229     // Number of active atoms.
230     assert (n % 3 == 0);
231     int nAtoms = n / 3;
232 
233     // Compute the Lambda = 0.0 energy.
234     double lambda = 0.0;
235     lambdaInterface.setLambda(lambda);
236     potential.getCoordinates(x);
237 
238     e0 = potential.energyAndGradient(x, gradient);
239 
240     if (!skipLambdaDerivatives) {
241       double dEdL = lambdaInterface.getdEdL();
242       logger.info(format(" L=%4.2f E=%12.6f dE/dL=%12.6f", lambda, e0, dEdL));
243     } else {
244       logger.info(format(" L=%4.2f E=%12.6f", lambda, e0));
245     }
246 
247     // Scan intermediate lambda values.
248     if (lambdaScan) {
249       for (int i = 1; i <= 9; i++) {
250         lambda = i * 0.1;
251         lambdaInterface.setLambda(lambda);
252         double e = potential.energyAndGradient(x, gradient);
253         if (!skipLambdaDerivatives) {
254           double dEdL = lambdaInterface.getdEdL();
255           logger.info(format(" L=%4.2f E=%12.6f dE/dL=%12.6f", lambda, e, dEdL));
256         } else {
257           logger.info(format(" L=%4.2f E=%12.6f", lambda, e));
258         }
259 
260       }
261     }
262 
263     // Compute the Lambda = 1.0 energy.
264     lambda = 1.0;
265     lambdaInterface.setLambda(lambda);
266     e1 = potential.energyAndGradient(x, gradient);
267     if (!skipLambdaDerivatives) {
268       double dEdL = lambdaInterface.getdEdL();
269       logger.info(format(" L=%4.2f E=%12.6f dE/dL=%12.6f", lambda, e1, dEdL));
270     } else {
271       logger.info(format(" L=%4.2f E=%12.6f", lambda, e1));
272     }
273 
274     logger.info(format(" E(1)-E(0): %12.6f.\n", e1 - e0));
275 
276     // Finite-difference step size.
277     double step = gradientOptions.getDx();
278     double width = 2.0 * step;
279     logger.info(" Finite-difference step size:\t" + step);
280 
281     // Print out the energy for each step.
282     boolean print = gradientOptions.getVerbose();
283     logger.info(" Verbose printing:\t\t" + print + "\n");
284 
285     // Error tolerence
286     double errTol = gradientOptions.getTolerance();
287 
288     // Upper bound for typical gradient sizes (expected gradient)
289     double expGrad = 1000.0;
290 
291     // Test Lambda gradient in the neighborhood of the lambda variable.
292     if (!skipLambdaDerivatives) {
293 
294       for (int j = 0; j < 3; j++) {
295         // Loop-local counter for printout.
296         int jd2EdXdLFailures = 0;
297 
298         lambda = alchemicalOptions.getInitialLambda() - lambdaMoveSize + lambdaMoveSize * j;
299 
300         if (lambda - gradientOptions.getDx() < 0.0 || lambda + gradientOptions.getDx() > 1.0) {
301           continue;
302         }
303 
304         logger.info(format(" Current lambda value %6.4f", lambda));
305         lambdaInterface.setLambda(lambda);
306 
307         // Calculate the energy, dE/dX, dE/dL, d2E/dL2 and dE/dL/dX
308         double e = potential.energyAndGradient(x, gradient);
309 
310         // Analytic dEdL, d2E/dL2 and dE/dL/dX
311         double dEdL = lambdaInterface.getdEdL();
312 
313         double d2EdL2 = lambdaInterface.getd2EdL2();
314         Arrays.fill(lambdaGrad, 0.0);
315         lambdaInterface.getdEdXdL(lambdaGrad);
316 
317         // Calculate the finite-difference dEdLambda, d2EdLambda2 and dEdLambdadX
318         lambdaInterface.setLambda(lambda + gradientOptions.getDx());
319         double lp = potential.energyAndGradient(x, lambdaGradFD[0]);
320         double dedlp = lambdaInterface.getdEdL();
321         lambdaInterface.setLambda(lambda - gradientOptions.getDx());
322         double lm = potential.energyAndGradient(x, lambdaGradFD[1]);
323         double dedlm = lambdaInterface.getdEdL();
324 
325         double dEdLFD = (lp - lm) / width;
326         double d2EdL2FD = (dedlp - dedlm) / width;
327 
328         double err = abs(dEdLFD - dEdL);
329         if (err < errTol) {
330           logger.info(format(" dE/dL passed:   %10.6f", err));
331         } else {
332           logger.info(format(" dE/dL failed: %10.6f", err));
333           ndEdLFailures++;
334         }
335         logger.info(format(" Numeric:   %15.8f", dEdLFD));
336         logger.info(format(" Analytic:  %15.8f", dEdL));
337 
338         if (!skipSecondDerivatives) {
339           err = abs(d2EdL2FD - d2EdL2);
340           if (err < errTol) {
341             logger.info(format(" d2E/dL2 passed: %10.6f", err));
342           } else {
343             logger.info(format(" d2E/dL2 failed: %10.6f", err));
344             nd2EdL2Failures++;
345           }
346           logger.info(format(" Numeric:   %15.8f", d2EdL2FD));
347           logger.info(format(" Analytic:  %15.8f", d2EdL2));
348 
349           double rmsError = 0;
350           for (int i = 0; i < nAtoms; i++) {
351             int ii = i * 3;
352             double dX = (lambdaGradFD[0][ii] - lambdaGradFD[1][ii]) / width;
353             double dXa = lambdaGrad[ii];
354             double eX = dX - dXa;
355             ii++;
356             double dY = (lambdaGradFD[0][ii] - lambdaGradFD[1][ii]) / width;
357             double dYa = lambdaGrad[ii];
358             double eY = dY - dYa;
359             ii++;
360             double dZ = (lambdaGradFD[0][ii] - lambdaGradFD[1][ii]) / width;
361             double dZa = lambdaGrad[ii];
362             double eZ = dZ - dZa;
363 
364             double error = eX * eX + eY * eY + eZ * eZ;
365             rmsError += error;
366             error = sqrt(error);
367             if (error < errTol) {
368               logger.fine(format(" dE/dX/dL for degree of freedom %d passed: %10.6f", i + 1, error));
369             } else {
370               logger.info(format(" dE/dX/dL for degree of freedom %d failed: %10.6f", i + 1, error));
371               logger.info(format(" Analytic: (%15.8f, %15.8f, %15.8f)", dXa, dYa, dZa));
372               logger.info(format(" Numeric:  (%15.8f, %15.8f, %15.8f)", dX, dY, dZ));
373               ndEdXdLFailures++;
374               jd2EdXdLFailures++;
375             }
376           }
377           rmsError = sqrt(rmsError / nAtoms);
378           if (ndEdXdLFailures == 0) {
379             logger.info(
380                 format(" dE/dX/dL passed for all degrees of freedom: RMS error %15.8f", rmsError));
381           } else {
382             logger.info(
383                 format(" dE/dX/dL failed for %d of %d atoms: RMS error %15.8f", jd2EdXdLFailures,
384                     nAtoms, rmsError));
385           }
386           logger.info("");
387         }
388       }
389     }
390 
391     lambdaInterface.setLambda(alchemicalOptions.getInitialLambda());
392     potential.getCoordinates(x);
393     potential.energyAndGradient(x, gradient, print);
394 
395     if (!skipAtomGradients) {
396       double[] numeric = new double[3];
397       double avLen = 0.0;
398       double avGrad = 0.0;
399 
400       // Collect atoms to test.
401       List<Integer> degreesOfFreedomToTest;
402       if (gradientOptions.getGradientAtoms().equalsIgnoreCase("NONE")) {
403         logger.info(" The gradient of no atoms will be evaluated.");
404         return this;
405       } else if (gradientOptions.getGradientAtoms().equalsIgnoreCase("ALL")) {
406         logger.info(" Checking gradient for all active atoms.\n");
407         degreesOfFreedomToTest = new ArrayList<>();
408         for (int i = 0; i < nAtoms; i++) {
409           degreesOfFreedomToTest.add(i);
410         }
411       } else {
412         degreesOfFreedomToTest =
413             parseAtomRanges(" Gradient atoms", gradientOptions.getGradientAtoms(), nAtoms);
414         logger.info(
415             " Checking gradient for active atoms in the range: " + gradientOptions.getGradientAtoms()
416                 +
417                 "\n");
418       }
419 
420       for (int i : degreesOfFreedomToTest) {
421         int i3 = i * 3;
422         int i0 = i3 + 0;
423         int i1 = i3 + 1;
424         int i2 = i3 + 2;
425 
426         // Find numeric dX
427         double orig = x[i0];
428         x[i0] = x[i0] + step;
429         double e = potential.energyAndGradient(x, lambdaGradFD[0], print);
430         x[i0] = orig - step;
431         e -= potential.energyAndGradient(x, lambdaGradFD[1], print);
432         x[i0] = orig;
433         numeric[0] = e / width;
434 
435         // Find numeric dY
436         orig = x[i1];
437         x[i1] = x[i1] + step;
438         e = potential.energyAndGradient(x, lambdaGradFD[0], print);
439         x[i1] = orig - step;
440         e -= potential.energyAndGradient(x, lambdaGradFD[1], print);
441         x[i1] = orig;
442         numeric[1] = e / width;
443 
444         // Find numeric dZ
445         orig = x[i2];
446         x[i2] = x[i2] + step;
447         e = potential.energyAndGradient(x, lambdaGradFD[0], print);
448         x[i2] = orig - step;
449         e -= potential.energyAndGradient(x, lambdaGradFD[1], print);
450         x[i2] = orig;
451         numeric[2] = e / width;
452 
453         double dx = gradient[i0] - numeric[0];
454         double dy = gradient[i1] - numeric[1];
455         double dz = gradient[i2] - numeric[2];
456         double len = dx * dx + dy * dy + dz * dz;
457         avLen += len;
458         len = Math.sqrt(len);
459 
460         double grad2 =
461             gradient[i0] * gradient[i0] + gradient[i1] * gradient[i1] + gradient[i2] * gradient[i2];
462         avGrad += grad2;
463 
464         if (len > errTol) {
465           logger.info(format(" Degree of freedom %d failed: %10.6f.", i + 1, len)
466               + format("\n Analytic: (%12.4f, %12.4f, %12.4f)\n", gradient[i0], gradient[i1],
467               gradient[i2])
468               + format(" Numeric:  (%12.4f, %12.4f, %12.4f)\n", numeric[0], numeric[1], numeric[2]));
469           ++ndEdXFailures;
470         } else {
471           logger.info(format(" Degree of freedom %d passed: %10.6f.", i + 1, len)
472               + format("\n Analytic: (%12.4f, %12.4f, %12.4f)\n", gradient[i0], gradient[i1],
473               gradient[i2])
474               + format(" Numeric:  (%12.4f, %12.4f, %12.4f)", numeric[0], numeric[1], numeric[2]));
475         }
476 
477         if (grad2 > expGrad) {
478           logger.info(
479               format(" Degree of freedom %d has an unusually large gradient: %10.6f", i + 1, grad2));
480         }
481         logger.info("\n");
482       }
483 
484       avLen = avLen / nAtoms;
485       avLen = sqrt(avLen);
486       if (avLen > errTol) {
487         logger.info(
488             format(" Test failure: RMSD from analytic solution is %10.6f > %10.6f", avLen, errTol));
489       } else {
490         logger.info(
491             format(" Test success: RMSD from analytic solution is %10.6f < %10.6f", avLen, errTol));
492       }
493       logger.info(format(" Number of atoms failing gradient test: %d", ndEdXFailures));
494 
495       avGrad = avGrad / nAtoms;
496       avGrad = sqrt(avGrad);
497       if (avGrad > expGrad) {
498         logger.info(format(" Unusually large RMS gradient: %10.6f > %10.6f", avGrad, expGrad));
499       } else {
500         logger.info(format(" RMS gradient: %10.6f", avGrad));
501       }
502     } else {
503       logger.info(" Skipping atomic dU/dX gradients.");
504     }
505 
506     return this;
507   }
508 
509   @Override
510   public List<Potential> getPotentials() {
511     if (potential == null) {
512       return Collections.emptyList();
513     } else {
514       return Collections.singletonList(potential);
515     }
516   }
517 }