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.FFXScript;
47 import groovy.lang.Binding;
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
56 import static java.lang.String.format;
57
58 /**
59 * Base class for scripts in the Algorithms package, providing some key functions.
60 *
61 * @author Michael J. Schnieders
62 * @since 1.0
63 */
64 public class AlgorithmsScript extends FFXScript {
65
66 /**
67 * An instance of AlgorithmFunctions passed into the current context.
68 */
69 public AlgorithmFunctions algorithmFunctions;
70
71 /**
72 * An active MolecularAssembly passed into the current context or loaded by the Script from a file
73 * argument.
74 */
75 public MolecularAssembly activeAssembly;
76
77 /**
78 * An instance of the AlgorithmListener interface.
79 */
80 public AlgorithmListener algorithmListener;
81
82 /**
83 * The directory in which to place output files. Mostly for tests.
84 */
85 protected File baseDir;
86
87 public AlgorithmsScript() {
88 this(new groovy.lang.Binding());
89 }
90
91 public AlgorithmsScript(Binding binding) {
92 super(binding);
93 }
94
95 /**
96 * Create a Script using the supplied command line arguments.
97 *
98 * @param args The command line arguments.
99 */
100 public AlgorithmsScript(String[] args) {
101 this(new Binding());
102 Binding binding = getBinding();
103 binding.setVariable("args", Arrays.asList(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> plist = new ArrayList<>();
127 if (activeAssembly != null && activeAssembly.getPotentialEnergy() != null) {
128 plist.add(activeAssembly.getPotentialEnergy());
129 }
130 return plist;
131 }
132
133 /**
134 * {@inheritDoc}
135 *
136 * <p>Execute the BaseScript init method, then load algorithm functions.
137 */
138 @Override
139 public boolean init() {
140 if (!super.init()) {
141 return false;
142 }
143
144 Binding binding = getBinding();
145
146 if (binding.hasVariable("functions")) {
147 algorithmFunctions = (AlgorithmFunctions) binding.getVariable("functions");
148 } else {
149 algorithmFunctions = new AlgorithmUtils();
150 binding.setVariable("functions", algorithmFunctions);
151 }
152
153 activeAssembly = null;
154 if (binding.hasVariable("active")) {
155 activeAssembly = (MolecularAssembly) binding.getVariable("active");
156 }
157
158 algorithmListener = null;
159 if (binding.hasVariable("listener")) {
160 algorithmListener = (AlgorithmListener) binding.getVariable("listener");
161 }
162
163 if (binding.hasVariable("baseDir")) {
164 baseDir = (File) binding.getVariable("baseDir");
165 }
166
167 return true;
168 }
169
170 /**
171 * Sets the directory this script should save files to. Mostly used for tests.
172 *
173 * @param baseDir Directory to save output to.
174 */
175 public void setBaseDir(File baseDir) {
176 this.baseDir = baseDir;
177 }
178
179 /**
180 * Return a File in the base directory with the same name as the input file.
181 * <p>
182 * This will just be the original file if baseDir was never set, which is the case for production
183 * runs.
184 *
185 * @param file File to find a save location for.
186 * @return Returns a File in the base directory with the same name as the input file.
187 */
188 protected File saveDirFile(File file) {
189 if (baseDir == null || !baseDir.exists() || !baseDir.isDirectory() || !baseDir.canWrite()) {
190 return file;
191 } else {
192 String baseName = file.getName();
193 String newName = baseDir.getAbsolutePath() + File.separator + baseName;
194 return new File(newName);
195 }
196 }
197
198 /**
199 * If a filename is supplied, open it and return the MolecularAssembly. Otherwise, the current
200 * activeAssembly is returned (which may be null).
201 *
202 * @param filename Filename to open.
203 * @return The active assembly.
204 */
205 public MolecularAssembly getActiveAssembly(@Nullable String filename) {
206 if (filename != null) {
207 // Open the supplied file.
208 MolecularAssembly[] assemblies = {algorithmFunctions.open(filename)};
209 activeAssembly = assemblies[0];
210 }
211 return activeAssembly;
212 }
213
214 /**
215 * If a filename is supplied, open it and return the MolecularAssemblies. Otherwise, the current
216 * activeAssembly is returned (which may be null).
217 *
218 * @param filename Filename to open.
219 * @return The active assemblies.
220 */
221 public MolecularAssembly[] getActiveAssemblies(@Nullable String filename) {
222 MolecularAssembly[] assemblies;
223 if (filename != null) {
224 // Open the supplied file.
225 assemblies = algorithmFunctions.openAll(filename);
226 activeAssembly = assemblies[0];
227 return assemblies;
228 } else {
229 assemblies = new MolecularAssembly[]{activeAssembly};
230 }
231 return assemblies;
232 }
233
234 /**
235 * Set the Active Assembly. This is a work-around for a strange Groovy static compilation bug where
236 * direct assignment of activeAssembly in Groovy scripts that extend AlgorithmsScript fails (a NPE
237 * results).
238 *
239 * @param molecularAssembly The MolecularAssembly that should be active.
240 */
241 public void setActiveAssembly(MolecularAssembly molecularAssembly) {
242 activeAssembly = molecularAssembly;
243 }
244
245 /**
246 * Update the title line of the structure with Energy and Density.
247 * @param energy Newly minimized energy value.
248 */
249 public void updateTitle(double energy){
250 // Replace existing energy and density label if present
251 String oldName = activeAssembly.getName();
252 Crystal crystal = activeAssembly.getCrystal();
253 if(crystal != null && !crystal.aperiodic()){
254 double density = crystal.getDensity(activeAssembly.getMass());
255 if (Strings.CI.contains(oldName, "Energy:")
256 || Strings.CI.contains(oldName, "Density:")) {
257 String[] tokens = oldName.trim().split(" +");
258 int numTokens = tokens.length;
259 // The first element should always be the number of atoms in XYZ.
260 StringBuilder sb = new StringBuilder();
261 for (int i = 0; i < numTokens; i++) {
262 if (Strings.CI.contains(tokens[i], "Energy:")){
263 // i++ skips the current entry (value associated with "Energy")
264 tokens[i++] = Double.toString(energy);
265 } else if (Strings.CI.contains(tokens[i], "Density:")){
266 // i++ skips the current entry (value associated with "Density")
267 tokens[i++] = Double.toString(density);
268 } else {
269 // Accrue previous name.
270 sb.append(tokens[i]).append(" ");
271 }
272 }
273 // Opted to add energy/density after to preserve formatting.
274 activeAssembly.setName(format("%s Energy: %9.4f Density: %9.4f",
275 sb, energy, density));
276 } else {
277 // Append energy and density to structure name (line 1 of XYZ).
278 activeAssembly.setName(format("%s Energy: %9.4f Density: %9.4f",
279 oldName, energy, density));
280 }
281 } else {
282 if (Strings.CI.contains(oldName, "Energy:")) {
283 String[] tokens = oldName.trim().split(" +");
284 int numTokens = tokens.length;
285 // The first element should always be number of atoms in XYZ.
286 StringBuilder sb = new StringBuilder();
287 for (int i = 0; i < numTokens; i++) {
288 if (Strings.CI.contains(tokens[i], "Energy:")){
289 // i++ skips the current entry (value associated with "Energy")
290 tokens[i++] = Double.toString(energy);
291 } else{
292 // Accrue previous name.
293 sb.append(tokens[i]).append(" ");
294 }
295 }
296 // Opted to add energy/density after to preserve formatting.
297 activeAssembly.setName(format("%s Energy: %9.4f", sb, energy));
298 } else {
299 // Append energy and density to the structure name (line 1 of XYZ).
300 activeAssembly.setName(format("%s Energy: %9.4f", oldName, energy));
301 }
302 }
303 }
304 }