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.commands;
39  
40  import ffx.algorithms.cli.AlgorithmsCommand;
41  import ffx.crystal.CrystalPotential;
42  import ffx.numerics.Potential;
43  import ffx.numerics.estimator.BennettAcceptanceRatio;
44  import ffx.numerics.estimator.EstimateBootstrapper;
45  import ffx.numerics.estimator.MBARFilter;
46  import ffx.numerics.estimator.MultistateBennettAcceptanceRatio;
47  import ffx.numerics.estimator.MultistateBennettAcceptanceRatio.SeedType;
48  import ffx.potential.MolecularAssembly;
49  import ffx.potential.bonded.LambdaInterface;
50  import ffx.potential.cli.AlchemicalOptions;
51  import ffx.potential.cli.TopologyOptions;
52  import ffx.potential.parsers.SystemFilter;
53  import ffx.utilities.FFXBinding;
54  import org.apache.commons.io.FilenameUtils;
55  import picocli.CommandLine.Command;
56  import picocli.CommandLine.Mixin;
57  import picocli.CommandLine.Option;
58  import picocli.CommandLine.Parameters;
59  
60  import java.io.File;
61  import java.util.Arrays;
62  import java.util.Collections;
63  import java.util.List;
64  
65  import static java.lang.String.format;
66  
67  /**
68   * Perform MBAR calculation or necessary energy evaluations. See PhEnergy for CpHMD energy evaluations.
69   * <br>
70   * Usage:
71   * <br>
72   * ffxc test.MBAR [options] &lt;path&gt;
73   */
74  @Command(description = " Evaluates a free energy change with the Multistate Bennett Acceptance Ratio algorithm.",
75      name = "MBAR")
76  public class MBAR extends AlgorithmsCommand {
77  
78    @Mixin
79    private AlchemicalOptions alchemicalOptions;
80  
81    @Mixin
82    private TopologyOptions topologyOptions;
83  
84    @Option(names = {"--bar"}, paramLabel = "true",
85        description = "Run BAR calculation as well using a subset of the MBAR data.")
86    private boolean bar = true;
87  
88    @Option(names = {"--convergence"}, paramLabel = "false",
89        description = "Run MBAR multiple times across different time periods of the data to examine the change in FE over time.")
90    private boolean convergence = false;
91  
92    @Option(names = {"--numBootstrap", "--nb"}, paramLabel = "0",
93        description = "Number of bootstrap samples to use.")
94    private int numBootstrap = 0;
95  
96    @Option(names = {"--numLambda", "--nL", "--nw"}, paramLabel = "-1",
97        description = "Required for lambda energy evaluations. Ensure numLambda is consistent with the trajectory lambdas, i.e. gaps between traj can be filled easily. nL >> nTraj is recommended.")
98    private int numLambda = -1;
99  
100   @Option(names = {"--lambdaDerivative", "--lD"}, paramLabel = "false",
101       description = "Calculate lambda derivatives for each snapshot.")
102   private boolean lambdaDerivative = false;
103 
104   @Option(names = {"--continuousLambda", "--cL"}, paramLabel = "false",
105       description = "Data comes from continuous lambda source and only contains mbar file.")
106   private boolean continuousLambda = false;
107 
108   @Option(names = {"--outputDir", "--oD"}, paramLabel = "",
109       description = "Where to place MBAR files. Default is ../mbarFiles/energy_(window#).mbar. Will write out a file called energy_0.mbar.")
110   private String outputDirectory = "";
111 
112   @Option(names = {"--seed"}, paramLabel = "BAR",
113       description = "Seed MBAR calculation with this: ZEROS, ZWANZIG, BAR. Fallback to ZEROS if input is does not or is unlikely to converge.")
114   private String seedWith = "BAR";
115 
116   @Option(names = {"--tol", "--tolerance"}, paramLabel = "1e-7",
117       description = "Iteration change tolerance.")
118   private double tol = 1e-7;
119 
120   @Option(names = {"--ss", "--startSnapshot"}, paramLabel = "-1",
121       description = "Start at this snapshot when reading in tinker BAR files.")
122   private int startingSnapshot = -1;
123 
124   @Option(names = {"--es", "--endSnapshot"}, paramLabel = "-1",
125       description = "End at this snapshot when reading in tinker BAR files.")
126   private int endingSnapshot = -1;
127 
128   @Option(names = {"--verbose"}, paramLabel = "false",
129       description = "Log weight matrices, iterations, and other details.")
130   private boolean verbose = false;
131 
132   /**
133    * The path to MBAR/BAR files.
134    */
135   @Parameters(arity = "1..*", paramLabel = "files",
136       description = "Path to MBAR/BAR files to analyze or an PDB/XYZ in a directory with archive(s).")
137   private List<String> fileList = null;
138 
139   public MultistateBennettAcceptanceRatio mbar = null;
140   private int numTopologies;
141 
142   /**
143    * MBAR Constructor.
144    */
145   public MBAR() {
146     super();
147   }
148 
149   /**
150    * MBAR Constructor.
151    *
152    * @param binding The Groovy Binding to use.
153    */
154   public MBAR(FFXBinding binding) {
155     super(binding);
156   }
157 
158   /**
159    * MBAR constructor that sets the command line arguments.
160    *
161    * @param args Command line arguments.
162    */
163   public MBAR(String[] args) {
164     super(args);
165   }
166 
167   /**
168    * {@inheritDoc}
169    */
170   @Override
171   public MBAR run() {
172     if (!init()) {
173       return this;
174     }
175 
176     // Load user supplied fileNames into an array.
177     if (fileList == null) {
178       logger.severe("No path to MBAR/BAR or trajectory(s) file names specified.");
179       return this;
180     }
181     int nFiles = fileList.size();
182     String[] fileNames = new String[nFiles];
183     File[] files = new File[nFiles];
184     for (int i = 0; i < nFiles; i++) {
185       fileNames[i] = fileList.get(i);
186       files[i] = (new File(fileNames[i])).getAbsoluteFile();
187       if (!files[i].exists()) {
188         logger.severe("File does not exist: " + fileNames[i]);
189         return this;
190       }
191     }
192     boolean isArc = !files[0].isDirectory();
193 
194     // Write MBAR file if option is set
195     if (isArc) {
196       if (numLambda == -1) {
197         logger.severe("numLambda must be specified for lambda energy evaluations.");
198         return this;
199       }
200       // Get list of fileNames & check validity
201       File parent = files[0].getParentFile(); // Run directory
202       int window;
203       File outputDir;
204       if (outputDirectory.isEmpty()) {
205         window = Integer.parseInt(parent.getName()); // Run name should be int
206         outputDir = new File(parent.getParentFile(), "mbarFiles"); // Make mbarFiles
207         if (!outputDir.exists()) {
208           outputDir.mkdir();
209         }
210       } else {
211         outputDir = new File(outputDirectory);
212         if (!outputDir.exists()) {
213           outputDir.mkdir();
214         }
215         window = 0;
216       }
217       // Write MBAR file with window number, although this will be reassigned by the file filter based on
218       // placement relative to other fileNames with energy values.
219       File outputFile = new File(outputDir, "energy_" + window + ".mbar");
220       //TODO: Fix atrocious setting of temperatures
221       double[][][] energiesAndDerivatives = getEnergyForLambdas(files, numLambda);
222       double[][] energies = energiesAndDerivatives[0]; // Long step!
223       MultistateBennettAcceptanceRatio.writeFile(energies, outputFile, 298); // Assume 298 K
224       if (lambdaDerivative) {
225         double[][] lambdaDerivatives = energiesAndDerivatives[1];
226         File outputDerivFile = new File(outputDir, "derivatives_" + window + ".mbar");
227         MultistateBennettAcceptanceRatio.writeFile(lambdaDerivatives, outputDerivFile, 298);
228       }
229       return this;
230     }
231 
232     // Run MBAR calculation if file write-out is not requested & files are correct
233     File path = new File(fileList.get(0));
234     if (!path.exists()) {
235       logger.severe("Path to MBAR/BAR fileNames does not exist: " + path);
236       return this;
237     }
238     if (!path.isDirectory() && !(path.isFile() && path.canRead())) {
239       logger.severe("Path to MBAR/BAR fileNames is not accessible: " + path);
240       return this;
241     }
242     MBARFilter filter = new MBARFilter(path, continuousLambda);
243     if (startingSnapshot >= 0) {
244       filter.setStartSnapshot(startingSnapshot);
245       logger.info("Starting with snapshot index: " + startingSnapshot);
246     }
247     if (endingSnapshot >= 0) {
248       filter.setEndSnapshot(endingSnapshot);
249       logger.info("Ending with snapshot index: " + endingSnapshot);
250     }
251     seedWith = seedWith.toUpperCase();
252     SeedType seed = SeedType.valueOf(seedWith);
253     if (seed == null) {
254       logger.severe("Invalid seed type: " + seedWith);
255       return this;
256     }
257     MultistateBennettAcceptanceRatio.VERBOSE = verbose;
258 
259     // Runs calculation on class creation
260     mbar = filter.getMBAR(seed, tol);
261     this.mbar = mbar;
262     if (mbar == null) {
263       logger.severe("Could not create MBAR object.");
264       return this;
265     }
266 
267     // Print out results
268     logger.info("\n MBAR Results:");
269     double[] dGs = mbar.getFreeEnergyDifferences();
270     double[] uncertainties = mbar.getFEDifferenceUncertainties();
271     logger.info(format(" Total dG = %10.4f +/- %10.4f kcal/mol\n",
272         mbar.getTotalFreeEnergyDifference(), mbar.getTotalFEDifferenceUncertainty()));
273     for (int i = 0; i < dGs.length; i++) {
274       logger.info(format("   dG %3d = %10.4f +/- %10.4f kcal/mol", i, dGs[i], uncertainties[i]));
275     }
276 
277     logger.info("\n MBAR Enthalpy & Entropy Results:");
278     double[] enthalpies = mbar.getEnthalpyDifferences();
279     double[] entropies = mbar.getBinEntropies();
280     double totalEnthalpy = sum(enthalpies);
281     double totalEntropy = sum(entropies);
282     logger.info(format(" Total dG = %10.4f (dH) - %10.4f (TdS) kcal/mol\n", totalEnthalpy, totalEntropy));
283     for (int i = 0; i < enthalpies.length; i++) {
284       logger.info(format("   dG %3d = %10.4f (dH) - %10.4f (TdS) kcal/mol", i, enthalpies[i], entropies[i]));
285     }
286 
287     logger.info("\n MBAR uncertainty between all i & j: ");
288     double[][] uncertaintyMatrix = mbar.getUncertaintyMatrix();
289     for (int i = 0; i < uncertaintyMatrix.length; i++) {
290       StringBuilder sb = new StringBuilder();
291       sb.append("    [");
292       for (int j = 0; j < uncertaintyMatrix[i].length; j++) {
293         sb.append(format(" %6.5f ", uncertaintyMatrix[i][j]));
294       }
295       sb.append("]");
296       logger.info(sb.toString());
297     }
298 
299     // Read in and compute expectations for observable data
300     boolean observableData = filter.readObservableData(true, false, true);
301     if (observableData) {
302       logger.info("\n Observable data read in.");
303     }
304     boolean biasData = filter.readObservableData(true, true, false);
305     if (biasData) {
306       logger.info(" Bias data read in.");
307     }
308     if (observableData) {
309       logger.info("\n MBAR Observable Data: ");
310       double[] observableValues = mbar.getObservationEnsembleAverages();
311       for (int i = 0; i < observableValues.length; i++) {
312         logger.info(format("     %3d = %10.4f ", i, observableValues[i]));
313       }
314       logger.info(" Integral:    " + mbar.getTIIntegral());
315     }
316     logger.info("\n");
317     // BAR to compare (negligible difference if properly converged and doesn't take long at all)
318     if (bar) {
319       try {
320         logger.info("\n BAR Results:");
321         BennettAcceptanceRatio bar = mbar.getBAR();
322         logger.info(format(" Total dG = %10.4f +/- %10.4f kcal/mol\n", bar.getTotalFreeEnergyDifference(),
323             bar.getTotalFEDifferenceUncertainty()));
324         dGs = bar.getFreeEnergyDifferences();
325         uncertainties = bar.getFEDifferenceUncertainties();
326         for (int i = 0; i < dGs.length; i++) {
327           logger.info(format("   dG %3d = %10.4f +/- %10.4f kcal/mol", i, dGs[i], uncertainties[i]));
328         }
329         enthalpies = bar.getEnthalpyDifferences();
330         totalEnthalpy = sum(enthalpies);
331         logger.info(format("\n Total dH = %10.4f kcal/mol\n", totalEnthalpy));
332         for (int i = 0; i < enthalpies.length; i++) {
333           logger.info(format("   dH %3d = %10.4f kcal/mol", i, enthalpies[i]));
334         }
335       } catch (Exception ignored) {
336         logger.warning(" BAR calculation failed to converge.");
337       }
338     }
339     logger.info("\n");
340     // Bootstrapping MBAR & BAR
341     if (numBootstrap != 0) {
342       EstimateBootstrapper bootstrapper = new EstimateBootstrapper(mbar);
343       bootstrapper.bootstrap(numBootstrap);
344       logger.info("\n MBAR Bootstrap Results from " + numBootstrap + " Samples:");
345       logger.info(format(" Total dG = %10.4f +/- %10.4f kcal/mol", bootstrapper.getTotalFreeEnergyDifference(),
346           bootstrapper.getTotalFEDifferenceUncertainty()));
347       dGs = bootstrapper.getFreeEnergyDifferences();
348       uncertainties = bootstrapper.getFEDifferenceStdDevs();
349       for (int i = 0; i < dGs.length; i++) {
350         logger.info(format("    dG %3d = %10.4f +/- %10.4f kcal/mol", i, dGs[i], uncertainties[i]));
351       }
352       logger.info("\n");
353       if (bar) {
354         try {
355           logger.info("\n BAR Bootstrap Results:");
356           bootstrapper = new EstimateBootstrapper(mbar.getBAR());
357           bootstrapper.bootstrap(numBootstrap);
358           logger.info(format(" Total dG = %10.4f +/- %10.4f kcal/mol", bootstrapper.getTotalFreeEnergyDifference(),
359               bootstrapper.getTotalFEDifferenceUncertainty()));
360           dGs = bootstrapper.getFreeEnergyDifferences();
361           uncertainties = bootstrapper.getFEDifferenceStdDevs();
362           for (int i = 0; i < dGs.length; i++) {
363             logger.info(format("    dG %3d = %10.4f +/- %10.4f kcal/mol", i, dGs[i], uncertainties[i]));
364           }
365         } catch (Exception ignored) {
366           logger.warning(" BAR calculation failed to converge.");
367         }
368       }
369     }
370     // Compare MBAR calculation across different tenths of the data
371     if (convergence) {
372       MultistateBennettAcceptanceRatio.FORCE_ZEROS_SEED = true;
373       MultistateBennettAcceptanceRatio[] mbarPeriodComparison =
374           filter.getPeriodComparisonMBAR(seed, 1e-7);
375       double[][] dGPeriod = new double[mbarPeriodComparison.length][];
376       for (int i = 0; i < mbarPeriodComparison.length; i++) {
377         dGPeriod[i] = mbarPeriodComparison[i].getFreeEnergyDifferences();
378       }
379       logger.info("\n MBAR Period Comparison Results:");
380       logger.info(format("     %10d%%%10d%%%10d%%%10d%%%10d%%%10d%%%10d%%%10d%%%10d%%%10d%% ",
381           10, 20, 30, 40, 50, 60, 70, 80, 90, 100));
382       double[] totals = new double[dGPeriod[0].length];
383       for (int i = 0; i < dGPeriod[0].length; i++) {
384         StringBuilder sb = new StringBuilder();
385         sb.append(" dG_").append(i).append(": ");
386         for (int j = 0; j < dGPeriod.length; j++) {
387           sb.append(format("%10.4f ", dGPeriod[j][i]));
388           totals[j] += dGPeriod[j][i];
389         }
390         logger.info(sb.toString());
391       }
392       StringBuilder totalsSB = new StringBuilder();
393       for (int i = 0; i < totals.length; i++) {
394         totalsSB.append(format("%10.4f ", totals[i]));
395       }
396       logger.info("");
397       logger.info("  Tot: " + totalsSB.toString());
398     }
399     return this;
400   }
401 
402   private static double sum(double[] values) {
403     double sum = 0;
404     for (double value : values) {
405       sum += value;
406     }
407     return sum;
408   }
409 
410   private double[][][] getEnergyForLambdas(File[] files, int nLambda) {
411 
412     // Determine the number of topologies to be read and allocate the array.
413     numTopologies = files.length;
414     int threadsPerTopology = topologyOptions.getThreadsPerTopology(numTopologies);
415     MolecularAssembly[] molecularAssemblies = new MolecularAssembly[numTopologies];
416     SystemFilter[] openers = new SystemFilter[numTopologies];
417 
418     // Turn on computation of lambda dependent properties.
419     alchemicalOptions.setAlchemicalProperties();
420     topologyOptions.setAlchemicalProperties(numTopologies);
421 
422     if (numTopologies == 2) {
423       logger.info(format(" Initializing two topologies for each window."));
424     } else {
425       logger.info(format(" Initializing a single topology for each window."));
426     }
427 
428     // Read in files and meta-details
429     for (int i = 0; i < numTopologies; i++) {
430       molecularAssemblies[i] = alchemicalOptions.openFile(algorithmFunctions, topologyOptions,
431           threadsPerTopology, files[i].getName(), i);
432       openers[i] = algorithmFunctions.getFilter();
433     }
434 
435     StringBuilder sb = new StringBuilder(format(
436         "\n Performing FEP evaluations for: %s\n ", Arrays.toString(files)));
437     CrystalPotential potential = (CrystalPotential) topologyOptions.assemblePotential(molecularAssemblies, sb);
438     String[] arcFileName = new String[files.length];
439     for (int j = 0; j < numTopologies; j++) {
440       // Only use the first arc file (even if restarts happened)
441       arcFileName[j] = FilenameUtils.removeExtension(files[j].getAbsolutePath()) + ".arc";
442       File archiveFile = new File(arcFileName[j]);
443       openers[j].setFile(archiveFile);
444       molecularAssemblies[j].setFile(archiveFile);
445     }
446 
447     // Slightly modified from BAR.groovy
448     int nSnapshots = openers[0].countNumModels();
449     double[] x = new double[potential.getNumberOfVariables()];
450     double[] lambdaValues = new double[nLambda];
451     double[][] energy = new double[nLambda][nSnapshots];
452     double[][] lambdaDerivatives = new double[nLambda][nSnapshots];
453     for (int k = 0; k < lambdaValues.length; k++) {
454       lambdaValues[k] = (double) k / (nLambda - 1);
455       energy[k] = new double[nSnapshots];
456       lambdaDerivatives[k] = new double[nSnapshots];
457     }
458     LambdaInterface linter1 = (LambdaInterface) potential;
459     logger.info(format("\n\n Performing energy evaluations for %d snapshots.", nSnapshots));
460     logger.info(format(" Using %d lambda values.", nLambda));
461     logger.info(format(" Using %d topologies.", numTopologies));
462     logger.info(" Lambda values: " + Arrays.toString(lambdaValues));
463     logger.info("");
464     for (int i = 0; i < nSnapshots; i++) {
465       // Read coords
466       boolean resetPosition = (i == 0);
467       for (int n = 0; n < openers.length; n++) {
468         openers[n].readNext(resetPosition, false);
469       }
470       x = potential.getCoordinates(x);
471 
472       // Compute energies for each lambda
473       StringBuilder sb2 = new StringBuilder().append("Snapshot ").append(i).append(" Energy Evaluations: ");
474       StringBuilder sb3 = new StringBuilder().append("Snapshot ").append(i).append(" Lambda Derivatives: ");
475       for (int k = 0; k < lambdaValues.length; k++) {
476         double lambda = lambdaValues[k];
477         if (lambda <= 1E-6) {
478           lambda += .00275;
479         }
480         if (lambda - 1.0 < 1E-6) {
481           lambda -= .00275;
482         }
483         linter1.setLambda(lambda);
484         energy[k][i] = potential.energyAndGradient(x, new double[x.length * 3]);
485         if (lambdaDerivative) {
486           lambdaDerivatives[k][i] = linter1.getdEdL();
487           sb3.append(" ").append(lambdaDerivatives[k][i]);
488         }
489         sb2.append(" ").append(energy[k][i]);
490       }
491       logger.info(sb2.append("\n").toString());
492       if (lambdaDerivative) {
493         logger.info(sb3.append("\n").toString());
494       }
495     }
496 
497     return new double[][][]{energy, lambdaDerivatives};
498   }
499 
500   /**
501    * {@inheritDoc}
502    */
503   @Override
504   public List<Potential> getPotentials() {
505     return Collections.emptyList();
506   }
507 }