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.potential.cli;
39  
40  import ffx.potential.MolecularAssembly;
41  import ffx.potential.bonded.Atom;
42  import ffx.potential.utils.PotentialsFunctions;
43  import picocli.CommandLine.ArgGroup;
44  import picocli.CommandLine.Option;
45  
46  import javax.annotation.Nullable;
47  import java.util.logging.Logger;
48  import java.util.regex.Pattern;
49  
50  import static ffx.potential.cli.AtomSelectionOptions.actOnAtoms;
51  import static ffx.potential.cli.AtomSelectionOptions.actOnResidueAtoms;
52  import static java.lang.String.format;
53  
54  /**
55   * Represents command line options for scripts that utilize alchemistry on at least one topology.
56   *
57   * @author Michael J. Schnieders
58   * @author Jacob M. Litman
59   * @since 1.0
60   */
61  public class AlchemicalOptions {
62  
63    /**
64     * The logger for this class.
65     */
66    public static final Logger logger = Logger.getLogger(AlchemicalOptions.class.getName());
67  
68    /**
69     * A regular expression used to parse ranges of atoms.
70     */
71    public static final Pattern rangeRegEx = Pattern.compile("([0-9]+)-?([0-9]+)?");
72  
73    /**
74     * The ArgGroup keeps the AlchemicalOptions together when printing help.
75     */
76    @ArgGroup(heading = "%n Alchemical Options%n", validate = false)
77    private final AlchemicalOptionGroup group = new AlchemicalOptionGroup();
78  
79    /**
80     * Sets the uncharged atoms for a MolecularAssembly.
81     *
82     * @param assembly       Assembly to which the atoms belong.
83     * @param unchargedAtoms Uncharged atoms selection string.
84     */
85    public static void setUnchargedAtoms(MolecularAssembly assembly, String unchargedAtoms) {
86      actOnAtoms(assembly, unchargedAtoms, (Atom a, Boolean b) -> a.setElectrostatics(!b), "Uncharged");
87    }
88  
89    /**
90     * Sets the alchemical atoms for a MolecularAssembly.
91     *
92     * @param assembly        Assembly to which the atoms belong.
93     * @param alchemicalAtoms Alchemical atoms selection string.
94     * @param alchemicalResidues Alchemical residues selection string.
95     */
96    public static void setAlchemicalAtoms(MolecularAssembly assembly, String alchemicalAtoms, String alchemicalResidues) {
97      actOnAtoms(assembly, alchemicalAtoms, Atom::setApplyLambda, "Alchemical");
98      // todo - doing both won't work - sets all atoms to false for subset - could remove that ?
99      actOnResidueAtoms(assembly, alchemicalResidues, Atom::setApplyLambda, "Alchemical");
100   }
101 
102   /**
103    * --ac or --alchemicalAtoms Specify alchemical atoms [ALL, NONE, Range(s): 1-3,6-N]."
104    *
105    * @return Returns alchemical atoms.
106    */
107   public String getAlchemicalAtoms() {
108     return group.alchemicalAtoms;
109   }
110 
111   /**
112    * --acRes or --alchemicalResidues Specify alchemical residues by chain and residue number [A4,B21].
113    *
114    * @return Returns alchemical residues.
115    */
116   public String getAlchemicalResidues() { return group.alchemicalResidues; }
117 
118   /**
119    * --uc or --unchargedAtoms Specify atoms without electrostatics [ALL, NONE, Range(s): 1-3,6-N].
120    *
121    * @return Returns atoms without electrostatics.
122    */
123   public String getUnchargedAtoms() {
124     return group.unchargedAtoms;
125   }
126 
127   /**
128    * -l or --lambda sets the initial lambda value.
129    *
130    * @return Returns the initial value of lambda.
131    */
132   public double getInitialLambda() {
133     return getInitialLambda(false);
134   }
135 
136   /**
137    * Gets the initial value of lambda.
138    *
139    * @param quiet No logging if quiet.
140    * @return Initial lambda.
141    */
142   public double getInitialLambda(boolean quiet) {
143     return getInitialLambda(1, 0, quiet);
144   }
145 
146   /**
147    * Gets the initial value of lambda.
148    *
149    * @param size The number of processes.
150    * @param rank THe rank of this process.
151    * @return Initial lambda.
152    */
153   public double getInitialLambda(int size, int rank) {
154     return getInitialLambda(size, rank, false);
155   }
156 
157   /**
158    * Gets the initial value of lambda.
159    *
160    * @param size  The number of processes.
161    * @param rank  The rank of this process.
162    * @param quiet No logging if quiet.
163    * @return Initial lambda.
164    */
165   public double getInitialLambda(int size, int rank, boolean quiet) {
166 
167     // Assume initial lambda is not set
168     // How to handle is we get nWindows and lambda
169     // nWindows is size and rank is the window you're looking at (0 - nWindows-1)
170 
171     double initialLambda = group.initialLambda;
172     if (initialLambda < 0.0 || initialLambda > 1.0) {
173       if (rank == 0 || size < 2) {
174         initialLambda = 0.0;
175       } else if (rank == size - 1) {
176         initialLambda = 1.0;
177       } else {
178         double dL = 1.0 / (size - 1);
179         initialLambda = dL * rank;
180       }
181 
182       if (!quiet) {
183         logger.info(format(" Setting lambda to %5.3f.", initialLambda));
184       }
185     }
186     return initialLambda;
187   }
188 
189   /**
190    * If any softcore Atoms have been detected.
191    *
192    * @return Presence of softcore Atoms.
193    */
194   public boolean hasSoftcore() {
195     String alchemicalAtoms = getAlchemicalAtoms();
196     boolean atoms = (alchemicalAtoms != null
197         && !alchemicalAtoms.equalsIgnoreCase("NONE")
198         && !alchemicalAtoms.equalsIgnoreCase(""));
199     String alchemicalResidues = getAlchemicalResidues();
200     boolean residues = (alchemicalResidues != null
201         && !alchemicalResidues.equalsIgnoreCase("NONE")
202         && !alchemicalResidues.equalsIgnoreCase(""));
203     return (atoms || residues);
204   }
205 
206   /**
207    * Set the alchemical atoms for this molecularAssembly.
208    *
209    * @param topology a {@link ffx.potential.MolecularAssembly} object.
210    */
211   public void setFirstSystemAlchemistry(MolecularAssembly topology) {
212     setAlchemicalAtoms(topology, getAlchemicalAtoms(), getAlchemicalResidues());
213   }
214 
215   /**
216    * Opens a File to a MolecularAssembly for alchemistry.
217    *
218    * @param potentialFunctions A utility object for opening Files into MolecularAssemblies.
219    * @param topologyOptions    TopologyOptions in case a dual-topology or greater is to be used.
220    * @param threadsPer         Number of threads to be used for this MolecularAssembly.
221    * @param toOpen             The name of the File to be opened.
222    * @param topNum             The index of this topology.
223    * @return The processed MolecularAssembly.
224    */
225   public MolecularAssembly openFile(PotentialsFunctions potentialFunctions,
226                                     TopologyOptions topologyOptions, int threadsPer,
227                                     String toOpen, int topNum) {
228     potentialFunctions.openAll(toOpen, threadsPer);
229     MolecularAssembly molecularAssembly = potentialFunctions.getActiveAssembly();
230     return processFile(topologyOptions, molecularAssembly, topNum);
231   }
232 
233   /**
234    * Performs processing on a MolecularAssembly for alchemistry.
235    *
236    * @param topologyOptions   TopologyOptions in case a dual-topology or greater is to be used.
237    * @param molecularAssembly The MolecularAssembly to be processed.
238    * @param topNum            The index of this topology, 0-indexed.
239    * @return The processed MolecularAssembly.
240    */
241   public MolecularAssembly processFile(@Nullable TopologyOptions topologyOptions,
242                                        MolecularAssembly molecularAssembly, int topNum) {
243 
244     int remainder = (topNum % 2) + 1;
245     switch (remainder) {
246       case 1 -> {
247         setFirstSystemAlchemistry(molecularAssembly);
248         setFirstSystemUnchargedAtoms(molecularAssembly);
249       }
250       case 2 -> {
251         if (topologyOptions == null) {
252           throw new IllegalArgumentException(
253               " For >= 2 systems, topologyOptions must not be empty!");
254         }
255         topologyOptions.setSecondSystemAlchemistry(molecularAssembly);
256         topologyOptions.setSecondSystemUnchargedAtoms(molecularAssembly);
257       }
258     }
259     return molecularAssembly;
260   }
261 
262   /**
263    * Set uncharged atoms for this molecularAssembly.
264    *
265    * @param topology a {@link ffx.potential.MolecularAssembly} object.
266    */
267   public void setFirstSystemUnchargedAtoms(MolecularAssembly topology) {
268     setUnchargedAtoms(topology, getUnchargedAtoms());
269   }
270 
271   /**
272    * Set system properties for alchemical simulations.
273    */
274   public void setAlchemicalProperties() {
275     if (hasSoftcore()) {
276       System.setProperty("lambdaterm", "true");
277     }
278   }
279 
280   /**
281    * Collection of Alchemical Options.
282    */
283   private static class AlchemicalOptionGroup {
284 
285     /**
286      * -l or --lambda sets the initial lambda value.
287      */
288     @Option(
289         names = {"-l", "--lambda"},
290         paramLabel = "-1",
291         description = "Initial lambda value.")
292     double initialLambda = -1.0;
293 
294     /**
295      * --ac or --alchemicalAtoms Specify alchemical atoms [ALL, NONE, Range(s): 1-3,6-N]."
296      */
297     @Option(
298         names = {"--ac", "--alchemicalAtoms"},
299         paramLabel = "<selection>",
300         defaultValue = "",
301         description = "Specify alchemical atoms [ALL, NONE, Range(s): 1-3,6-N].")
302     String alchemicalAtoms = "";
303 
304     /**
305      * --acRes or --alchemicalResidues Specify alchemical residues by chain and residue number [A4,B21].
306      */
307     @Option(
308         names = {"--acRes", "--alchemicalResidues"},
309         paramLabel = "<selection>",
310         defaultValue = "",
311         description = "Specify alchemical residues by chain and residue number [A4,B21]")
312     String alchemicalResidues = "";
313 
314     /**
315      * --uc or --unchargedAtoms Specify atoms without electrostatics [ALL, NONE, Range(s):
316      * 1-3,6-N]."
317      */
318     @Option(
319         names = {"--uc", "--unchargedAtoms"},
320         paramLabel = "<selection>",
321         defaultValue = "",
322         description = "Specify atoms without electrostatics [ALL, NONE, Range(s): 1-3,6-N].")
323     String unchargedAtoms = "";
324   }
325 }