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