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.cli;
39  
40  import ffx.algorithms.AlgorithmListener;
41  import ffx.algorithms.dynamics.MolecularDynamics;
42  import ffx.crystal.CrystalPotential;
43  import ffx.potential.MolecularAssembly;
44  import ffx.potential.bonded.LambdaInterface;
45  import ffx.potential.cli.WriteoutOptions;
46  import picocli.CommandLine.ArgGroup;
47  import picocli.CommandLine.Option;
48  
49  import java.io.File;
50  import java.util.Arrays;
51  import java.util.Collections;
52  import java.util.Set;
53  import java.util.TreeSet;
54  import java.util.logging.Logger;
55  
56  import static java.lang.String.format;
57  
58  /**
59   * Represents command line options for scripts that calculate thermodynamics.
60   *
61   * @author Michael J. Schnieders
62   * @author Jacob M. Litman
63   * @since 1.0
64   */
65  public class ThermodynamicsOptions {
66  
67    private static final Logger logger = Logger.getLogger(ThermodynamicsOptions.class.getName());
68  
69    /**
70     * The ArgGroup keeps the ThermodynamicsOptions together when printing help.
71     */
72    @ArgGroup(heading = "%n Thermodynamics Options%n", validate = false)
73    private final ThermodynamicsOptionGroup group = new ThermodynamicsOptionGroup();
74  
75    /**
76     * Return the selected Thermodynamics algorithm as an enumerated type.
77     *
78     * @return Corresponding thermodynamics algorithm
79     */
80    public ThermodynamicsAlgorithm getAlgorithm() {
81      return ThermodynamicsAlgorithm.parse(group.thermoAlgoString);
82    }
83  
84    /**
85     * getEquilSteps.
86     *
87     * @return The number of equilibration steps.
88     */
89    public long getEquilSteps() {
90      return group.equilibrationSteps;
91    }
92  
93    /**
94     * Getter for the field <code>resetNumSteps</code>.
95     *
96     * @return a boolean.
97     */
98    public boolean getResetNumSteps() {
99      return group.resetNumSteps;
100   }
101 
102   /**
103    * Run an alchemical free energy window.
104    *
105    * @param molecularAssemblies All involved MolecularAssemblies.
106    * @param crystalPotential    The Potential to be sampled.
107    * @param dynamicsOptions     DynamicsOptions.
108    * @param writeoutOptions     WriteoutOptions
109    * @param dyn                 MD restart file
110    * @param algorithmListener   AlgorithmListener
111    * @return The MolecularDynamics object constructed.
112    */
113   public MolecularDynamics runFixedAlchemy(MolecularAssembly[] molecularAssemblies,
114                                            CrystalPotential crystalPotential, DynamicsOptions dynamicsOptions,
115                                            WriteoutOptions writeoutOptions, File dyn, AlgorithmListener algorithmListener) {
116     dynamicsOptions.init();
117 
118     MolecularDynamics molDyn = dynamicsOptions.getDynamics(writeoutOptions, crystalPotential,
119         molecularAssemblies[0], algorithmListener);
120     for (int i = 1; i < molecularAssemblies.length; i++) {
121       molDyn.addAssembly(molecularAssemblies[i]);
122     }
123 
124     boolean initVelocities = true;
125     long nSteps = dynamicsOptions.getSteps();
126     molDyn.setRestartFrequency(dynamicsOptions.getCheckpoint());
127     // Start sampling.
128     if (group.equilibrationSteps > 0) {
129       logger.info("\n Beginning Equilibration");
130       runDynamics(molDyn, group.equilibrationSteps, dynamicsOptions, writeoutOptions, true, dyn);
131       logger.info(" Beginning Fixed-Lambda Alchemical Sampling");
132       initVelocities = false;
133     } else {
134       logger.info("\n Beginning Fixed-Lambda Alchemical Sampling Without Equilibration");
135       if (!group.resetNumSteps) {
136         // Workaround for being unable to pick up pre-existing steps.
137         initVelocities = true;
138       }
139     }
140 
141     if (nSteps > 0) {
142       runDynamics(molDyn, nSteps, dynamicsOptions, writeoutOptions, initVelocities, dyn);
143     } else {
144       logger.info(" No steps remaining for this process!");
145     }
146     return molDyn;
147   }
148 
149   /**
150    * Run a non-equilibrium alchemical free energy simulation.
151    *
152    * @param molecularAssemblies All involved MolecularAssemblies.
153    * @param crystalPotential    The Potential to be sampled.
154    * @param dynamicsOptions     DynamicsOptions.
155    * @param writeoutOptions     WriteoutOptions
156    * @param dyn                 MD restart file
157    * @param algorithmListener   AlgorithmListener
158    * @return The MolecularDynamics object constructed.
159    */
160   public MolecularDynamics runNEQ(MolecularAssembly[] molecularAssemblies,
161                                   CrystalPotential crystalPotential, DynamicsOptions dynamicsOptions,
162                                   WriteoutOptions writeoutOptions, File dyn, AlgorithmListener algorithmListener) {
163     dynamicsOptions.init();
164 
165     MolecularDynamics molDyn = dynamicsOptions.getDynamics(writeoutOptions, crystalPotential,
166         molecularAssemblies[0], algorithmListener);
167     for (int i = 1; i < molecularAssemblies.length; i++) {
168       molDyn.addAssembly(molecularAssemblies[i]);
169     }
170 
171     boolean initVelocities = true;
172     long nSteps = dynamicsOptions.getSteps();
173     molDyn.setRestartFrequency(dynamicsOptions.getCheckpoint());
174     // Start sampling.
175     if (group.equilibrationSteps > 0) {
176       double initialLambda = group.reverseNEQ ? 1.0 : 0.0;
177       logger.info(format("\n Beginning Equilibration (at L=%5.3f)", initialLambda));
178       LambdaInterface lambdaInterface = (LambdaInterface) crystalPotential;
179       lambdaInterface.setLambda(initialLambda);
180       runDynamics(molDyn, group.equilibrationSteps, dynamicsOptions, writeoutOptions, true, dyn);
181       if (nSteps > 0) {
182         logger.info(" Beginning Non-Equilibrium Sampling");
183       }
184       initVelocities = false;
185     } else if (nSteps > 0) {
186       logger.info("\n Beginning Non-Equilibrium Sampling Without Equilibration");
187       if (!group.resetNumSteps) {
188         // Workaround for being unable to pick up pre-existing steps.
189         initVelocities = true;
190       }
191     }
192 
193     if (nSteps > 0) {
194       molDyn.setNonEquilibriumLambda(true, group.nonEquilibriumSteps, group.reverseNEQ);
195       runDynamics(molDyn, nSteps, dynamicsOptions, writeoutOptions, initVelocities, dyn);
196     }
197 
198     return molDyn;
199   }
200 
201   /**
202    * The number of equilibration steps prior to production OST counts begin.
203    *
204    * @return Returns the number of equilibration steps.
205    */
206   public long getEquilibrationSteps() {
207     return group.equilibrationSteps;
208   }
209 
210   private void runDynamics(MolecularDynamics molecularDynamics, long nSteps,
211                            DynamicsOptions dynamicsOptions, WriteoutOptions writeoutOptions, boolean initVelocities,
212                            File dyn) {
213     molecularDynamics.dynamic(nSteps, dynamicsOptions.getDt(), dynamicsOptions.getReport(),
214         dynamicsOptions.getWrite(), dynamicsOptions.getTemperature(), initVelocities,
215         writeoutOptions.getFileType(), dynamicsOptions.getCheckpoint(), dyn);
216   }
217 
218   public void setEquilibrationSteps(long equilibrationSteps) {
219     group.equilibrationSteps = equilibrationSteps;
220   }
221 
222   /**
223    * Ignores steps detected in .lam lambda-restart files.
224    *
225    * @return Returns true if the number of steps is being reset.
226    */
227   public boolean isResetNumSteps() {
228     return group.resetNumSteps;
229   }
230 
231   public void setResetNumSteps(boolean resetNumSteps) {
232     group.resetNumSteps = resetNumSteps;
233   }
234 
235   /**
236    * The algorithm to be used (e.g. OST, window-based methods, etc.).
237    *
238    * @return Returns a String for requested algorithm.
239    */
240   public String getThermoAlgoString() {
241     return group.thermoAlgoString;
242   }
243 
244   public void setThermoAlgoString(String thermoAlgoString) {
245     group.thermoAlgoString = thermoAlgoString;
246   }
247 
248   /**
249    * Collection of Thermodynamics Options.
250    */
251   private static class ThermodynamicsOptionGroup {
252 
253     /**
254      * -Q or --equilibrate sets the number of equilibration steps prior to production OST counts
255      * begin.
256      */
257     @Option(names = {"-Q", "--equilibrate"}, paramLabel = "1000", defaultValue = "1000",
258         description = "Number of equilibration steps before evaluation of thermodynamics.")
259     private long equilibrationSteps = 1000;
260 
261     /**
262      * --nEQ or --nonEquilibriumSteps sets the number of non-equilibrium lambda steps.
263      */
264     @Option(names = {"--nEQ", "--nonEquilibriumSteps"}, paramLabel = "100", defaultValue = "100",
265         description = "Sets the number of non-equilibrium lambda steps.")
266     private int nonEquilibriumSteps = 100;
267 
268     /**
269      * --rNEQ or --reverseNonEquilibrium Run non-equilibrium dynamics from L=1 to L=0.
270      */
271     @Option(names = {"--rNEQ", "--reverseNonEquilibrium"}, defaultValue = "false",
272         description = "Run non-equilibrium dynamics from L=1 to L=0.")
273     private boolean reverseNEQ = false;
274 
275     /**
276      * -rn or --resetNumSteps, ignores steps detected in .lam lambda-restart files and thus resets
277      * the histogram; use -rn false to continue from the end of any prior simulation.
278      */
279     @Option(names = {"--rn", "--resetNumSteps"}, defaultValue = "false",
280         description = "Ignore prior steps logged in .lam or similar files.")
281     private boolean resetNumSteps = false;
282 
283     /**
284      * --tA or --thermodynamicsAlgorithm specifies the algorithm to be used; currently serves as a
285      * switch between OST, Fixed and NEQ methods.
286      */
287     @Option(names = {"--tA", "--thermodynamicsAlgorithm"}, paramLabel = "OST", defaultValue = "OST",
288         description = "Choice of thermodynamics algorithm [OST, FIXED, or NEQ].")
289     private String thermoAlgoString = "OST";
290   }
291 
292   /**
293    * Represents categories of thermodynamics algorithms that must be handled differently.
294    */
295   public enum ThermodynamicsAlgorithm {
296     OST("OST", "MC-OST", "MD-OST", "DEFAULT"),
297     FIXED("FIXED", "BAR", "MBAR", "FEP", "WINDOWED"),
298     NEQ("NEQ", "NON-EQUILIBRIUM");
299 
300     private final Set<String> aliases;
301 
302     ThermodynamicsAlgorithm(String... aliases) {
303       // If, for some reason, there are 100+ aliases, might change to a HashSet.
304       Set<String> names = new TreeSet<>(Arrays.asList(aliases));
305       names.add(this.name());
306       this.aliases = Collections.unmodifiableSet(names);
307     }
308 
309     /**
310      * Parse a String to a corresponding thermodynamics algorithm, recognizing aliases.
311      *
312      * @param name Name to parse
313      * @return A ThermodynamicsAlgorithm.
314      * @throws IllegalArgumentException If name did not correspond to any alias of any
315      *                                  ThermodynamicsAlgorithm.
316      */
317     public static ThermodynamicsAlgorithm parse(String name) throws IllegalArgumentException {
318       String ucName = name.toUpperCase();
319       for (ThermodynamicsAlgorithm thermodynamicsAlgorithm : values()) {
320         if (thermodynamicsAlgorithm.aliases.contains(ucName)) {
321           return thermodynamicsAlgorithm;
322         }
323       }
324       throw new IllegalArgumentException(
325           format(" Could not parse %s as a ThermodynamicsAlgorithm", name));
326     }
327   }
328 }