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.algorithms.groovy;
39  
40  import static java.lang.String.format;
41  import static java.lang.System.arraycopy;
42  import static java.util.Arrays.copyOf;
43  import static org.apache.commons.io.FileUtils.copyFile;
44  import static org.junit.Assert.assertArrayEquals;
45  import static org.junit.Assert.assertEquals;
46  import static org.junit.Assert.assertNotEquals;
47  import static org.junit.Assert.assertNotNull;
48  import static org.junit.Assert.assertTrue;
49  
50  import ffx.algorithms.misc.AlgorithmsTest;
51  import ffx.algorithms.thermodynamics.OrthogonalSpaceTempering;
52  import ffx.algorithms.thermodynamics.OrthogonalSpaceTempering.Histogram;
53  import ffx.crystal.CrystalPotential;
54  import ffx.potential.bonded.LambdaInterface;
55  import java.io.File;
56  import java.io.IOException;
57  import java.util.ArrayList;
58  import java.util.Arrays;
59  import java.util.Collection;
60  import java.util.Collections;
61  import java.util.HashMap;
62  import java.util.List;
63  import java.util.Map;
64  import java.util.regex.Pattern;
65  import java.util.stream.Collectors;
66  import java.util.stream.IntStream;
67  import org.apache.commons.configuration2.Configuration;
68  import org.apache.commons.configuration2.MapConfiguration;
69  import org.apache.commons.io.FilenameUtils;
70  import org.junit.Test;
71  import org.junit.runner.RunWith;
72  import org.junit.runners.Parameterized;
73  
74  /**
75   * Tests the functionality of the OST algorithm in Force Field X, both by running a very simple and
76   * quick OST run, and setting up a number of systems, comparing output energies and gradients from
77   * the starting algorithmConfig.
78   *
79   * @author Michael J. Schnieders
80   * @author Jacob M. Litman
81   * @since 1.0
82   */
83  @RunWith(Parameterized.class)
84  public class ThermodynamicsTest extends AlgorithmsTest {
85  
86    // Enables behavior where it prints out instead of testing information.
87    private static final boolean debugMode = false;
88    /**
89     * Set of default options, such as "-d", "0.5" to specify a half-femtosecond timestep. Can be
90     * over-ridden in test constructors.
91     */
92    private static final Map<String, String> DEFAULT_OPTIONS;
93    /**
94     * Set of default properties, such as "ost-alwaystemper", "true" to use standard tempering scheme.
95     * Can be over-ridden in test constructors.
96     */
97    private static final Map<String, String> DEFAULT_PROPERTIES;
98    /**
99     * Set of default boolean options, such as "-y", false to specify "do not add the -y --synchronous
100    * flag". Can be over-ridden in test constructors. Maps a flag to whether it should be included by
101    * default.
102    */
103   private static final Map<String, Boolean> DEFAULT_FLAGS;
104   /**
105    * Default number of atomic gradients to test; pick the first N (3) if ffx.ci is false. Otherwise,
106    * use the whole array to test.
107    */
108   private static final int DEFAULT_GRADIENT_EVALS = 3;
109   // Currently, everything is just 1.0E-2 kcal/mol per unit.
110   private static final double DEFAULT_PE_TOL = 1.0E-2; // kcal/mol.
111   private static final double DEFAULT_DUDL_TOL = 1.0E-2; // per lambda (unit-less).
112   private static final double DEFAULT_D2UDL2_TOL = 1.0E-2; // per lambda^2 (unit-less).
113   private static final double DEFAULT_DUDX_TOL = 1.0E-2; // kcal/mol/A.
114   private static final double DEFAULT_D2UDXDL_TOL = 1.0E-2; // kcal/mol/A.
115 
116   static {
117     String[] opts = {"--bM", "0.05", "--dw", "OFF", "--lf", "1.0E-18", "--lm", "1.0E-18", "--np",
118         "1", "--sf", "1.0", "--tp", "2.0", // Set low to encourage fast tempering for toy cases.
119         "-b", "ADIABATIC", // Used for Langevin dynamics.
120         "-C", "5000", // By default, evaluate 0 dynamics steps, and trigger adding counts manually.
121         "-d", "0.5", // Most of the tests are in vacuum, thus short timestep.
122         "-F", "XYZ", "-i", "STOCHASTIC",
123         // Stochastic because most toy systems work best with Langevin dynamics.
124         "-k", "5.0", // Infrequent because we should not need to restart.
125         "-l", "1.0",
126         // Set walker to start at the usually-physical topology. Likely commonly set by test.
127         "-n", "0", // Do not run any steps by default, just evaluate energy.
128         "-p", "0", // Use NVT, not NPT dynamics.
129         "-Q", "0", // Do not run any steps by default, just evaluate energy.
130         "-r", "2.0", // Very infrequent printouts, since it should work.
131         "-t", "298.15", "-w", "10.0"}; // Infrequent since we should not need the trajectory.
132 
133     int nOpts = opts.length;
134     Map<String, String> optMap = new HashMap<>(nOpts);
135     for (int i = 0; i < nOpts; i += 2) {
136       optMap.put(opts[i], opts[i + 1]);
137     }
138     DEFAULT_OPTIONS = Collections.unmodifiableMap(optMap);
139 
140     String[] props = {"ost-alwaystemper", "true", "ost-temperOffset", "0.5", // Small to temper fast.
141         "print-on-failure", "false", "disable-neighbor-updates", "false", "vdw-lambda-alpha", "0.25",
142         "vdw-lambda-exponent", "3", "permanent-lambda-alpha", "1.0", "permanent-lambda-exponent",
143         "3"};
144 
145     int nProps = props.length;
146     Map<String, String> propMap = new HashMap<>(nProps);
147     for (int i = 0; i < nProps; i += 2) {
148       propMap.put(props[i], props[i + 1]);
149     }
150     DEFAULT_PROPERTIES = Collections.unmodifiableMap(propMap);
151 
152     DEFAULT_FLAGS = Map.of("--rn", false, "-y", false, "-h", false);
153   }
154 
155   private final String info;
156   private final ThermoTestMode mode;
157   private final boolean doTest;
158   private final File[] copiedFiles;
159   private final Map<String, String> opts;
160   private final List<String> flags;
161   /**
162    * Free energy associated with this test, if calculated. Not used alongside testing gradients, and
163    * values are meaningless in that case.
164    */
165   private final double freeEnergy;
166   private final double feTol;
167   private final int numGradAtoms;
168   /** Potential energy of the underlying Potential (0), and OST after one bias drop (1). */
169   private final double[] pe;
170   private final double[] dudl;
171   private final double[] d2udl2;
172   /**
173    * Gradient of the underlying Potential ([0][0..n][0-2]), and OST after one bias drop
174    * ([1][0..n][0-2]). Axes: before/after bias, atoms, X/Y/Z.
175    */
176   private final double[][][] dudx;
177   private final double[][][] d2udxdl;
178   // TODO: Make these settable if needed.
179   private final double peTol = DEFAULT_PE_TOL;
180   private final double dudlTol = DEFAULT_DUDL_TOL;
181   private final double d2udl2Tol = DEFAULT_D2UDL2_TOL;
182   private final double dudxTol = DEFAULT_DUDX_TOL;
183   private final double d2udxdlTol = DEFAULT_D2UDXDL_TOL;
184   /** Configuration containing the properties to be used by OST. */
185   Configuration algorithmConfig;
186 
187   public ThermodynamicsTest(String info, String[] filenames, ThermoTestMode mode, boolean ciOnly,
188       double freeEnergy, double feTol, int[] gradAtomIndices, double[] pe, double[] dudl,
189       double[] d2udl2, double[][][] dudx, double[][][] d2udxdl, String[] options,
190       String[] properties, String[] flags) throws IOException {
191     this.info = info;
192     this.mode = mode;
193     doTest = (ffxCI || !ciOnly);
194 
195     File tempDir;
196     if (doTest) {
197       int nFiles = filenames.length;
198       tempDir = registerTemporaryDirectory().toFile();
199       String tempDirName = tempDir.getAbsolutePath() + File.separator;
200       logger.fine(format(" Running test %s in directory %s", info, tempDirName));
201       copiedFiles = new File[nFiles];
202 
203       String[] copiedExtensions = new String[] {"dyn", "key", "properties", "his", "lam", "prm"};
204       for (int i = 0; i < nFiles; i++) {
205         File srcFile = getResourceFile(filenames[i]);
206         File tempFile = new File(tempDirName + FilenameUtils.getName(filenames[i]));
207         copyFile(srcFile, tempFile);
208         logger.fine(format(" Copied file %s to %s", srcFile, tempFile));
209         copiedFiles[i] = tempFile;
210 
211         for (String ext : copiedExtensions) {
212           srcFile = new File(format("%s.%s", FilenameUtils.removeExtension(srcFile.getPath()), ext));
213           if (srcFile.exists()) {
214             logger.fine(" Copying extension " + ext);
215             tempFile = new File(
216                 format("%s.%s", FilenameUtils.removeExtension(tempFile.getPath()), ext));
217             logger.fine(format(" Copied file %s to %s", srcFile, tempFile));
218             copyFile(srcFile, tempFile);
219           }
220         }
221       }
222 
223       switch (mode) {
224         case HELP:
225           assertTrue(format("Help tests must have no file arguments, found %d for %s", nFiles, info),
226               nFiles == 0);
227           break;
228         default:
229           assertTrue(
230               format("Must have 1, 2, or 4 distinct filenames, found %d for test %s", nFiles, info),
231               nFiles == 1 || nFiles == 2 || nFiles == 4);
232           for (int i = 0; i < nFiles; i++) {
233             for (int j = i + 1; j < nFiles; j++) {
234               assertNotEquals(
235                   format(" Filenames %d and %d matched in test %s: files %s and %s", i, j, info,
236                       filenames[i], filenames[j]), filenames[i], filenames[j]);
237             }
238           }
239           break;
240       }
241       int nOpts = options.length;
242       int nProps = properties.length;
243       int nFlags = flags.length;
244 
245       if (nOpts > 0) {
246         assertTrue(format("Unmatched option key %s for test %s", options[nOpts - 1], info),
247             options.length % 2 == 0);
248       }
249       if (nProps > 0) {
250         assertTrue(format("Unmatched property key %s for test %s", properties[nProps - 1], info),
251             properties.length % 2 == 0);
252       }
253       if (nFlags > 0) {
254         assertTrue(format("Unmatched flag key %s for test %s", flags[nFlags - 1], info),
255             flags.length % 2 == 0);
256       }
257 
258       Pattern validOption = Pattern.compile("^--?[^D]");
259       Pattern validProperty = Pattern.compile("^[^-]");
260 
261       Map<String, String> groovyOpts = new HashMap<>(DEFAULT_OPTIONS);
262       for (int i = 0; i < nOpts; i += 2) {
263         String opti = options[i];
264         assertTrue(format(" Option %s for test %s does not look like a Groovy option!", opti, info),
265             validOption.matcher(opti).find());
266         groovyOpts.put(opti, options[i + 1]);
267       }
268       this.opts = Collections.unmodifiableMap(groovyOpts);
269 
270       Map<String, String> addedProps = new HashMap<>(DEFAULT_PROPERTIES);
271       for (int i = 0; i < nProps; i += 2) {
272         String propi = properties[i];
273         assertTrue(format(" Property %s for test %s does not look like a property!", propi, info),
274             validProperty.matcher(propi).find());
275         addedProps.put(propi, properties[i + 1]);
276       }
277       algorithmConfig = new MapConfiguration(addedProps);
278 
279       Map<String, Boolean> addedFlags = new HashMap<>(DEFAULT_FLAGS);
280       for (int i = 0; i < nFlags; i += 2) {
281         String flagi = flags[i];
282         assertTrue(format(" Flag %s for test %s does not look like a flag!", flagi, info),
283             validOption.matcher(flagi).find());
284         String vali = flags[i + 1];
285         assertTrue(
286             format(" Value %s for flag %s in test %s is not a true/false value!", vali, flagi, info),
287             vali.equalsIgnoreCase("TRUE") || vali.equalsIgnoreCase("FALSE"));
288         addedFlags.put(flagi, Boolean.parseBoolean(vali));
289       }
290       this.flags = Collections.unmodifiableList(
291           addedFlags.entrySet().stream().filter(Map.Entry::getValue).map(Map.Entry::getKey)
292               .collect(Collectors.toList()));
293 
294       // Only meaningful for free energy evaluations.
295       this.freeEnergy = freeEnergy;
296       this.feTol = feTol;
297       if (mode == ThermoTestMode.FREE) {
298         assertTrue(format(" Free energy tolerance for test %s was %10.4g <= 0!", info, feTol),
299             feTol > 0);
300       }
301 
302       // Only meaningful for gradient evaluations.
303       this.dudx = new double[2][][];
304       this.d2udxdl = new double[2][][];
305       boolean isGradient = (mode == ThermoTestMode.GRAD);
306       if (isGradient) {
307         numGradAtoms = ffxCI ? gradAtomIndices.length
308             : Math.min(DEFAULT_GRADIENT_EVALS, gradAtomIndices.length);
309 
310         if (debugMode) {
311           this.pe = this.dudl = this.d2udl2 = null;
312         } else {
313           assertNotNull(gradAtomIndices);
314           assertNotNull(pe);
315           assertNotNull(dudl);
316           assertNotNull(d2udl2);
317 
318           this.pe = copyOf(pe, 2);
319           this.dudl = copyOf(dudl, 2);
320           this.d2udl2 = copyOf(d2udl2, 2);
321           for (int i = 0; i < 2; i++) {
322             assertNotNull(dudx[i]);
323             assertNotNull(d2udxdl[i]);
324             this.dudx[i] = new double[numGradAtoms][3];
325             this.d2udxdl[i] = new double[numGradAtoms][3];
326 
327             for (int j = 0; j < numGradAtoms; j++) {
328               assertNotNull(dudx[i][j]);
329               assertNotNull(d2udxdl[i][j]);
330               arraycopy(dudx[i][j], 0, this.dudx[i][j], 0, 3);
331               arraycopy(d2udxdl[i][j], 0, this.d2udxdl[i][j], 0, 3);
332             }
333           }
334         }
335       } else {
336         // this.gradAtomIndices = null;
337         numGradAtoms = 0;
338         this.pe = null;
339         this.dudl = null;
340         this.d2udl2 = null;
341         for (int i = 0; i < 2; i++) {
342           this.dudx[i] = null;
343           this.d2udxdl[i] = null;
344         }
345       }
346     } else {
347       logger.fine(" Skipping test " + info);
348       tempDir = null;
349       copiedFiles = new File[0];
350       opts = Collections.emptyMap();
351       this.flags = Collections.emptyList();
352       this.freeEnergy = 0;
353       this.feTol = 0;
354       numGradAtoms = 0;
355       this.pe = new double[0];
356       this.dudl = new double[0];
357       this.d2udl2 = new double[0];
358       this.dudx = new double[0][0][0];
359       this.d2udxdl = new double[0][0][0];
360     }
361   }
362 
363   @Parameterized.Parameters
364   public static Collection<Object[]> data() {
365     /*
366      Test info, filenames, mode, dG, tol(dG), grad atoms, PE, dU/dL, d2U/dL2,
367      dU/dX, d2U/dXdL, Groovy options, properties, Groovy flags
368     */
369     return Arrays.asList(new Object[][] {
370         {"Thermodynamics Help Message Test", new String[] {}, ThermoTestMode.HELP, false, 0, 0, null,
371             null, null, null, null, null, new String[] {}, new String[] {},
372             new String[] {"-h", "true"}},
373         {"Acetamide Implicit Solvation Free Energy: -10.5 kcal/mol",
374             new String[] {"acetamide.gk.xyz"}, ThermoTestMode.FREE, true,
375             -9.2, 1.0, null, null, null, null, null, null,
376             new String[] {"-C", "10", "--ac", "1-9", "-d", "1.0", "-n", "20000", "-w", "5", "--bM",
377                 "0.25", "--tp", "2.0"},
378             new String[] {"randomseed", "42", "lambda-bin-width", "0.02"}, new String[] {}},
379 //            {
380 //                // Gradient atoms: a few random protein atoms, some of the coordinating carboxyls, the
381 //                // ions, and some water.
382 //                "Calbindin D9k Ca/Mg Simultaneous Bookending: L = 0.0",
383 //                new String[] {
384 //                    "4icb_ca_a.xyz",
385 //                    "4icb_ca_b.xyz",
386 //                    "4icb_mg_a.xyz",
387 //                    "4icb_mg_b.xyz"
388 //                },
389 //                ThermoTestMode.GRAD,
390 //                true,
391 //                0,
392 //                0,
393 //                new int[] {1, 100, 421, 426, 919, 921, 1203, 1204, 1205, 1206, 1207, 1208},
394 //                // Fill in the post-bias PE and dU/dL once the test harness supports bias deposition.
395 //                new double[] {-8422.43552052, 0},
396 //                new double[] {0, 0},
397 //                new double[] {-187.708505969645246, Double.NaN},
398 //                new double[][][] {
399 //                    {
400 //                        {1.51602997670091, -0.16798107175016064, 1.1909011229485826},
401 //                        {-1.2287108493416157, 0.3317143880292477, -1.168707082997385},
402 //                        {1.330781894105006, -0.8783264761133562, 0.1096093788802257},
403 //                        {-1.0136367827497708, 0.737243265172173, -0.5962871564399377},
404 //                        {-0.17816595179992722, -0.35265175625985923, -0.6111655937237352},
405 //                        {-0.25917771149474067, 0.07021711090701199, 0.15836021218566376},
406 //                        {-0.5324396799274522, 0.7187373577290392, -0.6184469870748088},
407 //                        {0.2993938879707754, -0.3907323336942321, 0.4139579476752431},
408 //                        {0.054758010534996515, 0.3356578298638757, 0.9215488358561414},
409 //                        {-0.882274050563856, 0.5899806670077385, -0.9015065344963205},
410 //                        {-0.09630989055561034, 0.9691000112562431, -0.2005707212562129},
411 //                        {0.167130858362869, -0.62725014243849, 1.175516097449706}
412 //                    },
413 //                    // Fill in once the test harness supports bias-deposition.
414 //                    new double[12][3]
415 //                },
416 //                new double[][][] {
417 //                    {
418 //                        {-2.9163424369832507E-16, 3.2314026482573344E-17, -2.2909015893365526E-16},
419 //                        {2.3636350519364876E-16, -6.38109246937693E-17, 2.248207565107174E-16},
420 //                        {-2.5599861294253615E-16, 1.6896109016191808E-16, -2.1085234990896332E-17},
421 //                        {1.9499033730541748E-16, -1.4182132633554818E-16, 1.1470601278862118E-16},
422 //                        {3.427326200965616E-17, 6.783858486066127E-17, 1.1756813416574204E-16},
423 //                        {4.985725680683067E-17, -1.3507459844962662E-17, -3.0463289923312223E-17},
424 //                        {1.0242386084510416E-16, -1.3826139915463767E-16, 1.1896883446563482E-16},
425 //                        {-5.75935248168813E-17, 7.516403394171084E-17, -7.963187723760774E-17},
426 //                        {-1.0533638011269165E-17, -6.456951304273904E-17, -1.7727564883699187E-16},
427 //                        {1.6972047349009049E-16, -1.1349285189852184E-16, 1.7342017006092682E-16},
428 //                        {1.8526851397737774E-17, -1.8642292909391106E-16, 3.8583201849920577E-17},
429 //                        {-3.215047342492132E-17, 1.2066227166417432E-16, -2.2613058666622065E-16}
430 //                    },
431 //                    // Fill in once the test harness supports bias-deposition.
432 //                    new double[12][3]
433 //                },
434 //                new String[] {
435 //                    "-l",
436 //                    "0.0",
437 //                    "--sf",
438 //                    "TRIG",
439 //                    "--uaA",
440 //                    "329-345.857-972.1008-1022.1204.1208-1213",
441 //                    "--uaB",
442 //                    "329-345.857-972.1008-1022.1204.1208-1213"
443 //                },
444 //                new String[] {"disable-neighbor-updates", "true"},
445 //                new String[] {}
446 //            },
447 //            {
448 //                // Gradient atoms: a few random protein atoms, some of the coordinating carboxyls, the
449 //                // ions, and some water.
450 //                "Calbindin D9k Ca/Mg Simultaneous Bookending: L = 0.5",
451 //                new String[] {
452 //                    "4icb_ca_a.xyz",
453 //                    "4icb_ca_b.xyz",
454 //                    "4icb_mg_a.xyz",
455 //                    "4icb_mg_b.xyz"
456 //                },
457 //                ThermoTestMode.GRAD,
458 //                false,
459 //                0,
460 //                0,
461 //                new int[] {1, 100, 421, 426, 919, 921, 1203, 1204, 1205, 1206, 1207, 1208},
462 //                // Fill in the post-bias PE and dU/dL once the test harness supports bias deposition.
463 //                new double[] {-8441.45436853, 0},
464 //                new double[] {-59.7520122, 0},
465 //                new double[] {0, Double.NaN},
466 //                new double[][][] {
467 //                    {
468 //                        {1.516028943632357, -0.16798187601812842, 1.1909000012817135},
469 //                        {-1.2287100421217505, 0.33171692634032457, -1.1687028725533608},
470 //                        {1.3307953311709682, -0.8783244163319, 0.10962280681203751},
471 //                        {-1.0136504919823237, 0.7372413665734416, -0.5963003206269726},
472 //                        {-0.17816209958414242, -0.3526516261736795, -0.6111638455124022},
473 //                        {-0.25917449280899385, 0.07021855608468686, 0.1583629255853478},
474 //                        {-0.5324365378704783, 0.718739459636424, -0.6184448359875749},
475 //                        {0.2993955300471849, -0.3907325347437327, 0.4139595896177193},
476 //                        {0.05475347093067029, 0.335655947922745, 0.9215447577884727},
477 //                        {-0.88227515311464, 0.5899832887545906, -0.901509658156534},
478 //                        {-0.09630970969676556, 0.9690984223562502, -0.20056743541628919},
479 //                        {0.16711991894934464, -0.6272519054249652, 1.1755119757400072}
480 //                    },
481 //                    // Fill in once the test harness supports bias-deposition.
482 //                    new double[12][3]
483 //                },
484 //                new double[][][] {
485 //                    {
486 //                        {-3.2454805314330315E-6, -2.5266823584502163E-6, -3.5238203937026924E-6},
487 //                        {2.535955989202421E-6, 7.974339453653556E-6, 1.3227500003987203E-5},
488 //                        {4.221378770274953E-5, 6.470994293295007E-6, 4.218509194231501E-5},
489 //                        {-4.306882429183645E-5, -5.964623852605655E-6, -4.135651328152079E-5},
490 //                        {1.2102092812327214E-5, 4.0867779382836034E-7, 5.492167883147658E-6},
491 //                        {1.0111799477741101E-5, 4.540159575405767E-6, 8.524396519327126E-6},
492 //                        {9.871063099353705E-6, 6.603336803578941E-6, 6.757839859261594E-6},
493 //                        {5.158735174148887E-6, -6.316156557772956E-7, 5.1583144315969776E-6},
494 //                        {-1.4261587612196536E-5, -5.912292426302201E-6, -1.2811627438935602E-5},
495 //                        {-3.463765445133049E-6, 8.236460654842404E-6, -9.81326797955262E-6},
496 //                        {5.681848183058946E-7, -4.991676597398964E-6, 1.0322770563675476E-5},
497 //                        {-3.436718115423787E-5, -5.538585353903613E-6, -1.2948732912576588E-5}
498 //                    },
499 //                    // Fill in once the test harness supports bias-deposition.
500 //                    new double[12][3]
501 //                },
502 //                new String[] {
503 //                    "-l",
504 //                    "0.5",
505 //                    "--sf",
506 //                    "TRIG",
507 //                    "--uaA",
508 //                    "329-345.857-972.1008-1022.1204.1208-1213",
509 //                    "--uaB",
510 //                    "329-345.857-972.1008-1022.1204.1208-1213"
511 //                },
512 //                new String[] {"disable-neighbor-updates", "true"},
513 //                new String[] {}
514 //            },
515         {
516             // Gradient atoms: a few random protein atoms, some of the coordinating carboxyls, the
517             // ions, and some water.
518             "Calbindin D9k Ca/Mg Simultaneous Bookending: L = 1.0",
519             new String[] {"4icb_ca_a.xyz", "4icb_ca_b.xyz", "4icb_mg_a.xyz", "4icb_mg_b.xyz"}, 
520             ThermoTestMode.GRAD, true, 0, 0,
521             new int[] {1, 100, 421, 426, 919, 921, 1203, 1204, 1205, 1206, 1207, 1208},
522             // Fill in the post-bias PE and dU/dL once the test harness supports bias deposition.
523             new double[] {-8478.143922064253, 0}, new double[] {0, 0},
524             new double[] {205.8018333335931, Double.NaN}, new double[][][] {
525             {{1.5229178263056111, -0.15677963159712682, 1.1816872925903983},
526                 {-1.224135286541821, 0.3201779704385803, -1.1483158020643944},
527                 {1.2969488018273694, -0.8376981151127962, 0.09660091060711373},
528                 {-1.0033472564565908, 0.7283612031936482, -0.5947650554794823},
529                 {-0.17802197322041113, -0.34399197898615985, -0.6113921282564965},
530                 {-0.25687293667935585, 0.07341668289540859, 0.15584681242862208},
531                 {-0.530878661936121, 0.7208093361614316, -0.6200446160740853},
532                 {0.29982215558798053, -0.40520052950990504, 0.4147977845799842},
533                 {0.05351938289450864, 0.3348510487959171, 0.9222262189021788},
534                 {-0.8823224520772985, 0.6039483343281296, -0.894180455908895},
535                 {-0.08825723194619872, 0.9833060568981935, -0.1570236438711472},
536                 {0.1637236555517756, -0.6228719840792594, 1.1789882530493676},},
537             // Fill in once the test harness supports bias-deposition.
538             new double[12][3]}, new double[][][] {
539             {{2.9163384624158903E-16, -3.231433591192038E-17, 2.290897273901192E-16},
540                 {-2.3636319462860973E-16, 6.381190126870034E-17, -2.248191366091609E-16},
541                 {2.560037826405337E-16, -1.6896029769367465E-16, 2.109040117467388E-17},
542                 {-1.9499561171519864E-16, 1.4182059587979724E-16, -1.147110775007824E-16},
543                 {-3.4271779930734056E-17, -6.783853481206671E-17, -1.1756746156916025E-16},
544                 {-4.9856018468543316E-17, 1.3508015854151776E-17, 3.0464333860802826E-17},
545                 {-1.0242265198852129E-16, 1.38262207830166E-16, -1.1896800686893987E-16},
546                 {5.759415657973307E-17, -7.51641112923199E-17, 7.963250894893407E-17},
547                 {1.0531891470506986E-17, 6.456878899573994E-17, 1.7727407986513553E-16},
548                 {-1.6972089767901735E-16, 1.1349386057403977E-16, -1.734213718396476E-16},
549                 {-1.8526781815166296E-17, 1.8642231778983455E-16, -3.858193767513045E-17},
550                 {3.214626465908195E-17, -1.2066294994525673E-16, 2.2612900090378923E-16}},
551             // Fill in once the test harness supports bias-deposition.
552             new double[12][3]}, new String[] {"-l", "1.0", "--sf", "TRIG", "--uaA",
553             "329-345.857-972.1008-1022.1204.1208-1213", "--uaB",
554             "329-345.857-972.1008-1022.1204.1208-1213"},
555             new String[] {"disable-neighbor-updates", "true"}, new String[] {}}, {
556         // Gradient atoms: a few random protein atoms, some of the coordinating carboxyls, the
557         // ions, and some water.
558         "Carp Parvalbumin Ca/Mg Simultaneous Bookending: L = 0.0",
559         new String[] {"5cpv_ca_a.xyz", "5cpv_ca_b.xyz", "5cpv_mg_a.xyz", "5cpv_mg_b.xyz"},
560         ThermoTestMode.GRAD, true, 0, 0,
561         new int[] {1, 303, 1401, 1402, 1482, 1488, 1489, 1602, 1603, 1604, 1605, 1606},
562         // Fill in the post-bias PE and dU/dL once the test harness supports bias deposition.
563         new double[] {-10389.28471472707, 0}, new double[] {0, 0},
564         new double[] {-204.10881484981655, Double.NaN}, new double[][][] {
565         {{-1.1609578854274971, 0.8877442339271191, -0.381997307744788},
566             {0.17136471973564316, -0.6936945821429843, -0.26469263318865544},
567             {1.7842973607219292, 0.06247987009760436, 0.6436711554923047},
568             {-0.80901615978728, -0.03278744764172048, -0.6933431691420533},
569             {-0.009221945771183293, -0.03024427307941413, -0.22321841071848292},
570             {0.44453213441769357, -0.03618099829897137, -0.09130836864141911},
571             {0.6143060586860591, -0.5593900343333869, 0.7793559372609127},
572             {-0.4099919820212454, 0.2771241953804595, -0.3596606825033808},
573             {0.2058491063283956, -1.6995564152580624, 0.3162941963413184},
574             {0.17045731075675363, 1.3626300067862616, 0.1829234871604033},
575             {-0.1369296520341554, 0.28864175741664555, -0.23181864232968774},
576             {-0.3317934170646355, 0.1069795784815214, 0.07598972633102496},},
577         // Fill in once the test harness supports bias-deposition.
578         new double[12][3]}, new double[][][] {
579         {{2.2334257856503093E-16, -1.707804485712817E-16, 7.346589191878714E-17},
580             {-3.2975061696419736E-17, 1.334426237792466E-16, 5.09166906746972E-17},
581             {-3.4327908122000154E-16, -1.202864843760528E-17, -1.2390581692419626E-16},
582             {1.5565048660891405E-16, 6.307561537504449E-18, 1.33392733415393E-16},
583             {1.7707118825002895E-18, 5.832093621377665E-18, 4.292309196176272E-17},
584             {-8.548440453491081E-17, 6.970235281623787E-18, 1.754463462531147E-17},
585             {-1.181759625502523E-16, 1.0762112290669118E-16, -1.4995022305853868E-16},
586             {7.888210075095321E-17, -5.330782752663655E-17, 6.918026206876034E-17},
587             {-3.962976007169519E-17, 3.269309449444003E-16, -6.080614905213747E-17},
588             {-3.278713882575124E-17, -2.62121095313077E-16, -3.519199394795007E-17},
589             {2.6347728499845737E-17, -5.55185665906526E-17, 4.459517949566215E-17},
590             {6.382453595443383E-17, -2.0571002605587047E-17, -1.4618517021800703E-17}},
591         // Fill in once the test harness supports bias-deposition.
592         new double[12][3]}, new String[] {"-l", "0.0", "--sf", "TRIG", "--uaA",
593         "810-829.1339-1447.1476-1490.1603.1605-1610", "--uaB",
594         "810-829.1339-1447.1476-1490.1603.1605-1610"},
595         new String[] {"disable-neighbor-updates", "true"}, new String[] {}},
596         {"Carp Parvalbumin Ca/Mg Simultaneous Bookending: L = 0.5",
597             new String[] {"5cpv_ca_a.xyz", "5cpv_ca_b.xyz", "5cpv_mg_a.xyz", "5cpv_mg_b.xyz"},
598             ThermoTestMode.GRAD, true, 0, 0,
599             new int[] {1, 303, 1401, 1402, 1482, 1488, 1489, 1602, 1603, 1604, 1605, 1606},
600             // Fill in the post-bias PE and dU/dL once the test harness supports bias deposition.
601             new double[] {-10409.965261439502, 0}, new double[] {-64.96985362393843, 0},
602             new double[] {0, Double.NaN}, new double[][][] {
603             {{-1.1610220532282816, 0.8877825087765396, -0.38190561321870153},
604                 {0.17141666802985345, -0.6936868435232721, -0.26468548230330735},
605                 {1.7844987915596682, 0.0625323990316684, 0.6441061559020982},
606                 {-0.8091313886502285, -0.032792749245311725, -0.6934234392602314},
607                 {-0.009206939878777853, -0.030315101956861712, -0.2231346110988761},
608                 {0.4443810630713887, -0.036232529267353986, -0.09120578149088898},
609                 {0.6143248825840462, -0.5594553644984039, 0.7794994337695262},
610                 {-0.4100603484233871, 0.2771150828568485, -0.3596269108370689},
611                 {0.2060121473201555, -1.6995185434801163, 0.31609655101286327},
612                 {0.1704401628733358, 1.3626093888666202, 0.18294114185436605},
613                 {-0.13696614734124157, 0.2886080953858223, -0.23182359983306977},
614                 {-0.33178545555019134, 0.10693683664021286, 0.07599211616273438}},
615             // Fill in once the test harness supports bias-deposition.
616             new double[12][3]}, new double[][][] {
617             {{2.755227653583603E-6, -5.374905775568095E-6, -2.803630881231811E-6},
618                 {-2.2802651855613476E-6, 1.9373526711774502E-6, -1.3042348507852353E-6},
619                 {-4.54644512082325E-6, 8.588679262899745E-6, -1.638592826225249E-5},
620                 {4.125879040195457E-6, -1.1162916089269004E-5, 1.46747768532407E-5},
621                 {-6.535106551908143E-6, 7.6152604604473595E-6, -1.0766383081950437E-5},
622                 {-1.988285510456933E-6, 4.732964988818367E-6, -5.78566637177147E-6},
623                 {-1.3921589704368742E-6, 5.4545292442753635E-6, -3.289226282676694E-6},
624                 {-1.1008869904571839E-7, -1.8223435560571488E-7, -1.842757221481861E-6},
625                 {3.151330700390531E-6, -7.456983421860741E-6, 6.8281705551953564E-6},
626                 {-1.0483102688141344E-6, 1.2986962580896488E-6, -2.329191170602485E-6},
627                 {-7.817337568383209E-7, 2.0726923768421557E-6, -7.678219411388909E-7},
628                 {-9.764459392158642E-7, 2.0130999769385483E-6, -2.226604200572524E-6}},
629             // Fill in once the test harness supports bias-deposition.
630             new double[12][3]}, new String[] {"-l", "0.5", "--sf", "TRIG", "--uaA",
631             "810-829.1339-1447.1476-1490.1603.1605-1610", "--uaB",
632             "810-829.1339-1447.1476-1490.1603.1605-1610"},
633             new String[] {"disable-neighbor-updates", "true"}, new String[] {}},
634         {"Carp Parvalbumin Ca/Mg Simultaneous Bookending: L = 1.0",
635             new String[] {"5cpv_ca_a.xyz", "5cpv_ca_b.xyz", "5cpv_mg_a.xyz", "5cpv_mg_b.xyz"},
636             ThermoTestMode.GRAD, true, 0, 0,
637             new int[] {1, 303, 1401, 1402, 1482, 1488, 1489, 1602, 1603, 1604, 1605, 1606},
638             // Fill in the post-bias PE and dU/dL once the test harness supports bias deposition.
639             new double[] {-10430.645808151916, 0}, new double[] {0, 0},
640             new double[] {204.1088148497838, Double.NaN}, new double[][][] {
641             {{-1.1609561125944228, 0.8877407807597617, -0.3819990910861595},
642                 {0.17136321710200875, -0.6936932915411145, -0.2646933907193276},
643                 {1.78429435562491, 0.06248553884406727, 0.6436609576859738},
644                 {-0.8090134265871809, -0.032794734757721766, -0.693334070333059},
645                 {-0.009226164435848005, -0.03023937880286831, -0.22322518458123763},
646                 {0.4445308404726829, -0.03617792937772801, -0.09131197683309833},
647                 {0.6143051128396353, -0.5593864905811907, 0.7793539392457256},
648                 {-0.40999206546827605, 0.27712408910913533, -0.35966183105159466},
649                 {0.205851177042927, -1.6995612712832284, 0.31629838909722086},
650                 {0.17045662254298133, 1.3626308508037734, 0.1829220337727313},
651                 {-0.13693017608623448, 0.2886431027192988, -0.23181908891834802},
652                 {-0.3317940476955181, 0.10698088476669532, 0.07598834048574243},},
653             // Fill in once the test harness supports bias-deposition.
654             new double[12][3]}, new double[][][] {
655             {{-2.2334224114695824E-16, 1.7077979033516612E-16, -7.346623526454586E-17},
656                 {3.297478244447361E-17, -1.3344238652197185E-16, -5.091685039740076E-17},
657                 {3.4327852444105517E-16, 1.202970024746214E-17, 1.2390381022673677E-16},
658                 {-1.5564998133445806E-16, -6.308928600450258E-18, -1.3339093627354278E-16},
659                 {-1.7715122022323975E-18, -5.831161020942922E-18, -4.292441046342064E-17},
660                 {8.548416104016219E-17, -6.969655660581429E-18, -1.7545343165091794E-17},
661                 {1.181757920599496E-16, -1.0762045491951309E-16, 1.499498202444948E-16},
662                 {-7.888211423293022E-17, 5.3307805209364545E-17, -6.918048774143343E-17},
663                 {3.963014599840059E-17, -3.2693185816148815E-16, 6.080698526185848E-17},
664                 {3.2787010444769745E-17, 2.621212543574982E-16, 3.5191708704298895E-17},
665                 {-2.6347824234619856E-17, 5.551882042226125E-17, -4.459527352673071E-17},
666                 {-6.382465553457344E-17, 2.057124913923137E-17, 1.4618244341429974E-17}},
667             // Fill in once the test harness supports bias-deposition.
668             new double[12][3]}, new String[] {"-l", "1.0", "--sf", "TRIG", "--uaA",
669             "810-829.1339-1447.1476-1490.1603.1605-1610", "--uaB",
670             "810-829.1339-1447.1476-1490.1603.1605-1610"},
671             new String[] {"disable-neighbor-updates", "true"}, new String[] {}},
672         {"Water-Sodium to Water Dimer Softcoring Test: L = 0.0",
673             new String[] {"water-dimer.xyz", "water-na.xyz"}, ThermoTestMode.GRAD, true, 0, 0,
674             intRange(1, 8),
675             // Fill in the post-bias PE and dU/dL once the test harness supports bias deposition.
676             new double[] {2.61926128937, 0}, new double[] {17.7107919457, 0},
677             new double[] {-403.664911547, Double.NaN}, new double[][][] {
678             {{-5.677212525948178, 1.424643968949981, -2.2981580317913504},
679                 {-16.262337326126076, -4.011524859855653, 2.8309493626240676},
680                 {20.234576106276727, -2.5759425460558103, 0.34157831359509483},
681                 {-4.107688957377534E-5, -3.0491094575808766E-4, 1.5088773834586063E-4},
682                 {3.855038213179264E-5, 3.0678248136167547E-4, -1.527835575474326E-4},
683                 {2.526507441982699E-6, -1.8715356035878577E-6, 1.8958192015719713E-6},
684                 {1.7049737457975327, 5.1628234369614825, -0.8743696444278124},},
685             // Fill in once the test harness supports bias-deposition.
686             new double[7][3]}, new double[][][] {
687             {{34.717862394027094, -6.918833553692483, 13.770559137316184},
688                 {44.6501751603733, 28.43111292615495, -17.0699023179925},
689                 {-125.31576661843137, 11.778366126287878, -3.1016651209654698}, {0.0, 0.0, 0.0},
690                 {0.0, 0.0, 0.0}, {0.0, 0.0, 0.0},
691                 {45.94772906403098, -33.29064549875034, 6.401008301641785}},
692             // Fill in once the test harness supports bias-deposition.
693             new double[7][3]}, new String[] {"-l", "0.0", "--ac", "1-3", "--ac2", "1"},
694             new String[] {}, new String[] {}},
695         {"Water-Sodium to Water Dimer Softcoring Test: L = 0.1",
696             new String[] {"water-dimer.xyz", "water-na.xyz"}, ThermoTestMode.GRAD, true, 0, 0,
697             intRange(1, 8),
698             // Fill in the post-bias PE and dU/dL once the test harness supports bias deposition.
699             new double[] {2.850550906, 0}, new double[] {-8.71629068942, 0},
700             new double[] {-141.329823284, Double.NaN}, new double[][][] {
701             {{-2.9145419263022183, 0.8087080881505835, -1.1826969161418945},
702                 {-10.75486342640376, -1.8502906018217953, 1.441002156956333},
703                 {10.24415407077068, -1.5066777027112754, 0.13037812748160396},
704                 {-3.940188541217106E-5, -3.046447158662098E-4, 1.504274409501606E-4},
705                 {4.348535004930246E-5, 3.0537557756647634E-4, -1.5274632518595476E-4},
706                 {5.712456879867664E-6, -9.857865557163607E-7, 1.145744081896436E-6},
707                 {3.425241486013779, 2.548260471307343, -0.38868219515588814}},
708             // Fill in once the test harness supports bias-deposition.
709             new double[7][3]}, new double[][][] {
710             {{21.219764941164165, -5.301043079657732, 8.702685150224386},
711                 {59.404183336652935, 15.559560209446712, -10.86515770297192},
712                 {-76.64822167834306, 9.426371612748719, -1.3109489861942616},
713                 {8.074643396228061E-5, 1.2233389408248271E-5, -2.0734472804670857E-5},
714                 {2.2173087875276693E-4, -6.414406402810961E-5, 2.6740754942568507E-6},
715                 {1.7176239889004114E-4, 3.3106310006263015E-5, -3.056405648898283E-5},
716                 {-3.9762008391856365, -19.684869938173087, 3.4734701633955916}},
717             // Fill in once the test harness supports bias-deposition.
718             new double[7][3]}, new String[] {"-l", "0.1", "--ac", "1-3", "--ac2", "1"},
719             new String[] {}, new String[] {}},
720         {"Water-Sodium to Water Dimer Softcoring Test: L = 0.25",
721             new String[] {"water-dimer.xyz", "water-na.xyz"}, ThermoTestMode.GRAD, false, 0, 0,
722             intRange(1, 8),
723             // Fill in the post-bias PE and dU/dL once the test harness supports bias deposition.
724             new double[] {0.974157909812, 0}, new double[] {-10.6503844408, 0},
725             new double[] {85.6111397243, Double.NaN}, new double[][][] {
726             {{-0.8087917281519832, 0.230920162100479, -0.31677660566831417},
727                 {-3.2828492735876607, -0.4439185686570825, 0.3694313047851273},
728                 {2.7179567720851567, -0.4453048785572774, 0.02913967164983769},
729                 {1.1191654242300788E-4, -2.8436948793288743E-4, 1.1810143809370248E-4},
730                 {3.948395026959898E-4, 1.992169489195181E-4, -1.434611972003E-4},
731                 {4.178187790317917E-4, 1.7909105729778537E-5, -3.118731586794815E-5},
732                 {1.3727596548303362, 0.6583705285471645, -0.08173782369167623}},
733             // Fill in once the test harness supports bias-deposition.
734             new double[7][3]}, new double[][][] {
735             {{8.203573000276744, -2.407260166945433, 3.3325694163231305},
736                 {34.07610421632959, 4.853369454929932, -4.037664708947347},
737                 {-28.691594190062066, 4.581997199828557, -0.30050567027187414},
738                 {0.0030722784633713677, 4.013278190369727E-4, -6.302447025815397E-4},
739                 {0.006927249436665227, -0.0021139142799777464, 2.0753922987915073E-4},
740                 {0.008777938573427544, 2.1069797116961348E-4, -5.578096868382651E-4},
741                 {-13.606860493017741, -7.026604599323282, 1.006581478055631}},
742             // Fill in once the test harness supports bias-deposition.
743             new double[7][3]}, new String[] {"-l", "0.25", "--ac", "1-3", "--ac2", "1"},
744             new String[] {}, new String[] {}},
745         {"Water-Sodium to Water Dimer Softcoring Test: L = 0.4",
746             new String[] {"water-dimer.xyz", "water-na.xyz"}, ThermoTestMode.GRAD, true, 0, 0,
747             intRange(1, 8),
748             // Fill in the post-bias PE and dU/dL once the test harness supports bias deposition.
749             new double[] {0.110037916292, 0}, new double[] {-2.26607999096, 0},
750             new double[] {30.1632932351, Double.NaN}, new double[][][] {
751             {{-0.1135918356399739, 0.031116766115897736, -0.041752199556192734},
752                 {-0.4844299250151845, -0.052731371080211295, 0.044681832632104526},
753                 {0.34363344315228866, -0.058702201304027674, 0.003020221799192616},
754                 {0.0015984934497727645, -9.5805104515793E-5, -1.7187445022315994E-4},
755                 {0.0036936320812564253, -8.190880189616524E-4, -2.9551973622635126E-5},
756                 {0.004965647007413021, 1.9626489810683822E-5, -2.4565648661058724E-4},
757                 {0.24413054496442746, 0.08121207290200799, -0.005502771964648001}},
758             // Fill in once the test harness supports bias-deposition.
759             new double[7][3]}, new double[][][] {
760             {{1.9790078473632116, -0.5466360053457613, 0.7442354166761601},
761                 {7.5148029466680395, 1.0124425518627698, -0.8308708232627937},
762                 {-6.43514862353287, 1.0832749443479637, -0.07362559817071525},
763                 {0.020724339018201313, 0.002599652594099935, -0.003961656523433774},
764                 {0.04599278248257384, -0.014257367173055743, 0.0016722631878542601},
765                 {0.0653051230131746, -5.218785659658921E-4, -0.002695798950450919},
766                 {-3.1906844150123317, -1.5369018977200506, 0.16524619704337926}},
767             // Fill in once the test harness supports bias-deposition.
768             new double[7][3]}, new String[] {"-l", "0.4", "--ac", "1-3", "--ac2", "1"},
769             new String[] {}, new String[] {}},
770         {"Water-Sodium to Water Dimer Softcoring Test: L = 0.5",
771             new String[] {"water-dimer.xyz", "water-na.xyz"}, ThermoTestMode.GRAD, true, 0, 0,
772             intRange(1, 8),
773             // Fill in the post-bias PE and dU/dL once the test harness supports bias deposition.
774             new double[] {-0.00563228503, 0}, new double[] {-0.384474276341, 0},
775             new double[] {9.92344157784, Double.NaN}, new double[][][] {
776             {{-0.009836012990445158, 0.0029589899860514987, -0.0034155479511113907},
777                 {-0.10184729530732066, 5.327593924820649E-4, 0.0057353060076526016},
778                 {0.016303644915009106, -0.003689130235597261, -0.0025312483345677947},
779                 {0.0038107513266330815, 0.003205959229414844, -8.159303166222604E-4},
780                 {0.015696774651107862, -0.0037059448135031083, 2.7367574864321055E-4},
781                 {0.013110199299505135, -0.0030608519096883596, -6.447454233376755E-4},
782                 {0.06276193810551062, 0.00375821835084032, 0.0013984902693433081}},
783             // Fill in once the test harness supports bias-deposition.
784             new double[7][3]}, new double[][][] {
785             {{0.34392939856659827, -0.09158200189901379, 0.12910527928175775},
786                 {1.1760470096366977, 0.23616310164517534, -0.05057458142688201},
787                 {-1.0121014533251116, 0.15248505767669873, -0.07620425570370314},
788                 {0.011511611202389766, 0.10091080897132744, -0.010683387778191795},
789                 {0.26561965255269504, -0.057037692991738595, 0.004170051640670491},
790                 {0.06748923520446536, -0.09680681673784088, -0.004103281313994225},
791                 {-0.8524954538377344, -0.24413245666460826, 0.008290175300342928}},
792             // Fill in once the test harness supports bias-deposition.
793             new double[7][3]}, new String[] {"-l", "0.5", "--ac", "1-3", "--ac2", "1"},
794             new String[] {}, new String[] {}},
795         {"Water-Sodium to Water Dimer Softcoring Test: L = 0.6",
796             new String[] {"water-dimer.xyz", "water-na.xyz"}, ThermoTestMode.GRAD, true, 0, 0,
797             intRange(1, 8),
798             // Fill in the post-bias PE and dU/dL once the test harness supports bias deposition.
799             new double[] {-0.010482021204, 0}, new double[] {0.180367758296, 0},
800             new double[] {3.43489697899, Double.NaN}, new double[][][] {
801             {{-0.008649580249889975, 0.0022485600112194097, -8.650726609048702E-4},
802                 {-0.11266057761820013, 0.024694362034725272, 0.0260361864833212},
803                 {0.015218945213069365, -0.00914553962283122, -0.02282021358689983},
804                 {2.5066123005239126E-4, 0.031224802161732783, -0.0034016265001422597},
805                 {0.08190367312390062, -0.018650512458739874, 3.4326425602877264E-4},
806                 {0.0064502702618650975, -0.028665281917196323, -2.4419049163833308E-5},
807                 {0.017486608039202628, -0.0017063902089100456, 7.31881057760824E-4}},
808             // Fill in once the test harness supports bias-deposition.
809             new double[7][3]}, new double[][][] {
810             {{-0.2058541574059063, 0.04582469500939303, -0.032210244958406097},
811                 {-1.0401985420569162, 0.37454108615584125, 0.47159019722227746},
812                 {0.621062316619004, -0.20684943525738805, -0.39772097441817644},
813                 {-0.10747378808409644, 0.5344440256235907, -0.051334182756052026},
814                 {1.2489110819012346, -0.2984439482946815, -0.007849154127628813},
815                 {-0.2968358770072303, -0.4711447951314603, 0.027030911889720764},
816                 {-0.21961103396608955, 0.02162837189470477, -0.009506552851734766}},
817             // Fill in once the test harness supports bias-deposition.
818             new double[7][3]}, new String[] {"-l", "0.6", "--ac", "1-3", "--ac2", "1"},
819             new String[] {}, new String[] {}},
820         {"Water-Sodium to Water Dimer Softcoring Test: L = 0.75",
821             new String[] {"water-dimer.xyz", "water-na.xyz"}, ThermoTestMode.GRAD, false, 0, 0,
822             intRange(1, 8),
823             // Fill in the post-bias PE and dU/dL once the test harness supports bias deposition.
824             new double[] {0.0829830017977, 0}, new double[] {1.27996801166, 0},
825             new double[] {11.9168667081, Double.NaN}, new double[][][] {
826             {{-0.08520546853249629, 0.02002391005107648, -0.015337962582326878},
827                 {-0.5072588908421117, 0.17043258439271164, 0.2050240947794576},
828                 {0.25349423271871935, -0.0805529898238775, -0.17425804293531943},
829                 {-0.0506141349576535, 0.22437919802386463, -0.02673413275920512},
830                 {0.5612744057443229, -0.15045901450002216, -0.007975329992730817},
831                 {-0.17340305926115046, -0.1836639508243553, 0.019217340035074935},
832                 {0.0017129151303696705, -1.5973731939765025E-4, 6.403345504972649E-5}},
833             // Fill in once the test harness supports bias-deposition.
834             new double[7][3]}, new double[][][] {
835             {{-0.9272841243095468, 0.22051093159987895, -0.19112897890018363},
836                 {-4.804523425931252, 1.8204016841668706, 2.205227241175209},
837                 {2.994591363051441, -0.8476847561428067, -1.8543652609150763},
838                 {-0.6630357716342526, 2.3143378077667545, -0.3143349864556007},
839                 {5.981286181791292, -1.7558797902974297, -0.1325990985326137},
840                 {-2.5478012735172113, -1.7548767300847088, 0.28852239893802845},
841                 {-0.033232949450470756, 0.003190852991440346, -0.0013213153097627725}},
842             // Fill in once the test harness supports bias-deposition.
843             new double[7][3]}, new String[] {"-l", "0.75", "--ac", "1-3", "--ac2", "1"},
844             new String[] {}, new String[] {}},
845         {"Water-Sodium to Water Dimer Softcoring Test: L = 0.9",
846             new String[] {"water-dimer.xyz", "water-na.xyz"}, ThermoTestMode.GRAD, true, 0, 0,
847             intRange(1, 8),
848             // Fill in the post-bias PE and dU/dL once the test harness supports bias deposition.
849             new double[] {0.397347160918, 0}, new double[] {2.75351951766, 0},
850             new double[] {5.37105253479, Double.NaN}, new double[][][] {
851             {{-0.31777515190383454, 0.06654081254942476, -0.061155191586619},
852                 {-1.6398611619313017, 0.5966978877811928, 0.7834547554508479},
853                 {1.0488049692859205, -0.2533323908962118, -0.6671198866431937},
854                 {-0.2740673658300599, 0.8081103620907575, -0.10766129298323851},
855                 {2.047913761610311, -0.6075094312234967, -0.06048821698815812},
856                 {-0.8650389649167101, -0.6105053956998422, 0.11296922972554538},
857                 {2.3913685674263586E-5, -1.84460182457579E-6, 6.030248161425271E-7}},
858             // Fill in once the test harness supports bias-deposition.
859             new double[7][3]}, new double[][][] {
860             {{-2.2518864370570797, 0.34060365423349903, -0.39403631649939214},
861                 {-10.009715065439687, 3.6614921088174124, 5.8097209404351},
862                 {7.999725481943763, -1.2317995270963276, -5.0518709728060704},
863                 {-2.770170337863771, 5.712033354860753, -0.7489455032193499},
864                 {13.829283560251287, -4.313426777210048, -0.6802886194434065},
865                 {-6.796164346239962, -4.168990151630222, 1.065450769329658},
866                 {-0.001072855594543585, 8.733802493437212E-5, -3.0297796539917875E-5}},
867             // Fill in once the test harness supports bias-deposition.
868             new double[7][3]}, new String[] {"-l", "0.9", "--ac", "1-3", "--ac2", "1"},
869             new String[] {}, new String[] {}},
870         {"Water-Sodium to Water Dimer Softcoring Test: L = 1.0",
871             new String[] {"water-dimer.xyz", "water-na.xyz"}, ThermoTestMode.GRAD, true, 0, 0,
872             intRange(1, 8),
873             // Fill in the post-bias PE and dU/dL once the test harness supports bias deposition.
874             new double[] {0.678991455919, 0}, new double[] {2.62955540214, 0},
875             new double[] {-9.66072732215, Double.NaN}, new double[][][] {
876             {{-0.5967303838923714, 0.09294823835932634, -0.10065639850802355},
877                 {-2.7182918130174105, 0.9717379671351778, 1.5205922317233873},
878                 {2.059991765566265, -0.34808314525235373, -1.325262659176951},
879                 {-0.6963970726796157, 1.5240570186788684, -0.18878016013974244},
880                 {3.628422351985993, -1.0902413269297175, -0.16292088956721837},
881                 {-1.6769948479628611, -1.150418751991301, 0.2570278756685478}, {0.0, 0.0, 0.0}},
882             // Fill in once the test harness supports bias-deposition.
883             new double[7][3]}, new double[][][] {
884             {{-3.3452690489458763, 0.1345828814666643, -0.35806274829197704},
885                 {-10.93663217405725, 3.541368037656955, 8.988383737482236},
886                 {12.27657065565445, -0.4907488150661682, -8.232590002516137},
887                 {-5.952373992671734, 8.67661156047595, -0.8110392583955152},
888                 {17.201456551782357, -5.049385958157128, -1.4130599622896023},
889                 {-9.243751991761942, -6.81242770637627, 1.826368234010993}, {0.0, 0.0, 0.0}},
890             // Fill in once the test harness supports bias-deposition.
891             new double[7][3]}, new String[] {"-l", "1.0", "--ac", "1-3", "--ac2", "1"},
892             new String[] {}, new String[] {}}, {"Water-Sodium to Water Dimer Free Energy Test",
893         new String[] {"water-dimer.xyz", "water-na.xyz"}, ThermoTestMode.FREE, true, 16.0, 16.0, null,
894         null, null, null, null, null,
895         new String[] {"-d", "1.0", "-l", "0.5", "--ac", "1-3", "--ac2", "1", "-n", "20000", "-Q",
896             "5000", "-k", "20.0", "-w", "20.0", "-r", "5.0", "-C", "10"},
897         new String[] {"disable-neighbor-updates", "true", "lambda-bin-width", "0.025",
898             "flambda-bin-width", "5.0", "randomseed", "2019"}, new String[] {}},
899         {"Dual Well Gradient Test: L = 0.5",
900             new String[] {"dualWell-cis.xyz", "dualWell-trans.xyz"}, ThermoTestMode.GRAD, false, 0, 0,
901             new int[] {0, 1, 2, 3}, new double[] {1.0, 1.0}, new double[] {2.0, 2.0},
902             new double[] {0, Double.NaN}, new double[][][] {
903             {{8.561159028211611E-6, 5.743324479222663E-6, -1.4984967045837012E-10},
904                 {-7.001815021252073E-6, -4.427074139684514E-6, 1.2219984010349385E-10},
905                 {-2.1489889641599235E-6, 7.92540413855834E-9, 8.443778889936074E-12},
906                 {5.896449572003855E-7, -1.3241757436767075E-6, 1.9206051464940198E-11}},
907             new double[4][3]}, new double[][][] {
908             {{-4.504521213498369E-18, -1.6250327093073752E-17, -8.651645616845026E-13},
909                 {-4.504521213498369E-18, -1.6250327093073752E-17, -8.651646266038085E-13},
910                 {4.504097697024742E-18, 1.6249944935630753E-17, 8.651645891779005E-13},
911                 {4.504203576143149E-18, 1.6249903576600125E-17, 8.651645991104025E-13}},
912             new double[4][3]}, new String[] {"-l", "0.5"},
913             new String[] {"pj.nt", "1", "lambda-bin-width", "0.02", "flambda-bin-width", "0.20",
914                 "disable-neighbor-updates", "true", "ost-temperOffset", "6.0", "randomseed", "2020"},
915             new String[0]}, {"Dual Well 1-Step MC-OST Test",
916         new String[] {"dualWell-cis.xyz",
917             "dualWell-trans.xyz"}, ThermoTestMode.FREE, true, 0, 1.0, null,
918         null, null, null, null, null,
919         new String[] {"-l", "0.5", "--bM", "0.1", "-b", "ADIABATIC", "-i", "VERLET", "-d", "2.0",
920             "-k", "1000.0", "-w", "1000.0", "-r", "1.0", "-Q", "1000", "-n", "250000", "-t",
921             "298.15", "--tp", "6.0", "--mcMD", "10", "--mcL", "0.10"},
922         new String[] {"pj.nt", "1", "lambda-bin-width", "0.02", "flambda-bin-width", "0.20",
923             "disable-neighbor-updates", "true", "ost-temperOffset", "6.0", "randomseed", "445"},
924         new String[] {"--mc", "true"}}, {"Dual Well 2-Step MC-OST Test",
925         new String[] {"dualWell-cis.xyz",
926             "dualWell-trans.xyz"}, ThermoTestMode.FREE, true, 0, 1.0, null,
927         null, null, null, null, null,
928         new String[] {"-l", "0.5", "--bM", "0.1", "-b", "ADIABATIC", "-i", "VERLET", "-d", "2.0",
929             "-k", "1000.0", "-w", "1000.0", "-r", "1.0", "-Q", "1000", "-n", "250000", "-t",
930             "298.15", "--tp", "6.0", "--mcMD", "10", "--mcL", "0.10"},
931         new String[] {"pj.nt", "1", "lambda-bin-width", "0.02", "flambda-bin-width", "0.20",
932             "disable-neighbor-updates", "true", "ost-temperOffset", "6.0", "randomseed", "445"},
933         new String[] {"--mc", "true", "--ts", "true"}}, {"Short 1-Step MC-OST Test",
934         new String[] {"dualWell-cis.xyz",
935             "dualWell-trans.xyz"}, ThermoTestMode.FREE, true, 0, 2.0, null,
936         null, null, null, null, null,
937         new String[] {"-l", "0.5", "--bM", "0.1", "-b", "ADIABATIC", "-i", "VERLET", "-d", "2.0",
938             "-k", "1000.0", "-w", "1000.0", "-r", "0.01", "-Q", "100", "-n", "200", "-t", "100.0",
939             "--tp", "6.0", "--mcMD", "10", "--mcL", "0.10"},
940         new String[] {"pj.nt", "1", "lambda-bin-width", "0.02", "flambda-bin-width", "0.20",
941             "disable-neighbor-updates", "true", "ost-temperOffset", "6.0", "randomseed", "445"},
942         new String[] {"--mc", "true"}}, {"Short 2-Step MC-OST Test",
943         new String[] {"dualWell-cis.xyz",
944             "dualWell-trans.xyz"}, ThermoTestMode.FREE, true, 0, 2.0, null,
945         null, null, null, null, null,
946         new String[] {"-l", "0.5", "--bM", "0.1", "-b", "ADIABATIC", "-i", "VERLET", "-d", "2.0",
947             "-k", "1000.0", "-w", "1000.0", "-r", "0.01", "-Q", "100", "-n", "200", "-t", "100.0",
948             "--tp", "6.0", "--mcMD", "10", "--mcL", "0.10"},
949         new String[] {"pj.nt", "1", "lambda-bin-width", "0.02", "flambda-bin-width", "0.20",
950             "disable-neighbor-updates", "true", "ost-temperOffset", "6.0", "randomseed", "445"},
951         new String[] {"--mc", "true", "--ts", "true"}}});
952   }
953 
954   /**
955    * Checks if two double values are approximately equal to within a tolerance.
956    *
957    * @param v1 One value to compare.
958    * @param v2 Second value to compare.
959    * @param absTol Tolerance for inequality (absolute, not relative).
960    * @return True if v1 approximately equal to v2.
961    */
962   private static boolean approxEquals(double v1, double v2, double absTol) {
963     double diff = v1 - v2;
964     return Math.abs(diff) < absTol;
965   }
966 
967   private static int[] intRange(int low, int high) {
968     return IntStream.range(low, high).toArray();
969   }
970 
971   @Test
972   public void testThermodynamics() {
973     if (doTest) {
974       logger.info(format(" Thermodynamics test: %s\n", info));
975       switch (mode) {
976         case HELP:
977           testHelp();
978           break;
979         case FREE:
980           testFreeEnergy();
981           break;
982         case GRAD:
983           testStaticGradients();
984           break;
985         default:
986           throw new IllegalStateException(
987               format(" Thermodynamics test mode %s not recognized!", mode));
988       }
989     } else {
990       logger.info(format(" Skipping test %s: use ffx.ci true to enable!", info));
991     }
992   }
993 
994   private void testHelp() {
995     String[] args = {"-h"};
996     binding.setVariable("args", args);
997 
998     // Construct and evaluate the Thermodynamics script.
999     Thermodynamics thermodynamics = new Thermodynamics(binding).run();
1000     algorithmsScript = thermodynamics;
1001   }
1002 
1003   private String[] assembleArgs() {
1004     List<String> argList = new ArrayList<>(flags);
1005     opts.forEach((String k, String v) -> {
1006       argList.add(k);
1007       argList.add(v);
1008     });
1009     argList.addAll(Arrays.stream(copiedFiles).map(File::getPath).collect(Collectors.toList()));
1010     return argList.toArray(new String[0]);
1011   }
1012 
1013   /**
1014    * Test a free energy evaluation; very expensive, so likely only done for one or a few tests.
1015    * Currently not implemented.
1016    */
1017   private void testFreeEnergy() {
1018     binding.setVariable("args", assembleArgs());
1019 
1020     // Construct and evaluate the Thermodynamics script.
1021     Thermodynamics thermodynamics = new Thermodynamics(binding);
1022     thermodynamics.setProperties(algorithmConfig);
1023     algorithmsScript = thermodynamics;
1024 
1025     thermodynamics.run();
1026     OrthogonalSpaceTempering orthogonalSpaceTempering = thermodynamics.getOST();
1027     Histogram histogram = orthogonalSpaceTempering.getHistogram();
1028     double delG = histogram.updateFreeEnergyDifference(true, false);
1029     assertEquals(format(" Test %s: not within tolerance %12.5g", info, feTol), freeEnergy, delG, feTol);
1030   }
1031 
1032   /** Tests gradients & energies for a static structure, before and after dropping a bias. */
1033   private void testStaticGradients() {
1034 
1035     binding.setVariable("args", assembleArgs());
1036 
1037     // Construct and evaluate the Thermodynamics script.
1038     Thermodynamics thermodynamics = new Thermodynamics(binding);
1039     thermodynamics.setProperties(algorithmConfig);
1040     algorithmsScript = thermodynamics;
1041     thermodynamics.run();
1042 
1043     OrthogonalSpaceTempering orthogonalSpaceTempering = thermodynamics.getOST();
1044     orthogonalSpaceTempering.setPropagateLambda(false);
1045     CrystalPotential under = thermodynamics.getPotential();
1046     int nVars = orthogonalSpaceTempering.getNumberOfVariables();
1047 
1048     double[] x = new double[nVars];
1049     x = orthogonalSpaceTempering.getCoordinates(x);
1050     double[] gOSTPre = new double[nVars];
1051     double[] gUnderPre = new double[nVars];
1052 
1053     logger.info(" Testing the OST potential before bias added.");
1054     EnergyResult ostPre = testGradientSet("Unbiased OST", orthogonalSpaceTempering, x, gOSTPre, 0);
1055     logger.info(" Testing the underlying CrystalPotential before bias added.");
1056     EnergyResult underPre = testGradientSet("Unbiased potential", under, x, gUnderPre, 0);
1057 
1058     // Assert that, before biases, OST and underlying potential are equal.
1059     ostPre.assertResultsEqual(underPre);
1060   }
1061 
1062   /**
1063    * Generates and tests an EnergyResult against tabulated values.
1064    *
1065    * @param description Description (such as unbiased OST)
1066    * @param potential A CrystalPotential (either OST or underlying).
1067    * @param x Coordinates.
1068    * @param g Array to add gradients to.
1069    * @param tableIndex 0 for unbiased potential, 1 for a biased potential.
1070    * @return The generated EnergyResult.
1071    */
1072   private EnergyResult testGradientSet(String description, CrystalPotential potential, double[] x,
1073       double[] g, int tableIndex) {
1074     assertTrue(format(" Potential %s is not a lambda interface!", potential),
1075         potential instanceof LambdaInterface);
1076 
1077     EnergyResult er = new EnergyResult(description, potential, x, g);
1078 
1079     checkThGradScalar(er.energy, pe, tableIndex, peTol, "potential energy");
1080     checkThGradScalar(er.firstLam, dudl, tableIndex, dudlTol, "dU/dL");
1081     checkThGradArray(er.gradient, dudx, tableIndex, dudxTol, "dU/dX gradient");
1082     if (er.hasSecondDerivatives) {
1083       checkThGradScalar(er.secondLam, d2udl2, tableIndex, d2udl2Tol, "d2U/dL2");
1084       checkThGradArray(er.lamGradient, d2udxdl, tableIndex, d2udxdlTol, "d2U/dXdL gradient");
1085     }
1086 
1087     return er;
1088   }
1089 
1090   /**
1091    * Checks a scalar value against its expected value. Mostly a convenience formatting method.
1092    *
1093    * @param actual Value from the test.
1094    * @param expected Array of expected values (see tableIndex).
1095    * @param tableIndex 0 for unbiased potential, 1 for biased OST potential.
1096    * @param tol Tolerance for this test.
1097    * @param description Scalar to be tested.
1098    */
1099   private void checkThGradScalar(double actual, double[] expected, int tableIndex, double tol,
1100       String description) {
1101     if (debugMode) {
1102       logger.info(format(" %s is %20.12g", description, actual));
1103     } else {
1104       assertEquals(format(" Expected %s %12.6g, received %12.6g from test %s", description,
1105           expected[tableIndex], actual, info), expected[tableIndex], actual, tol);
1106     }
1107   }
1108 
1109   /**
1110    * Checks an array value (generally 1-D flat) against its expected value (generally 3-D; indices
1111    * tableIndex, then atom number, then X/Y/Z).
1112    *
1113    * @param actual Array from the test, flat.
1114    * @param expected Array of expected values, indexed by tableIndex, atoms, and XYZ.
1115    * @param tableIndex 0 for unbiased potential, 1 for biased OST potential.
1116    * @param tol Tolerance for this test.
1117    * @param description Array to be tested.
1118    */
1119   private void checkThGradArray(double[] actual, double[][][] expected, int tableIndex, double tol,
1120       String description) {
1121     double[] actualSlice = new double[3];
1122     StringBuilder sb = null;
1123     for (int i = 0; i < numGradAtoms; i++) {
1124       int i3 = i * 3;
1125       arraycopy(actual, i3, actualSlice, 0, 3);
1126       if (debugMode) {
1127         if (i == 0) {
1128           sb = new StringBuilder(
1129               format("\n Array %s for test %s at atom %d\n", description, info, i));
1130           sb.append(format(" Expected %s\n", Arrays.toString(expected[tableIndex][i])));
1131         }
1132         sb.append(format("%s\n", Arrays.toString(actualSlice)));
1133       } else {
1134         double[] exp = expected[tableIndex][i];
1135         assertArrayEquals(format(" Discrepancy found on array of %s for test %s on atom %d."
1136                 + "\n Expected: %s\n Found: %s", description, info, i, Arrays.toString(exp),
1137             Arrays.toString(actualSlice)), exp, actualSlice, tol);
1138       }
1139     }
1140     if (debugMode) {
1141       logger.info(sb.toString());
1142     }
1143   }
1144 
1145   private enum ThermoTestMode {
1146     HELP, FREE, GRAD
1147   }
1148 
1149   /** Contains the result of an energy evaluation: potential energy and several derivatives. */
1150   private class EnergyResult {
1151 
1152     final double energy;
1153     final double firstLam;
1154     final double secondLam;
1155     final int nVars;
1156     final boolean hasSecondDerivatives;
1157     private final String description;
1158 
1159     private final double[] gradient;
1160     private final double[] lamGradient;
1161 
1162     public EnergyResult(String description, CrystalPotential potential, double[] x, double[] g) {
1163       this.description = description;
1164 
1165       LambdaInterface linter = (LambdaInterface) potential;
1166       energy = potential.energyAndGradient(x, g, false);
1167       firstLam = linter.getdEdL();
1168       nVars = g.length;
1169       gradient = copyOf(g, nVars);
1170 
1171       hasSecondDerivatives = !(potential instanceof OrthogonalSpaceTempering);
1172       if (hasSecondDerivatives) {
1173         secondLam = linter.getd2EdL2();
1174         lamGradient = new double[g.length];
1175         linter.getdEdXdL(lamGradient);
1176       } else {
1177         secondLam = 0;
1178         lamGradient = null;
1179       }
1180     }
1181 
1182     /**
1183      * Asserts that two energy results are equivalent to each other. Always tests U, dU/dX, and
1184      * dU/dL. Also tests d2U/dL2 and d2U/dXdL if these second gradients are available.
1185      *
1186      * <p>All values must be identical to tolerance.
1187      *
1188      * @param other Another EnergyResult
1189      */
1190     public void assertResultsEqual(EnergyResult other) {
1191       assertEquals(
1192           format(" Test %s: potential energy for %s did not " + "match %s, %12.6g != %12.6g.", info,
1193               this.toString(), other.toString(), this.energy, other.energy), this.energy,
1194           other.energy, peTol);
1195       assertEquals(format(" Test %s: dU/dL for %s did not " + "match %s, %12.6g != %12.6g.", info,
1196               this.toString(), other.toString(), this.firstLam, other.firstLam), this.firstLam,
1197           other.firstLam, dudlTol);
1198       assertArrayEquals(format(" Test %s: dU/dX for %s did not match %s.", info, this.toString(),
1199           other.toString()), this.gradient, other.gradient, dudxTol);
1200 
1201       if (hasSecondDerivatives && other.hasSecondDerivatives) {
1202         assertEquals(
1203             format(" Test %s: d2U/dL2 for %s did not " + "match %s, %12.6g != %12.6g.", info,
1204                 toString(), other.toString(), this.secondLam, other.secondLam), this.secondLam,
1205             other.secondLam, d2udl2Tol);
1206         assertArrayEquals(
1207             format(" Test %s: d2U/dXdL for %s did not match %s.", info, this.toString(),
1208                 other.toString()), this.lamGradient, other.lamGradient, d2udxdlTol);
1209       }
1210     }
1211 
1212     /**
1213      * Asserts that two energy results are not equivalent to each other. Always tests U, dU/dX, and
1214      * dU/dL. Also tests d2U/dL2 and d2U/dXdL if these second gradients are available.
1215      *
1216      * <p>Prints all equalities, fails if no inequalities are found. At least one value must not be
1217      * identical to tolerance.
1218      *
1219      * @param other Another EnergyResult
1220      */
1221     public void assertResultsInequal(EnergyResult other) {
1222       int diffsFound = 0;
1223       StringBuilder sb = new StringBuilder(" Equalities found between ");
1224       sb = sb.append(toString()).append(" and ").append(other.toString());
1225       sb = sb.append(" for test ").append(info);
1226       boolean equalFound = false;
1227 
1228       if (approxEquals(energy, other.energy, peTol)) {
1229         equalFound = true;
1230         sb.append(format("\n Potential energy %12.6g == other potential energy %12.6g", energy,
1231             other.energy));
1232       } else {
1233         ++diffsFound;
1234       }
1235 
1236       if (approxEquals(firstLam, other.firstLam, dudlTol)) {
1237         equalFound = true;
1238         sb.append(format("\n Lambda derivative %12.6g == other lambda derivative %12.6g", firstLam,
1239             other.firstLam));
1240       } else {
1241         ++diffsFound;
1242       }
1243 
1244       for (int i = 0; i < nVars; i++) {
1245         if (approxEquals(gradient[i], other.gradient[i], dudxTol)) {
1246           equalFound = true;
1247           sb.append(format("\n Gradient %d %12.6g == other gradient %12.6g", i, gradient[i],
1248               other.gradient[i]));
1249         } else {
1250           ++diffsFound;
1251         }
1252       }
1253 
1254       if (hasSecondDerivatives && other.hasSecondDerivatives) {
1255         if (approxEquals(secondLam, other.secondLam, d2udl2Tol)) {
1256           equalFound = true;
1257           sb.append(
1258               format("\n Lambda derivative %12.6g == other lambda derivative %12.6g", secondLam,
1259                   other.secondLam));
1260         } else {
1261           ++diffsFound;
1262         }
1263 
1264         for (int i = 0; i < nVars; i++) {
1265           if (approxEquals(lamGradient[i], other.lamGradient[i], d2udxdlTol)) {
1266             equalFound = true;
1267             sb.append(format("\n Lambda gradient %d %12.6g == other lambda gradient %12.6g", i,
1268                 lamGradient[i], other.lamGradient[i]));
1269           } else {
1270             ++diffsFound;
1271           }
1272         }
1273       }
1274 
1275       if (equalFound) {
1276         logger.info(sb.toString());
1277       }
1278       assertTrue(format(" No inequalities found between %s and %s for test %s!", this.toString(),
1279           other.toString(), info), diffsFound > 0);
1280     }
1281 
1282     @Override
1283     public String toString() {
1284       return description;
1285     }
1286   }
1287 }