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.dynamics;
39  
40  import edu.rit.pj.Comm;
41  import ffx.algorithms.AlgorithmListener;
42  import ffx.algorithms.Terminatable;
43  import ffx.algorithms.dynamics.integrators.BetterBeeman;
44  import ffx.algorithms.dynamics.integrators.Integrator;
45  import ffx.algorithms.dynamics.integrators.IntegratorEnum;
46  import ffx.algorithms.dynamics.integrators.Respa;
47  import ffx.algorithms.dynamics.integrators.Stochastic;
48  import ffx.algorithms.dynamics.integrators.VelocityVerlet;
49  import ffx.algorithms.dynamics.thermostats.Adiabatic;
50  import ffx.algorithms.dynamics.thermostats.Berendsen;
51  import ffx.algorithms.dynamics.thermostats.Bussi;
52  import ffx.algorithms.dynamics.thermostats.Thermostat;
53  import ffx.algorithms.dynamics.thermostats.ThermostatEnum;
54  import ffx.algorithms.thermodynamics.OrthogonalSpaceTempering;
55  import ffx.crystal.Crystal;
56  import ffx.numerics.Constraint;
57  import ffx.numerics.Potential;
58  import ffx.numerics.math.RunningStatistics;
59  import ffx.potential.MolecularAssembly;
60  import ffx.potential.SystemState;
61  import ffx.potential.UnmodifiableState;
62  import ffx.potential.bonded.Atom;
63  import ffx.potential.bonded.LambdaInterface;
64  import ffx.potential.extended.ExtendedSystem;
65  import ffx.potential.openmm.OpenMMDualTopologyEnergy;
66  import ffx.potential.openmm.OpenMMEnergy;
67  import ffx.potential.parameters.ForceField;
68  import ffx.potential.parsers.DYNFilter;
69  import ffx.potential.parsers.PDBFilter;
70  import ffx.potential.parsers.XPHFilter;
71  import ffx.potential.parsers.XYZFilter;
72  import ffx.utilities.TinkerUtils;
73  import org.apache.commons.configuration2.CompositeConfiguration;
74  
75  import java.io.File;
76  import java.util.ArrayList;
77  import java.util.Arrays;
78  import java.util.EnumSet;
79  import java.util.List;
80  import java.util.logging.Level;
81  import java.util.logging.Logger;
82  
83  import static ffx.utilities.Constants.FSEC_TO_PSEC;
84  import static ffx.utilities.Constants.KCAL_TO_GRAM_ANG2_PER_PS2;
85  import static ffx.utilities.Constants.NS2SEC;
86  import static ffx.utilities.FileUtils.relativePathTo;
87  import static java.lang.String.format;
88  import static java.util.Arrays.fill;
89  import static org.apache.commons.io.FilenameUtils.getExtension;
90  import static org.apache.commons.io.FilenameUtils.removeExtension;
91  
92  /**
93   * Run NVE, NVT, or NPT molecular dynamics.
94   *
95   * @author Michael J. Schnieders
96   * @since 1.0
97   */
98  public class MolecularDynamics implements Runnable, Terminatable {
99  
100     private static final Logger logger = Logger.getLogger(MolecularDynamics.class.getName());
101 
102     /**
103      * The default interval (in picoseconds) between logging the current state.
104      */
105     private static final double DEFAULT_LOG_INTERVAL = 0.25;
106     /**
107      * The default interval (in picoseconds) between writing the current state to a DYN file.
108      */
109     private static final double DEFAULT_RESTART_INTERVAL = 1.0;
110     /**
111      * The default interval (in picoseconds) between writing the current coordinates.
112      */
113     private static final double DEFAULT_TRAJECTORY_INTERVAL = 10.0;
114     /**
115      * By default, wait 25000 nsec (25 usec) in between polling the dynamics thread.
116      */
117     private static final int DEFAULT_DYNAMICS_SLEEP_TIME = 25000;
118 
119     /**
120      * MolecularAssembly to run dynamics on.
121      * <p>
122      * For dual or quad topology simulations, this is an array of length 2 or 4.
123      */
124     protected MolecularAssembly[] molecularAssembly;
125     /**
126      * Propagate dynamics on this potential surface.
127      */
128     private final Potential potential;
129     /**
130      * An Algorithm Listener to send updates to the GUI.
131      */
132     protected final AlgorithmListener algorithmListener;
133     /**
134      * Integrator instance.
135      */
136     private final Integrator integrator;
137     /**
138      * Thermostat instance.
139      */
140     private Thermostat thermostat;
141     /**
142      * Flag to indicate use of constant pressure.
143      */
144     boolean constantPressure = false;
145     /**
146      * The Barostat in use, or null if constantPressure is false.
147      */
148     Barostat barostat = null;
149     /**
150      * Stores the current molecular dynamics state.
151      */
152     protected final SystemState state;
153     /**
154      * State of the system as of the last init call (and start of the last dynamics run).
155      */
156     protected UnmodifiableState initialState;
157     /**
158      * A copy of the current MD State. This can be used to revert to a previous state after a rejected
159      * MC Move.
160      */
161     private UnmodifiableState storedState;
162 
163     /**
164      * Temperature Stats for logging.
165      */
166     private final RunningStatistics temperatureStats = new RunningStatistics();
167     /**
168      * Potential Energy Stats for logging.
169      */
170     private final RunningStatistics potentialEnergyStats = new RunningStatistics();
171     /**
172      * Kinetic Energy Stats for logging.
173      */
174     private final RunningStatistics kineticEnergyStats = new RunningStatistics();
175     /**
176      * Total Energy Stats for logging.
177      */
178     private final RunningStatistics totalEnergyStats = new RunningStatistics();
179 
180     /**
181      * The time step (picoseconds).
182      * <p>
183      * The method <code>setTimeStep</code> should always be used to set this value.
184      */
185     protected double dt = 0.001;
186     /**
187      * The number of MD steps to take.
188      */
189     private long nSteps = 1000;
190     /**
191      * Target temperature. ToDo: use the Thermostat instance.
192      */
193     double targetTemperature = 298.15;
194     /**
195      * Flag to indicate velocities should be initialized.
196      */
197     protected boolean initVelocities = true;
198     /**
199      * Whether MD handles writing restart/trajectory files itself (true), or will be commanded by
200      * another class (false) to do it. The latter is true for MC-OST, for example.
201      */
202     protected boolean automaticWriteouts = true;
203     /**
204      * The total simulation time.
205      */
206     protected double totalSimTime = 0.0;
207 
208     /**
209      * Time between writing out restart/checkpoint files in picoseconds.
210      */
211     protected double restartInterval = DEFAULT_RESTART_INTERVAL;
212     /**
213      * Time steps between writing out restart/checkpoint files. Set by the init method.
214      */
215     protected int restartFrequency;
216     /**
217      * Time between appending to the trajectory file in picoseconds.
218      */
219     private double trajectoryInterval = DEFAULT_TRAJECTORY_INTERVAL;
220     /**
221      * Time steps between adding a frame to the trajectory file. Set by the init method.
222      */
223     protected int trajectoryFrequency;
224     /**
225      * Time between logging information to the screen in picoseconds.
226      */
227     private double logInterval = DEFAULT_LOG_INTERVAL;
228     /**
229      * Time steps between logging information to the screen. Set by the init method.
230      */
231     protected int logFrequency;
232 
233     /**
234      * File type to use when saving files.
235      */
236     protected String fileType = "XYZ";
237     /**
238      * Save snapshots in PDB format.
239      */
240     boolean saveSnapshotAsPDB = true;
241     /**
242      * PDB Filter.
243      */
244     PDBFilter[] pdbFilter = null;
245     /**
246      * Dynamics restart file.
247      */
248     File restartFile = null;
249     /**
250      * Flag to indicate loading of restart file.
251      */
252     boolean loadRestart = false;
253     /**
254      * Filter to parse the dynamics restart file.
255      */
256     DYNFilter dynFilter = null;
257     /**
258      * If asked to perform dynamics with a null dynamics file, write here.
259      */
260     private File fallbackDynFile;
261 
262     /**
263      * Log basic information at this level.
264      */
265     Level basicLogging = Level.INFO;
266     /**
267      * Indicates how verbose MD should be.
268      */
269     private MDVerbosity verbosityLevel = MDVerbosity.VERBOSE;
270 
271     /**
272      * Flag to indicate dynamics has been initialized.
273      */
274     boolean initialized = false;
275     /**
276      * Flag to indicate MD should be terminated.
277      */
278     private boolean terminate = false;
279     /**
280      * Flag to indicate a run has finished.
281      */
282     protected boolean done;
283 
284     /**
285      * ESV System.
286      */
287     private ExtendedSystem esvSystem;
288     /**
289      * ESV Stochastic integrator.
290      */
291     private Stochastic esvIntegrator;
292     /**
293      * ESV Adiabatic thermostat.
294      */
295     private Adiabatic esvThermostat;
296     /**
297      * Frequency to print ESV info.
298      */
299     private int printEsvFrequency = -1;
300 
301     /**
302      * If true, the lambda value will be updated each integration time step.
303      */
304     private boolean nonEquilibriumLambda;
305     /**
306      * Support for non-equilibrium lambda dynamics.
307      */
308     private NonEquilbriumDynamics nonEquilibriumDynamics;
309 
310     /**
311      * Constructor for MolecularDynamics.
312      *
313      * @param assembly            a {@link ffx.potential.MolecularAssembly} object.
314      * @param potentialEnergy     a {@link ffx.numerics.Potential} object.
315      * @param listener            a {@link ffx.algorithms.AlgorithmListener} object.
316      * @param requestedThermostat a {@link ThermostatEnum} object.
317      * @param requestedIntegrator a {@link ffx.algorithms.dynamics.integrators.IntegratorEnum}
318      *                            object.
319      */
320     public MolecularDynamics(MolecularAssembly assembly, Potential potentialEnergy,
321                              AlgorithmListener listener, ThermostatEnum requestedThermostat,
322                              IntegratorEnum requestedIntegrator) {
323         this.molecularAssembly = new MolecularAssembly[1];
324         this.molecularAssembly[0] = assembly;
325         this.algorithmListener = listener;
326         this.potential = potentialEnergy;
327         if (potentialEnergy instanceof Barostat) {
328             constantPressure = true;
329             barostat = (Barostat) potentialEnergy;
330         }
331 
332         int numberOfVariables = potentialEnergy.getNumberOfVariables();
333         state = new SystemState(numberOfVariables);
334         state.setMass(potential.getMass());
335 
336         CompositeConfiguration properties = molecularAssembly[0].getProperties();
337         // If an Integrator wasn't passed to the MD constructor, check for one specified as a property.
338         if (requestedIntegrator == null) {
339             String integrate = properties.getString("integrate", "VERLET").trim();
340             try {
341                 integrate = integrate.toUpperCase().replace("-", "_");
342                 requestedIntegrator = IntegratorEnum.valueOf(integrate.toUpperCase());
343             } catch (Exception e) {
344                 requestedIntegrator = IntegratorEnum.VERLET;
345             }
346         }
347 
348         boolean oMMLogging = potential instanceof OpenMMEnergy;
349         List<Constraint> constraints = potentialEnergy.getConstraints();
350 
351         // Set the integrator.
352         integrator = switch (requestedIntegrator) {
353             case RESPA, MTS -> {
354                 Respa respa = new Respa(state);
355                 int in = molecularAssembly[0].getProperties().getInt("respa-dt", 4);
356                 if (in < 2) {
357                     in = 2;
358                 }
359                 if (!oMMLogging) {
360                     respa.setInnerTimeSteps(in);
361                 }
362                 logger.log(Level.FINE, format(" Created a RESPA integrator with %d inner time steps.", in));
363                 yield respa;
364             }
365             case STOCHASTIC, LANGEVIN -> {
366                 double friction = properties.getDouble("friction", 91.0);
367                 logger.log(Level.FINE, format(" Friction set at %.3f collisions/picosecond", friction));
368                 Stochastic stochastic = new Stochastic(friction, state);
369                 if (properties.containsKey("randomseed")) {
370                     stochastic.setRandomSeed(properties.getInt("randomseed", 0));
371                 }
372                 // The stochastic dynamics integration procedure will thermostat
373                 // the system. The ADIABTIC thermostat just serves to report the
374                 // temperature and initialize velocities if necessary.
375                 requestedThermostat = ThermostatEnum.ADIABATIC;
376                 yield stochastic;
377             }
378             case BEEMAN -> new BetterBeeman(state);
379             default -> new VelocityVerlet(state);
380         };
381 
382         integrator.addConstraints(constraints);
383 
384         // If a Thermostat wasn't passed to the MD constructor, check for one specified as a property.
385         if (requestedThermostat == null) {
386             String thermo = properties.getString("thermostat", "Berendsen").trim();
387             try {
388                 thermo = thermo.toUpperCase().replace("-", "_");
389                 requestedThermostat = ThermostatEnum.valueOf(thermo.toUpperCase());
390             } catch (Exception e) {
391                 requestedThermostat = ThermostatEnum.BERENDSEN;
392             }
393         }
394 
395         // Set the thermostat.
396         thermostat = switch (requestedThermostat) {
397             case BERENDSEN -> {
398                 double tau = properties.getDouble("tau-temperature", 0.2);
399                 yield new Berendsen(state, potentialEnergy.getVariableTypes(), targetTemperature, tau,
400                         constraints);
401             }
402             case BUSSI -> {
403                 double tau = properties.getDouble("tau-temperature", 0.2);
404                 Bussi bussi = new Bussi(state, potentialEnergy.getVariableTypes(), targetTemperature, tau,
405                         constraints);
406                 if (properties.containsKey("randomseed")) {
407                     bussi.setRandomSeed(properties.getInt("randomseed", 0));
408                 }
409                 yield bussi;
410             }
411             default -> new Adiabatic(state, potentialEnergy.getVariableTypes(), constraints);
412         };
413 
414         if (properties.containsKey("randomseed")) {
415             thermostat.setRandomSeed(properties.getInt("randomseed", 0));
416         }
417 
418         // For Stochastic dynamics, center of mass motion will not be removed.
419         if (integrator instanceof Stochastic) {
420             boolean removecom = assembly.getForceField().getBoolean("removecom", false);
421             thermostat.setRemoveCenterOfMassMotion(removecom);
422             if (removecom) {
423                 logger.info(" Removing center of mass motion from stochastic simulation.");
424             }
425         }
426 
427         done = true;
428         fallbackDynFile = defaultFallbackDyn(assembly);
429     }
430 
431     /**
432      * Method that determines whether a dynamics is done by the java implementation native to ffx or
433      * the OpenMM implementation
434      *
435      * @param assembly            a {@link ffx.potential.MolecularAssembly} object.
436      * @param potentialEnergy     a {@link ffx.numerics.Potential} object.
437      * @param listener            a {@link ffx.algorithms.AlgorithmListener} object.
438      * @param requestedThermostat a {@link ThermostatEnum} object.
439      * @param requestedIntegrator a {@link ffx.algorithms.dynamics.integrators.IntegratorEnum}
440      *                            object.
441      * @return a {@link MolecularDynamics} object.
442      */
443     public static MolecularDynamics dynamicsFactory(MolecularAssembly assembly,
444                                                     Potential potentialEnergy, AlgorithmListener listener, ThermostatEnum requestedThermostat,
445                                                     IntegratorEnum requestedIntegrator) {
446 
447         return dynamicsFactory(assembly, potentialEnergy, listener, requestedThermostat,
448                 requestedIntegrator, defaultEngine(assembly, potentialEnergy));
449     }
450 
451     /**
452      * dynamicsFactory.
453      *
454      * @param assembly            a {@link ffx.potential.MolecularAssembly} object.
455      * @param potentialEnergy     a {@link ffx.numerics.Potential} object.
456      * @param listener            a {@link ffx.algorithms.AlgorithmListener} object.
457      * @param requestedThermostat a {@link ThermostatEnum} object.
458      * @param requestedIntegrator a {@link ffx.algorithms.dynamics.integrators.IntegratorEnum}
459      *                            object.
460      * @param engine              a {@link MDEngine} object.
461      * @return a {@link MolecularDynamics} object.
462      */
463     public static MolecularDynamics dynamicsFactory(MolecularAssembly assembly,
464                                                     Potential potentialEnergy, AlgorithmListener listener, ThermostatEnum requestedThermostat,
465                                                     IntegratorEnum requestedIntegrator, MDEngine engine) {
466         switch (engine) {
467             case OPENMM, OMM -> {
468                 Barostat barostat = null;
469                 Potential openmmPotential = potentialEnergy;
470                 if (potentialEnergy instanceof Barostat) {
471                     barostat = (Barostat) potentialEnergy;
472                     logger.info(" dynamicsFactory: Molecular dynamics has barostat:" + barostat);
473                     openmmPotential = barostat.getCrystalPotential();
474                 }
475                 if (openmmPotential instanceof OpenMMEnergy ||
476                         openmmPotential instanceof OpenMMDualTopologyEnergy) {
477                     MolecularDynamicsOpenMM molecularDynamicsOpenMM =
478                             new MolecularDynamicsOpenMM(assembly, openmmPotential, listener,
479                                     requestedThermostat, requestedIntegrator);
480                     if (barostat != null) {
481                         molecularDynamicsOpenMM.setBarostat(barostat);
482                     }
483                     return molecularDynamicsOpenMM;
484                 } else {
485                     logger.info(format(" Requested OpenMM molecular dynamics engine %s, but the potential does not use OpenMM: %s",
486                             engine, potentialEnergy));
487                     return new MolecularDynamics(assembly, potentialEnergy, listener, requestedThermostat, requestedIntegrator);
488                 }
489             }
490             default -> {
491                 return new MolecularDynamics(assembly, potentialEnergy, listener, requestedThermostat, requestedIntegrator);
492             }
493         }
494     }
495 
496     private static MDEngine defaultEngine(MolecularAssembly molecularAssembly,
497                                           Potential potentialEnergy) {
498         CompositeConfiguration properties = molecularAssembly.getProperties();
499         String mdEngine = properties.getString("MD-engine");
500         if (mdEngine != null) {
501             if (mdEngine.equalsIgnoreCase("OMM")) {
502                 logger.info(" Creating OpenMM Dynamics Object");
503                 return MDEngine.OPENMM;
504             } else {
505                 logger.info(" Creating FFX Dynamics Object");
506                 return MDEngine.FFX;
507             }
508         } else {
509             // TODO: Replace this with a better check.
510             boolean ommLeaves = potentialEnergy.getUnderlyingPotentials().stream()
511                     .anyMatch((Potential p) -> p instanceof OpenMMEnergy);
512             ommLeaves = ommLeaves || potentialEnergy instanceof OpenMMEnergy;
513             if (ommLeaves) {
514                 return MDEngine.OPENMM;
515             } else {
516                 return MDEngine.FFX;
517             }
518         }
519     }
520 
521     /**
522      * Creates the "default" fallback dynamics file object (does not create actual file!)
523      *
524      * @param assembly First assembly.
525      * @return Default fallback file.
526      */
527     private static File defaultFallbackDyn(MolecularAssembly assembly) {
528         String firstFileName = removeExtension(assembly.getFile().getAbsolutePath());
529         return new File(firstFileName + ".dyn");
530     }
531 
532     /**
533      * Adds a MolecularAssembly to be tracked by this MolecularDynamics. Note: does not affect the
534      * underlying Potential.
535      *
536      * @param assembly A MolecularAssembly to be tracked.
537      */
538     public void addAssembly(MolecularAssembly assembly) {
539         molecularAssembly = Arrays.copyOf(molecularAssembly, molecularAssembly.length + 1);
540         molecularAssembly[molecularAssembly.length - 1] = assembly;
541     }
542 
543     /**
544      * attachExtendedSystem.
545      *
546      * @param system a {@link ffx.potential.extended.ExtendedSystem} object.
547      */
548     public void attachExtendedSystem(ExtendedSystem system, double reportFreq) {
549         if (esvSystem != null) {
550             logger.warning("An ExtendedSystem is already attached to this MD!");
551         }
552         esvSystem = system;
553         SystemState esvState = esvSystem.getState();
554         this.esvIntegrator = new Stochastic(esvSystem.getThetaFriction(), esvState);
555         if (!esvSystem.getConstraints().isEmpty()) {
556             esvIntegrator.addConstraints(esvSystem.getConstraints());
557         }
558         this.esvThermostat = new Adiabatic(esvState, potential.getVariableTypes());
559         printEsvFrequency = intervalToFreq(reportFreq, "Reporting (logging) interval");
560         logger.info(
561                 format("  Attached extended system (%s) to molecular dynamics.", esvSystem.toString()));
562         logger.info(format("  Extended System Theta Friction: %f", esvSystem.getThetaFriction()));
563         logger.info(format("  Extended System Theta Mass: %f", esvSystem.getThetaMass()));
564         logger.info(format("  Extended System Lambda Print Frequency: %d (fsec)", printEsvFrequency));
565     }
566 
567     /**
568      * Enables non-equilibrium lambda dynamics.
569      *
570      * @param nonEquilibrium True if non-equilibrium lambda dynamics should be enabled.
571      * @param nEQSteps       Number of lambda steps.
572      * @param reverseNEQ     True if lambda path should be reversed.
573      */
574     public void setNonEquilibriumLambda(boolean nonEquilibrium, int nEQSteps, boolean reverseNEQ) {
575         nonEquilibriumLambda = nonEquilibrium;
576         if (nonEquilibriumLambda) {
577             nonEquilibriumDynamics = new NonEquilbriumDynamics(nEQSteps, reverseNEQ);
578         } else {
579             nonEquilibriumDynamics = null;
580         }
581     }
582 
583     /**
584      * Blocking molecular dynamics. When this method returns, the MD run is done.
585      *
586      * @param nSteps             Number of MD steps
587      * @param timeStep           Time step in femtoseconds
588      * @param loggingInterval    Interval between printing/logging information in picoseconds.
589      * @param trajectoryInterval Interval between adding a frame to the trajectory file in
590      *                           picoseconds.
591      * @param temperature        Temperature in Kelvins.
592      * @param initVelocities     Initialize new velocities from a Maxwell-Boltzmann distribution.
593      * @param fileType           XYZ or ARC to save to .arc, PDB for .pdb files
594      * @param restartInterval    Interval between writing new restart files in picoseconds.
595      * @param dyn                A {@link java.io.File} object to write the restart file to.
596      */
597     public void dynamic(final long nSteps, final double timeStep, final double loggingInterval,
598                         final double trajectoryInterval, final double temperature, final boolean initVelocities,
599                         String fileType, double restartInterval, final File dyn) {
600         this.fileType = fileType;
601         setRestartFrequency(restartInterval);
602         dynamic(nSteps, timeStep, loggingInterval, trajectoryInterval, temperature, initVelocities, dyn);
603     }
604 
605     /**
606      * Blocking molecular dynamics. When this method returns, the MD run is done.
607      *
608      * @param nSteps             Number of MD steps
609      * @param timeStep           Time step (fsec)
610      * @param loggingInterval    Interval between printing/logging information in picoseconds.
611      * @param trajectoryInterval Interval between adding a frame to the trajectory file in
612      *                           picoseconds.
613      * @param temperature        Temperature in Kelvins.
614      * @param initVelocities     Initialize new velocities from a Maxwell-Boltzmann distribution.
615      * @param dyn                A {@link java.io.File} object to write the restart file to.
616      */
617     public void dynamic(final long nSteps, final double timeStep, final double loggingInterval,
618                         final double trajectoryInterval, final double temperature, final boolean initVelocities,
619                         final File dyn) {
620         // Return if already running;
621         // Could happen if two threads call dynamic on the same MolecularDynamics instance.
622         if (!done) {
623             logger.warning(" Programming error - a thread invoked dynamic when it was already running.");
624             return;
625         }
626 
627         init(nSteps, timeStep, loggingInterval, trajectoryInterval, fileType, restartInterval,
628                 temperature, initVelocities, dyn);
629 
630         Thread dynamicThread = new Thread(this);
631         dynamicThread.start();
632         synchronized (this) {
633             try {
634                 while (dynamicThread.isAlive()) {
635                     wait(0, DEFAULT_DYNAMICS_SLEEP_TIME);
636                 }
637             } catch (InterruptedException e) {
638                 String message = " Molecular dynamics interrupted.";
639                 logger.log(Level.WARNING, message, e);
640             }
641         }
642         if (!verbosityLevel.isQuiet()) {
643             logger.info(" Done with an MD round.");
644         }
645     }
646 
647     /**
648      * Returns the MolecularAssembly array.
649      *
650      * @return A copy of the MolecularAssembly array.
651      */
652     public MolecularAssembly[] getAssemblyArray() {
653         return molecularAssembly.clone();
654     }
655 
656     /**
657      * Returns the associated dynamics file.
658      *
659      * @return Dynamics restart File.
660      */
661     public File getDynFile() {
662         return restartFile;
663     }
664 
665     /**
666      * Gets the kinetic energy at the start of the last dynamics run.
667      *
668      * @return Kinetic energy at the start of the run.
669      */
670     public double getInitialKineticEnergy() {
671         return initialState.kineticEnergy();
672     }
673 
674     /**
675      * Gets the potential energy at the start of the last dynamics run.
676      *
677      * @return potential energy at the start of the run.
678      */
679     public double getInitialPotentialEnergy() {
680         return initialState.potentialEnergy();
681     }
682 
683     /**
684      * Gets the temperature at the start of the last dynamics run.
685      *
686      * @return temperature at the start of the run.
687      */
688     public double getInitialTemperature() {
689         return initialState.temperature();
690     }
691 
692     /**
693      * Gets the total energy at the start of the last dynamics run.
694      *
695      * @return total energy at the start of the run.
696      */
697     public double getInitialTotalEnergy() {
698         return initialState.getTotalEnergy();
699     }
700 
701     /**
702      * getIntervalSteps.
703      *
704      * @return Always 1 for this implementation.
705      */
706     public int getIntervalSteps() {
707         return 1;
708     }
709 
710     /**
711      * No-op; FFX does not need to occasionally return information from FFX.
712      *
713      * @param intervalSteps Ignored.
714      */
715     public void setIntervalSteps(int intervalSteps) {
716         // Not meaningful for FFX MD.
717     }
718 
719     /**
720      * Get the system kinetic energy.
721      *
722      * @return kinetic energy.
723      */
724     public double getKineticEnergy() {
725         return state.getKineticEnergy();
726     }
727 
728     /**
729      * Get the system potential energy.
730      *
731      * @return potential energy.
732      */
733     public double getPotentialEnergy() {
734         return state.getPotentialEnergy();
735     }
736 
737     /**
738      * Get the current temperature of the system
739      *
740      * @return currentTemperature
741      */
742     public double getTemperature() {
743         return state.getTemperature();
744     }
745 
746     /**
747      * Getter for the field <code>thermostat</code>.
748      *
749      * @return a {@link ffx.algorithms.dynamics.thermostats.Thermostat} object.
750      */
751     public Thermostat getThermostat() {
752         return thermostat;
753     }
754 
755     /**
756      * Setter for the field <code>thermostat</code>.
757      *
758      * @param thermostat a {@link ffx.algorithms.dynamics.thermostats.Thermostat} object.
759      */
760     public void setThermostat(Thermostat thermostat) {
761         this.thermostat = thermostat;
762     }
763 
764     /**
765      * getTimeStep.
766      *
767      * @return Time step in picoseconds.
768      */
769     public double getTimeStep() {
770         return dt;
771     }
772 
773     /**
774      * Sets the time step and resets frequencies based on stored intervals.
775      *
776      * @param step Time step in femtoseconds.
777      */
778     private void setTimeStep(double step) {
779         dt = step * FSEC_TO_PSEC;
780         // Reset frequencies to be consistent with new time step.
781         setRestartFrequency(restartInterval);
782         setLoggingFrequency(logInterval);
783         setTrajectoryFrequency(trajectoryInterval);
784     }
785 
786     /**
787      * Get the total system energy (kinetic plus potential).
788      *
789      * @return total energy.
790      */
791     public double getTotalEnergy() {
792         return state.getTotalEnergy();
793     }
794 
795     public MDVerbosity getVerbosityLevel() {
796         return verbosityLevel;
797     }
798 
799     public void setVerbosityLevel(MDVerbosity level) {
800         verbosityLevel = level;
801         if (level == MDVerbosity.SILENT) {
802             basicLogging = Level.FINE;
803         } else {
804             basicLogging = Level.INFO;
805         }
806     }
807 
808     /**
809      * init
810      *
811      * @param nSteps             Number of MD steps
812      * @param timeStep           Time step in femtoseconds
813      * @param loggingInterval    Interval between printing/logging information in picoseconds.
814      * @param trajectoryInterval Interval between adding a frame to the trajectory file in
815      *                           picoseconds.
816      * @param fileType           XYZ or ARC to save to .arc, PDB for .pdb files
817      * @param restartInterval    Interval between writing new restart files in picoseconds.
818      * @param temperature        Temperature in Kelvins.
819      * @param initVelocities     Initialize new velocities from a Maxwell-Boltzmann distribution.
820      * @param dyn                A {@link java.io.File} object to write the restart file to.
821      */
822     public void init(final long nSteps, final double timeStep, final double loggingInterval,
823                      final double trajectoryInterval, final String fileType, final double restartInterval,
824                      final double temperature, final boolean initVelocities, final File dyn) {
825 
826         // Return if already running.
827         if (!done) {
828             logger.warning(
829                     " Programming error - attempt to modify parameters of a running MolecularDynamics instance.");
830             return;
831         }
832 
833         if (integrator instanceof Stochastic) {
834             if (constantPressure) {
835                 logger.log(basicLogging, "\n Stochastic dynamics in the NPT ensemble");
836             } else {
837                 logger.log(basicLogging, "\n Stochastic dynamics in the NVT ensemble");
838             }
839         } else if (!(thermostat instanceof Adiabatic)) {
840             if (constantPressure) {
841                 logger.log(basicLogging, "\n Molecular dynamics in the NPT ensemble");
842             } else {
843                 logger.log(basicLogging, "\n Molecular dynamics in the NVT ensemble");
844             }
845         } else {
846             if (constantPressure) {
847                 logger.severe("\n NPT Molecular dynamics requires a thermostat");
848             } else {
849                 logger.log(basicLogging, "\n Molecular dynamics in the NVE ensemble");
850             }
851         }
852 
853         this.nSteps = nSteps;
854         totalSimTime = 0.0;
855 
856         // Convert the time step from femtoseconds to picoseconds.
857         setTimeStep(timeStep);
858 
859         // Convert intervals in ps to frequencies in time steps.
860         setLoggingFrequency(loggingInterval);
861         setTrajectoryFrequency(trajectoryInterval);
862         setRestartFrequency(restartInterval);
863 
864         checkFrequency("Reporting           ", logFrequency);
865         if (automaticWriteouts) {
866             // Only sanity check these values if MD is doing this itself.
867             checkFrequency("Trajectory Write-Out", trajectoryFrequency);
868             checkFrequency("Restart             ", restartFrequency);
869         }
870 
871         // Set snapshot file type.
872         saveSnapshotAsPDB = true;
873         if (fileType.equalsIgnoreCase("XYZ") || fileType.equalsIgnoreCase("ARC")) {
874             saveSnapshotAsPDB = false;
875         } else if (!fileType.equalsIgnoreCase("PDB")) {
876             logger.warning("Snapshot file type unrecognized; saving snapshots as PDB.\n");
877         }
878 
879         setArchiveFile();
880 
881         this.restartFile = (dyn == null) ? fallbackDynFile : dyn;
882         loadRestart = restartFile.exists() && !initialized;
883 
884         if (dynFilter == null) {
885             dynFilter = new DYNFilter(molecularAssembly[0].getName());
886         }
887 
888         this.targetTemperature = temperature;
889         this.initVelocities = initVelocities;
890         done = false;
891 
892         if (loadRestart) {
893             logger.info("  Continuing from " + restartFile.getAbsolutePath());
894         }
895 
896         if (!verbosityLevel.isQuiet()) {
897             logger.info(format("  Integrator:          %15s", integrator.toString()));
898             logger.info(format("  Thermostat:          %15s", thermostat.toString()));
899             logger.info(format("  Number of steps:     %8d", nSteps));
900             logger.info(format("  Time step:           %8.3f (fsec)", timeStep));
901             logger.info(format("  Print interval:      %8.3f (psec)", loggingInterval));
902             logger.info(format("  Save interval:       %8.3f (psec)", trajectoryInterval));
903             if (molecularAssembly.length > 1) {
904                 for (int i = 0; i < molecularAssembly.length; i++) {
905                     File archiveFile = molecularAssembly[i].getArchiveFile();
906                     logger.info(format("  Archive file %3d: %s", (i + 1), archiveFile.getAbsolutePath()));
907                 }
908             } else {
909                 logger.info(format("  Archive file:     %s",
910                         molecularAssembly[0].getArchiveFile().getAbsolutePath()));
911             }
912             logger.info(format("  Restart file:     %s", restartFile.getAbsolutePath()));
913         }
914     }
915 
916     /**
917      * A version of init with the original method header. Redirects to the new method with default
918      * values for added parameters. Needed by (at least) ReplicaExchange, which calls this directly.
919      *
920      * @param nSteps             Number of MD steps
921      * @param timeStep           Time step in femtoseconds
922      * @param loggingInterval    Interval between printing/logging information in picoseconds.
923      * @param trajectoryInterval Interval between adding a frame to the trajectory file in
924      *                           picoseconds.
925      * @param temperature        Temperature in Kelvins.
926      * @param initVelocities     Initialize new velocities from a Maxwell-Boltzmann distribution.
927      * @param dyn                A {@link java.io.File} object to write the restart file to.
928      */
929     public void init(final long nSteps, final double timeStep, final double loggingInterval,
930                      final double trajectoryInterval, final double temperature, final boolean initVelocities,
931                      final File dyn) {
932         init(nSteps, timeStep, loggingInterval, trajectoryInterval, "XYZ", restartInterval, temperature,
933                 initVelocities, dyn);
934     }
935 
936     /**
937      * {@inheritDoc}
938      */
939     @Override
940     public void run() {
941         try {
942             preRunOps();
943         } catch (IllegalStateException ise) {
944             return;
945         }
946         initializeEnergies();
947         postInitEnergies();
948         mainLoop();
949         postRun();
950     }
951 
952     /**
953      * Enable or disable automatic writeout of trajectory snapshots and restart files.
954      *
955      * <p>Primarily intended for use with MC-OST, which handles the timing itself.
956      *
957      * @param autoWriteout Whether to automatically write out trajectory and restart files.
958      */
959     public void setAutomaticWriteouts(boolean autoWriteout) {
960         this.automaticWriteouts = autoWriteout;
961     }
962 
963     /**
964      * Sets the "fallback" .dyn file to write to if none is passed to the dynamic method.
965      *
966      * @param fallback Fallback dynamics restart file.
967      */
968     public void setFallbackDynFile(File fallback) {
969         this.fallbackDynFile = fallback;
970     }
971 
972     /**
973      * Method to set file type from groovy scripts.
974      *
975      * @param fileType the type of snapshot files to write.
976      */
977     public void setFileType(String fileType) {
978         this.fileType = fileType;
979     }
980 
981     /**
982      * Not meaningful for FFX dynamics (no need to obtain velocities/accelerations from a different
983      * program, especially one running on a GPU). Is a no-op.
984      *
985      * @param obtainVA Not meaningful for this implementation.
986      */
987     public void setObtainVelAcc(boolean obtainVA) {
988         // Not meaningful for FFX dynamics.
989     }
990 
991     /**
992      * Method to set the Restart Frequency.
993      *
994      * @param restartInterval Time in ps between writing restart files.
995      * @throws java.lang.IllegalArgumentException If restart frequency is not a positive number
996      */
997     public void setRestartFrequency(double restartInterval) throws IllegalArgumentException {
998         restartFrequency = intervalToFreq(restartInterval, "Restart interval");
999         this.restartInterval = restartInterval;
1000     }
1001 
1002     /**
1003      * Set the archive file for each MolecularAssembly.
1004      *
1005      * @param archiveFiles An array of archive files.
1006      */
1007     public void setArchiveFiles(File[] archiveFiles) {
1008         logger.info(" Setting archive files:\n " + Arrays.toString(archiveFiles));
1009         int n = molecularAssembly.length;
1010         assert archiveFiles.length == n;
1011         for (int i = 0; i < n; i++) {
1012             molecularAssembly[i].setArchiveFile(archiveFiles[i]);
1013         }
1014     }
1015 
1016     /**
1017      * {@inheritDoc}
1018      */
1019     @Override
1020     public void terminate() {
1021         terminate = true;
1022         while (!done) {
1023             synchronized (this) {
1024                 try {
1025                     wait(1);
1026                 } catch (Exception e) {
1027                     logger.log(Level.WARNING, " Exception terminating dynamics.\n", e);
1028                 }
1029             }
1030         }
1031     }
1032 
1033     /**
1034      * Write restart and trajectory files if the provided step matches the frequency and that file type
1035      * is requested.
1036      *
1037      * @param step        Step to write files (if any) for.
1038      * @param trySnapshot If false, do not write snapshot even if the time step is correct.
1039      * @param tryRestart  If false, do not write a restart file even if the time step is correct.
1040      * @return EnumSet of actions taken by this method.
1041      */
1042     public EnumSet<MDWriteAction> writeFilesForStep(long step, boolean trySnapshot,
1043                                                     boolean tryRestart) {
1044         return writeFilesForStep(step, trySnapshot, tryRestart, null);
1045     }
1046 
1047     /**
1048      * Write restart and trajectory files if the provided step matches the frequency and that file type
1049      * is requested.
1050      *
1051      * @param step        Step to write files (if any) for.
1052      * @param trySnapshot If false, do not write snapshot even if the time step is correct.
1053      * @param tryRestart  If false, do not write a restart file even if the time step is correct.
1054      * @param extraLines  Additional lines to append into the comments section of the snapshot (or
1055      *                    null).
1056      * @return EnumSet of actions taken by this method.
1057      */
1058     public EnumSet<MDWriteAction> writeFilesForStep(long step, boolean trySnapshot, boolean tryRestart,
1059                                                     String[] extraLines) {
1060         List<String> linesList =
1061                 (extraLines == null) ? new ArrayList<>() : new ArrayList<>(Arrays.asList(extraLines));
1062 
1063         if (potential instanceof LambdaInterface) {
1064             String lamString = format("Lambda: %.8f", ((LambdaInterface) potential).getLambda());
1065             linesList.add(lamString);
1066         }
1067 
1068         String tempString = format("Temp: %.2f", thermostat.getTargetTemperature());
1069         linesList.add(tempString);
1070 
1071         String timeString = format(" Time: %7.3e", totalSimTime);
1072         linesList.add(timeString);
1073 
1074         if (esvSystem != null) {
1075             String pHString = format("pH: %.2f", esvSystem.getConstantPh());
1076             linesList.add(pHString);
1077         }
1078 
1079         Comm world = Comm.world();
1080         if (world != null && world.size() > 1) {
1081             String rankString = format("Rank: %d", world.rank());
1082             linesList.add(rankString);
1083         }
1084 
1085         String[] allLines = new String[linesList.size()];
1086         allLines = linesList.toArray(allLines);
1087 
1088         EnumSet<MDWriteAction> written = EnumSet.noneOf(MDWriteAction.class);
1089         if (step != 0) {
1090             // Write out snapshots in selected format every saveSnapshotFrequency steps.
1091             if (trySnapshot && trajectoryFrequency > 0 && step % trajectoryFrequency == 0) {
1092 
1093                 // Load current coordinates into atom instances.
1094                 potential.setCoordinates(state.x());
1095 
1096                 // Log stats.
1097                 if (totalEnergyStats.getCount() > 0) {
1098                     logger.info(format("\n Average Values for the Last %d Out of %d Dynamics Steps\n",
1099                             trajectoryFrequency, step));
1100                     logger.info(format("  Simulation Time  %16.4f Picosecond", step * dt));
1101                     logger.info(format("  Total Energy     %16.4f Kcal/mole   (+/-%9.4f)", totalEnergyStats.getMean(),
1102                             totalEnergyStats.getStandardDeviation()));
1103                     logger.info(format("  Potential Energy %16.4f Kcal/mole   (+/-%9.4f)",
1104                             potentialEnergyStats.getMean(), potentialEnergyStats.getStandardDeviation()));
1105                     logger.info(format("  Kinetic Energy   %16.4f Kcal/mole   (+/-%9.4f)", kineticEnergyStats.getMean(),
1106                             kineticEnergyStats.getStandardDeviation()));
1107                     logger.info(format("  Temperature      %16.4f Kelvin      (+/-%9.4f)\n", temperatureStats.getMean(),
1108                             temperatureStats.getStandardDeviation()));
1109                     totalEnergyStats.reset();
1110                     potentialEnergyStats.reset();
1111                     kineticEnergyStats.reset();
1112                     temperatureStats.reset();
1113                 }
1114 
1115                 if (esvSystem != null) {
1116                     for (Atom atom : molecularAssembly[0].getAtomList()) {
1117                         int atomIndex = atom.getIndex() - 1;
1118                         atom.setOccupancy(esvSystem.getTitrationLambda(atomIndex));
1119                         atom.setTempFactor(esvSystem.getTautomerLambda(atomIndex));
1120                     }
1121                 }
1122                 appendSnapshot(allLines);
1123                 written.add(MDWriteAction.SNAPSHOT);
1124             }
1125 
1126             // Write out restart files every saveRestartFileFrequency steps.
1127             if (tryRestart && restartFrequency > 0 && step % restartFrequency == 0) {
1128                 writeRestart();
1129                 written.add(MDWriteAction.RESTART);
1130             }
1131         }
1132         return written;
1133     }
1134 
1135     /**
1136      * Write out a restart file.
1137      */
1138     public void writeRestart() {
1139         String dynName = relativePathTo(restartFile).toString();
1140         double[] x = state.x();
1141         double[] v = state.v();
1142         double[] a = state.a();
1143         double[] aPrevious = state.aPrevious();
1144         if (dynFilter.writeDYN(restartFile, molecularAssembly[0].getCrystal(), x, v, a, aPrevious)) {
1145             logger.log(basicLogging, format(" Wrote dynamics restart to:  %s.", dynName));
1146         } else {
1147             logger.log(basicLogging, format(" Writing dynamics restart failed:  %s.", dynName));
1148         }
1149         if (esvSystem != null) {
1150             esvSystem.writeRestart();
1151             esvSystem.writeLambdaHistogram(false);
1152         }
1153         potential.writeAdditionalRestartInfo(true);
1154     }
1155 
1156     /**
1157      * Set the archive file for each MolecularAssembly (if not already set).
1158      */
1159     private void setArchiveFile() {
1160         for (MolecularAssembly assembly : molecularAssembly) {
1161             File file = assembly.getFile();
1162             String filename = removeExtension(file.getAbsolutePath());
1163             File archiveFile = assembly.getArchiveFile();
1164             if (archiveFile == null) {
1165                 archiveFile = new File(filename + ".arc");
1166                 assembly.setArchiveFile(XYZFilter.version(archiveFile));
1167             }
1168         }
1169     }
1170 
1171     /**
1172      * Converts an interval in ps to a frequency in time steps.
1173      *
1174      * @param interval Interval between events in ps
1175      * @param describe Description of the event.
1176      * @return Frequency of event in time steps per event.
1177      * @throws IllegalArgumentException If interval is not a positive finite value.
1178      */
1179     private int intervalToFreq(double interval, String describe) throws IllegalArgumentException {
1180         if (!Double.isFinite(interval) || interval <= 0) {
1181             throw new IllegalArgumentException(
1182                     format(" %s must be " + "positive finite value in ps, was %10.4g", describe, interval));
1183         }
1184         if (interval >= dt) {
1185             return (int) (interval / dt);
1186         } else {
1187             logger.warning(format(" Specified %s of %.6f ps < time step %.6f ps; "
1188                     + "interval is set to once per time step!", describe, interval, dt));
1189             return 1;
1190         }
1191     }
1192 
1193     /**
1194      * Method to set the logging/printing frequency.
1195      *
1196      * @param logInterval Time in ps between logging information to the screen.
1197      */
1198     private void setLoggingFrequency(double logInterval) {
1199         logFrequency = intervalToFreq(logInterval, "Reporting (logging) interval");
1200         this.logInterval = logInterval;
1201     }
1202 
1203     /**
1204      * Method to set the frequency of appending snapshots to the trajectory file.
1205      *
1206      * @param snapshotInterval Time in ps between appending snapshots to the trajectory file.
1207      */
1208     private void setTrajectoryFrequency(double snapshotInterval) {
1209         trajectoryFrequency = intervalToFreq(snapshotInterval, "Trajectory writeout interval");
1210         this.trajectoryInterval = snapshotInterval;
1211     }
1212 
1213     /**
1214      * Performs basic pre-MD operations such as loading the restart file.
1215      */
1216     void preRunOps() throws IllegalStateException {
1217         done = false;
1218         terminate = false;
1219 
1220         // Set the target temperature.
1221         thermostat.setTargetTemperature(targetTemperature);
1222         boolean quiet = verbosityLevel.isQuiet();
1223         thermostat.setQuiet(quiet);
1224         if (integrator instanceof Stochastic stochastic) {
1225             stochastic.setTemperature(targetTemperature);
1226         }
1227 
1228         // Set the step size.
1229         integrator.setTimeStep(dt);
1230 
1231         if (!initialized) {
1232             // Initialize from a restart file.
1233             if (loadRestart) {
1234                 Crystal crystal = molecularAssembly[0].getCrystal();
1235                 double[] x = state.x();
1236                 double[] v = state.v();
1237                 double[] a = state.a();
1238                 double[] aPrevious = state.aPrevious();
1239                 if (!dynFilter.readDYN(restartFile, crystal, x, v, a, aPrevious)) {
1240                     String message = " Could not load the restart file - dynamics terminated.";
1241                     logger.log(Level.WARNING, message);
1242                     done = true;
1243                     throw new IllegalStateException(message);
1244                 } else {
1245                     molecularAssembly[0].setCrystal(crystal);
1246                     potential.setCoordinates(x);
1247                     potential.setVelocity(v);
1248                     potential.setAcceleration(a);
1249                     potential.setPreviousAcceleration(aPrevious);
1250                 }
1251             } else {
1252                 // Initialize using current atomic coordinates.
1253                 potential.getCoordinates(state.x());
1254                 // Initialize atomic velocities from a Maxwell-Boltzmann distribution or set to 0.
1255                 if (initVelocities) {
1256                     thermostat.maxwell(targetTemperature);
1257                 } else {
1258                     fill(state.v(), 0.0);
1259                 }
1260             }
1261         } else {
1262             // If MD has already been run (i.e., Annealing or RepEx), then initialize velocities if
1263             // requested.
1264             if (initVelocities) {
1265                 thermostat.maxwell(targetTemperature);
1266             }
1267         }
1268     }
1269 
1270     /**
1271      * Initializes energy fields, esp. potential energy.
1272      */
1273     private void initializeEnergies() {
1274         // Compute the current potential energy.
1275         double[] x = state.x();
1276         double[] gradient = state.gradient();
1277 
1278         boolean propagateLambda = true;
1279         if (potential instanceof OrthogonalSpaceTempering orthogonalSpaceTempering) {
1280             propagateLambda = orthogonalSpaceTempering.getPropagateLambda();
1281             orthogonalSpaceTempering.setPropagateLambda(false);
1282         }
1283 
1284         if (esvSystem != null && potential instanceof OpenMMEnergy) {
1285             state.setPotentialEnergy(((OpenMMEnergy) potential).energyAndGradientFFX(x, gradient));
1286         } else {
1287             state.setPotentialEnergy(potential.energyAndGradient(x, gradient));
1288         }
1289 
1290         if (potential instanceof OrthogonalSpaceTempering orthogonalSpaceTempering) {
1291             orthogonalSpaceTempering.setPropagateLambda(propagateLambda);
1292         }
1293 
1294         // Initialize current and previous accelerations.
1295         if (!loadRestart || initialized || integrator instanceof Respa) {
1296             // For the Respa integrator, initial accelerations are from the slowly varying forces.
1297             if (integrator instanceof Respa) {
1298                 potential.setEnergyTermState(Potential.STATE.SLOW);
1299                 potential.energyAndGradient(x, gradient);
1300             }
1301 
1302             int numberOfVariables = state.getNumberOfVariables();
1303             double[] a = state.a();
1304             double[] mass = state.getMass();
1305             for (int i = 0; i < numberOfVariables; i++) {
1306                 a[i] = -KCAL_TO_GRAM_ANG2_PER_PS2 * gradient[i] / mass[i];
1307             }
1308             state.copyAccelerationsToPrevious();
1309         }
1310 
1311         if (esvSystem != null) {
1312             SystemState esvState = esvSystem.getState();
1313             double[] esvA = esvState.a();
1314             double[] esvMass = esvState.getMass();
1315             int nESVs = esvState.getNumberOfVariables();
1316             double[] gradESV = esvSystem.postForce();
1317             for (int i = 0; i < nESVs; i++) {
1318                 esvA[i] = -KCAL_TO_GRAM_ANG2_PER_PS2 * gradESV[i] / esvMass[i];
1319             }
1320         }
1321 
1322         // Compute the current kinetic energy.
1323         thermostat.computeKineticEnergy();
1324         if (esvSystem != null) {
1325             esvThermostat.computeKineticEnergy();
1326             double kineticEnergy = thermostat.getKineticEnergy();
1327             double esvKineticEnergy = esvThermostat.getKineticEnergy();
1328             state.setKineticEnergy(kineticEnergy + esvKineticEnergy);
1329         }
1330 
1331         // Store the initial state.
1332         initialState = new UnmodifiableState(state);
1333 
1334         // Reset the statistics.
1335         temperatureStats.reset();
1336         potentialEnergyStats.reset();
1337         kineticEnergyStats.reset();
1338         totalEnergyStats.reset();
1339     }
1340 
1341     /**
1342      * Pre-run operations (mostly logging) that require knowledge of system energy.
1343      */
1344     void postInitEnergies() {
1345         initialized = true;
1346 
1347         logger.log(basicLogging,
1348                 format("\n  %8s %12s %12s %12s %8s %8s", "Time", "Kinetic", "Potential", "Total", "Temp", "CPU"));
1349         logger.log(basicLogging,
1350                 format("  %8s %12s %12s %12s %8s %8s", "psec", "kcal/mol", "kcal/mol", "kcal/mol", "K", "sec"));
1351         logger.log(basicLogging, format("  %8s %12.4f %12.4f %12.4f %8.2f", "", state.getKineticEnergy(),
1352                 state.getPotentialEnergy(), state.getTotalEnergy(), state.getTemperature()));
1353 
1354         // Store the initialized state.
1355         storeState();
1356     }
1357 
1358     /**
1359      * Post-run cleanup operations.
1360      */
1361     void postRun() {
1362         // Add the potential energy of the slow degrees of freedom.
1363         if (integrator instanceof Respa) {
1364             potential.setEnergyTermState(Potential.STATE.BOTH);
1365         }
1366 
1367         // Log normal completion.
1368         if (!terminate) {
1369             logger.log(basicLogging, format(" Completed %8d time steps\n", nSteps));
1370         }
1371 
1372         // Reset the done and terminate flags.
1373         done = true;
1374         terminate = false;
1375     }
1376 
1377     /**
1378      * Append a snapshot to the trajectory file.
1379      *
1380      * @param extraLines Strings of meta-data to include.
1381      */
1382     protected void appendSnapshot(String[] extraLines) {
1383         int numAssemblies = molecularAssembly.length;
1384         int currentAssembly = 0;
1385         // Loop over all molecular assemblies.
1386         for (MolecularAssembly assembly : molecularAssembly) {
1387             File archiveFile = assembly.getArchiveFile();
1388             ForceField forceField = assembly.getForceField();
1389             CompositeConfiguration properties = assembly.getProperties();
1390 
1391             //Remove energy/density from name of assembly as it likely changed during MD.
1392             String name = assembly.getName();
1393             String[] tokens = name.split(" +");
1394             StringBuilder stringBuilder = new StringBuilder();
1395             int numTokens = tokens.length;
1396             for (int i = 0; i < numTokens; i++) {
1397                 if (tokens[i].equalsIgnoreCase("Energy:") || tokens[i].equalsIgnoreCase("Density:")) {
1398                     //Skip next value.
1399                     i++;
1400                 } else {
1401                     stringBuilder.append(" ").append(tokens[i]);
1402                 }
1403             }
1404             assembly.setName(stringBuilder.toString());
1405 
1406             // Save as an ARC file.
1407             if (archiveFile != null && !saveSnapshotAsPDB) {
1408                 String aiName = relativePathTo(archiveFile).toString();
1409                 if (esvSystem == null) {
1410                     XYZFilter xyzFilter = new XYZFilter(archiveFile, assembly, forceField, properties);
1411                     if (xyzFilter.writeFile(archiveFile, true, extraLines)) {
1412                         logger.log(basicLogging, format(" Appended snapshot to:       %s", aiName));
1413                     } else {
1414                         logger.warning(format(" Appending snapshot failed:  %s", aiName));
1415                     }
1416                 } else {
1417                     XPHFilter xphFilter = new XPHFilter(archiveFile, assembly, forceField, properties, esvSystem);
1418                     if (xphFilter.writeFile(archiveFile, true, extraLines)) {
1419                         logger.log(basicLogging, format(" Appended to XPH archive %s", aiName));
1420                     } else {
1421                         logger.warning(format(" Appending to XPH archive %s failed.", aiName));
1422                     }
1423                 }
1424             } else if (saveSnapshotAsPDB) {
1425                 if (pdbFilter == null) {
1426                     pdbFilter = new PDBFilter[numAssemblies];
1427                 }
1428                 if (pdbFilter[currentAssembly] == null) {
1429                     File file = assembly.getFile();
1430                     String extName = getExtension(file.getName());
1431                     File pdbFile;
1432                     if (extName.toLowerCase().startsWith("pdb")) {
1433                         // Version the file to avoid appending to the original input file.
1434                         pdbFile = TinkerUtils.version(file);
1435                     } else {
1436                         String filename = removeExtension(file.getAbsolutePath());
1437                         pdbFile = new File(filename + ".pdb");
1438                     }
1439                     pdbFilter[currentAssembly] = new PDBFilter(pdbFile, assembly, forceField, properties);
1440                     pdbFilter[currentAssembly].setModelNumbering(0);
1441                 }
1442                 File pdbFile = pdbFilter[currentAssembly].getFile();
1443                 String aiName = relativePathTo(pdbFile).toString();
1444                 if (pdbFilter[currentAssembly].writeFile(pdbFile, true, extraLines)) {
1445                     logger.log(basicLogging, format(" Appended to PDB file %s", aiName));
1446                 } else {
1447                     logger.warning(format(" Appending to PDB file to %s failed.", aiName));
1448                 }
1449             }
1450             currentAssembly++;
1451         }
1452     }
1453 
1454     /**
1455      * Checks if thermodynamics must be logged. If logged, current time is returned, else the time
1456      * passed in is returned.
1457      *
1458      * @param step Time step to possibly log thermodynamics for.
1459      * @param time Clock time (in nsec) thermodynamics was last logged.
1460      * @return Either current time (if logged), else the time variable passed in.
1461      */
1462     protected long logThermoForTime(long step, long time) {
1463         if (step % logFrequency == 0) {
1464             return logThermodynamics(time);
1465         } else {
1466             return time;
1467         }
1468     }
1469 
1470     /**
1471      * Log thermodynamics to the screen.
1472      *
1473      * @param time Clock time (in nsec) since this was last called.
1474      * @return Clock time at the end of this call.
1475      */
1476     private long logThermodynamics(long time) {
1477         time = System.nanoTime() - time;
1478         logger.log(basicLogging,
1479                 format(" %7.3e %12.4f %12.4f %12.4f %8.2f %8.3f", totalSimTime, state.getKineticEnergy(),
1480                         state.getPotentialEnergy(), state.getTotalEnergy(), state.getTemperature(),
1481                         time * NS2SEC));
1482         return System.nanoTime();
1483     }
1484 
1485     /**
1486      * Main loop of the run method.
1487      */
1488     private void mainLoop() {
1489         // Integrate Newton's equations of motion for the requested number of steps,
1490         // unless early termination is requested.
1491         long time = System.nanoTime();
1492         int removeCOMMotionFrequency = molecularAssembly[0].getForceField().getInteger("REMOVE-COM-FREQUENCY", 100);
1493         if (thermostat.getRemoveCenterOfMassMotion()) {
1494             logger.info(format(" COM will be removed every %3d step(s).", removeCOMMotionFrequency));
1495         }
1496 
1497         if (nonEquilibriumLambda) {
1498             // Configure the number of non-equilibrium dynamics.
1499             nSteps = nonEquilibriumDynamics.setMDSteps(nSteps);
1500             LambdaInterface lambdaInterface = (LambdaInterface) potential;
1501             double lambda = nonEquilibriumDynamics.getInitialLambda();
1502             lambdaInterface.setLambda(lambda);
1503         }
1504 
1505         // Main MD loop to take molecular dynamics steps.
1506         for (long step = 1; step <= nSteps; step++) {
1507 
1508             // Update lambda for non-equilibrium simulations.
1509             if (nonEquilibriumLambda && nonEquilibriumDynamics.isUpdateStep(step)) {
1510                 Potential.STATE respaState = potential.getEnergyTermState();
1511                 if (integrator instanceof Respa) {
1512                     if (respaState != Potential.STATE.BOTH) {
1513                         potential.setEnergyTermState(Potential.STATE.BOTH);
1514                     }
1515                 }
1516                 LambdaInterface lambdaInterface = (LambdaInterface) potential;
1517                 double currentLambda = lambdaInterface.getLambda();
1518                 double currentEnergy = state.getPotentialEnergy();
1519                 // Update the lambda value.
1520                 double newLambda = nonEquilibriumDynamics.getNextLambda(step, currentLambda);
1521                 lambdaInterface.setLambda(newLambda);
1522                 // Compute the new energy.
1523                 double newEnergy = potential.energy(state.x());
1524                 // The non-equilibrium work is the difference in energy.
1525                 double dW = newEnergy - currentEnergy;
1526                 nonEquilibriumDynamics.addWork(dW);
1527                 logger.info(format(" Non-equilibrium L=%5.3f Work=%12.6f", newLambda, nonEquilibriumDynamics.getWork()));
1528 
1529                 // Reset the Respa State.
1530                 if (integrator instanceof Respa) {
1531                     potential.setEnergyTermState(respaState);
1532                 }
1533             }
1534 
1535             if (step > 1) {
1536                 List<Constraint> constraints = potential.getConstraints();
1537                 // TODO: Replace magic numbers with named constants.
1538                 long constraintFails = constraints.stream()
1539                         .filter((Constraint c) -> !c.constraintSatisfied(state.x(), state.v(), 1E-7, 1E-7)).count();
1540                 if (constraintFails > 0) {
1541                     logger.info(format(" %d constraint failures in step %d", constraintFails, step));
1542                 }
1543             }
1544 
1545             // Do the half-step thermostat operation.
1546             thermostat.halfStep(dt);
1547 
1548             // Do the half-step integration operation.
1549             integrator.preForce(potential);
1550             if (esvSystem != null) {
1551                 esvIntegrator.preForce(potential);
1552                 //preForce processes theta values after
1553                 esvSystem.preForce();
1554             }
1555 
1556             // Compute the potential energy and gradients.
1557             if (esvSystem != null && potential instanceof OpenMMEnergy) {
1558                 state.setPotentialEnergy(((OpenMMEnergy) potential).energyAndGradientFFX(state.x(), state.gradient()));
1559             } else {
1560                 state.setPotentialEnergy(potential.energyAndGradient(state.x(), state.gradient()));
1561             }
1562 
1563             // Add the potential energy of the slow degrees of freedom.
1564             if (integrator instanceof Respa r) {
1565                 double potentialEnergy = state.getPotentialEnergy();
1566                 state.setPotentialEnergy(potentialEnergy + r.getHalfStepEnergy());
1567             }
1568 
1569             // Do the full-step integration operation.
1570             integrator.postForce(state.gradient());
1571             if (esvSystem != null) {
1572                 double[] dEdL = esvSystem.postForce();
1573                 esvIntegrator.postForce(dEdL);
1574             }
1575 
1576             // Compute the full-step kinetic energy.
1577             thermostat.computeKineticEnergy();
1578 
1579             // Do the full-step thermostat operation.
1580             thermostat.fullStep(dt);
1581 
1582             // Recompute the kinetic energy after the full-step thermostat operation.
1583             thermostat.computeKineticEnergy();
1584             if (esvSystem != null) {
1585                 // Adiabatic thermostat does nothing at half step.
1586                 esvThermostat.computeKineticEnergy();
1587             }
1588 
1589             // Remove center of mass motion if requested.
1590             if (thermostat.getRemoveCenterOfMassMotion() && step % removeCOMMotionFrequency == 0) {
1591                 thermostat.centerOfMassMotion(true, false);
1592             }
1593 
1594             // Collect current kinetic energy, temperature, and total energy.
1595             if (esvSystem != null) {
1596                 double kineticEnergy = thermostat.getKineticEnergy();
1597                 double esvKineticEnergy = esvThermostat.getKineticEnergy();
1598                 state.setKineticEnergy(kineticEnergy + esvKineticEnergy);
1599             }
1600 
1601             // Collect running statistics.
1602             temperatureStats.addValue(state.getTemperature());
1603             potentialEnergyStats.addValue(state.getPotentialEnergy());
1604             kineticEnergyStats.addValue(state.getKineticEnergy());
1605             totalEnergyStats.addValue(state.getTotalEnergy());
1606 
1607             // Update atomic velocity, acceleration and previous acceleration.
1608             potential.setVelocity(state.v());
1609             potential.setAcceleration(state.a());
1610             potential.setPreviousAcceleration(state.aPrevious());
1611 
1612             // Log the current state every printFrequency steps.
1613             totalSimTime += dt;
1614             time = logThermoForTime(step, time);
1615             if (step % printEsvFrequency == 0 && esvSystem != null) {
1616                 logger.log(basicLogging, format(" %s", esvSystem.getLambdaList()));
1617             }
1618 
1619             if (automaticWriteouts) {
1620                 writeFilesForStep(step, true, true);
1621             }
1622 
1623             // Notify the algorithmListeners.
1624             if (algorithmListener != null && step % logFrequency == 0) {
1625                 for (MolecularAssembly assembly : molecularAssembly) {
1626                     algorithmListener.algorithmUpdate(assembly);
1627                 }
1628             }
1629 
1630             // Check for a termination request.
1631             if (terminate) {
1632                 logger.info(format("\n Terminating after %8d time steps\n", step));
1633                 break;
1634             }
1635         }
1636     }
1637 
1638     /**
1639      * Perform a sanity check on a frequency to ensure it's not longer than total runtime. Currently,
1640      * the setter parameter is ignored due to issues with our test suite.
1641      *
1642      * @param describe  Description of the frequency.
1643      * @param frequency Frequency in time steps.
1644      */
1645     private void checkFrequency(String describe, int frequency) {
1646         if (frequency > nSteps) {
1647             logger.fine(
1648                     format(" Specified %s frequency of %d is greater than the number of steps %d", describe,
1649                             frequency, nSteps));
1650         }
1651     }
1652 
1653     /**
1654      * Set the coordinates.
1655      *
1656      * @param coords The coordinates to copy into MD coordinates array.
1657      */
1658     public void setCoordinates(double[] coords) {
1659         state.setCoordinates(coords);
1660     }
1661 
1662     /**
1663      * Get the coordinates.
1664      *
1665      * @return A copy of the current coordinates are returned.
1666      */
1667     public double[] getCoordinates() {
1668         return state.getCoordinatesCopy();
1669     }
1670 
1671     /**
1672      * Store the current state of the molecular dynamics simulation in a MDState record.
1673      */
1674     public void storeState() {
1675         storedState = state.getUnmodifiableState();
1676     }
1677 
1678     /**
1679      * Revert the state of the MolecularDynamics instance to the stored MDState.
1680      *
1681      * @throws Exception is thrown if the stored state is null.
1682      */
1683     public void revertState() throws Exception {
1684         if (storedState == null) {
1685             throw new Exception();
1686         }
1687         revertState(storedState);
1688     }
1689 
1690     /**
1691      * Revert the state of the MolecularDynamics instance to the provided MDState.
1692      *
1693      * @param state The MDState to revert to.
1694      */
1695     private void revertState(UnmodifiableState state) {
1696         this.state.revertState(state);
1697         potential.setVelocity(state.v());
1698         potential.setAcceleration(state.a());
1699         potential.setPreviousAcceleration(state.aPrevious());
1700         // ToDo -- move these methods into the Potential interface.
1701         int numberOfVariables = this.state.getNumberOfVariables();
1702         Atom[] atoms = molecularAssembly[0].getActiveAtomArray();
1703         if (atoms.length * 3 == numberOfVariables) {
1704             int index = 0;
1705             double[] x = state.x();
1706             double[] gradient = state.gradient();
1707             for (Atom atom : atoms) {
1708                 atom.moveTo(x[index], x[index + 1], x[index + 2]);
1709                 atom.setXYZGradient(gradient[index], gradient[index + 1], gradient[index + 2]);
1710                 index += 3;
1711             }
1712         }
1713     }
1714 
1715 }