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