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