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.ParallelStateEnergy;
41  import ffx.algorithms.cli.AlgorithmsCommand;
42  import ffx.crystal.Crystal;
43  import ffx.crystal.CrystalPotential;
44  import ffx.numerics.Potential;
45  import ffx.numerics.estimator.FreeEnergyDifferenceReporter;
46  import ffx.potential.MolecularAssembly;
47  import ffx.potential.cli.AlchemicalOptions;
48  import ffx.potential.cli.TopologyOptions;
49  import ffx.potential.parsers.BARFilter;
50  import ffx.potential.parsers.SystemFilter;
51  import ffx.utilities.FFXBinding;
52  import org.apache.commons.configuration2.Configuration;
53  import org.apache.commons.io.FilenameUtils;
54  import picocli.CommandLine.Command;
55  import picocli.CommandLine.Mixin;
56  import picocli.CommandLine.Option;
57  import picocli.CommandLine.Parameters;
58  
59  import java.io.File;
60  import java.io.IOException;
61  import java.nio.file.Files;
62  import java.nio.file.Path;
63  import java.nio.file.Paths;
64  import java.util.ArrayList;
65  import java.util.Collections;
66  import java.util.Comparator;
67  import java.util.List;
68  import java.util.regex.Matcher;
69  import java.util.regex.Pattern;
70  import java.util.stream.Stream;
71  
72  import static java.lang.String.format;
73  
74  /**
75   * The BAR script finds the free energy difference across a lambda window. It presently assumes
76   * that the number of files composing the first end of the window equals the number of files
77   * composing the other end.
78   * <br>
79   * Usage:
80   * <br>
81   * ffxc BAR [options] &lt;structures1&gt; &lt;structures2&gt;
82   */
83  @Command(description = " Evaluates a free energy change with the Bennett Acceptance Ratio algorithm using pregenerated snapshots.", name = "BAR")
84  public class BAR extends AlgorithmsCommand {
85  
86    @Mixin
87    private AlchemicalOptions alchemicalOptions;
88  
89    @Mixin
90    private TopologyOptions topologyOptions;
91  
92    /**
93     * --eb or --evaluateBARFiles Read in Tinker BAR files and evaluate the free energy difference.
94     */
95    @Option(names = {"--eb", "--evaluateBAR"}, paramLabel = "false", defaultValue = "false",
96        description = "Evaluate Free Energy Differences from Tinker BAR files.")
97    private boolean evaluateBARFiles = false;
98  
99    @Option(names = {"--l2", "--lambdaTwo"}, paramLabel = "1.0",
100       description = "Lambda value for the upper edge of the window")
101   private double lambda2 = 1.0;
102 
103   @Option(names = {"-t", "--temperature"}, paramLabel = "298.15",
104       description = "Temperature for system")
105   private double temperature = 298.15;
106 
107   @Option(names = {"--dV", "--volume"}, paramLabel = "false",
108       description = "Write out snapshot volumes to the Tinker BAR file.")
109   private boolean includeVolume = false;
110 
111   @Option(names = {"--ns", "--nStates"}, paramLabel = "2",
112       description = "If not equal to two, auto-determine lambda values and subdirectories (overrides other flags).")
113   private int nStates = 2;
114 
115   @Option(names = {"--sa", "--sortedArc"}, paramLabel = "false",
116       description = "If set, use sorted archive values.")
117   private boolean sortedArc = false;
118 
119   @Option(names = {"--ss", "--startSnapshot"}, paramLabel = "0",
120       description = "Start at this snapshot when reading in Tinker BAR files (indexed from 0).")
121   private int startingSnapshot = 0;
122 
123   @Option(names = {"--es", "--endSnapshot"}, paramLabel = "0",
124       description = "End at this snapshot when reading in Tinker BAR files (indexed from 0).")
125   private int endingSnapshot = 0;
126 
127   /**
128    * --ni or --nIterations Maximum number of allowable iterations for BAR calculation.
129    */
130   @Option(names = {"--ni", "--nIterations"}, paramLabel = "1000",
131       description = "Specify the maximum number of iterations for BAR convergence.")
132   private int nIterations = 1000;
133 
134   /**
135    * -e or --eps Convergence criterion for BAR iteration.
136    */
137   @Option(names = {"-e", "--eps"}, paramLabel = "1.0E-4",
138       description = "Specify convergence cutoff for BAR calculation.")
139   private double eps = 1.0E-4;
140 
141   /**
142    * The final argument(s) can be filenames for lambda windows in order.
143    */
144   @Parameters(arity = "0..*", paramLabel = "files",
145       description = "A single PDB/XYZ when windows are auto-determined (or two for dual topology). Two trajectory files for BAR between two ensembles (or four for dual topology).")
146   private List<String> filenames = null;
147 
148   /**
149    * Additional properties.
150    */
151   private Configuration additionalProperties;
152 
153   /**
154    * Molecular assemblies to compute energies.
155    */
156   MolecularAssembly[] topologies;
157 
158   /**
159    * Number of Topologies.
160    */
161   private int numTopologies;
162 
163   /**
164    * Potential object for the crystal.
165    */
166   CrystalPotential potential;
167 
168   /**
169    * Number of input files.
170    */
171   int nFiles;
172 
173   /**
174    * Input files for BAR.
175    */
176   private String[] files;
177 
178   /**
179    * Lambda values for each state.
180    */
181   double[] lambdaValues;
182 
183   /**
184    * Compute and log out the free energy differences.
185    */
186   private FreeEnergyDifferenceReporter reporter;
187 
188   /**
189    * BAR Constructor.
190    */
191   public BAR() {
192     super();
193   }
194 
195   /**
196    * BAR Constructor.
197    *
198    * @param binding The Binding to use.
199    */
200   public BAR(FFXBinding binding) {
201     super(binding);
202   }
203 
204   /**
205    * BAR constructor that sets the command line arguments.
206    *
207    * @param args Command line arguments.
208    */
209   public BAR(String[] args) {
210     super(args);
211   }
212 
213   /**
214    * Sets an optional Configuration with additional properties.
215    *
216    * @param additionalProps Additional properties configuration.
217    */
218   public void setProperties(Configuration additionalProps) {
219     this.additionalProperties = additionalProps;
220   }
221 
222   /**
223    * {@inheritDoc}
224    */
225   @Override
226   public BAR run() {
227     // Initialize the script.
228     if (!init()) {
229       return this;
230     }
231 
232     /**
233      * Load user supplied files into an array.
234      */
235     if (filenames == null) {
236       nFiles = 0;
237       files = null;
238     } else {
239       nFiles = filenames.size();
240       files = new String[nFiles];
241       for (int i = 0; i < nFiles; i++) {
242         files[i] = filenames.get(i);
243       }
244     }
245 
246     // Evaluate free energy differences from BAR files.
247     if (evaluateBARFiles) {
248       // 1) Define the number of states and concordant BAR files (windows = states - 1).
249       // 2) Read in the energy and volume data from BAR files.
250       // 3) Compute the free energy differences.
251       if (nFiles == 0) {
252         logger.info(" Please supply a BAR file or directory to be evaluated.");
253         return this;
254       }
255 
256       BARFilter[] barFilters = null;
257       if (nStates == 2 && nFiles == 1) {
258         // If the number of states is 2, evaluate a single BAR file.
259         logger.info(format(" Evaluating a single BAR file: %s.", files[0]));
260         barFilters = new BARFilter[1];
261         barFilters[0] = new BARFilter(new File(files[0]), startingSnapshot, endingSnapshot);
262         // Define the lambda values.
263         lambdaValues = new double[2];
264         lambdaValues[0] = alchemicalOptions.getInitialLambda();
265         lambdaValues[1] = lambda2;
266       } else if (nStates > 2 && nFiles == 1) {
267         // If the number of states is greater than 2, evaluate multiple BAR files from a directory.
268         logger.info(format(" Evaluating BAR files from directory %s.", files[0]));
269         // Auto-determine bar files from the "bar" subdirectory.
270         logger.info(" Auto-detecting BAR files and lambda values.");
271         Path subdirectoryPath = Paths.get(files[0]);
272         // Regular expression to match files with the form "filename_XX.bar"
273         Pattern pattern = Pattern.compile(".*_(\\d+)\\.bar");
274         // List to hold file paths
275         List<String> fileList = new ArrayList<>();
276         try (Stream<Path> paths = Files.list(subdirectoryPath)) {
277           paths.filter(Files::isRegularFile)
278               .filter(path -> {
279                 Matcher matcher = pattern.matcher(path.getFileName().toString());
280                 return matcher.matches();
281               })
282               .sorted(Comparator.comparingInt(path -> {
283                 Matcher matcher = pattern.matcher(path.getFileName().toString());
284                 matcher.matches();
285                 return Integer.parseInt(matcher.group(1));
286               }))
287               .forEach(path -> fileList.add(path.toString()));
288         } catch (IOException e) {
289           logger.info(" Error reading files from a directory.\n" + e.toString());
290           e.printStackTrace();
291           return this;
292         }
293 
294         int numFiles = fileList.size();
295         if (numFiles != nStates - 1) {
296           logger.info(format(" The number of bar files (%d) does not concord with the number of states (%d).", numFiles, nStates));
297           return this;
298         }
299 
300         // Define the lambda values.
301         lambdaValues = new double[nStates];
302         for (int l = 0; l < nStates; l++) {
303           lambdaValues[l] = alchemicalOptions.getInitialLambda(nStates, l, true);
304         }
305         barFilters = new BARFilter[numFiles];
306         for (int i = 0; i < numFiles; i++) {
307           String filename = fileList.get(i);
308           File barFile = new File(filename);
309           barFilters[i] = new BARFilter(barFile, startingSnapshot, endingSnapshot);
310           logger.info(format(" Window L=%6.4f to L=%6.4f from %s",
311               lambdaValues[i], lambdaValues[i + 1], filename));
312         }
313       }
314 
315       // Allocate space for energy and volume arrays.
316       double[][] energiesLow = new double[nStates][];
317       double[][] energiesAt = new double[nStates][];
318       double[][] energiesHigh = new double[nStates][];
319       double[][] volume = new double[nStates][];
320       double[] temperatures = new double[nStates];
321 
322       // Load BAR files.
323       readBARFiles(barFilters, energiesLow, energiesAt, energiesHigh, volume, temperatures);
324 
325       // Compute free energy differences.
326       reporter = new FreeEnergyDifferenceReporter(nStates, lambdaValues, temperatures, eps, nIterations,
327           energiesLow, energiesAt, energiesHigh, volume);
328       reporter.report();
329 
330       // Exit the script.
331       return this;
332     }
333 
334     // Create BAR files for later evaluation of free energy differences.
335     logger.info(" Writing BAR files.");
336 
337     numTopologies = 1;
338     if (nFiles <= 1 && nStates < 2) {
339       logger.info(" At least two states must be specified");
340       return this;
341     } else if (nFiles == 1 && nStates >= 2) {
342       logger.info(format(" Auto-detecting %d states for single topology:\n %s.", nStates, files[0]));
343     } else if (nFiles == 2 && nStates >= 2) {
344       logger.info(format(" Auto-detecting %d states for dual topology:\n %s\n %s.", nStates, files[0], files[1]));
345       numTopologies = 2;
346     } else if (nFiles == 2) {
347       logger.info(format(" Applying BAR between two single topology ensembles:\n %s\n %s.", files[0], files[1]));
348       nStates = 2;
349     } else if (nFiles == 4) {
350       logger.info(format(" Applying BAR between two dual topology ensembles:\n %s %s\n %s %s.",
351           files[0], files[1], files[2], files[3]));
352       numTopologies = 2;
353       nStates = 2;
354     } else {
355       logger.info(format(" Inconsistent input of files (%3d) and/or states (%3d).", nFiles, nStates));
356       return this;
357     }
358 
359     boolean autodetect = false;
360     if (nStates > 2) {
361       autodetect = true;
362       lambdaValues = new double[nStates];
363       for (int i = 0; i < nStates; i++) {
364         lambdaValues[i] = alchemicalOptions.getInitialLambda(nStates, i, true);
365       }
366     } else {
367       // Otherwise we assume two ensembles at the given lambda values.
368       lambdaValues = new double[2];
369       lambdaValues[0] = alchemicalOptions.getInitialLambda(2, 0, true);
370       lambdaValues[1] = lambda2;
371       nStates = 2;
372     }
373 
374     // Could set "getInitialLambda"'s quiet flag to false, but better logging here?
375     logger.info(" Lambda values for each window: ");
376     int nLambda = lambdaValues.length;
377     for (int i = 0; i < nLambda; i++) {
378       logger.info(format(" Window %3d: %6.4f", i, lambdaValues[i]));
379     }
380 
381     // Open assemblies if not using Tinker BAR files.
382     boolean isPBC;
383 
384     // Allocate space for each topology
385     int threadsPerTopology = topologyOptions.getThreadsPerTopology(numTopologies);
386     topologies = new MolecularAssembly[numTopologies];
387     SystemFilter[] openers = new SystemFilter[numTopologies];
388 
389     alchemicalOptions.setAlchemicalProperties();
390     topologyOptions.setAlchemicalProperties(numTopologies);
391 
392     if (numTopologies == 2) {
393       logger.info(format(" Initializing two topologies for each window."));
394     } else {
395       logger.info(format(" Initializing a single topology for each window."));
396     }
397 
398     for (int i = 0; i < numTopologies; i++) {
399       MolecularAssembly ma = alchemicalOptions.openFile(algorithmFunctions, topologyOptions,
400           threadsPerTopology, filenames.get(i), i);
401       topologies[i] = ma;
402       openers[i] = algorithmFunctions.getFilter();
403     }
404 
405     StringBuilder sb = new StringBuilder(format(
406         "\n Using BAR to analyze a free energy change for %s\n ", filenames));
407     potential = (CrystalPotential) topologyOptions.assemblePotential(topologies, sb);
408     Crystal unitCell = potential.getCrystal().getUnitCell();
409     isPBC = includeVolume && !unitCell.aperiodic();
410 
411     String[][] fullFilePaths;
412     String directoryPath;
413     if (nFiles > 0) {
414       // For Dual-Topology systems that only compare two states, simplify filePaths to use only the 2 files belonging
415       // to each ensemble
416       int dtIndex = 0;
417       if (numTopologies == 2 && nFiles == 4) {
418         fullFilePaths = new String[nStates][2];
419         dtIndex = 2;
420       } else {
421         fullFilePaths = new String[nStates][nFiles];
422       }
423 
424       // Loop over ensembles
425       for (int i = 0; i < nStates; i++) {
426         // Loop over user supplied files.
427         for (int j = 0; j < nFiles; j++) {
428           // For Dual-Topology, start at index 2 for second ensemble
429           if (i == 1 && j == 0) {
430             j = dtIndex;
431           }
432           File file = new File(files[j]);
433           directoryPath = file.getAbsoluteFile().getParent() + File.separator;
434           String archiveName;
435           if (sortedArc) {
436             archiveName = FilenameUtils.getBaseName(files[j]) + "_E" + String.valueOf(i) + ".arc";
437           } else {
438             archiveName = FilenameUtils.getBaseName(files[j]) + ".arc";
439           }
440           if (!autodetect) {
441             // Path to a file in the same directory as supplied archives.
442             fullFilePaths[i][j - dtIndex] = directoryPath + File.separator + archiveName;
443           } else {
444             // Paths to auto-detected subdirectories.
445             fullFilePaths[i][j - dtIndex] = directoryPath + i + File.separator + archiveName;
446           }
447           // For Dual-Topology, stop after two files for first ensemble
448           if (i == 0 && j == 1) {
449             j += dtIndex * nFiles;
450           }
451         }
452       }
453 
454       // Initialize the ParallelEnergy class.
455       ParallelStateEnergy pe = new ParallelStateEnergy(nStates, lambdaValues,
456           topologies, potential, fullFilePaths, openers);
457 
458       // Evaluate energies from snapshots.
459       double[][] energiesLow = new double[nStates][];
460       double[][] energiesAt = new double[nStates][];
461       double[][] energiesHigh = new double[nStates][];
462       double[][] volume = new double[nStates][];
463       pe.evaluateStates(energiesLow, energiesAt, energiesHigh, volume);
464 
465       // Only node 0 will write out Tinker BAR files.
466       if (pe.getRank() != 0) {
467         return this;
468       }
469 
470       // Create file objects to write out TINKER style bar files.
471       File file = new File(files[0]);
472       directoryPath = file.getAbsoluteFile().getParent() + File.separator;
473       String tinkerDirectoryPath = directoryPath + File.separator + "windows";
474       File directory = new File(tinkerDirectoryPath);
475       String barFilePath = tinkerDirectoryPath + File.separator;
476       directory.mkdir();
477 
478       for (int state = 0; state < nStates - 1; state++) {
479         File xyzFile = new File(filenames.get(0));
480         BARFilter barFilter;
481         barFilter = new BARFilter(xyzFile, energiesAt[state], energiesHigh[state], energiesLow[state + 1],
482             energiesAt[state + 1], volume[state], volume[state + 1], temperature, temperature);
483         String barFileName = barFilePath + "window_" + String.valueOf(state) + ".bar";
484         // Write out the TINKER style bar file. An existing file will be overwritten.
485         barFilter.writeFile(barFileName, isPBC, false);
486       }
487     }
488 
489     return this;
490   }
491 
492   /**
493    * Read energy and volume values from BAR files. The first dimension of the energy arrays
494    * corresponds to the state index. The second dimension corresponds to the snapshot index.
495    * <p>
496    * The first dimension of each array should be allocated to the number of states prior
497    * to calling this method.
498    *
499    * @param barFilters   The BAR filters to use.
500    * @param energyLow    The energy of each snapshot in state L evaluated at L-dL.
501    * @param energyAt     The energy of each snapshot in L evaluated at L.
502    * @param energyHigh   The energy from state L evaluated at L+dL.
503    * @param volume       The volume of each snapshot from state L.
504    * @param temperatures The temperatures for each state.
505    */
506   void readBARFiles(BARFilter[] barFilters,
507                     double[][] energyLow, double[][] energyAt,
508                     double[][] energyHigh, double[][] volume,
509                     double[] temperatures) {
510 
511     // Read energy values from BAR files.
512     for (BARFilter barFilter : barFilters) {
513       barFilter.readFile();
514     }
515 
516     // Assign values to the state arrays.
517     for (int state = 0; state < nStates; state++) {
518       if (state == 0) {
519         energyAt[state] = barFilters[state].getE1l1();
520         energyHigh[state] = barFilters[state].getE1l2();
521         volume[state] = barFilters[state].getVolume1();
522         temperatures[state] = barFilters[state].getTemperature1();
523       } else if (state == nStates - 1) {
524         energyLow[state] = barFilters[state - 1].getE2l1();
525         energyAt[state] = barFilters[state - 1].getE2l2();
526         volume[state] = barFilters[state - 1].getVolume2();
527         temperatures[state] = barFilters[state - 1].getTemperature2();
528       } else {
529         energyLow[state] = barFilters[state - 1].getE2l1();
530         energyAt[state] = barFilters[state].getE1l1();
531         energyHigh[state] = barFilters[state].getE1l2();
532         volume[state] = barFilters[state].getVolume1();
533         temperatures[state] = barFilters[state].getTemperature1();
534       }
535     }
536   }
537 
538   /**
539    * {@inheritDoc}
540    */
541   @Override
542   public List<Potential> getPotentials() {
543     List<Potential> potentials;
544     if (potential == null) {
545       potentials = Collections.emptyList();
546     } else {
547       potentials = new ArrayList<>();
548       potentials.add(potential);
549     }
550     return potentials;
551   }
552 
553   /**
554    * Obtain the Free Energy Difference reporter for this class.
555    *
556    * @return The Free Energy Difference reporter.
557    */
558   public FreeEnergyDifferenceReporter getReporter() {
559     return reporter;
560   }
561 
562 }