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.commands;
39  
40  import ffx.algorithms.cli.AlgorithmsCommand;
41  import ffx.algorithms.cli.MinimizeOptions;
42  import ffx.algorithms.optimize.CrystalMinimize;
43  import ffx.algorithms.optimize.Minimize;
44  import ffx.numerics.Potential;
45  import ffx.potential.ForceFieldEnergy;
46  import ffx.potential.MolecularAssembly.FractionalMode;
47  import ffx.potential.XtalEnergy;
48  import ffx.potential.cli.AtomSelectionOptions;
49  import ffx.potential.parsers.PDBFilter;
50  import ffx.potential.parsers.SystemFilter;
51  import ffx.potential.parsers.XYZFilter;
52  import ffx.utilities.FFXBinding;
53  import ffx.utilities.FileUtils;
54  import org.apache.commons.io.FilenameUtils;
55  import picocli.CommandLine.Command;
56  import picocli.CommandLine.Mixin;
57  import picocli.CommandLine.Option;
58  import picocli.CommandLine.Parameters;
59  
60  import java.io.File;
61  import java.util.Collections;
62  import java.util.List;
63  
64  import static java.lang.String.format;
65  import static org.apache.commons.math3.util.FastMath.abs;
66  
67  /**
68   * The MinimizeCrystals script uses a limited-memory BFGS algorithm to minimize the
69   * energy of a crystal, including both coordinates and unit cell parameters.
70   * <br>
71   * Usage:
72   * <br>
73   * ffxc MinimizeCrystals [options] &lt;filename&gt;
74   */
75  @Command(description = " Minimize crystal unit cell parameters.", name = "MinimizeCrystals")
76  public class MinimizeCrystals extends AlgorithmsCommand {
77  
78    @Mixin
79    private MinimizeOptions minimizeOptions;
80  
81    @Mixin
82    private AtomSelectionOptions atomSelectionOptions;
83  
84    /**
85     * -c or --coords to cycle between lattice and coordinate optimization until both satisfy the convergence criteria.
86     */
87    @Option(names = {"-c", "--coords"}, paramLabel = "false", defaultValue = "false",
88        description = "Cycle between lattice and coordinate optimization instead of optimizing both together.")
89    private boolean coords;
90  
91    /**
92     * -f or --fractional to set the optimization to maintain fractional coordinates [ATOM/MOLECULE/OFF].
93     */
94    @Option(names = {"-f", "--fractional"}, paramLabel = "OFF", defaultValue = "OFF",
95        description = "Maintain fractional coordinates during lattice optimization [OFF/MOLECULE/ATOM].")
96    private String fractional;
97  
98    /**
99     * -t or --tensor to print out partial derivatives of the energy with respect to unit cell parameters.
100    */
101   @Option(names = {"-t", "--tensor"}, paramLabel = "false", defaultValue = "false",
102       description = "Compute partial derivatives of the energy with respect to unit cell parameters.")
103   private boolean tensor;
104 
105   /**
106    * --et or --energyTolerance End minimization if new energy deviates less than this tolerance.
107    */
108   @Option(names = {"--et", "--energyTolerance"}, paramLabel = "1.0e-10", defaultValue = "1.0e-10",
109       description = "End minimization if new energy deviates less than this tolerance.")
110   private double tolerance;
111 
112   /**
113    * --mi or --minimumIterations End minimization if fewer iterations were taken for both coordinate and lattice minimization.
114    */
115   @Option(names = {"--mi", "--minimumIterations"}, paramLabel = "-1", defaultValue = "-1",
116       description = "End minimization if it starts to cycle between small coordinate and lattice parameter fluctuations.")
117   private int minIterations;
118 
119   /**
120    * --cy or --cycles End minimization if it has cycled between lattice parameters and coordinates more than this value.
121    */
122   @Option(names = {"--cy", "--cycles"}, paramLabel = "-1", defaultValue = "-1",
123       description = "End minimization if it has cycled between lattice parameters and coordinates more than this value.")
124   private int cycles;
125 
126   /**
127    * The final argument(s) should be an XYZ or PDB filename.
128    */
129   @Parameters(arity = "1", paramLabel = "file", description = "Atomic coordinate files in PDB or XYZ format.")
130   private String filename = null;
131 
132   private XtalEnergy xtalEnergy;
133   private CrystalMinimize crystalMinimize;
134 
135   /**
136    * CrystalMin Constructor.
137    */
138   public MinimizeCrystals() {
139     super();
140   }
141 
142   /**
143    * CrystalMin Constructor.
144    *
145    * @param binding The Binding to use.
146    */
147   public MinimizeCrystals(FFXBinding binding) {
148     super(binding);
149   }
150 
151   /**
152    * MinimizeCrystals constructor that sets the command line arguments.
153    *
154    * @param args Command line arguments.
155    */
156   public MinimizeCrystals(String[] args) {
157     super(args);
158   }
159 
160   /**
161    * {@inheritDoc}
162    */
163   @Override
164   public MinimizeCrystals run() {
165 
166     // Init the context and bind variables.
167     if (!init()) {
168       return this;
169     }
170 
171     // Load the MolecularAssembly.
172     activeAssembly = getActiveAssembly(filename);
173     if (activeAssembly == null) {
174       logger.info(helpString());
175       return this;
176     }
177 
178     // Set the filename
179     filename = activeAssembly.getFile().getAbsolutePath();
180 
181     logger.info("\n Running CrystalMinimize on " + filename);
182     logger.info("\n RMS gradient convergence criteria: " + minimizeOptions.getEps());
183 
184     atomSelectionOptions.setActiveAtoms(activeAssembly);
185 
186     ForceFieldEnergy forceFieldEnergy = activeAssembly.getPotentialEnergy();
187     xtalEnergy = new XtalEnergy(forceFieldEnergy, activeAssembly, coords);
188     xtalEnergy.setFractionalCoordinateMode(FractionalMode.MOLECULE);
189     SystemFilter systemFilter = algorithmFunctions.getFilter();
190 
191     // Apply fractional coordinate mode.
192     if (fractional != null && !fractional.isEmpty()) {
193       try {
194         FractionalMode mode = FractionalMode.valueOf(fractional.toUpperCase());
195         xtalEnergy.setFractionalCoordinateMode(mode);
196       } catch (Exception ignored) {
197         logger.info(" Unrecognized fractional coordinate mode: " + fractional);
198         logger.info(" Fractional coordinate mode is set to MOLECULE.");
199       }
200     }
201 
202     runMinimize();
203 
204     if (tensor) {
205       crystalMinimize.printTensor();
206     }
207 
208     // Handle Single Topology Cases.
209     if (baseDir == null || !baseDir.exists() || !baseDir.isDirectory() || !baseDir.canWrite()) {
210       baseDir = new File(FilenameUtils.getFullPath(filename));
211     }
212 
213     String dirName = baseDir.toString() + File.separator;
214     String name = FilenameUtils.getName(filename);
215     String ext = FilenameUtils.getExtension(name);
216     name = FilenameUtils.removeExtension(name);
217     File saveFile;
218     PDBFilter pdbFilter = null;
219     XYZFilter xyzFilter = null;
220 
221     if (ext.toUpperCase().contains("XYZ")) {
222       saveFile = new File(dirName + name + ".xyz");
223       xyzFilter = new XYZFilter(saveFile, activeAssembly, activeAssembly.getForceField(),
224           activeAssembly.getProperties());
225       algorithmFunctions.saveAsXYZ(activeAssembly, saveFile);
226     } else if (ext.toUpperCase().contains("ARC")) {
227       saveFile = new File(dirName + name + ".arc");
228       saveFile = algorithmFunctions.versionFile(saveFile);
229       xyzFilter = new XYZFilter(saveFile, activeAssembly, activeAssembly.getForceField(),
230           activeAssembly.getProperties());
231       algorithmFunctions.saveAsXYZ(activeAssembly, saveFile);
232     } else {
233       saveFile = new File(dirName + name + ".pdb");
234       saveFile = algorithmFunctions.versionFile(saveFile);
235       pdbFilter = new PDBFilter(saveFile, activeAssembly, activeAssembly.getForceField(),
236           activeAssembly.getProperties());
237       int numModels = systemFilter.countNumModels();
238       if (numModels > 1) {
239         pdbFilter.setModelNumbering(0);
240       }
241       pdbFilter.writeFile(saveFile, true, false, false);
242     }
243 
244     if (systemFilter instanceof XYZFilter || systemFilter instanceof PDBFilter) {
245       while (systemFilter.readNext()) {
246         if (systemFilter instanceof PDBFilter) {
247           FileUtils.append(saveFile, "ENDMDL\n");
248           runMinimize();
249           pdbFilter.writeFile(saveFile, true, false, false);
250         } else if (systemFilter instanceof XYZFilter) {
251           runMinimize();
252           xyzFilter.writeFile(saveFile, true);
253         }
254       }
255 
256       if (systemFilter instanceof PDBFilter) {
257         FileUtils.append(saveFile, "END\n");
258       }
259     }
260 
261     return this;
262   }
263 
264   private void runMinimize() {
265     logger.info("\n Crystal Minimizing " + activeAssembly.getName());
266     crystalMinimize = new CrystalMinimize(activeAssembly, xtalEnergy, algorithmListener);
267     crystalMinimize.minimize(minimizeOptions.getNBFGS(), minimizeOptions.getEps(), minimizeOptions.getIterations());
268     logger.info("\n Crystal Minimization Complete.");
269     double energy = crystalMinimize.getEnergy();
270 
271     // Complete rounds of coordinate and lattice optimization.
272     if (coords) {
273       ForceFieldEnergy forceFieldEnergy = activeAssembly.getPotentialEnergy();
274       Minimize minimize = new Minimize(activeAssembly, forceFieldEnergy, algorithmListener);
275       int numCycles = 0;
276       while (true) {
277         // Complete a round of coordinate optimization.
278         minimize.minimize(minimizeOptions.getNBFGS(), minimizeOptions.getEps(), minimizeOptions.getIterations());
279         double newEnergy = minimize.getEnergy();
280         int status = minimize.getStatus();
281         if (status != 0) {
282           break;
283         }
284         if (abs(newEnergy - energy) <= tolerance) {
285           break;
286         }
287         energy = newEnergy;
288 
289         // Complete a round of lattice optimization.
290         crystalMinimize.minimize(minimizeOptions.getNBFGS(), minimizeOptions.getEps(), minimizeOptions.getIterations());
291         newEnergy = crystalMinimize.getEnergy();
292         status = crystalMinimize.getStatus();
293         if (status != 0) {
294           break;
295         }
296         if (abs(newEnergy - energy) <= tolerance) {
297           break;
298         }
299         energy = newEnergy;
300         if (minIterations > 0) {
301           int coordIters = minimize.getIterations();
302           int latticeIters = crystalMinimize.getIterations();
303           if (coordIters < minIterations && latticeIters < minIterations) {
304             // Prevent looping between similar structures (i.e., A-min to->B, B-min to->A)
305             logger.info(format(" Current iteration (coords: %3d, lattice: %3d) has exceeded maximum allowable iterations (%3d).", coordIters, latticeIters, minIterations));
306             break;
307           }
308         }
309         // Minimization has cycled between lattice and coords. Therefore increase number of cycles and check if done.
310         if (cycles > 0 && ++numCycles >= cycles) {
311           logger.info(format(" Current cycle (%3d) has exceeded maximum allowable cycles (%3d).", numCycles, cycles));
312           break;
313         }
314       }
315     }
316     updateTitle(energy);
317   }
318 
319   /**
320    * {@inheritDoc}
321    */
322   @Override
323   public List<Potential> getPotentials() {
324     List<Potential> potentials;
325     if (xtalEnergy == null) {
326       potentials = Collections.emptyList();
327     } else {
328       potentials = Collections.singletonList((Potential) xtalEnergy);
329     }
330     return potentials;
331   }
332 
333 }