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     Barostat barostat = null;
467     if (potentialEnergy instanceof Barostat) {
468       barostat = (Barostat) potentialEnergy;
469       potentialEnergy = barostat.getCrystalPotential();
470     }
471     switch (engine) {
472       case OPENMM, OMM -> {
473         if (potentialEnergy instanceof OpenMMEnergy ||
474             potentialEnergy instanceof OpenMMDualTopologyEnergy) {
475           MolecularDynamicsOpenMM molecularDynamicsOpenMM =
476               new MolecularDynamicsOpenMM(assembly, potentialEnergy, listener, requestedThermostat,
477                   requestedIntegrator);
478           if (barostat != null) {
479             molecularDynamicsOpenMM.setBarostat(barostat);
480           }
481           return molecularDynamicsOpenMM;
482         } else {
483           logger.info(format(" Requested OpenMM molecular dynamics engine %s, but the potential does not use OpenMM: %s",
484               engine, potentialEnergy));
485           return new MolecularDynamics(assembly, potentialEnergy, listener, requestedThermostat, requestedIntegrator);
486         }
487       }
488       default -> {
489         return new MolecularDynamics(assembly, potentialEnergy, listener, requestedThermostat, requestedIntegrator);
490       }
491     }
492   }
493 
494   private static MDEngine defaultEngine(MolecularAssembly molecularAssembly,
495                                         Potential potentialEnergy) {
496     CompositeConfiguration properties = molecularAssembly.getProperties();
497     String mdEngine = properties.getString("MD-engine");
498     if (mdEngine != null) {
499       if (mdEngine.equalsIgnoreCase("OMM")) {
500         logger.info(" Creating OpenMM Dynamics Object");
501         return MDEngine.OPENMM;
502       } else {
503         logger.info(" Creating FFX Dynamics Object");
504         return MDEngine.FFX;
505       }
506     } else {
507       // TODO: Replace this with a better check.
508       boolean ommLeaves = potentialEnergy.getUnderlyingPotentials().stream()
509           .anyMatch((Potential p) -> p instanceof OpenMMEnergy);
510       ommLeaves = ommLeaves || potentialEnergy instanceof OpenMMEnergy;
511       if (ommLeaves) {
512         return MDEngine.OPENMM;
513       } else {
514         return MDEngine.FFX;
515       }
516     }
517   }
518 
519   /**
520    * Creates the "default" fallback dynamics file object (does not create actual file!)
521    *
522    * @param assembly First assembly.
523    * @return Default fallback file.
524    */
525   private static File defaultFallbackDyn(MolecularAssembly assembly) {
526     String firstFileName = removeExtension(assembly.getFile().getAbsolutePath());
527     return new File(firstFileName + ".dyn");
528   }
529 
530   /**
531    * Adds a MolecularAssembly to be tracked by this MolecularDynamics. Note: does not affect the
532    * underlying Potential.
533    *
534    * @param assembly A MolecularAssembly to be tracked.
535    */
536   public void addAssembly(MolecularAssembly assembly) {
537     molecularAssembly = Arrays.copyOf(molecularAssembly, molecularAssembly.length + 1);
538     molecularAssembly[molecularAssembly.length - 1] = assembly;
539   }
540 
541   /**
542    * attachExtendedSystem.
543    *
544    * @param system a {@link ffx.potential.extended.ExtendedSystem} object.
545    */
546   public void attachExtendedSystem(ExtendedSystem system, double reportFreq) {
547     if (esvSystem != null) {
548       logger.warning("An ExtendedSystem is already attached to this MD!");
549     }
550     esvSystem = system;
551     SystemState esvState = esvSystem.getState();
552     this.esvIntegrator = new Stochastic(esvSystem.getThetaFriction(), esvState);
553     if (!esvSystem.getConstraints().isEmpty()) {
554       esvIntegrator.addConstraints(esvSystem.getConstraints());
555     }
556     this.esvThermostat = new Adiabatic(esvState, potential.getVariableTypes());
557     printEsvFrequency = intervalToFreq(reportFreq, "Reporting (logging) interval");
558     logger.info(
559         format("  Attached extended system (%s) to molecular dynamics.", esvSystem.toString()));
560     logger.info(format("  Extended System Theta Friction: %f", esvSystem.getThetaFriction()));
561     logger.info(format("  Extended System Theta Mass: %f", esvSystem.getThetaMass()));
562     logger.info(format("  Extended System Lambda Print Frequency: %d (fsec)", printEsvFrequency));
563   }
564 
565   /**
566    * Enables non-equilibrium lambda dynamics.
567    *
568    * @param nonEquilibrium True if non-equilibrium lambda dynamics should be enabled.
569    * @param nEQSteps       Number of lambda steps.
570    * @param reverseNEQ     True if lambda path should be reversed.
571    */
572   public void setNonEquilibriumLambda(boolean nonEquilibrium, int nEQSteps, boolean reverseNEQ) {
573     nonEquilibriumLambda = nonEquilibrium;
574     if (nonEquilibriumLambda) {
575       nonEquilibriumDynamics = new NonEquilbriumDynamics(nEQSteps, reverseNEQ);
576     } else {
577       nonEquilibriumDynamics = null;
578     }
579   }
580 
581   /**
582    * Blocking molecular dynamics. When this method returns, the MD run is done.
583    *
584    * @param nSteps             Number of MD steps
585    * @param timeStep           Time step in femtoseconds
586    * @param loggingInterval    Interval between printing/logging information in picoseconds.
587    * @param trajectoryInterval Interval between adding a frame to the trajectory file in
588    *                           picoseconds.
589    * @param temperature        Temperature in Kelvins.
590    * @param initVelocities     Initialize new velocities from a Maxwell-Boltzmann distribution.
591    * @param fileType           XYZ or ARC to save to .arc, PDB for .pdb files
592    * @param restartInterval    Interval between writing new restart files in picoseconds.
593    * @param dyn                A {@link java.io.File} object to write the restart file to.
594    */
595   public void dynamic(final long nSteps, final double timeStep, final double loggingInterval,
596                       final double trajectoryInterval, final double temperature, final boolean initVelocities,
597                       String fileType, double restartInterval, final File dyn) {
598     this.fileType = fileType;
599     setRestartFrequency(restartInterval);
600     dynamic(nSteps, timeStep, loggingInterval, trajectoryInterval, temperature, initVelocities, dyn);
601   }
602 
603   /**
604    * Blocking molecular dynamics. When this method returns, the MD run is done.
605    *
606    * @param nSteps             Number of MD steps
607    * @param timeStep           Time step (fsec)
608    * @param loggingInterval    Interval between printing/logging information in picoseconds.
609    * @param trajectoryInterval Interval between adding a frame to the trajectory file in
610    *                           picoseconds.
611    * @param temperature        Temperature in Kelvins.
612    * @param initVelocities     Initialize new velocities from a Maxwell-Boltzmann distribution.
613    * @param dyn                A {@link java.io.File} object to write the restart file to.
614    */
615   public void dynamic(final long nSteps, final double timeStep, final double loggingInterval,
616                       final double trajectoryInterval, final double temperature, final boolean initVelocities,
617                       final File dyn) {
618     // Return if already running;
619     // Could happen if two threads call dynamic on the same MolecularDynamics instance.
620     if (!done) {
621       logger.warning(" Programming error - a thread invoked dynamic when it was already running.");
622       return;
623     }
624 
625     init(nSteps, timeStep, loggingInterval, trajectoryInterval, fileType, restartInterval,
626         temperature, initVelocities, dyn);
627 
628     Thread dynamicThread = new Thread(this);
629     dynamicThread.start();
630     synchronized (this) {
631       try {
632         while (dynamicThread.isAlive()) {
633           wait(0, DEFAULT_DYNAMICS_SLEEP_TIME);
634         }
635       } catch (InterruptedException e) {
636         String message = " Molecular dynamics interrupted.";
637         logger.log(Level.WARNING, message, e);
638       }
639     }
640     if (!verbosityLevel.isQuiet()) {
641       logger.info(" Done with an MD round.");
642     }
643   }
644 
645   /**
646    * Returns the MolecularAssembly array.
647    *
648    * @return A copy of the MolecularAssembly array.
649    */
650   public MolecularAssembly[] getAssemblyArray() {
651     return molecularAssembly.clone();
652   }
653 
654   /**
655    * Returns the associated dynamics file.
656    *
657    * @return Dynamics restart File.
658    */
659   public File getDynFile() {
660     return restartFile;
661   }
662 
663   /**
664    * Gets the kinetic energy at the start of the last dynamics run.
665    *
666    * @return Kinetic energy at the start of the run.
667    */
668   public double getInitialKineticEnergy() {
669     return initialState.kineticEnergy();
670   }
671 
672   /**
673    * Gets the potential energy at the start of the last dynamics run.
674    *
675    * @return potential energy at the start of the run.
676    */
677   public double getInitialPotentialEnergy() {
678     return initialState.potentialEnergy();
679   }
680 
681   /**
682    * Gets the temperature at the start of the last dynamics run.
683    *
684    * @return temperature at the start of the run.
685    */
686   public double getInitialTemperature() {
687     return initialState.temperature();
688   }
689 
690   /**
691    * Gets the total energy at the start of the last dynamics run.
692    *
693    * @return total energy at the start of the run.
694    */
695   public double getInitialTotalEnergy() {
696     return initialState.getTotalEnergy();
697   }
698 
699   /**
700    * getIntervalSteps.
701    *
702    * @return Always 1 for this implementation.
703    */
704   public int getIntervalSteps() {
705     return 1;
706   }
707 
708   /**
709    * No-op; FFX does not need to occasionally return information from FFX.
710    *
711    * @param intervalSteps Ignored.
712    */
713   public void setIntervalSteps(int intervalSteps) {
714     // Not meaningful for FFX MD.
715   }
716 
717   /**
718    * Get the system kinetic energy.
719    *
720    * @return kinetic energy.
721    */
722   public double getKineticEnergy() {
723     return state.getKineticEnergy();
724   }
725 
726   /**
727    * Get the system potential energy.
728    *
729    * @return potential energy.
730    */
731   public double getPotentialEnergy() {
732     return state.getPotentialEnergy();
733   }
734 
735   /**
736    * Get the current temperature of the system
737    *
738    * @return currentTemperature
739    */
740   public double getTemperature() {
741     return state.getTemperature();
742   }
743 
744   /**
745    * Getter for the field <code>thermostat</code>.
746    *
747    * @return a {@link ffx.algorithms.dynamics.thermostats.Thermostat} object.
748    */
749   public Thermostat getThermostat() {
750     return thermostat;
751   }
752 
753   /**
754    * Setter for the field <code>thermostat</code>.
755    *
756    * @param thermostat a {@link ffx.algorithms.dynamics.thermostats.Thermostat} object.
757    */
758   public void setThermostat(Thermostat thermostat) {
759     this.thermostat = thermostat;
760   }
761 
762   /**
763    * getTimeStep.
764    *
765    * @return Time step in picoseconds.
766    */
767   public double getTimeStep() {
768     return dt;
769   }
770 
771   /**
772    * Sets the time step and resets frequencies based on stored intervals.
773    *
774    * @param step Time step in femtoseconds.
775    */
776   private void setTimeStep(double step) {
777     dt = step * FSEC_TO_PSEC;
778     // Reset frequencies to be consistent with new time step.
779     setRestartFrequency(restartInterval);
780     setLoggingFrequency(logInterval);
781     setTrajectoryFrequency(trajectoryInterval);
782   }
783 
784   /**
785    * Get the total system energy (kinetic plus potential).
786    *
787    * @return total energy.
788    */
789   public double getTotalEnergy() {
790     return state.getTotalEnergy();
791   }
792 
793   public MDVerbosity getVerbosityLevel() {
794     return verbosityLevel;
795   }
796 
797   public void setVerbosityLevel(MDVerbosity level) {
798     verbosityLevel = level;
799     if (level == MDVerbosity.SILENT) {
800       basicLogging = Level.FINE;
801     } else {
802       basicLogging = Level.INFO;
803     }
804   }
805 
806   /**
807    * init
808    *
809    * @param nSteps             Number of MD steps
810    * @param timeStep           Time step in femtoseconds
811    * @param loggingInterval    Interval between printing/logging information in picoseconds.
812    * @param trajectoryInterval Interval between adding a frame to the trajectory file in
813    *                           picoseconds.
814    * @param fileType           XYZ or ARC to save to .arc, PDB for .pdb files
815    * @param restartInterval    Interval between writing new restart files in picoseconds.
816    * @param temperature        Temperature in Kelvins.
817    * @param initVelocities     Initialize new velocities from a Maxwell-Boltzmann distribution.
818    * @param dyn                A {@link java.io.File} object to write the restart file to.
819    */
820   public void init(final long nSteps, final double timeStep, final double loggingInterval,
821                    final double trajectoryInterval, final String fileType, final double restartInterval,
822                    final double temperature, final boolean initVelocities, final File dyn) {
823 
824     // Return if already running.
825     if (!done) {
826       logger.warning(
827           " Programming error - attempt to modify parameters of a running MolecularDynamics instance.");
828       return;
829     }
830 
831     if (integrator instanceof Stochastic) {
832       if (constantPressure) {
833         logger.log(basicLogging, "\n Stochastic dynamics in the NPT ensemble");
834       } else {
835         logger.log(basicLogging, "\n Stochastic dynamics in the NVT ensemble");
836       }
837     } else if (!(thermostat instanceof Adiabatic)) {
838       if (constantPressure) {
839         logger.log(basicLogging, "\n Molecular dynamics in the NPT ensemble");
840       } else {
841         logger.log(basicLogging, "\n Molecular dynamics in the NVT ensemble");
842       }
843     } else {
844       if (constantPressure) {
845         logger.severe("\n NPT Molecular dynamics requires a thermostat");
846       } else {
847         logger.log(basicLogging, "\n Molecular dynamics in the NVE ensemble");
848       }
849     }
850 
851     this.nSteps = nSteps;
852     totalSimTime = 0.0;
853 
854     // Convert the time step from femtoseconds to picoseconds.
855     setTimeStep(timeStep);
856 
857     // Convert intervals in ps to frequencies in time steps.
858     setLoggingFrequency(loggingInterval);
859     setTrajectoryFrequency(trajectoryInterval);
860     setRestartFrequency(restartInterval);
861 
862     checkFrequency("Reporting           ", logFrequency);
863     if (automaticWriteouts) {
864       // Only sanity check these values if MD is doing this itself.
865       checkFrequency("Trajectory Write-Out", trajectoryFrequency);
866       checkFrequency("Restart             ", restartFrequency);
867     }
868 
869     // Set snapshot file type.
870     saveSnapshotAsPDB = true;
871     if (fileType.equalsIgnoreCase("XYZ") || fileType.equalsIgnoreCase("ARC")) {
872       saveSnapshotAsPDB = false;
873     } else if (!fileType.equalsIgnoreCase("PDB")) {
874       logger.warning("Snapshot file type unrecognized; saving snapshots as PDB.\n");
875     }
876 
877     setArchiveFile();
878 
879     this.restartFile = (dyn == null) ? fallbackDynFile : dyn;
880     loadRestart = restartFile.exists() && !initialized;
881 
882     if (dynFilter == null) {
883       dynFilter = new DYNFilter(molecularAssembly[0].getName());
884     }
885 
886     this.targetTemperature = temperature;
887     this.initVelocities = initVelocities;
888     done = false;
889 
890     if (loadRestart) {
891       logger.info("  Continuing from " + restartFile.getAbsolutePath());
892     }
893 
894     if (!verbosityLevel.isQuiet()) {
895       logger.info(format("  Integrator:          %15s", integrator.toString()));
896       logger.info(format("  Thermostat:          %15s", thermostat.toString()));
897       logger.info(format("  Number of steps:     %8d", nSteps));
898       logger.info(format("  Time step:           %8.3f (fsec)", timeStep));
899       logger.info(format("  Print interval:      %8.3f (psec)", loggingInterval));
900       logger.info(format("  Save interval:       %8.3f (psec)", trajectoryInterval));
901       if (molecularAssembly.length > 1) {
902         for (int i = 0; i < molecularAssembly.length; i++) {
903           File archiveFile = molecularAssembly[i].getArchiveFile();
904           logger.info(format("  Archive file %3d: %s", (i + 1), archiveFile.getAbsolutePath()));
905         }
906       } else {
907         logger.info(format("  Archive file:     %s",
908             molecularAssembly[0].getArchiveFile().getAbsolutePath()));
909       }
910       logger.info(format("  Restart file:     %s", restartFile.getAbsolutePath()));
911     }
912   }
913 
914   /**
915    * A version of init with the original method header. Redirects to the new method with default
916    * values for added parameters. Needed by (at least) ReplicaExchange, which calls this directly.
917    *
918    * @param nSteps             Number of MD steps
919    * @param timeStep           Time step in femtoseconds
920    * @param loggingInterval    Interval between printing/logging information in picoseconds.
921    * @param trajectoryInterval Interval between adding a frame to the trajectory file in
922    *                           picoseconds.
923    * @param temperature        Temperature in Kelvins.
924    * @param initVelocities     Initialize new velocities from a Maxwell-Boltzmann distribution.
925    * @param dyn                A {@link java.io.File} object to write the restart file to.
926    */
927   public void init(final long nSteps, final double timeStep, final double loggingInterval,
928                    final double trajectoryInterval, final double temperature, final boolean initVelocities,
929                    final File dyn) {
930     init(nSteps, timeStep, loggingInterval, trajectoryInterval, "XYZ", restartInterval, temperature,
931         initVelocities, dyn);
932   }
933 
934   /**
935    * {@inheritDoc}
936    */
937   @Override
938   public void run() {
939     try {
940       preRunOps();
941     } catch (IllegalStateException ise) {
942       return;
943     }
944     initializeEnergies();
945     postInitEnergies();
946     mainLoop();
947     postRun();
948   }
949 
950   /**
951    * Enable or disable automatic writeout of trajectory snapshots and restart files.
952    *
953    * <p>Primarily intended for use with MC-OST, which handles the timing itself.
954    *
955    * @param autoWriteout Whether to automatically write out trajectory and restart files.
956    */
957   public void setAutomaticWriteouts(boolean autoWriteout) {
958     this.automaticWriteouts = autoWriteout;
959   }
960 
961   /**
962    * Sets the "fallback" .dyn file to write to if none is passed to the dynamic method.
963    *
964    * @param fallback Fallback dynamics restart file.
965    */
966   public void setFallbackDynFile(File fallback) {
967     this.fallbackDynFile = fallback;
968   }
969 
970   /**
971    * Method to set file type from groovy scripts.
972    *
973    * @param fileType the type of snapshot files to write.
974    */
975   public void setFileType(String fileType) {
976     this.fileType = fileType;
977   }
978 
979   /**
980    * Not meaningful for FFX dynamics (no need to obtain velocities/accelerations from a different
981    * program, especially one running on a GPU). Is a no-op.
982    *
983    * @param obtainVA Not meaningful for this implementation.
984    */
985   public void setObtainVelAcc(boolean obtainVA) {
986     // Not meaningful for FFX dynamics.
987   }
988 
989   /**
990    * Method to set the Restart Frequency.
991    *
992    * @param restartInterval Time in ps between writing restart files.
993    * @throws java.lang.IllegalArgumentException If restart frequency is not a positive number
994    */
995   public void setRestartFrequency(double restartInterval) throws IllegalArgumentException {
996     restartFrequency = intervalToFreq(restartInterval, "Restart interval");
997     this.restartInterval = restartInterval;
998   }
999 
1000   /**
1001    * Set the archive file for each MolecularAssembly.
1002    *
1003    * @param archiveFiles An array of archive files.
1004    */
1005   public void setArchiveFiles(File[] archiveFiles) {
1006     logger.info(" Setting archive files:\n " + Arrays.toString(archiveFiles));
1007     int n = molecularAssembly.length;
1008     assert archiveFiles.length == n;
1009     for (int i = 0; i < n; i++) {
1010       molecularAssembly[i].setArchiveFile(archiveFiles[i]);
1011     }
1012   }
1013 
1014   /**
1015    * {@inheritDoc}
1016    */
1017   @Override
1018   public void terminate() {
1019     terminate = true;
1020     while (!done) {
1021       synchronized (this) {
1022         try {
1023           wait(1);
1024         } catch (Exception e) {
1025           logger.log(Level.WARNING, " Exception terminating dynamics.\n", e);
1026         }
1027       }
1028     }
1029   }
1030 
1031   /**
1032    * Write restart and trajectory files if the provided step matches the frequency and that file type
1033    * is requested.
1034    *
1035    * @param step        Step to write files (if any) for.
1036    * @param trySnapshot If false, do not write snapshot even if the time step is correct.
1037    * @param tryRestart  If false, do not write a restart file even if the time step is correct.
1038    * @return EnumSet of actions taken by this method.
1039    */
1040   public EnumSet<MDWriteAction> writeFilesForStep(long step, boolean trySnapshot,
1041                                                   boolean tryRestart) {
1042     return writeFilesForStep(step, trySnapshot, tryRestart, null);
1043   }
1044 
1045   /**
1046    * Write restart and trajectory files if the provided step matches the frequency and that file type
1047    * is requested.
1048    *
1049    * @param step        Step to write files (if any) for.
1050    * @param trySnapshot If false, do not write snapshot even if the time step is correct.
1051    * @param tryRestart  If false, do not write a restart file even if the time step is correct.
1052    * @param extraLines  Additional lines to append into the comments section of the snapshot (or
1053    *                    null).
1054    * @return EnumSet of actions taken by this method.
1055    */
1056   public EnumSet<MDWriteAction> writeFilesForStep(long step, boolean trySnapshot, boolean tryRestart,
1057                                                   String[] extraLines) {
1058     List<String> linesList =
1059         (extraLines == null) ? new ArrayList<>() : new ArrayList<>(Arrays.asList(extraLines));
1060 
1061     if (potential instanceof LambdaInterface) {
1062       String lamString = format("Lambda: %.8f", ((LambdaInterface) potential).getLambda());
1063       linesList.add(lamString);
1064     }
1065 
1066     String tempString = format("Temp: %.2f", thermostat.getTargetTemperature());
1067     linesList.add(tempString);
1068 
1069     String timeString = format(" Time: %7.3e", totalSimTime);
1070     linesList.add(timeString);
1071 
1072     if (esvSystem != null) {
1073       String pHString = format("pH: %.2f", esvSystem.getConstantPh());
1074       linesList.add(pHString);
1075     }
1076 
1077     Comm world = Comm.world();
1078     if (world != null && world.size() > 1) {
1079       String rankString = format("Rank: %d", world.rank());
1080       linesList.add(rankString);
1081     }
1082 
1083     String[] allLines = new String[linesList.size()];
1084     allLines = linesList.toArray(allLines);
1085 
1086     EnumSet<MDWriteAction> written = EnumSet.noneOf(MDWriteAction.class);
1087     if (step != 0) {
1088       // Write out snapshots in selected format every saveSnapshotFrequency steps.
1089       if (trySnapshot && trajectoryFrequency > 0 && step % trajectoryFrequency == 0) {
1090 
1091         // Load current coordinates into atom instances.
1092         potential.setCoordinates(state.x());
1093 
1094         // Log stats.
1095         if (totalEnergyStats.getCount() > 0) {
1096           logger.info(format("\n Average Values for the Last %d Out of %d Dynamics Steps\n",
1097               trajectoryFrequency, step));
1098           logger.info(format("  Simulation Time  %16.4f Picosecond", step * dt));
1099           logger.info(format("  Total Energy     %16.4f Kcal/mole   (+/-%9.4f)", totalEnergyStats.getMean(),
1100               totalEnergyStats.getStandardDeviation()));
1101           logger.info(format("  Potential Energy %16.4f Kcal/mole   (+/-%9.4f)",
1102               potentialEnergyStats.getMean(), potentialEnergyStats.getStandardDeviation()));
1103           logger.info(format("  Kinetic Energy   %16.4f Kcal/mole   (+/-%9.4f)", kineticEnergyStats.getMean(),
1104               kineticEnergyStats.getStandardDeviation()));
1105           logger.info(format("  Temperature      %16.4f Kelvin      (+/-%9.4f)\n", temperatureStats.getMean(),
1106               temperatureStats.getStandardDeviation()));
1107           totalEnergyStats.reset();
1108           potentialEnergyStats.reset();
1109           kineticEnergyStats.reset();
1110           temperatureStats.reset();
1111         }
1112 
1113         if (esvSystem != null) {
1114           for (Atom atom : molecularAssembly[0].getAtomList()) {
1115             int atomIndex = atom.getIndex() - 1;
1116             atom.setOccupancy(esvSystem.getTitrationLambda(atomIndex));
1117             atom.setTempFactor(esvSystem.getTautomerLambda(atomIndex));
1118           }
1119         }
1120         appendSnapshot(allLines);
1121         written.add(MDWriteAction.SNAPSHOT);
1122       }
1123 
1124       // Write out restart files every saveRestartFileFrequency steps.
1125       if (tryRestart && restartFrequency > 0 && step % restartFrequency == 0) {
1126         writeRestart();
1127         written.add(MDWriteAction.RESTART);
1128       }
1129     }
1130     return written;
1131   }
1132 
1133   /**
1134    * Write out a restart file.
1135    */
1136   public void writeRestart() {
1137     String dynName = relativePathTo(restartFile).toString();
1138     double[] x = state.x();
1139     double[] v = state.v();
1140     double[] a = state.a();
1141     double[] aPrevious = state.aPrevious();
1142     if (dynFilter.writeDYN(restartFile, molecularAssembly[0].getCrystal(), x, v, a, aPrevious)) {
1143       logger.log(basicLogging, format(" Wrote dynamics restart to:  %s.", dynName));
1144     } else {
1145       logger.log(basicLogging, format(" Writing dynamics restart failed:  %s.", dynName));
1146     }
1147     if (esvSystem != null) {
1148       esvSystem.writeRestart();
1149       esvSystem.writeLambdaHistogram(false);
1150     }
1151     potential.writeAdditionalRestartInfo(true);
1152   }
1153 
1154   /**
1155    * Set the archive file for each MolecularAssembly (if not already set).
1156    */
1157   private void setArchiveFile() {
1158     for (MolecularAssembly assembly : molecularAssembly) {
1159       File file = assembly.getFile();
1160       String filename = removeExtension(file.getAbsolutePath());
1161       File archiveFile = assembly.getArchiveFile();
1162       if (archiveFile == null) {
1163         archiveFile = new File(filename + ".arc");
1164         assembly.setArchiveFile(XYZFilter.version(archiveFile));
1165       }
1166     }
1167   }
1168 
1169   /**
1170    * Converts an interval in ps to a frequency in time steps.
1171    *
1172    * @param interval Interval between events in ps
1173    * @param describe Description of the event.
1174    * @return Frequency of event in time steps per event.
1175    * @throws IllegalArgumentException If interval is not a positive finite value.
1176    */
1177   private int intervalToFreq(double interval, String describe) throws IllegalArgumentException {
1178     if (!Double.isFinite(interval) || interval <= 0) {
1179       throw new IllegalArgumentException(
1180           format(" %s must be " + "positive finite value in ps, was %10.4g", describe, interval));
1181     }
1182     if (interval >= dt) {
1183       return (int) (interval / dt);
1184     } else {
1185       logger.warning(format(" Specified %s of %.6f ps < time step %.6f ps; "
1186           + "interval is set to once per time step!", describe, interval, dt));
1187       return 1;
1188     }
1189   }
1190 
1191   /**
1192    * Method to set the logging/printing frequency.
1193    *
1194    * @param logInterval Time in ps between logging information to the screen.
1195    */
1196   private void setLoggingFrequency(double logInterval) {
1197     logFrequency = intervalToFreq(logInterval, "Reporting (logging) interval");
1198     this.logInterval = logInterval;
1199   }
1200 
1201   /**
1202    * Method to set the frequency of appending snapshots to the trajectory file.
1203    *
1204    * @param snapshotInterval Time in ps between appending snapshots to the trajectory file.
1205    */
1206   private void setTrajectoryFrequency(double snapshotInterval) {
1207     trajectoryFrequency = intervalToFreq(snapshotInterval, "Trajectory writeout interval");
1208     this.trajectoryInterval = snapshotInterval;
1209   }
1210 
1211   /**
1212    * Performs basic pre-MD operations such as loading the restart file.
1213    */
1214   void preRunOps() throws IllegalStateException {
1215     done = false;
1216     terminate = false;
1217 
1218     // Set the target temperature.
1219     thermostat.setTargetTemperature(targetTemperature);
1220     boolean quiet = verbosityLevel.isQuiet();
1221     thermostat.setQuiet(quiet);
1222     if (integrator instanceof Stochastic stochastic) {
1223       stochastic.setTemperature(targetTemperature);
1224     }
1225 
1226     // Set the step size.
1227     integrator.setTimeStep(dt);
1228 
1229     if (!initialized) {
1230       // Initialize from a restart file.
1231       if (loadRestart) {
1232         Crystal crystal = molecularAssembly[0].getCrystal();
1233         double[] x = state.x();
1234         double[] v = state.v();
1235         double[] a = state.a();
1236         double[] aPrevious = state.aPrevious();
1237         if (!dynFilter.readDYN(restartFile, crystal, x, v, a, aPrevious)) {
1238           String message = " Could not load the restart file - dynamics terminated.";
1239           logger.log(Level.WARNING, message);
1240           done = true;
1241           throw new IllegalStateException(message);
1242         } else {
1243           molecularAssembly[0].setCrystal(crystal);
1244           potential.setCoordinates(x);
1245           potential.setVelocity(v);
1246           potential.setAcceleration(a);
1247           potential.setPreviousAcceleration(aPrevious);
1248         }
1249       } else {
1250         // Initialize using current atomic coordinates.
1251         potential.getCoordinates(state.x());
1252         // Initialize atomic velocities from a Maxwell-Boltzmann distribution or set to 0.
1253         if (initVelocities) {
1254           thermostat.maxwell(targetTemperature);
1255         } else {
1256           fill(state.v(), 0.0);
1257         }
1258       }
1259     } else {
1260       // If MD has already been run (i.e., Annealing or RepEx), then initialize velocities if
1261       // requested.
1262       if (initVelocities) {
1263         thermostat.maxwell(targetTemperature);
1264       }
1265     }
1266   }
1267 
1268   /**
1269    * Initializes energy fields, esp. potential energy.
1270    */
1271   private void initializeEnergies() {
1272     // Compute the current potential energy.
1273     double[] x = state.x();
1274     double[] gradient = state.gradient();
1275 
1276     boolean propagateLambda = true;
1277     if (potential instanceof OrthogonalSpaceTempering orthogonalSpaceTempering) {
1278       propagateLambda = orthogonalSpaceTempering.getPropagateLambda();
1279       orthogonalSpaceTempering.setPropagateLambda(false);
1280     }
1281 
1282     if (esvSystem != null && potential instanceof OpenMMEnergy) {
1283       state.setPotentialEnergy(((OpenMMEnergy) potential).energyAndGradientFFX(x, gradient));
1284     } else {
1285       state.setPotentialEnergy(potential.energyAndGradient(x, gradient));
1286     }
1287 
1288     if (potential instanceof OrthogonalSpaceTempering orthogonalSpaceTempering) {
1289       orthogonalSpaceTempering.setPropagateLambda(propagateLambda);
1290     }
1291 
1292     // Initialize current and previous accelerations.
1293     if (!loadRestart || initialized || integrator instanceof Respa) {
1294       // For the Respa integrator, initial accelerations are from the slowly varying forces.
1295       if (integrator instanceof Respa) {
1296         potential.setEnergyTermState(Potential.STATE.SLOW);
1297         potential.energyAndGradient(x, gradient);
1298       }
1299 
1300       int numberOfVariables = state.getNumberOfVariables();
1301       double[] a = state.a();
1302       double[] mass = state.getMass();
1303       for (int i = 0; i < numberOfVariables; i++) {
1304         a[i] = -KCAL_TO_GRAM_ANG2_PER_PS2 * gradient[i] / mass[i];
1305       }
1306       state.copyAccelerationsToPrevious();
1307     }
1308 
1309     if (esvSystem != null) {
1310       SystemState esvState = esvSystem.getState();
1311       double[] esvA = esvState.a();
1312       double[] esvMass = esvState.getMass();
1313       int nESVs = esvState.getNumberOfVariables();
1314       double[] gradESV = esvSystem.postForce();
1315       for (int i = 0; i < nESVs; i++) {
1316         esvA[i] = -KCAL_TO_GRAM_ANG2_PER_PS2 * gradESV[i] / esvMass[i];
1317       }
1318     }
1319 
1320     // Compute the current kinetic energy.
1321     thermostat.computeKineticEnergy();
1322     if (esvSystem != null) {
1323       esvThermostat.computeKineticEnergy();
1324       double kineticEnergy = thermostat.getKineticEnergy();
1325       double esvKineticEnergy = esvThermostat.getKineticEnergy();
1326       state.setKineticEnergy(kineticEnergy + esvKineticEnergy);
1327     }
1328 
1329     // Store the initial state.
1330     initialState = new UnmodifiableState(state);
1331 
1332     // Reset the statistics.
1333     temperatureStats.reset();
1334     potentialEnergyStats.reset();
1335     kineticEnergyStats.reset();
1336     totalEnergyStats.reset();
1337   }
1338 
1339   /**
1340    * Pre-run operations (mostly logging) that require knowledge of system energy.
1341    */
1342   void postInitEnergies() {
1343     initialized = true;
1344 
1345     logger.log(basicLogging,
1346         format("\n  %8s %12s %12s %12s %8s %8s", "Time", "Kinetic", "Potential", "Total", "Temp", "CPU"));
1347     logger.log(basicLogging,
1348         format("  %8s %12s %12s %12s %8s %8s", "psec", "kcal/mol", "kcal/mol", "kcal/mol", "K", "sec"));
1349     logger.log(basicLogging, format("  %8s %12.4f %12.4f %12.4f %8.2f", "", state.getKineticEnergy(),
1350         state.getPotentialEnergy(), state.getTotalEnergy(), state.getTemperature()));
1351 
1352     // Store the initialized state.
1353     storeState();
1354   }
1355 
1356   /**
1357    * Post-run cleanup operations.
1358    */
1359   void postRun() {
1360     // Add the potential energy of the slow degrees of freedom.
1361     if (integrator instanceof Respa) {
1362       potential.setEnergyTermState(Potential.STATE.BOTH);
1363     }
1364 
1365     // Log normal completion.
1366     if (!terminate) {
1367       logger.log(basicLogging, format(" Completed %8d time steps\n", nSteps));
1368     }
1369 
1370     // Reset the done and terminate flags.
1371     done = true;
1372     terminate = false;
1373   }
1374 
1375   /**
1376    * Append a snapshot to the trajectory file.
1377    *
1378    * @param extraLines Strings of meta-data to include.
1379    */
1380   protected void appendSnapshot(String[] extraLines) {
1381     int numAssemblies = molecularAssembly.length;
1382     int currentAssembly = 0;
1383     // Loop over all molecular assemblies.
1384     for (MolecularAssembly assembly : molecularAssembly) {
1385       File archiveFile = assembly.getArchiveFile();
1386       ForceField forceField = assembly.getForceField();
1387       CompositeConfiguration properties = assembly.getProperties();
1388 
1389       //Remove energy/density from name of assembly as it likely changed during MD.
1390       String name = assembly.getName();
1391       String[] tokens = name.split(" +");
1392       StringBuilder stringBuilder = new StringBuilder();
1393       int numTokens = tokens.length;
1394       for (int i = 0; i < numTokens; i++) {
1395         if (tokens[i].equalsIgnoreCase("Energy:") || tokens[i].equalsIgnoreCase("Density:")) {
1396           //Skip next value.
1397           i++;
1398         } else {
1399           stringBuilder.append(" ").append(tokens[i]);
1400         }
1401       }
1402       assembly.setName(stringBuilder.toString());
1403 
1404       // Save as an ARC file.
1405       if (archiveFile != null && !saveSnapshotAsPDB) {
1406         String aiName = relativePathTo(archiveFile).toString();
1407         if (esvSystem == null) {
1408           XYZFilter xyzFilter = new XYZFilter(archiveFile, assembly, forceField, properties);
1409           if (xyzFilter.writeFile(archiveFile, true, extraLines)) {
1410             logger.log(basicLogging, format(" Appended snapshot to:       %s", aiName));
1411           } else {
1412             logger.warning(format(" Appending snapshot failed:  %s", aiName));
1413           }
1414         } else {
1415           XPHFilter xphFilter = new XPHFilter(archiveFile, assembly, forceField, properties, esvSystem);
1416           if (xphFilter.writeFile(archiveFile, true, extraLines)) {
1417             logger.log(basicLogging, format(" Appended to XPH archive %s", aiName));
1418           } else {
1419             logger.warning(format(" Appending to XPH archive %s failed.", aiName));
1420           }
1421         }
1422       } else if (saveSnapshotAsPDB) {
1423         if (pdbFilter == null) {
1424           pdbFilter = new PDBFilter[numAssemblies];
1425         }
1426         if (pdbFilter[currentAssembly] == null) {
1427           File file = assembly.getFile();
1428           String extName = getExtension(file.getName());
1429           File pdbFile;
1430           if (extName.toLowerCase().startsWith("pdb")) {
1431             // Version the file to avoid appending to the original input file.
1432             pdbFile = TinkerUtils.version(file);
1433           } else {
1434             String filename = removeExtension(file.getAbsolutePath());
1435             pdbFile = new File(filename + ".pdb");
1436           }
1437           pdbFilter[currentAssembly] = new PDBFilter(pdbFile, assembly, forceField, properties);
1438           pdbFilter[currentAssembly].setModelNumbering(0);
1439         }
1440         File pdbFile = pdbFilter[currentAssembly].getFile();
1441         String aiName = relativePathTo(pdbFile).toString();
1442         if (pdbFilter[currentAssembly].writeFile(pdbFile, true, extraLines)) {
1443           logger.log(basicLogging, format(" Appended to PDB file %s", aiName));
1444         } else {
1445           logger.warning(format(" Appending to PDB file to %s failed.", aiName));
1446         }
1447       }
1448       currentAssembly++;
1449     }
1450   }
1451 
1452   /**
1453    * Checks if thermodynamics must be logged. If logged, current time is returned, else the time
1454    * passed in is returned.
1455    *
1456    * @param step Time step to possibly log thermodynamics for.
1457    * @param time Clock time (in nsec) thermodynamics was last logged.
1458    * @return Either current time (if logged), else the time variable passed in.
1459    */
1460   protected long logThermoForTime(long step, long time) {
1461     if (step % logFrequency == 0) {
1462       return logThermodynamics(time);
1463     } else {
1464       return time;
1465     }
1466   }
1467 
1468   /**
1469    * Log thermodynamics to the screen.
1470    *
1471    * @param time Clock time (in nsec) since this was last called.
1472    * @return Clock time at the end of this call.
1473    */
1474   private long logThermodynamics(long time) {
1475     time = System.nanoTime() - time;
1476     logger.log(basicLogging,
1477         format(" %7.3e %12.4f %12.4f %12.4f %8.2f %8.3f", totalSimTime, state.getKineticEnergy(),
1478             state.getPotentialEnergy(), state.getTotalEnergy(), state.getTemperature(),
1479             time * NS2SEC));
1480     return System.nanoTime();
1481   }
1482 
1483   /**
1484    * Main loop of the run method.
1485    */
1486   private void mainLoop() {
1487     // Integrate Newton's equations of motion for the requested number of steps,
1488     // unless early termination is requested.
1489     long time = System.nanoTime();
1490     int removeCOMMotionFrequency = molecularAssembly[0].getForceField().getInteger("removecomfrequency", 100);
1491     if (thermostat.getRemoveCenterOfMassMotion()) {
1492       logger.info(format(" COM will be removed every %3d step(s).", removeCOMMotionFrequency));
1493     }
1494 
1495     if (nonEquilibriumLambda) {
1496       // Configure the number of non-equilibrium dynamics.
1497       nSteps = nonEquilibriumDynamics.setMDSteps(nSteps);
1498       LambdaInterface lambdaInterface = (LambdaInterface) potential;
1499       double lambda = nonEquilibriumDynamics.getInitialLambda();
1500       lambdaInterface.setLambda(lambda);
1501     }
1502 
1503     // Main MD loop to take molecular dynamics steps.
1504     for (long step = 1; step <= nSteps; step++) {
1505 
1506       // Update lambda for non-equilibrium simulations.
1507       if (nonEquilibriumLambda && nonEquilibriumDynamics.isUpdateStep(step)) {
1508         Potential.STATE respaState = potential.getEnergyTermState();
1509         if (integrator instanceof Respa) {
1510           if (respaState != Potential.STATE.BOTH) {
1511             potential.setEnergyTermState(Potential.STATE.BOTH);
1512           }
1513         }
1514         LambdaInterface lambdaInterface = (LambdaInterface) potential;
1515         double currentLambda = lambdaInterface.getLambda();
1516         double currentEnergy = state.getPotentialEnergy();
1517         // Update the lambda value.
1518         double newLambda = nonEquilibriumDynamics.getNextLambda(step, currentLambda);
1519         lambdaInterface.setLambda(newLambda);
1520         // Compute the new energy.
1521         double newEnergy = potential.energy(state.x());
1522         // The non-equilibrium work is the difference in energy.
1523         double dW = newEnergy - currentEnergy;
1524         nonEquilibriumDynamics.addWork(dW);
1525         logger.info(format(" Non-equilibrium L=%5.3f Work=%12.6f", newLambda, nonEquilibriumDynamics.getWork()));
1526 
1527         // Reset the Respa State.
1528         if (integrator instanceof Respa) {
1529           potential.setEnergyTermState(respaState);
1530         }
1531       }
1532 
1533       if (step > 1) {
1534         List<Constraint> constraints = potential.getConstraints();
1535         // TODO: Replace magic numbers with named constants.
1536         long constraintFails = constraints.stream()
1537             .filter((Constraint c) -> !c.constraintSatisfied(state.x(), state.v(), 1E-7, 1E-7)).count();
1538         if (constraintFails > 0) {
1539           logger.info(format(" %d constraint failures in step %d", constraintFails, step));
1540         }
1541       }
1542 
1543       // Do the half-step thermostat operation.
1544       thermostat.halfStep(dt);
1545 
1546       // Do the half-step integration operation.
1547       integrator.preForce(potential);
1548       if (esvSystem != null) {
1549         esvIntegrator.preForce(potential);
1550         //preForce processes theta values after
1551         esvSystem.preForce();
1552       }
1553 
1554       // Compute the potential energy and gradients.
1555       if (esvSystem != null && potential instanceof OpenMMEnergy) {
1556         state.setPotentialEnergy(((OpenMMEnergy) potential).energyAndGradientFFX(state.x(), state.gradient()));
1557       } else {
1558         state.setPotentialEnergy(potential.energyAndGradient(state.x(), state.gradient()));
1559       }
1560 
1561       // Add the potential energy of the slow degrees of freedom.
1562       if (integrator instanceof Respa r) {
1563         double potentialEnergy = state.getPotentialEnergy();
1564         state.setPotentialEnergy(potentialEnergy + r.getHalfStepEnergy());
1565       }
1566 
1567       // Do the full-step integration operation.
1568       integrator.postForce(state.gradient());
1569       if (esvSystem != null) {
1570         double[] dEdL = esvSystem.postForce();
1571         esvIntegrator.postForce(dEdL);
1572       }
1573 
1574       // Compute the full-step kinetic energy.
1575       thermostat.computeKineticEnergy();
1576 
1577       // Do the full-step thermostat operation.
1578       thermostat.fullStep(dt);
1579 
1580       // Recompute the kinetic energy after the full-step thermostat operation.
1581       thermostat.computeKineticEnergy();
1582       if (esvSystem != null) {
1583         // Adiabatic thermostat does nothing at half step.
1584         esvThermostat.computeKineticEnergy();
1585       }
1586 
1587       // Remove center of mass motion if requested.
1588       if (thermostat.getRemoveCenterOfMassMotion() && step % removeCOMMotionFrequency == 0) {
1589         thermostat.centerOfMassMotion(true, false);
1590       }
1591 
1592       // Collect current kinetic energy, temperature, and total energy.
1593       if (esvSystem != null) {
1594         double kineticEnergy = thermostat.getKineticEnergy();
1595         double esvKineticEnergy = esvThermostat.getKineticEnergy();
1596         state.setKineticEnergy(kineticEnergy + esvKineticEnergy);
1597       }
1598 
1599       // Collect running statistics.
1600       temperatureStats.addValue(state.getTemperature());
1601       potentialEnergyStats.addValue(state.getPotentialEnergy());
1602       kineticEnergyStats.addValue(state.getKineticEnergy());
1603       totalEnergyStats.addValue(state.getTotalEnergy());
1604 
1605       // Update atomic velocity, acceleration and previous acceleration.
1606       potential.setVelocity(state.v());
1607       potential.setAcceleration(state.a());
1608       potential.setPreviousAcceleration(state.aPrevious());
1609 
1610       // Log the current state every printFrequency steps.
1611       totalSimTime += dt;
1612       time = logThermoForTime(step, time);
1613       if (step % printEsvFrequency == 0 && esvSystem != null) {
1614         logger.log(basicLogging, format(" %s", esvSystem.getLambdaList()));
1615       }
1616 
1617       if (automaticWriteouts) {
1618         writeFilesForStep(step, true, true);
1619       }
1620 
1621       // Notify the algorithmListeners.
1622       if (algorithmListener != null && step % logFrequency == 0) {
1623         for (MolecularAssembly assembly : molecularAssembly) {
1624           algorithmListener.algorithmUpdate(assembly);
1625         }
1626       }
1627 
1628       // Check for a termination request.
1629       if (terminate) {
1630         logger.info(format("\n Terminating after %8d time steps\n", step));
1631         break;
1632       }
1633     }
1634   }
1635 
1636   /**
1637    * Perform a sanity check on a frequency to ensure it's not longer than total runtime. Currently,
1638    * the setter parameter is ignored due to issues with our test suite.
1639    *
1640    * @param describe  Description of the frequency.
1641    * @param frequency Frequency in time steps.
1642    */
1643   private void checkFrequency(String describe, int frequency) {
1644     if (frequency > nSteps) {
1645       logger.fine(
1646           format(" Specified %s frequency of %d is greater than the number of steps %d", describe,
1647               frequency, nSteps));
1648     }
1649   }
1650 
1651   /**
1652    * Set the coordinates.
1653    *
1654    * @param coords The coordinates to copy into MD coordinates array.
1655    */
1656   public void setCoordinates(double[] coords) {
1657     state.setCoordinates(coords);
1658   }
1659 
1660   /**
1661    * Get the coordinates.
1662    *
1663    * @return A copy of the current coordinates are returned.
1664    */
1665   public double[] getCoordinates() {
1666     return state.getCoordinatesCopy();
1667   }
1668 
1669   /**
1670    * Store the current state of the molecular dynamics simulation in a MDState record.
1671    */
1672   public void storeState() {
1673     storedState = state.getUnmodifiableState();
1674   }
1675 
1676   /**
1677    * Revert the state of the MolecularDynamics instance to the stored MDState.
1678    *
1679    * @throws Exception is thrown if the stored state is null.
1680    */
1681   public void revertState() throws Exception {
1682     if (storedState == null) {
1683       throw new Exception();
1684     }
1685     revertState(storedState);
1686   }
1687 
1688   /**
1689    * Revert the state of the MolecularDynamics instance to the provided MDState.
1690    *
1691    * @param state The MDState to revert to.
1692    */
1693   private void revertState(UnmodifiableState state) {
1694     this.state.revertState(state);
1695     potential.setVelocity(state.v());
1696     potential.setAcceleration(state.a());
1697     potential.setPreviousAcceleration(state.aPrevious());
1698     // ToDo -- move these methods into the Potential interface.
1699     int numberOfVariables = this.state.getNumberOfVariables();
1700     Atom[] atoms = molecularAssembly[0].getActiveAtomArray();
1701     if (atoms.length * 3 == numberOfVariables) {
1702       int index = 0;
1703       double[] x = state.x();
1704       double[] gradient = state.gradient();
1705       for (Atom atom : atoms) {
1706         atom.moveTo(x[index], x[index + 1], x[index + 2]);
1707         atom.setXYZGradient(gradient[index], gradient[index + 1], gradient[index + 2]);
1708         index += 3;
1709       }
1710     }
1711   }
1712 
1713 }