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.AlgorithmUtils;
43 import ffx.crystal.Crystal;
44 import ffx.numerics.Potential;
45 import ffx.potential.MolecularAssembly;
46 import ffx.utilities.FFXCommand;
47 import ffx.utilities.FFXBinding;
48 import org.apache.commons.lang3.Strings;
49
50 import javax.annotation.Nullable;
51 import java.io.File;
52 import java.util.ArrayList;
53 import java.util.Arrays;
54 import java.util.List;
55 import java.util.Objects;
56 import java.util.stream.Collectors;
57
58 import static java.lang.String.format;
59
60 /**
61 * Base class for scripts in the Algorithms package, providing some key functions.
62 *
63 * @author Michael J. Schnieders
64 * @since 1.0
65 */
66 public class AlgorithmsCommand extends FFXCommand {
67
68 /**
69 * An instance of AlgorithmFunctions passed into the current context.
70 */
71 public AlgorithmFunctions algorithmFunctions;
72
73 /**
74 * An active MolecularAssembly passed into the current context or loaded by the Script from a file
75 * argument.
76 */
77 public MolecularAssembly activeAssembly;
78
79 /**
80 * An instance of the AlgorithmListener interface.
81 */
82 public AlgorithmListener algorithmListener;
83
84 /**
85 * The directory in which to place output files. Mostly for tests.
86 */
87 protected File baseDir;
88
89 public AlgorithmsCommand() {
90 super();
91 }
92
93 public AlgorithmsCommand(FFXBinding binding) {
94 super(binding);
95 }
96
97 /**
98 * Create a Script using the supplied command line arguments.
99 *
100 * @param args The command line arguments.
101 */
102 public AlgorithmsCommand(String[] args) {
103 super(args);
104 }
105
106 /**
107 * Reclaims resources associated with all Potential objects associated with this script.
108 *
109 * @return If all Potentials had resources reclaimed.
110 */
111 public boolean destroyPotentials() {
112 boolean allSucceeded = true;
113 for (Potential potent : getPotentials()) {
114 logger.fine(format(" Potential %s is being destroyed. ", potent));
115 allSucceeded = allSucceeded && potent.destroy();
116 }
117 return allSucceeded;
118 }
119
120 /**
121 * Returns a List of all Potential objects associated with this script.
122 *
123 * @return All Potentials. Sometimes empty, never null.
124 */
125 public List<Potential> getPotentials() {
126 List<Potential> potentials = new ArrayList<>();
127 if (activeAssembly != null && activeAssembly.getPotentialEnergy() != null) {
128 potentials.add(activeAssembly.getPotentialEnergy());
129 }
130 return potentials;
131 }
132
133 /**
134 * Returns a List of all Potential objects from the supplied MolecularAssembly array.
135 * Should be written to tolerate nulls, as many tests run help() and exit without
136 * instantiating their Potentials.
137 *
138 * @param assemblies An array of MolecularAssembly instances.
139 * @return All Potentials. Sometimes empty, never null.
140 */
141 public List<Potential> getPotentialsFromAssemblies(MolecularAssembly[] assemblies) {
142 if (assemblies == null) {
143 return new ArrayList<>();
144 }
145 return Arrays.stream(assemblies)
146 .filter(Objects::nonNull)
147 .map(MolecularAssembly::getPotentialEnergy)
148 .filter(Objects::nonNull)
149 .collect(Collectors.toList());
150 }
151
152 /**
153 * {@inheritDoc}
154 *
155 * <p>Execute the BaseScript init method, then load algorithm functions.
156 */
157 @Override
158 public boolean init() {
159 if (!super.init()) {
160 return false;
161 }
162
163 if (binding.hasVariable("functions")) {
164 algorithmFunctions = (AlgorithmFunctions) binding.getVariable("functions");
165 } else {
166 algorithmFunctions = new AlgorithmUtils();
167 binding.setVariable("functions", algorithmFunctions);
168 }
169
170 activeAssembly = null;
171 if (binding.hasVariable("active")) {
172 activeAssembly = (MolecularAssembly) binding.getVariable("active");
173 }
174
175 algorithmListener = null;
176 if (binding.hasVariable("listener")) {
177 algorithmListener = (AlgorithmListener) binding.getVariable("listener");
178 }
179
180 if (binding.hasVariable("baseDir")) {
181 baseDir = (File) binding.getVariable("baseDir");
182 }
183
184 return true;
185 }
186
187 /**
188 * Sets the directory this script should save files to. Mostly used for tests.
189 *
190 * @param baseDir Directory to save output to.
191 */
192 public void setBaseDir(File baseDir) {
193 this.baseDir = baseDir;
194 }
195
196 /**
197 * Return a File in the base directory with the same name as the input file.
198 * <p>
199 * This will just be the original file if baseDir was never set, which is the case for production
200 * runs.
201 *
202 * @param file File to find a save location for.
203 * @return Returns a File in the base directory with the same name as the input file.
204 */
205 protected File saveDirFile(File file) {
206 if (baseDir == null || !baseDir.exists() || !baseDir.isDirectory() || !baseDir.canWrite()) {
207 return file;
208 } else {
209 String baseName = file.getName();
210 String newName = baseDir.getAbsolutePath() + File.separator + baseName;
211 return new File(newName);
212 }
213 }
214
215 /**
216 * If a filename is supplied, open it and return the MolecularAssembly. Otherwise, the current
217 * activeAssembly is returned (which may be null).
218 *
219 * @param filename Filename to open.
220 * @return The active assembly.
221 */
222 public MolecularAssembly getActiveAssembly(@Nullable String filename) {
223 if (filename != null) {
224 // Open the supplied file.
225 MolecularAssembly[] assemblies = {algorithmFunctions.open(filename)};
226 activeAssembly = assemblies[0];
227 }
228 return activeAssembly;
229 }
230
231 /**
232 * If a filename is supplied, open it and return the MolecularAssemblies. Otherwise, the current
233 * activeAssembly is returned (which may be null).
234 *
235 * @param filename Filename to open.
236 * @return The active assemblies.
237 */
238 public MolecularAssembly[] getActiveAssemblies(@Nullable String filename) {
239 MolecularAssembly[] assemblies;
240 if (filename != null) {
241 // Open the supplied file.
242 assemblies = algorithmFunctions.openAll(filename);
243 activeAssembly = assemblies[0];
244 return assemblies;
245 } else {
246 assemblies = new MolecularAssembly[]{activeAssembly};
247 }
248 return assemblies;
249 }
250
251 /**
252 * Update the title line of the structure with Energy and Density.
253 *
254 * @param energy Newly minimized energy value.
255 */
256 public void updateTitle(double energy) {
257 // Replace existing energy and density label if present
258 String oldName = activeAssembly.getName();
259 Crystal crystal = activeAssembly.getCrystal();
260 if (crystal != null && !crystal.aperiodic()) {
261 double density = crystal.getDensity(activeAssembly.getMass());
262 if (Strings.CI.contains(oldName, "Energy:")
263 || Strings.CI.contains(oldName, "Density:")) {
264 String[] tokens = oldName.trim().split(" +");
265 int numTokens = tokens.length;
266 // The first element should always be the number of atoms in XYZ.
267 StringBuilder sb = new StringBuilder();
268 for (int i = 0; i < numTokens; i++) {
269 if (Strings.CI.contains(tokens[i], "Energy:")) {
270 // i++ skips the current entry (value associated with "Energy")
271 tokens[i++] = Double.toString(energy);
272 } else if (Strings.CI.contains(tokens[i], "Density:")) {
273 // i++ skips the current entry (value associated with "Density")
274 tokens[i++] = Double.toString(density);
275 } else {
276 // Accrue previous name.
277 sb.append(tokens[i]).append(" ");
278 }
279 }
280 // Opted to add energy/density after to preserve formatting.
281 activeAssembly.setName(format("%s Energy: %9.4f Density: %9.4f",
282 sb, energy, density));
283 } else {
284 // Append energy and density to structure name (line 1 of XYZ).
285 activeAssembly.setName(format("%s Energy: %9.4f Density: %9.4f",
286 oldName, energy, density));
287 }
288 } else {
289 if (Strings.CI.contains(oldName, "Energy:")) {
290 String[] tokens = oldName.trim().split(" +");
291 int numTokens = tokens.length;
292 // The first element should always be number of atoms in XYZ.
293 StringBuilder sb = new StringBuilder();
294 for (int i = 0; i < numTokens; i++) {
295 if (Strings.CI.contains(tokens[i], "Energy:")) {
296 // i++ skips the current entry (value associated with "Energy")
297 tokens[i++] = Double.toString(energy);
298 } else {
299 // Accrue previous name.
300 sb.append(tokens[i]).append(" ");
301 }
302 }
303 // Opted to add energy/density after to preserve formatting.
304 activeAssembly.setName(format("%s Energy: %9.4f", sb, energy));
305 } else {
306 // Append energy and density to the structure name (line 1 of XYZ).
307 activeAssembly.setName(format("%s Energy: %9.4f", oldName, energy));
308 }
309 }
310 }
311 }