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.AlgorithmFunctions;
41  import ffx.algorithms.AlgorithmListener;
42  import ffx.algorithms.optimize.RotamerOptimization;
43  import ffx.crystal.CrystalPotential;
44  import ffx.numerics.Potential;
45  import ffx.potential.DualTopologyEnergy;
46  import ffx.potential.MolecularAssembly;
47  import ffx.potential.QuadTopologyEnergy;
48  import ffx.potential.bonded.LambdaInterface;
49  import ffx.potential.bonded.Polymer;
50  import ffx.potential.bonded.Residue;
51  import ffx.potential.bonded.RotamerLibrary;
52  import ffx.potential.cli.AlchemicalOptions;
53  import ffx.potential.cli.TopologyOptions;
54  import org.apache.commons.configuration2.CompositeConfiguration;
55  import picocli.CommandLine.ArgGroup;
56  import picocli.CommandLine.Option;
57  
58  import java.io.File;
59  import java.util.ArrayList;
60  import java.util.List;
61  import java.util.logging.Logger;
62  import java.util.regex.Matcher;
63  import java.util.regex.Pattern;
64  
65  import static java.lang.String.format;
66  
67  /**
68   * Represents command line options for scripts that can create multiple walkers, such as multi-walker
69   * OST. Should be kept agnostic to whether it is an MD-based algorithm, or some other flavor of Monte
70   * Carlo.
71   *
72   * @author Michael J. Schnieders
73   * @author Jacob M. Litman
74   * @since 1.0
75   */
76  public class MultiDynamicsOptions {
77  
78    private static final Logger logger = Logger.getLogger(MultiDynamicsOptions.class.getName());
79  
80    /**
81     * The ArgGroup keeps the MultiDynamicsOptionGroup together when printing help.
82     */
83    @ArgGroup(heading = "%n Multiple Walker Options for MPI Simulations%n", validate = false)
84    private final MultiDynamicsOptionGroup group = new MultiDynamicsOptionGroup();
85  
86    /**
87     * If residues selected for distributing initial configurations, performs many-body optimization
88     * for this distribution.
89     *
90     * @param molecularAssemblies an array of {@link ffx.potential.MolecularAssembly} objects.
91     * @param potentials ForceFieldEnergy for each topology.
92     * @param crystalPotential Overall CrystalPotential in use.
93     * @param algorithmFunctions a {@link ffx.algorithms.AlgorithmFunctions} object.
94     * @param rank The MPI rank of this process.
95     * @param worldSize The number of MPI processes.
96     */
97    public void distribute(MolecularAssembly[] molecularAssemblies, Potential[] potentials,
98        CrystalPotential crystalPotential, AlgorithmFunctions algorithmFunctions, int rank,
99        int worldSize) {
100     if (!group.distributeWalkersString.equalsIgnoreCase("AUTO")
101         && !group.distributeWalkersString.equalsIgnoreCase("OFF")) {
102       logger.info(" Distributing walker conformations.");
103       int nSys = molecularAssemblies.length;
104       assert nSys == potentials.length;
105       switch (nSys) {
106         case 1 -> optStructure(molecularAssemblies[0], crystalPotential, algorithmFunctions, rank,
107             worldSize);
108         case 2 -> {
109           DualTopologyEnergy dte = (DualTopologyEnergy) crystalPotential;
110           if (dte.getNumSharedVariables() == dte.getNumberOfVariables()) {
111             logger.info(" Generating starting structures based on dual-topology:");
112             optStructure(molecularAssemblies[0], dte, algorithmFunctions, rank, worldSize);
113           } else {
114             logger.info(
115                 " Generating separate starting structures for each topology of the dual topology:");
116             optStructure(molecularAssemblies[0], potentials[0], algorithmFunctions, rank, worldSize);
117             optStructure(molecularAssemblies[1], potentials[1], algorithmFunctions, rank, worldSize);
118           }
119         }
120         case 4 -> {
121           QuadTopologyEnergy qte = (QuadTopologyEnergy) crystalPotential;
122           optStructure(molecularAssemblies[0], qte.getDualTopA(), algorithmFunctions, rank,
123               worldSize);
124           optStructure(molecularAssemblies[3], qte.getDualTopB(), algorithmFunctions, rank,
125               worldSize);
126         }
127         // Oct-topology is deprecated on account of not working as intended.
128         default -> logger.severe(" First: must have 1, 2, or 4 topologies.");
129       }
130     } else {
131       logger.finer(" Skipping RO-based distribution of initial configurations.");
132     }
133   }
134 
135   /**
136    * If residues selected for distributing initial configurations, performs many-body optimization
137    * for this distribution.
138    *
139    * @param molecularAssemblies an array of {@link ffx.potential.MolecularAssembly} objects.
140    * @param crystalPotential Overall CrystalPotential in use.
141    * @param algorithmFunctions a {@link ffx.algorithms.AlgorithmFunctions} object.
142    * @param rank The MPI rank of this process.
143    * @param worldSize The number of MPI processes.
144    */
145   public void distribute(MolecularAssembly[] molecularAssemblies, CrystalPotential crystalPotential,
146       AlgorithmFunctions algorithmFunctions, int rank, int worldSize) {
147     int ntops = molecularAssemblies.length;
148     Potential[] energies = new Potential[ntops];
149     for (int i = 0; i < ntops; i++) {
150       energies[i] = molecularAssemblies[i].getPotentialEnergy();
151     }
152     distribute(molecularAssemblies, energies, crystalPotential, algorithmFunctions, rank, worldSize);
153   }
154 
155   /**
156    * Synchronous walker communication.
157    *
158    * <p>isSynchronous.
159    *
160    * @return a boolean.
161    */
162   public boolean isSynchronous() {
163     return group.synchronous;
164   }
165 
166   public void setSynchronous(boolean synchronous) {
167     group.synchronous = synchronous;
168   }
169 
170   /**
171    * Opens a file and processes it. Extends the behavior of AlchemicalOptions.openFile by permitting
172    * use of a rank-dependent File.
173    *
174    * @param algorithmFunctions AlgorithmFunctions object.
175    * @param topologyOptions Topology Options.
176    * @param threadsPer Threads to use per system.
177    * @param toOpen Filename to open.
178    * @param topNum Number of the topology to open.
179    * @param alchemicalOptions Alchemical Options.
180    * @param rank Rank in the world communicator.
181    * @param structureFile a {@link java.io.File} object.
182    * @return a {@link ffx.potential.MolecularAssembly} object.
183    */
184   public MolecularAssembly openFile(AlgorithmFunctions algorithmFunctions,
185       TopologyOptions topologyOptions, int threadsPer, String toOpen, int topNum,
186       AlchemicalOptions alchemicalOptions, File structureFile, int rank) {
187     boolean autoDist = group.distributeWalkersString.equalsIgnoreCase("AUTO");
188 
189     if (autoDist) {
190       String openName = format("%s_%d", toOpen, rank + 1);
191       File testFile = new File(openName);
192       if (testFile.exists()) {
193         toOpen = openName;
194       } else {
195         logger.warning(format(" File %s does not exist; using default %s", openName, toOpen));
196       }
197     }
198     MolecularAssembly assembly = alchemicalOptions.openFile(algorithmFunctions, topologyOptions,
199         threadsPer, toOpen, topNum);
200     assembly.setFile(structureFile);
201     return assembly;
202   }
203 
204   /**
205    * Parses --dw into optimization tokens if it's not "OFF", "AUTO", or null.
206    *
207    * @return An array of Strings from splitting the distributed flag.
208    */
209   private String[] parseDistributed() {
210     String distributeWalkersString = group.distributeWalkersString;
211     if (distributeWalkersString.equalsIgnoreCase("OFF")
212         || distributeWalkersString.equalsIgnoreCase("AUTO") || distributeWalkersString.isEmpty()) {
213       return null;
214     }
215     return distributeWalkersString.split("\\.");
216   }
217 
218   /**
219    * Allows walkers to start from multiple conformations; AUTO picks up per-walker conformations as
220    * filename.pdb_(walker number), and specifying a residue starts a rotamer optimization to generate
221    * side-chain configurations to start from.
222    *
223    * @return Returns the Distribute Walkers string.
224    */
225   public String getDistributeWalkersString() {
226     return group.distributeWalkersString;
227   }
228 
229   /**
230    * Distribute side-chain conformations of molecularAssembly.
231    *
232    * @param molecularAssembly To distribute
233    * @param potential Potential to use
234    */
235   private void optStructure(MolecularAssembly molecularAssembly, Potential potential,
236       AlgorithmFunctions algorithmFunctions, int rank, int worldSize) {
237     RotamerLibrary rLib = new RotamerLibrary(false);
238     String[] distribRes = parseDistributed();
239 
240     if (distribRes == null || distribRes.length == 0) {
241       throw new IllegalArgumentException(
242           " Programming error: Must have list of residues to split on!");
243     }
244 
245     LambdaInterface lambdaInterface =
246         (potential instanceof LambdaInterface) ? (LambdaInterface) potential : null;
247     double initLam = -1.0;
248     if (lambdaInterface != null) {
249       initLam = lambdaInterface.getLambda();
250       lambdaInterface.setLambda(0.5);
251     }
252 
253     Pattern chainMatcher = Pattern.compile("^([a-zA-Z])?([0-9]+)$");
254 
255     List<Residue> residueList = new ArrayList<>(distribRes.length);
256 
257     for (String ts : distribRes) {
258       Matcher m = chainMatcher.matcher(ts);
259       Character chainID;
260       int resNum;
261       if (m.find()) {
262         if (m.groupCount() == 2) {
263           chainID = m.group(1).charAt(0);
264           resNum = Integer.parseInt(m.group(2));
265         } else {
266           chainID = ' ';
267           resNum = Integer.parseInt(m.group(1));
268         }
269       } else {
270         logger.warning(format(" Could not parse %s as a valid residue!", ts));
271         continue;
272       }
273       logger.info(format(" Looking for chain %c residue %d", chainID, resNum));
274 
275       for (Polymer p : molecularAssembly.getChains()) {
276         if (p.getChainID() == chainID) {
277           for (Residue r : p.getResidues()) {
278             if (r.getResidueNumber() == resNum && r.setRotamers(rLib) != null) {
279               residueList.add(r);
280             }
281           }
282         }
283       }
284     }
285 
286     if (residueList.isEmpty()) {
287       throw new IllegalArgumentException(" No valid entries for distWalkers!");
288     }
289 
290     AlgorithmListener alist = algorithmFunctions.getDefaultListener();
291     RotamerOptimization ropt = new RotamerOptimization(molecularAssembly, potential, alist);
292     ropt.setRotamerLibrary(rLib);
293 
294     ropt.setThreeBodyEnergy(false);
295 
296     CompositeConfiguration properties = molecularAssembly.getProperties();
297     if (!properties.containsKey("ro-ensembleNumber") && !properties.containsKey(
298         "ro-ensembleEnergy")) {
299       logger.info(format(" Setting ensemble to default of number of walkers %d", worldSize));
300       ropt.setEnsemble(worldSize);
301     }
302 
303     ropt.setPrintFiles(false);
304     ropt.setResiduesIgnoreNull(residueList);
305 
306     RotamerLibrary.measureRotamers(residueList, false);
307     ropt.optimize(RotamerOptimization.Algorithm.ALL);
308     ropt.setCoordinatesToEnsemble(rank);
309 
310     // One final energy call to ensure the coordinates are properly set at the
311     // end of rotamer optimization.
312     double[] xyz = new double[potential.getNumberOfVariables()];
313     potential.getCoordinates(xyz);
314     logger.info(" Final Optimized Energy:");
315     potential.energy(xyz, true);
316 
317     if (lambdaInterface != null) {
318       lambdaInterface.setLambda(initLam);
319     }
320   }
321 
322   public void setDistributeWalkersString(String distributeWalkersString) {
323     group.distributeWalkersString = distributeWalkersString;
324   }
325 
326   public int getFirstDir() {
327     return group.firstDir;
328   }
329 
330   public void setFirstDir(int firstDir) {
331     group.firstDir = firstDir;
332   }
333 
334   /**
335    * Collection of Multi-Walker Dynamics Options.
336    */
337   private static class MultiDynamicsOptionGroup {
338 
339     /** --firstDir The first directory to use for multiple walker jobs. */
340     @Option(names = {"--firstDir"}, defaultValue = "0", paramLabel = "0",
341         description = "The first directory to use for multiple walker jobs.")
342     private int firstDir = 0;
343 
344     /** -y or --synchronous sets synchronous walker communication (not recommended) */
345     @Option(names = {"-y", "--synchronous"}, defaultValue = "false",
346         description = "Walker communication is synchronous")
347     private boolean synchronous = false;
348 
349     /**
350      * -dw or --distributeWalkers allows walkers to start from multiple conformations; AUTO picks up
351      * per-walker conformations as filename.pdb_(walker number), and specifying a residue starts a
352      * rotamer optimization to generate side-chain configurations to start from.
353      */
354     @Option(names = {"--dw", "--distributeWalkers"}, paramLabel = "OFF", defaultValue = "OFF",
355         description = "AUTO: Pick up per-walker configurations as [filename.pdb]_[num], or specify a residue to distribute on.")
356     private String distributeWalkersString = "OFF";
357   }
358 }