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 }