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 ffx.algorithms.AlgorithmListener;
41  import ffx.algorithms.dynamics.integrators.IntegratorEnum;
42  import ffx.algorithms.dynamics.thermostats.ThermostatEnum;
43  import ffx.crystal.Crystal;
44  import ffx.crystal.CrystalPotential;
45  import ffx.numerics.Potential;
46  import ffx.potential.MolecularAssembly;
47  import ffx.potential.UnmodifiableState;
48  import ffx.potential.bonded.Atom;
49  import ffx.potential.bonded.LambdaInterface;
50  import ffx.potential.openmm.OpenMMContext;
51  import ffx.potential.openmm.OpenMMPotential;
52  import ffx.potential.openmm.OpenMMState;
53  import ffx.potential.openmm.OpenMMSystem;
54  
55  import javax.annotation.Nullable;
56  import java.io.File;
57  import java.util.logging.Logger;
58  
59  import static edu.uiowa.jopenmm.OpenMMLibrary.OpenMM_State_DataType.OpenMM_State_Energy;
60  import static edu.uiowa.jopenmm.OpenMMLibrary.OpenMM_State_DataType.OpenMM_State_Forces;
61  import static edu.uiowa.jopenmm.OpenMMLibrary.OpenMM_State_DataType.OpenMM_State_Positions;
62  import static edu.uiowa.jopenmm.OpenMMLibrary.OpenMM_State_DataType.OpenMM_State_Velocities;
63  import static ffx.utilities.Constants.NS2SEC;
64  import static java.lang.String.format;
65  
66  /**
67   * Runs Molecular Dynamics using OpenMM implementation
68   *
69   * @author Michael J. Schnieders
70   */
71  public class MolecularDynamicsOpenMM extends MolecularDynamics {
72  
73    private static final Logger logger = Logger.getLogger(MolecularDynamicsOpenMM.class.getName());
74    /**
75     * Integrator Type.
76     */
77    private final IntegratorEnum integratorType;
78    /**
79     * Thermostat Type.
80     */
81    private final ThermostatEnum thermostatType;
82    /**
83     * The potential energy function.
84     */
85    private final CrystalPotential crystalPotential;
86    /**
87     * Features specific to OpenMM.
88     */
89    private final OpenMMPotential openMMPotential;
90    /**
91     * Integrator String.
92     */
93    private String integratorString;
94    /**
95     * Number of OpenMM MD steps per iteration.
96     */
97    private int intervalSteps;
98    /**
99     * Flag to indicate OpenMM MD interactions are running.
100    */
101   private boolean running;
102   /**
103    * Run time.
104    */
105   private long time;
106   /**
107    * Obtain all variables with each update (i.e., include velocities, gradients).
108    */
109   private boolean getAllVars = true;
110   /**
111    * Method to run on update for obtaining variables. Will either grab everything (default) or
112    * energies + positions (MC-OST).
113    */
114   private Runnable obtainVariables = this::getAllOpenMMVariables;
115 
116   /**
117    * Constructs a MolecularDynamicsOpenMM object, to perform molecular dynamics using native OpenMM
118    * routines, avoiding the cost of communicating coordinates, gradients, and energies back and forth
119    * across the PCI bus.
120    *
121    * @param assembly   MolecularAssembly to operate on
122    * @param potential  Either an OpenMMEnergy or an OpenMMDualTopologyEnergy.
123    * @param listener   a {@link ffx.algorithms.AlgorithmListener} object.
124    * @param thermostat May have to be slightly modified for native OpenMM routines
125    * @param integrator May have to be slightly modified for native OpenMM routines
126    */
127   public MolecularDynamicsOpenMM(MolecularAssembly assembly, Potential potential,
128                                  AlgorithmListener listener, ThermostatEnum thermostat, IntegratorEnum integrator) {
129     super(assembly, potential, listener, thermostat, integrator);
130 
131     logger.info("\n Initializing OpenMM molecular dynamics.");
132 
133     // Initialization specific to MolecularDynamicsOpenMM
134     running = false;
135     crystalPotential = (CrystalPotential) potential;
136     openMMPotential = (OpenMMPotential) potential;
137 
138     thermostatType = thermostat;
139     integratorType = integrator;
140     integratorToString(integratorType);
141   }
142 
143   /**
144    * Set the barostat for this MolecularDynamicsOpenMM instance.
145    * @param barostat The Barostat to set, or null to disable constant pressure.
146    */
147   public void setBarostat(@Nullable Barostat barostat) {
148     if (barostat != null) {
149       this.barostat = barostat;
150       this.constantPressure = true;
151       barostat.setActive(false);
152     } else {
153       this.barostat = null;
154       this.constantPressure = false;
155     }
156   }
157 
158   /**
159    * {@inheritDoc}
160    *
161    * <p>Execute <code>numSteps</code> of dynamics using the provided <code>timeStep</code> and
162    * <code>temperature</code>. The <code>printInterval</code> and <code>saveInterval</code>
163    * control logging the state of the system to the console and writing a restart file, respectively.
164    * If the <code>dyn</code> File is not null, the simulation will be initialized from the contents.
165    * If the <code>iniVelocities</code> is true, the velocities will be initialized from a Maxwell
166    * Boltzmann distribution.
167    */
168   @Override
169   public void dynamic(long numSteps, double timeStep, double printInterval, double saveInterval,
170                       double temperature, boolean initVelocities, File dyn) {
171     // Return if already running and a second thread calls the dynamic method.
172     if (!done) {
173       logger.warning(" Programming error - a thread invoked dynamic when it was already running.");
174       return;
175     }
176 
177     // Call the init method.
178     init(numSteps, timeStep, printInterval, saveInterval, fileType, restartInterval, temperature,
179         initVelocities, dyn);
180 
181     if (intervalSteps == 0 || intervalSteps > numSteps) {
182       // Safe cast: if intervalSteps > numSteps, then numSteps must be less than Integer.MAX_VALUE.
183       intervalSteps = (int) numSteps;
184     }
185 
186     // Initialization, including reading a dyn file or initialization of velocity.
187     preRunOps();
188 
189     // Send coordinates, velocities, etc. to OpenMM.
190     setOpenMMState();
191 
192     // Retrieve starting energy values.
193     getOpenMMEnergies();
194 
195     // Store the initial state.
196     initialState = new UnmodifiableState(state);
197 
198     // Set the mass of inactive atoms to zero. If there are inactive atoms, we force creation of a new OpenMM Context.
199     boolean forceCreation = openMMPotential.setActiveAtoms();
200 
201     // Check that our context is using correct Integrator, time step, and target temperature. If there are inactive
202     // atoms, a new Context will be created.
203     openMMPotential.updateContext(integratorString, dt, targetTemperature, forceCreation);
204 
205     // Pre-run operations (mostly logging) that require knowledge of system energy.
206     postInitEnergies();
207 
208     // Run the MD steps.
209     mainLoop(numSteps);
210 
211     // Post-run cleanup operations.
212     postRun();
213   }
214 
215   /**
216    * {@inheritDoc}
217    */
218   @Override
219   public int getIntervalSteps() {
220     return intervalSteps;
221   }
222 
223   /**
224    * Setter for the field <code>intervalSteps</code>.
225    *
226    * @param intervalSteps The number of interval steps.
227    */
228   @Override
229   public void setIntervalSteps(int intervalSteps) {
230     this.intervalSteps = intervalSteps;
231   }
232 
233   /**
234    * {@inheritDoc}
235    */
236   @Override
237   public double getTimeStep() {
238     return dt;
239   }
240 
241   /**
242    * {@inheritDoc}
243    */
244   @Override
245   public void init(long numSteps, double timeStep, double loggingInterval, double trajectoryInterval,
246                    String fileType, double restartInterval, double temperature, boolean initVelocities,
247                    File dyn) {
248 
249     super.init(numSteps, timeStep, loggingInterval, trajectoryInterval, fileType, restartInterval,
250         temperature, initVelocities, dyn);
251 
252     boolean isLangevin = IntegratorEnum.isStochastic(integratorType);
253 
254     OpenMMSystem openMMSystem = openMMPotential.getSystem();
255     if (!isLangevin && !thermostatType.equals(ThermostatEnum.ADIABATIC)) {
256       // Add Andersen thermostat, or if already present update its target temperature.
257       openMMSystem.addAndersenThermostatForce(targetTemperature);
258     }
259 
260     if (constantPressure) {
261       // Add an isotropic Monte Carlo barostat.
262       // If it is already present, update its target temperature, pressure and frequency.
263       double pressure = barostat.getPressure();
264       int frequency = barostat.getMeanBarostatInterval();
265       openMMSystem.addMonteCarloBarostatForce(pressure, targetTemperature, frequency);
266     }
267 
268     // For Langevin/Stochastic dynamics, center of mass motion will not be removed.
269     if (!isLangevin) {
270       // No action is taken if a COMMRemover is already present.
271       openMMSystem.addCOMMRemoverForce();
272     }
273 
274     // Set the current value of lambda.
275     if (crystalPotential instanceof LambdaInterface lambdaInferface) {
276       lambdaInferface.setLambda(lambdaInferface.getLambda());
277     }
278 
279   }
280 
281   @Override
282   public void revertState() throws Exception {
283     super.revertState();
284     setOpenMMState();
285   }
286 
287   /**
288    * {@inheritDoc}
289    */
290   @Override
291   public void setFileType(String fileType) {
292     this.fileType = fileType;
293   }
294 
295   /**
296    * Sets whether to obtain all variables (velocities, gradients) from OpenMM, or just positions and
297    * energies.
298    *
299    * @param obtainVA If true, obtain all variables from OpenMM each update.
300    */
301   @Override
302   public void setObtainVelAcc(boolean obtainVA) {
303     // TODO: Make this more generic by letting it obtain any weird combination of variables.
304     getAllVars = obtainVA;
305     obtainVariables = obtainVA ? this::getAllOpenMMVariables : this::getOpenMMEnergiesAndPositions;
306   }
307 
308   @Override
309   public void writeRestart() {
310     if (!getAllVars) {
311       // If !getAllVars, need to ensure all variables are synced before writing the restart.
312       getAllOpenMMVariables();
313     }
314     super.writeRestart();
315   }
316 
317   @Override
318   protected void appendSnapshot(String[] extraLines) {
319     if (!getAllVars) {
320       // If !getAllVars, need to ensure coordinates are synced before writing a snapshot.
321       getOpenMMEnergiesAndPositions();
322     }
323     super.appendSnapshot(extraLines);
324   }
325 
326   /**
327    * Integrate the simulation using the defined Context and Integrator.
328    *
329    * @param intervalSteps Number of MD steps to take.
330    */
331   private void takeOpenMMSteps(int intervalSteps) {
332     OpenMMContext openMMContext = openMMPotential.getContext();
333     openMMContext.integrate(intervalSteps);
334   }
335 
336   /**
337    * Load coordinates, box vectors and velocities.
338    */
339   private void setOpenMMState() {
340     OpenMMContext openMMContext = openMMPotential.getContext();
341     // Load box vectors into OpenMM.
342     openMMContext.setPeriodicBoxVectors(crystalPotential.getCrystal());
343     // Load coordinates into atom instances and into OpenMM.
344     crystalPotential.setCoordinates(state.x());
345     // Load velocities into atom instances and into OpenMM.
346     crystalPotential.setVelocity(state.v());
347   }
348 
349   /**
350    * Get OpenMM Energies.
351    */
352   private void getOpenMMEnergies() {
353     OpenMMState openMMState = openMMPotential.getOpenMMState(OpenMM_State_Energy);
354     state.setKineticEnergy(openMMState.kineticEnergy);
355     state.setPotentialEnergy(openMMState.potentialEnergy);
356     state.setTemperature(openMMPotential.getSystem().getTemperature(openMMState.kineticEnergy));
357     openMMState.destroy();
358   }
359 
360   /**
361    * Do some logging of the beginning energy values.
362    */
363   void postInitEnergies() {
364     super.postInitEnergies();
365     running = true;
366   }
367 
368   private void mainLoop(long numSteps) {
369     long i = 0;
370     time = System.nanoTime();
371 
372     while (i < numSteps) {
373 
374       // Take MD steps in OpenMM.
375       long takeStepsTime = -System.nanoTime();
376       takeOpenMMSteps(intervalSteps);
377       takeStepsTime += System.nanoTime();
378       logger.finest(String.format("\n Took steps in %6.3f", takeStepsTime * NS2SEC));
379       totalSimTime += intervalSteps * dt;
380 
381       // Update the total step count.
382       i += intervalSteps;
383 
384       long secondUpdateTime = -System.nanoTime();
385       updateFromOpenMM(i, running);
386       secondUpdateTime += System.nanoTime();
387 
388       logger.finest(format("\n Update finished in %6.3f", secondUpdateTime * NS2SEC));
389     }
390   }
391 
392   /**
393    * Get OpenMM Energies and Positions.
394    */
395   private void getOpenMMEnergiesAndPositions() {
396     int mask = OpenMM_State_Energy | OpenMM_State_Positions;
397     OpenMMState openMMState = openMMPotential.getOpenMMState(mask);
398     state.setPotentialEnergy(openMMState.potentialEnergy);
399     state.setKineticEnergy(openMMState.kineticEnergy);
400     state.setTemperature(openMMPotential.getSystem().getTemperature(openMMState.kineticEnergy));
401     Crystal crystal = crystalPotential.getCrystal();
402     if (!crystal.aperiodic()) {
403       double[][] cellVectors = openMMState.getPeriodicBoxVectors();
404       crystal.setCellVectors(cellVectors);
405       crystalPotential.setCrystal(crystal);
406     }
407 
408     // Load positions into the MD state.
409     Atom[] atoms = openMMPotential.getSystem().getAtoms();
410     openMMState.getActivePositions(state.x(), atoms);
411     openMMState.destroy();
412   }
413 
414   /**
415    * Get OpenMM energies, positions, velocities, and accelerations.
416    */
417   private void getAllOpenMMVariables() {
418     int mask = OpenMM_State_Energy | OpenMM_State_Positions | OpenMM_State_Velocities | OpenMM_State_Forces;
419     OpenMMState openMMState = openMMPotential.getOpenMMState(mask);
420     state.setPotentialEnergy(openMMState.potentialEnergy);
421     state.setKineticEnergy(openMMState.kineticEnergy);
422     state.setTemperature(openMMPotential.getSystem().getTemperature(openMMState.kineticEnergy));
423     Crystal crystal = crystalPotential.getCrystal();
424     if (!crystal.aperiodic()) {
425       double[][] cellVectors = openMMState.getPeriodicBoxVectors();
426       crystal.setCellVectors(cellVectors);
427       crystalPotential.setCrystal(crystal);
428     }
429 
430     // Load positions, velocities, and accelerations into the MD state.
431     Atom[] atoms = openMMPotential.getSystem().getAtoms();
432     openMMState.getActivePositions(state.x(), atoms);
433     openMMState.getActiveVelocities(state.v(), atoms);
434     openMMState.getActiveAccelerations(state.a(), atoms);
435     openMMState.destroy();
436   }
437 
438   /**
439    * updateFromOpenMM obtains the state of the simulation from OpenMM, completes some logging, and
440    * saves restart files.
441    *
442    * @param i       Number of OpenMM MD rounds.
443    * @param running True if OpenMM MD rounds have begun running.
444    */
445   private void updateFromOpenMM(long i, boolean running) {
446 
447     double priorPE = state.getPotentialEnergy();
448 
449     obtainVariables.run();
450 
451     if (running) {
452       if (i == 0) {
453         logger.log(basicLogging,
454             format("\n  %8s %12s %12s %12s %8s %8s", "Time", "Kinetic", "Potential", "Total", "Temp", "CPU"));
455         logger.log(basicLogging,
456             format("  %8s %12s %12s %12s %8s %8s", "psec", "kcal/mol", "kcal/mol", "kcal/mol", "K", "sec"));
457         logger.log(basicLogging,
458             format("  %8s %12.4f %12.4f %12.4f %8.2f", "", state.getKineticEnergy(),
459                 state.getPotentialEnergy(), state.getTotalEnergy(), state.getTemperature()));
460       }
461       time = logThermoForTime(i, time);
462 
463       if (automaticWriteouts) {
464         writeFilesForStep(i, true, true);
465       }
466     }
467   }
468 
469   /**
470    * integratorToString.
471    *
472    * @param integrator a {@link ffx.algorithms.dynamics.integrators.IntegratorEnum} object.
473    */
474   private void integratorToString(IntegratorEnum integrator) {
475     if (integrator == null) {
476       integratorString = "VERLET";
477       logger.info(" An integrator was not specified. Verlet will be used.");
478     } else {
479       switch (integratorType) {
480         default -> integratorString = "VERLET";
481         case STOCHASTIC, LANGEVIN -> integratorString = "LANGEVIN";
482         case RESPA, MTS -> integratorString = "MTS";
483         case STOCHASTIC_MTS, LANGEVIN_MTS -> integratorString = "LANGEVIN-MTS";
484       }
485     }
486   }
487 }