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.optimize;
39  
40  import ffx.algorithms.AlgorithmListener;
41  import ffx.algorithms.Terminatable;
42  import ffx.algorithms.dynamics.MDEngine;
43  import ffx.algorithms.dynamics.MolecularDynamics;
44  import ffx.numerics.Potential;
45  import ffx.numerics.optimization.LBFGS;
46  import ffx.numerics.optimization.LineSearch;
47  import ffx.numerics.optimization.OptimizationListener;
48  import ffx.potential.ForceFieldEnergy;
49  import ffx.potential.MolecularAssembly;
50  import ffx.potential.Platform;
51  import ffx.potential.openmm.OpenMMEnergy;
52  import org.apache.commons.configuration2.CompositeConfiguration;
53  
54  import java.util.ArrayList;
55  import java.util.EnumSet;
56  import java.util.List;
57  import java.util.logging.Level;
58  import java.util.logging.Logger;
59  
60  import static java.lang.String.format;
61  import static java.util.Arrays.fill;
62  
63  /**
64   * Minimize the potential energy of a system to an RMS gradient per atom convergence criteria.
65   *
66   * @author Michael J. Schnieders
67   * @since 1.0
68   */
69  public class Minimize implements OptimizationListener, Terminatable {
70  
71    private static final Logger logger = Logger.getLogger(Minimize.class.getName());
72  
73    /**
74     * The MolecularAssembly being operated on.
75     */
76    protected final MolecularAssembly molecularAssembly;
77    /**
78     * The potential energy to optimize.
79     */
80    protected final Potential potential;
81    /**
82     * The AlgorithmListener to update the UI.
83     */
84    protected final AlgorithmListener algorithmListener;
85    /**
86     * Number of variables.
87     */
88    protected final int n;
89    /**
90     * Current value of each variable.
91     */
92    protected final double[] x;
93    /**
94     * The gradient.
95     */
96    protected final double[] grad;
97    /**
98     * Scaling applied to each variable.
99     */
100   protected final double[] scaling;
101   /**
102    * A flag to indicate the algorithm is done.
103    */
104   protected boolean done = false;
105   /**
106    * A flag to indicate the algorithm should be terminated.
107    */
108   protected boolean terminate = false;
109   /**
110    * Minimization time in nanoseconds.
111    */
112   protected long time;
113   /**
114    * The energy of each step in the minimization.
115    */
116   protected List<Double> energyList = new ArrayList<>();
117   /**
118    * The final potential energy.
119    */
120   protected double energy;
121   /**
122    * The return status of the optimization.
123    */
124   protected int status;
125   /**
126    * The number of optimization steps taken.
127    */
128   protected int nSteps;
129   /**
130    * The final RMS gradient.
131    */
132   double rmsGradient;
133 
134   /**
135    * The default number of correction vectors used by the limited-memory L-BFGS optimization
136    * routine.
137    * <p>
138    * Values of less than 3 are not recommended and large values will result in excessive computing
139    * time. The range from <code>3 &lt;= mSave &lt;= 7</code> is recommended.
140    */
141   public static final int DEFAULT_LBFGS_VECTORS = 7;
142 
143   /**
144    * Constructor for Minimize.
145    *
146    * @param molecularAssembly a {@link ffx.potential.MolecularAssembly} object.
147    * @param potential         a {@link ffx.numerics.Potential} object.
148    * @param algorithmListener a {@link ffx.algorithms.AlgorithmListener} object.
149    */
150   public Minimize(MolecularAssembly molecularAssembly, Potential potential,
151                   AlgorithmListener algorithmListener) {
152     this.molecularAssembly = molecularAssembly;
153     this.algorithmListener = algorithmListener;
154     this.potential = potential;
155     n = potential.getNumberOfVariables();
156     x = new double[n];
157     grad = new double[n];
158     scaling = new double[n];
159     fill(scaling, 12.0);
160   }
161 
162   /**
163    * Constructor for Minimize.
164    *
165    * @param molecularAssembly a {@link ffx.potential.MolecularAssembly} object.
166    * @param algorithmListener a {@link ffx.algorithms.AlgorithmListener} object.
167    */
168   public Minimize(MolecularAssembly molecularAssembly, AlgorithmListener algorithmListener) {
169     this.molecularAssembly = molecularAssembly;
170     this.algorithmListener = algorithmListener;
171     if (molecularAssembly.getPotentialEnergy() == null) {
172       molecularAssembly.setPotential(ForceFieldEnergy.energyFactory(molecularAssembly));
173     }
174     potential = molecularAssembly.getPotentialEnergy();
175     n = potential.getNumberOfVariables();
176     x = new double[n];
177     grad = new double[n];
178     scaling = new double[n];
179     fill(scaling, 12.0);
180   }
181 
182   public static MinimizationEngine defaultEngine(MolecularAssembly molecularAssembly,
183                                                  Potential potentialEnergy) {
184     CompositeConfiguration properties = molecularAssembly.getProperties();
185     String minimizeEngine = properties.getString("minimize-engine", null);
186     if (minimizeEngine != null) {
187       if (minimizeEngine.equalsIgnoreCase("OMM")) {
188         return MinimizationEngine.OPENMM;
189       } else {
190         return MinimizationEngine.FFX;
191       }
192     } else {
193       if (potentialEnergy instanceof OpenMMEnergy) {
194         return MinimizationEngine.OPENMM;
195       } else {
196         return MinimizationEngine.FFX;
197       }
198     }
199   }
200 
201   /**
202    * dynamicsFactory.
203    *
204    * @param assembly        a {@link ffx.potential.MolecularAssembly} object.
205    * @param potentialEnergy a {@link ffx.numerics.Potential} object.
206    * @param listener        a {@link ffx.algorithms.AlgorithmListener} object.
207    * @param engine          a {@link MDEngine} object.
208    * @return a {@link MolecularDynamics} object.
209    */
210   public static Minimize minimizeFactory(MolecularAssembly assembly, Potential potentialEnergy,
211                                          AlgorithmListener listener, MinimizationEngine engine) {
212     return switch (engine) {
213       case OPENMM -> new MinimizeOpenMM(assembly, (OpenMMEnergy) potentialEnergy, listener);
214       default -> new Minimize(assembly, potentialEnergy, listener);
215     };
216   }
217 
218   /**
219    * Getter for the field <code>energy</code>.
220    *
221    * @return a double.
222    */
223   public double getEnergy() {
224     return energy;
225   }
226 
227   /**
228    * getRMSGradient.
229    *
230    * @return a double.
231    */
232   public double getRMSGradient() {
233     return rmsGradient;
234   }
235 
236   /**
237    * Getter for the field <code>status</code>.
238    *
239    * @return The status of the optimization.
240    */
241   public int getStatus() {
242     return status;
243   }
244 
245   /**
246    * Getter for the number of iterations completed this minimization.
247    *
248    * @return The number of iterations
249    */
250   public int getIterations() {
251     return nSteps;
252   }
253 
254   /**
255    * Get the energy for each step in the minimization.
256    *
257    * @return The energy for each step in the minimization.
258    */
259   public List<Double> getEnergyList() {
260     return energyList;
261   }
262 
263   /**
264    * minimize
265    *
266    * @return a {@link ffx.numerics.Potential} object.
267    */
268   public Potential minimize() {
269     return minimize(DEFAULT_LBFGS_VECTORS, 1.0, Integer.MAX_VALUE);
270   }
271 
272   /**
273    * minimize
274    *
275    * @param eps The convergence criteria.
276    * @return a {@link ffx.numerics.Potential} object.
277    */
278   public Potential minimize(double eps) {
279     return minimize(DEFAULT_LBFGS_VECTORS, eps, Integer.MAX_VALUE);
280   }
281 
282   /**
283    * minimize
284    *
285    * @param eps           The convergence criteria.
286    * @param maxIterations The maximum number of iterations.
287    * @return a {@link ffx.numerics.Potential} object.
288    */
289   public Potential minimize(double eps, int maxIterations) {
290     return minimize(DEFAULT_LBFGS_VECTORS, eps, maxIterations);
291   }
292 
293   /**
294    * minimize
295    *
296    * @param m             The number of previous steps used to estimate the Hessian.
297    * @param eps           The convergence criteria.
298    * @param maxIterations The maximum number of iterations.
299    * @return a {@link ffx.numerics.Potential} object.
300    */
301   public Potential minimize(int m, double eps, int maxIterations) {
302     time = System.nanoTime();
303     potential.getCoordinates(x);
304     potential.setScaling(scaling);
305 
306     // Scale coordinates.
307     for (int i = 0; i < n; i++) {
308       x[i] *= scaling[i];
309     }
310 
311     done = false;
312     energy = potential.energyAndGradient(x, grad);
313 
314     if (logger.isLoggable(Level.FINE)) {
315       logger.fine(format(" Minimize initial energy: %16.8f", energy));
316     }
317 
318     status = LBFGS.minimize(n, m, x, energy, grad, eps, maxIterations, potential, this);
319     done = true;
320 
321     switch (status) {
322       case 0 -> logger.info(format("\n Optimization achieved convergence criteria: %8.5f", rmsGradient));
323       case 1 -> logger.info(format("\n Optimization terminated at step %d.", nSteps));
324       default -> logger.warning("\n Optimization failed.");
325     }
326 
327     potential.setScaling(null);
328     return potential;
329   }
330 
331   /**
332    * {@inheritDoc}
333    */
334   @Override
335   public String toString() {
336     StringBuilder sb = new StringBuilder();
337     sb.append(" Minimize Results:\n");
338     sb.append(format("  Number of Variables:    %16d\n", n));
339     sb.append(format("  Number of Iterations:   %16d\n", nSteps));
340     sb.append(format("  Final Energy:           %16.6f (kcal/mol)\n", energy));
341     sb.append(format("  RMS Gradient:           %16.6f (kcal/mol/A)\n", rmsGradient));
342     return sb.toString();
343   }
344 
345   /**
346    * {@inheritDoc}
347    *
348    * <p>Implement the OptimizationListener interface.
349    *
350    * @since 1.0
351    */
352   @Override
353   public boolean optimizationUpdate(int iteration, int nBFGS, int functionEvaluations,
354                                     double rmsGradient, double rmsCoordinateChange, double energy, double energyChange,
355                                     double angle, LineSearch.LineSearchResult lineSearchResult) {
356     long currentTime = System.nanoTime();
357     Double seconds = (currentTime - time) * 1.0e-9;
358     time = currentTime;
359     this.rmsGradient = rmsGradient;
360     this.nSteps = iteration;
361     this.energy = energy;
362 
363     if (iteration == 0) {
364       energyList.clear();
365       if (nBFGS > 0) {
366         logger.info("\n Limited Memory BFGS Quasi-Newton Optimization: \n");
367       } else {
368         logger.info("\n Steepest Decent Optimization: \n");
369       }
370       logger.info(" Cycle       Energy      G RMS    Delta E   Delta X    Angle  Evals     Time\n");
371     }
372     energyList.add(energy);
373     if (lineSearchResult == null) {
374       logger.info(format("%6d%13.4f%11.4f", iteration, energy, rmsGradient));
375     } else {
376       if (lineSearchResult == LineSearch.LineSearchResult.Success) {
377         logger.info(
378             format("%6d%13.4f%11.4f%11.4f%10.4f%9.2f%7d %8.3f", iteration, energy, rmsGradient,
379                 energyChange, rmsCoordinateChange, angle, functionEvaluations, seconds));
380       } else {
381         logger.info(format("%6d%13.4f%11.4f%11.4f%10.4f%9.2f%7d %8s", iteration, energy, rmsGradient,
382             energyChange, rmsCoordinateChange, angle, functionEvaluations, lineSearchResult));
383       }
384     }
385     // Update the listener and check for a termination request.
386     if (algorithmListener != null) {
387       algorithmListener.algorithmUpdate(molecularAssembly);
388     }
389 
390     if (terminate) {
391       logger.info(" The optimization received a termination request.");
392       // Tell the L-BFGS optimizer to terminate.
393       return false;
394     }
395     return true;
396   }
397 
398   /**
399    * {@inheritDoc}
400    */
401   @Override
402   public void terminate() {
403     terminate = true;
404     while (!done) {
405       synchronized (this) {
406         try {
407           wait(1);
408         } catch (Exception e) {
409           logger.log(Level.WARNING, "Exception terminating minimization.\n", e);
410         }
411       }
412     }
413   }
414 
415   /**
416    * Enumerates available molecular minimization engines; presently limited to the FFX reference
417    * engine and the OpenMM engine.
418    *
419    * <p>Distinct from the force field energy Platform, as the FFX engine can use OpenMM energies,
420    * but not vice-versa.
421    */
422   public enum MinimizationEngine {
423     FFX(true, true), OPENMM(false, true);
424 
425     // Set of supported Platforms. The EnumSet paradigm is very efficient, as it
426     // is internally stored as a bit field.
427     private final EnumSet<Platform> platforms = EnumSet.noneOf(Platform.class);
428 
429     /**
430      * Constructs a DynamicsEngine using the two presently known types of Platform.
431      *
432      * @param ffx    Add support for the FFX reference energy platform.
433      * @param openMM Add support for the OpenMM energy platforms.
434      */
435     MinimizationEngine(boolean ffx, boolean openMM) {
436       if (ffx) {
437         platforms.add(Platform.FFX);
438       }
439       if (openMM) {
440         platforms.add(Platform.OMM);
441         platforms.add(Platform.OMM_REF);
442         platforms.add(Platform.OMM_CUDA);
443         platforms.add(Platform.OMM_OPENCL);
444         platforms.add(Platform.OMM_CPU);
445       }
446     }
447 
448     /**
449      * Gets the set of Platforms supported by this DynamicsEngine
450      *
451      * @return An EnumSet
452      */
453     public EnumSet<Platform> getSupportedPlatforms() {
454       return EnumSet.copyOf(platforms);
455     }
456 
457     /**
458      * Checks if this energy Platform is supported by this DynamicsEngine
459      *
460      * @param platform The requested platform.
461      * @return If supported
462      */
463     public boolean supportsPlatform(Platform platform) {
464       return platforms.contains(platform);
465     }
466   }
467 }