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.thermodynamics;
39  
40  import static java.lang.String.format;
41  import static java.util.Arrays.fill;
42  
43  import edu.rit.mp.LongBuf;
44  import edu.rit.pj.Comm;
45  import ffx.algorithms.cli.DynamicsOptions;
46  import ffx.algorithms.cli.OSTOptions;
47  import ffx.algorithms.dynamics.MolecularDynamics;
48  import ffx.algorithms.dynamics.MDWriteAction;
49  import ffx.algorithms.mc.BoltzmannMC;
50  import ffx.potential.MolecularAssembly;
51  import ffx.potential.cli.WriteoutOptions;
52  import ffx.utilities.Constants;
53  
54  import java.io.File;
55  import java.io.IOException;
56  import java.util.Arrays;
57  import java.util.EnumSet;
58  import java.util.Optional;
59  import java.util.Random;
60  import java.util.concurrent.ThreadLocalRandom;
61  import java.util.function.LongConsumer;
62  import java.util.logging.Level;
63  import java.util.logging.Logger;
64  import java.util.stream.IntStream;
65  import org.apache.commons.configuration2.CompositeConfiguration;
66  import org.apache.commons.io.FilenameUtils;
67  
68  /**
69   * An implementation of RepEx between Orthogonal Space Tempering potentials.
70   *
71   * @author Jacob Litman
72   * @author Michael J. Schnieders
73   * @since 1.0
74   */
75  public class RepExOST {
76  
77    private static final Logger logger = Logger.getLogger(RepExOST.class.getName());
78    private static final int mainLoopTag = 2020;
79    private final OrthogonalSpaceTempering orthogonalSpaceTempering;
80    private final OrthogonalSpaceTempering.Histogram[] allHistograms;
81    private final SendSynchronous[] sendSynchronous;
82    private final LongConsumer algoRun;
83    private final MolecularDynamics molecularDynamics;
84    private final DynamicsOptions dynamicsOptions;
85    private final String fileType;
86    private final MonteCarloOST monteCarloOST;
87    private final long stepsBetweenExchanges;
88    private final Comm world;
89    private final int rank;
90    private final int numPairs;
91    private final int[] rankToHisto;
92    private final int[] histoToRank;
93    private final boolean isMC;
94    private final Random random;
95    private final double invKT;
96    private final String basePath;
97    private final String[] allFilenames;
98    private final File dynFile;
99    private final String extension;
100   private final long[] totalSwaps;
101   private final long[] acceptedSwaps;
102   private boolean reinitVelocities = true;
103   private int currentHistoIndex;
104   private double currentLambda;
105 
106   /**
107    * Private constructor used here to centralize shared logic.
108    *
109    * @param orthogonalSpaceTempering An OrthogonalSpaceTempering for each RepEx rung.
110    * @param monteCarloOST A MonteCarloOST for each RepEx rung, or null (for MD).
111    * @param molecularDynamics A MolecularDynamics for each RepEx rung (never null).
112    * @param ostType Type of OST to run (MD, MC 1-step, MC 2-step).
113    * @param dynamicsOptions DynamicsOptions to apply universally.
114    * @param ostOptions OST options to apply.
115    * @param fileType File type to save to.
116    * @param repexInterval Interval in psec between RepEx attempts.
117    */
118   private RepExOST(OrthogonalSpaceTempering orthogonalSpaceTempering, MonteCarloOST monteCarloOST,
119       MolecularDynamics molecularDynamics, OstType ostType, DynamicsOptions dynamicsOptions,
120       OSTOptions ostOptions, CompositeConfiguration properties, String fileType,
121       double repexInterval) throws IOException {
122     this.orthogonalSpaceTempering = orthogonalSpaceTempering;
123     switch (ostType) {
124       case MD -> {
125         algoRun = this::runMD;
126         isMC = false;
127       }
128       case MC_ONESTEP -> {
129         algoRun = this::runMCOneStep;
130         isMC = true;
131       }
132       case MC_TWOSTEP -> {
133         algoRun = this::runMCTwoStep;
134         isMC = true;
135       }
136       default -> throw new IllegalArgumentException(
137           " Could not recognize whether this is supposed to be MD, MC 1-step, or MC 2-step!");
138     }
139     this.molecularDynamics = molecularDynamics;
140     this.molecularDynamics.setAutomaticWriteouts(false);
141     this.dynamicsOptions = dynamicsOptions;
142     this.fileType = fileType;
143     this.monteCarloOST = monteCarloOST;
144     if (monteCarloOST != null) {
145       monteCarloOST.setAutomaticWriteouts(false);
146     }
147     this.extension = WriteoutOptions.toArchiveExtension(fileType);
148 
149     this.world = Comm.world();
150     this.rank = world.rank();
151     int size = world.size();
152 
153     MolecularAssembly[] allAssemblies = this.molecularDynamics.getAssemblyArray();
154     allFilenames = Arrays.stream(allAssemblies).map(MolecularAssembly::getFile).map(File::getName)
155         .map(FilenameUtils::getBaseName).toArray(String[]::new);
156 
157     File firstFile = allAssemblies[0].getFile();
158     basePath = FilenameUtils.getFullPath(firstFile.getAbsolutePath()) + File.separator;
159     String baseFileName = FilenameUtils.getBaseName(firstFile.getAbsolutePath());
160     dynFile = new File(format("%s%d%s%s.dyn", basePath, rank, File.separator, baseFileName));
161     this.molecularDynamics.setFallbackDynFile(dynFile);
162 
163     currentHistoIndex = orthogonalSpaceTempering.getHistogram().ld.histogramIndex;
164 
165     allHistograms = orthogonalSpaceTempering.getAllHistograms();
166     this.numPairs = size - 1;
167     this.invKT = -1.0 / (Constants.R * dynamicsOptions.getTemperature());
168 
169     long seed;
170     // TODO: Set this per-process individually if we move back to sending accept/reject messages
171     // up/down the chain.
172     LongBuf seedBuf = LongBuf.buffer(0L);
173     if (rank == 0) {
174       seed = properties.getLong("randomseed", ThreadLocalRandom.current().nextLong());
175       seedBuf.put(0, seed);
176       world.broadcast(0, seedBuf);
177     } else {
178       world.broadcast(0, seedBuf);
179       seed = seedBuf.get(0);
180     }
181     this.random = new Random(seed);
182 
183     double timestep = dynamicsOptions.getDt() * Constants.FSEC_TO_PSEC;
184     stepsBetweenExchanges = Math.max(1, (int) (repexInterval / timestep));
185 
186     sendSynchronous = Arrays.stream(allHistograms)
187         .map(OrthogonalSpaceTempering.Histogram::getSynchronousSend).map(Optional::get)
188         .toArray(SendSynchronous[]::new);
189     if (sendSynchronous.length < 1) {
190       throw new IllegalArgumentException(" No SynchronousSend objects were found!");
191     }
192 
193     rankToHisto = IntStream.range(0, size).toArray();
194     // TODO: Properly back-copy instead of assuming everything is in order at the start.
195     histoToRank = Arrays.copyOf(rankToHisto, size);
196 
197     Arrays.stream(sendSynchronous)
198         .forEach((SendSynchronous ss) -> ss.setHistograms(allHistograms, rankToHisto));
199 
200     totalSwaps = new long[numPairs];
201     acceptedSwaps = new long[numPairs];
202     fill(totalSwaps, 0);
203     fill(acceptedSwaps, 0);
204 
205     setFiles();
206     setHistogram(rank);
207   }
208 
209   /**
210    * Construct a RepExOST for Monte Carlo orthogonal space tempering.
211    *
212    * @param orthogonalSpaceTempering An OrthogonalSpaceTempering for each RepEx rung.
213    * @param monteCarloOST A MonteCarloOST for each RepEx rung
214    * @param dynamicsOptions DynamicsOptions to apply universally.
215    * @param ostOptions OST options to apply.
216    * @param compositeConfiguration CompositeConfiguration properties to use.
217    * @param fileType File type to save to.
218    * @param twoStep Whether to use the 2-step MC algorithm (instead of the 1-step).
219    * @param repexInterval Interval in psec between RepEx attempts.
220    * @return A RepExOST.
221    * @throws IOException if one occurs constructing RepExOST.
222    */
223   public static RepExOST repexMC(OrthogonalSpaceTempering orthogonalSpaceTempering,
224       MonteCarloOST monteCarloOST, DynamicsOptions dynamicsOptions, OSTOptions ostOptions,
225       CompositeConfiguration compositeConfiguration, String fileType, boolean twoStep,
226       double repexInterval) throws IOException {
227     MolecularDynamics md = monteCarloOST.getMD();
228     OstType type = twoStep ? OstType.MC_TWOSTEP : OstType.MC_ONESTEP;
229     return new RepExOST(orthogonalSpaceTempering, monteCarloOST, md, type, dynamicsOptions,
230         ostOptions, compositeConfiguration, fileType, repexInterval);
231   }
232 
233   /**
234    * Construct a RepExOST for Molecular Dynamics orthogonal space tempering.
235    *
236    * @param orthogonalSpaceTempering An OrthogonalSpaceTempering for each RepEx rung.
237    * @param molecularDynamics A MolecularDynamics for each RepEx rung.
238    * @param dynamicsOptions DynamicsOptions to apply universally.
239    * @param ostOptions OST options to apply.
240    * @param compositeConfiguration CompositeConfiguration properties to use.
241    * @param fileType File type to save to.
242    * @param repexInterval Interval in psec between RepEx attempts.
243    * @return A RepExOST.
244    * @throws IOException if one occurs constructing RepExOST.
245    */
246   public static RepExOST repexMD(OrthogonalSpaceTempering orthogonalSpaceTempering,
247       MolecularDynamics molecularDynamics, DynamicsOptions dynamicsOptions, OSTOptions ostOptions,
248       CompositeConfiguration compositeConfiguration, String fileType, double repexInterval)
249       throws IOException {
250     return new RepExOST(orthogonalSpaceTempering, null, molecularDynamics, OstType.MD,
251         dynamicsOptions, ostOptions, compositeConfiguration, fileType, repexInterval);
252   }
253 
254   public OrthogonalSpaceTempering getOST() {
255     return orthogonalSpaceTempering;
256   }
257 
258   /**
259    * Executes the main loop of RepExOST.
260    *
261    * @param numTimesteps Number of times steps.
262    * @param equilibrate If true, perform equilibration.
263    * @throws IOException Possible from Parallel Java.
264    */
265   public void mainLoop(long numTimesteps, boolean equilibrate) throws IOException {
266     if (isMC) {
267       monteCarloOST.setEquilibration(equilibrate);
268     }
269     currentLambda = orthogonalSpaceTempering.getLambda();
270 
271     fill(totalSwaps, 0);
272     fill(acceptedSwaps, 0);
273 
274     if (equilibrate) {
275       logger.info(
276           format(" Equilibrating RepEx OST without exchanges on histogram %d.", currentHistoIndex));
277       algoRun.accept(numTimesteps);
278       reinitVelocities = false;
279     } else {
280       long numExchanges = numTimesteps / stepsBetweenExchanges;
281       for (int i = 0; i < numExchanges; i++) {
282         logger.info(format(" Beginning of RepEx loop %d of %d, operating on histogram %d", (i + 1),
283             numExchanges, currentHistoIndex));
284         world.barrier(mainLoopTag);
285         algoRun.accept(stepsBetweenExchanges);
286         orthogonalSpaceTempering.logOutputFiles(currentHistoIndex);
287         world.barrier(mainLoopTag);
288         proposeSwaps((i % 2), 2);
289         setFiles();
290 
291         long mdMoveNum = i * stepsBetweenExchanges;
292         currentLambda = orthogonalSpaceTempering.getLambda();
293         boolean trySnapshot = currentLambda >= orthogonalSpaceTempering.getLambdaWriteOut();
294         // False if the RepEx OST is not responsible for writing files out.
295         EnumSet<MDWriteAction> written = molecularDynamics.writeFilesForStep(mdMoveNum, trySnapshot,
296             true);
297         if (written.contains(MDWriteAction.RESTART)) {
298           orthogonalSpaceTempering.writeAdditionalRestartInfo(false);
299         }
300 
301         reinitVelocities = false;
302       }
303     }
304 
305     logger.info(" Final rank-to-histogram mapping: " + Arrays.toString(rankToHisto));
306   }
307 
308   // Below logIf methods are intended to reduce redundant logging
309 
310   private void setHistogram(int index) {
311     currentHistoIndex = index;
312     orthogonalSpaceTempering.switchHistogram(index);
313   }
314 
315   /**
316    * Logs a message if this is the master (rank 0) process.
317    *
318    * @param message Message to log (at INFO)
319    */
320   private void logIfMaster(String message) {
321     logIfMaster(Level.INFO, message);
322   }
323 
324   /**
325    * Logs a message if this is the master (rank 0) process.
326    *
327    * @param message Message to log
328    * @param level Logging level to log at
329    */
330   private void logIfMaster(Level level, String message) {
331     if (rank == 0) {
332       logger.log(level, message);
333     }
334   }
335 
336   /**
337    * Logs a message if it is the lowest-ranked process computing a given swap. Currently, this just
338    * wraps logIfMaster, as all processes compute all swaps.
339    *
340    * @param message Message to log (at INFO)
341    */
342   private void logIfSwapping(String message) {
343     logIfMaster(message);
344   }
345 
346   private void setFiles() {
347     File[] trajFiles = Arrays.stream(allFilenames).map(
348         (String fn) -> format("%s%d%s%s.%s", basePath, currentHistoIndex, File.separator, fn,
349             extension)).map(File::new).toArray(File[]::new);
350     molecularDynamics.setArchiveFiles(trajFiles);
351   }
352 
353   /**
354    * Main loop for consistent PRNG-based RepEx (i.e. every process tests every swap independently).
355    * Typically, to create an odd-even staggered schedule (i.e. each pair is tested every other
356    * cycle), offset is either 0 or 1, and stride is 2.
357    *
358    * @param offset Index of the first pair to test swaps for.
359    * @param stride Test every nth pair.
360    */
361   private void proposeSwaps(final int offset, final int stride) {
362     for (int i = offset; i < numPairs; i += stride) {
363       int rankLow = histoToRank[i];
364       int rankHigh = histoToRank[i + 1];
365       OrthogonalSpaceTempering.Histogram histoLow = allHistograms[i];
366       OrthogonalSpaceTempering.Histogram histoHigh = allHistograms[i + 1];
367 
368       double lamLow = histoLow.getLastReceivedLambda();
369       double dUdLLow = histoLow.getLastReceivedDUDL();
370       double lamHigh = histoHigh.getLastReceivedLambda();
371       double dUdLHigh = histoHigh.getLastReceivedDUDL();
372 
373       double eii = histoLow.computeBiasEnergy(lamLow, dUdLLow);
374       double eij = histoLow.computeBiasEnergy(lamHigh, dUdLHigh);
375       double eji = histoHigh.computeBiasEnergy(lamLow, dUdLLow);
376       double ejj = histoHigh.computeBiasEnergy(lamHigh, dUdLHigh);
377 
378       logIfSwapping(format(
379           "\n Proposing exchange between histograms %d (rank %d) and %d (rank %d).\n"
380               + " Li: %.6f dU/dLi: %.6f Lj: %.6f dU/dLj: %.6f", i, rankLow, i + 1, rankHigh, lamLow,
381           dUdLLow, lamHigh, dUdLHigh));
382 
383       double e1 = eii + ejj;
384       double e2 = eji + eij;
385       boolean accept = BoltzmannMC.evaluateMove(random, invKT, e1, e2);
386       double acceptChance = BoltzmannMC.acceptChance(invKT, e1, e2);
387 
388       String desc = accept ? "Accepted" : "Rejected";
389       logIfSwapping(format(
390           " %s exchange with probability %.5f based on Eii %.6f, Ejj %.6f, Eij %.6f, Eji %.6f kcal/mol",
391           desc, acceptChance, eii, ejj, eij, eji));
392 
393       ++totalSwaps[i];
394       if (accept) {
395         ++acceptedSwaps[i];
396         switchHistos(rankLow, rankHigh, i);
397       }
398 
399       double acceptRate = ((double) acceptedSwaps[i]) / ((double) totalSwaps[i]);
400       logIfSwapping(format(" Replica exchange acceptance rate for pair %d-%d is %.3f%%", i, (i + 1),
401           acceptRate * 100));
402     }
403   }
404 
405   private void switchHistos(int rankLow, int rankHigh, int histoLow) {
406     int histoHigh = histoLow + 1;
407     rankToHisto[rankLow] = histoHigh;
408     rankToHisto[rankHigh] = histoLow;
409     histoToRank[histoLow] = rankHigh;
410     histoToRank[histoHigh] = rankLow;
411     setHistogram(rankToHisto[rank]);
412 
413     orthogonalSpaceTempering.setLambda(currentLambda);
414     /* TODO: If there is ever a case where an algorithm will not update coordinates itself at the start, we have to
415      * update coordinates here (from the OST we used to be running on to the new OST). */
416 
417     for (SendSynchronous send : sendSynchronous) {
418       send.updateRanks(rankToHisto);
419     }
420   }
421 
422   /**
423    * Run 1-step MC-OST for the specified number of MD steps.
424    *
425    * @param numSteps Number of MD steps (not MC cycles) to run.
426    */
427   private void runMCOneStep(long numSteps) {
428     monteCarloOST.setTotalSteps(numSteps);
429     monteCarloOST.sampleOneStep();
430   }
431 
432   /**
433    * Run 2-step MC-OST for the specified number of MD steps.
434    *
435    * @param numSteps Number of MD steps (not MC cycles) to run.
436    */
437   private void runMCTwoStep(long numSteps) {
438     monteCarloOST.setTotalSteps(numSteps);
439     monteCarloOST.sampleTwoStep();
440   }
441 
442   /**
443    * Run MD for the specified number of steps.
444    *
445    * @param numSteps MD steps to run.
446    */
447   private void runMD(long numSteps) {
448     molecularDynamics.dynamic(numSteps, dynamicsOptions.getDt(), dynamicsOptions.getReport(),
449         dynamicsOptions.getSnapshotInterval(), dynamicsOptions.getTemperature(), reinitVelocities,
450         fileType, dynamicsOptions.getCheckpoint(), dynFile);
451   }
452 
453   private enum OstType {
454     MD, MC_ONESTEP, MC_TWOSTEP
455   }
456 }