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.numerics.optimization;
39  
40  import ffx.numerics.OptimizationInterface;
41  
42  import static ffx.numerics.optimization.LBFGS.DEFAULT_ANGLEMAX;
43  import static ffx.numerics.optimization.LBFGS.DEFAULT_CAPPA;
44  import static ffx.numerics.optimization.LBFGS.DEFAULT_INTMAX;
45  import static ffx.numerics.optimization.LBFGS.DEFAULT_SLOPEMAX;
46  import static ffx.numerics.optimization.LBFGS.DEFAULT_STEPMAX;
47  import static ffx.numerics.optimization.LBFGS.DEFAULT_STEPMIN;
48  import static ffx.numerics.optimization.LBFGS.aV1PlusV2;
49  import static ffx.numerics.optimization.LBFGS.v1DotV2;
50  import static java.lang.System.arraycopy;
51  import static org.apache.commons.math3.util.FastMath.abs;
52  import static org.apache.commons.math3.util.FastMath.acos;
53  import static org.apache.commons.math3.util.FastMath.max;
54  import static org.apache.commons.math3.util.FastMath.min;
55  import static org.apache.commons.math3.util.FastMath.sqrt;
56  import static org.apache.commons.math3.util.FastMath.toDegrees;
57  
58  /**
59   * This class implements an algorithm for uni-dimensional line search using parabolic extrapolation
60   * and cubic interpolation with both function and gradient values.
61   *
62   * <p>The algorithm works as follows:
63   * <ol>
64   *   <li>Initialize with the current point, function value, gradient, and search direction</li>
65   *   <li>Check if the search direction makes a reasonable angle with the negative gradient</li>
66   *   <li>Set an initial step size based on previous function decrease</li>
67   *   <li>Perform parabolic extrapolation to find a better step size</li>
68   *   <li>If needed, perform cubic interpolation to refine the step size</li>
69   *   <li>Return the best point found along the search direction</li>
70   * </ol>
71   *
72   * <p>This implementation is a translation of FORTRAN code written by Jay Ponder (search.f).
73   *
74   * @author Michael J. Schnieders
75   * @since 1.0
76   */
77  public class LineSearch {
78  
79    /**
80     * Default factor to scale step size when gradient change is too large.
81     */
82    private static final double STEP_SCALE_FACTOR = 10.0;
83  
84    /**
85     * Default factor for parabolic step size adjustment.
86     */
87    private static final double PARABOLIC_UPPER_LIMIT = 2.0;
88  
89    /**
90     * Default factor for parabolic step size adjustment.
91     */
92    private static final double PARABOLIC_LOWER_LIMIT = 0.5;
93  
94    /**
95     * Default factor for step size reduction during restart.
96     */
97    private static final double RESTART_STEP_SCALE = 10.0;
98  
99    /**
100    * Number of parameters to optimize.
101    */
102   private final int n;
103 
104   /**
105    * Step direction.
106    */
107   private final double[] s;
108 
109   /**
110    * Storage for a copy of the parameters.
111    */
112   private final double[] x0;
113 
114   /**
115    * Implementation of the energy and gradient for the system.
116    */
117   private OptimizationInterface optimizationSystem;
118 
119   /**
120    * Number of function evaluations (pass by reference).
121    */
122   private int[] functionEvaluations;
123 
124   /**
125    * Line search result (pass by reference).
126    */
127   private LineSearchResult[] info;
128 
129   /**
130    * Array of current coordinates.
131    */
132   private double[] x;
133 
134   /**
135    * The gradient array.
136    */
137   private double[] g;
138 
139   /**
140    * Current step size.
141    */
142   private double step;
143 
144   /**
145    * Counter for the number of interpolation attempts.
146    */
147   private int interpolation;
148 
149   /**
150    * Function values at different points:
151    * f0: Initial function value
152    * fA: Function value at previous point
153    * fB: Function value at current point
154    * fC: Function value at interpolated point
155    */
156   private double f0, fA, fB, fC;
157 
158   /**
159    * Dot products of search direction and gradient at different points:
160    * sg0: Initial dot product
161    * sgA: Dot product at previous point
162    * sgB: Dot product at current point
163    * sgC: Dot product at interpolated point
164    */
165   private double sg0, sgA, sgB, sgC;
166 
167   /**
168    * True if a restart is allowed (set to true at the beginning of the algorithm).
169    */
170   private boolean restart;
171 
172   /**
173    * LineSearch constructor.
174    *
175    * @param n Number of variables.
176    * @since 1.0
177    */
178   LineSearch(int n) {
179     s = new double[n];
180     x0 = new double[n];
181     this.n = n;
182   }
183 
184   /**
185    * Minimize a function along a search direction.
186    *
187    * <p>This method performs a unidimensional line search along the specified search direction
188    * using parabolic extrapolation and cubic interpolation with both function and gradient values.
189    * The search attempts to find a step size that provides sufficient decrease in the function value
190    * while maintaining a reasonable angle between the search direction and negative gradient.
191    *
192    * <p>If the search is forced to proceed in an uphill direction (angle > DEFAULT_ANGLEMAX),
193    * the method returns after the initial step without performing the search.
194    *
195    * <p>The method modifies the input arrays x and g to contain the coordinates and gradient
196    * at the best point found along the search direction.
197    *
198    * @param n                   Number of variables in the optimization problem.
199    * @param x                   Current variable values (modified to contain the best point found).
200    * @param f                   Current function value at point x.
201    * @param g                   Current gradient values at point x (modified to contain the gradient at the best point).
202    * @param p                   Search direction vector.
203    * @param angle               Output parameter that will contain the angle between the gradient and search direction.
204    * @param fMove               Change in function value due to the previous optimization step.
205    * @param info                Output parameter that will contain the line search result status.
206    * @param functionEvaluations Input/output parameter for tracking the number of function evaluations.
207    * @param optimizationSystem  Implementation of the {@link ffx.numerics.OptimizationInterface} that provides
208    *                            function values and gradients.
209    * @return The final function value at the best point found.
210    * @since 1.0
211    */
212   public double search(int n, double[] x, double f, double[] g, double[] p, double[] angle,
213                        double fMove, LineSearchResult[] info, int[] functionEvaluations,
214                        OptimizationInterface optimizationSystem) {
215 
216     // Validate input parameters
217     if (n <= 0) {
218       throw new IllegalArgumentException("Number of variables must be positive");
219     }
220     if (x == null || x.length < n) {
221       throw new IllegalArgumentException("Coordinate array is null or too small");
222     }
223     if (g == null || g.length < n) {
224       throw new IllegalArgumentException("Gradient array is null or too small");
225     }
226     if (p == null || p.length < n) {
227       throw new IllegalArgumentException("Search direction array is null or too small");
228     }
229     if (angle == null || angle.length < 1) {
230       throw new IllegalArgumentException("Angle array is null or too small");
231     }
232     if (info == null || info.length < 1) {
233       throw new IllegalArgumentException("Info array is null or too small");
234     }
235     if (functionEvaluations == null || functionEvaluations.length < 1) {
236       throw new IllegalArgumentException("Function evaluations array is null or too small");
237     }
238     if (optimizationSystem == null) {
239       throw new IllegalArgumentException("Optimization system cannot be null");
240     }
241 
242     // Initialize the line search.
243     this.x = x;
244     this.g = g;
245     this.optimizationSystem = optimizationSystem;
246     this.functionEvaluations = functionEvaluations;
247     this.info = info;
248     fA = 0.0;
249     fB = 0.0;
250     fC = 0.0;
251     sgA = 0.0;
252     sgB = 0.0;
253     sgC = 0.0;
254 
255     // Zero out the status indicator.
256     info[0] = null;
257 
258     // Copy the search direction p into a new vector s.
259     arraycopy(p, 0, s, 0, n);
260 
261     // Compute the length of the gradient and search direction.
262     double gNorm = sqrt(v1DotV2(n, g, 0, 1, g, 0, 1));
263     double sNorm = sqrt(v1DotV2(n, s, 0, 1, s, 0, 1));
264 
265     // Handle the case where the search direction or gradient has zero length
266     if (sNorm < Double.MIN_NORMAL) {
267       info[0] = LineSearchResult.IntplnErr;
268       return f;
269     }
270 
271     // Store the initial function, then normalize the search vector
272     f0 = f;
273     arraycopy(x, 0, x0, 0, n);
274     for (int i = 0; i < n; i++) {
275       s[i] /= sNorm;
276     }
277 
278     // Find the projected gradient
279     sg0 = v1DotV2(n, s, 0, 1, g, 0, 1);
280 
281     // Check the angle between the search direction and the negative gradient vector
282     double cosang = (gNorm < Double.MIN_NORMAL) ? 0.0 : -sg0 / gNorm;
283     cosang = min(1.0, max(-1.0, cosang)); // Ensure cosang is in [-1, 1]
284     angle[0] = toDegrees(acos(cosang));
285     if (angle[0] > DEFAULT_ANGLEMAX) {
286       info[0] = LineSearchResult.WideAngle;
287       return f;
288     }
289 
290     // Set the initial step size based on previous function decrease or search vector length
291     if (sg0 != 0.0) {
292       step = 2.0 * abs(fMove / sg0);
293     } else {
294       step = sNorm;
295     }
296     step = min(step, sNorm);
297     step = min(max(step, DEFAULT_STEPMIN), DEFAULT_STEPMAX);
298 
299     return begin();
300   }
301 
302   /**
303    * Begin the parabolic extrapolation procedure.
304    *
305    * <p>This method initializes the line search by setting up the initial conditions
306    * for the parabolic extrapolation. It marks that a restart is allowed, resets the
307    * interpolation counter, and sets the initial function value and gradient projection.
308    *
309    * @return The final function value after completing the line search.
310    */
311   private double begin() {
312     restart = true;
313     interpolation = 0;
314     fB = f0;
315     sgB = sg0;
316     return step();
317   }
318 
319   /**
320    * Take a step along the search direction and evaluate the function and gradient.
321    *
322    * <p>This method:
323    * <ol>
324    *   <li>Stores the previous function value and gradient projection</li>
325    *   <li>Takes a step along the search direction</li>
326    *   <li>Evaluates the function and gradient at the new point</li>
327    *   <li>Checks if the step size needs to be scaled down (if gradient change is too large)</li>
328    *   <li>Checks if we've found a suitable point (small gradient and decreased function)</li>
329    *   <li>Decides whether to interpolate or continue extrapolation</li>
330    * </ol>
331    *
332    * @return The final function value after completing the line search.
333    */
334   private double step() {
335     // Store previous values and take a step
336     fA = fB;
337     sgA = sgB;
338     aV1PlusV2(n, step, s, 0, 1, x, 0, 1);
339 
340     // Get new function and projected gradient following a step
341     functionEvaluations[0]++;
342     fB = optimizationSystem.energyAndGradient(x, g);
343     sgB = v1DotV2(n, s, 0, 1, g, 0, 1);
344 
345     // Scale step size if initial gradient change is too large
346     if (abs(sgB / sgA) >= DEFAULT_SLOPEMAX && restart) {
347       arraycopy(x0, 0, x, 0, n);
348       step /= STEP_SCALE_FACTOR;
349       info[0] = LineSearchResult.ScaleStep;
350       begin();
351     }
352     restart = false;
353 
354     // Return if we've found a suitable point (small gradient and decreased function)
355     if (abs(sgB / sg0) <= DEFAULT_CAPPA && fB < fA) {
356       if (info[0] == null) {
357         info[0] = LineSearchResult.Success;
358       }
359       f0 = fB;
360       sg0 = sgB;
361       return f0;
362     }
363 
364     // Interpolate if gradient changes sign or function increases
365     if (sgB * sgA < 0.0 || fB > fA) {
366       return cubic();
367     }
368 
369     // Continue extrapolation with adjusted step size
370     step = 2.0 * step;
371 
372     // If the finite difference curvature is positive, use parabolic estimate
373     if (sgB > sgA) {
374       double parab = (fA - fB) / (sgB - sgA);
375       parab = min(PARABOLIC_UPPER_LIMIT * step, max(PARABOLIC_LOWER_LIMIT * step, parab));
376       step = parab;
377     }
378 
379     // Ensure step size doesn't exceed maximum
380     step = min(step, DEFAULT_STEPMAX);
381 
382     return step();
383   }
384 
385   /**
386    * Perform cubic interpolation to refine the step size.
387    *
388    * <p>This method implements cubic interpolation to find a better step size when:
389    * <ul>
390    *   <li>The gradient changes sign between two points (indicating a minimum between them)</li>
391    *   <li>The function value increases (indicating we've stepped too far)</li>
392    * </ul>
393    *
394    * <p>The cubic interpolation uses both function values and gradients at the bracketing
395    * points to estimate the location of the minimum. If the interpolation fails or doesn't
396    * produce a better point, the method falls back to using the best point found so far.
397    *
398    * @return The final function value after completing the line search.
399    */
400   private double cubic() {
401     interpolation++;
402 
403     // Calculate cubic interpolation coefficients
404     double sss = 3.0 * (fB - fA) / step - sgA - sgB;
405     double ttt = sss * sss - sgA * sgB;
406 
407     // Check if cubic interpolation is possible (discriminant must be non-negative)
408     if (ttt < 0.0) {
409       info[0] = LineSearchResult.IntplnErr;
410       f0 = fB;
411       sg0 = sgB;
412       return f0;
413     }
414 
415     // Calculate the cubic step size
416     ttt = sqrt(ttt);
417     double cube = step * (sgB + ttt + sss) / (sgB - sgA + 2.0 * ttt);
418 
419     // Check if the cubic step is valid (must be between 0 and step)
420     if (cube < 0 || cube > step) {
421       info[0] = LineSearchResult.IntplnErr;
422       f0 = fB;
423       sg0 = sgB;
424       return f0;
425     }
426 
427     // Move to the interpolated point
428     aV1PlusV2(n, -cube, s, 0, 1, x, 0, 1);
429 
430     // Evaluate function and gradient at the interpolated point
431     functionEvaluations[0]++;
432     fC = optimizationSystem.energyAndGradient(x, g);
433     sgC = v1DotV2(n, s, 0, 1, g, 0, 1);
434 
435     // Check if we've found a suitable point (small gradient)
436     if (abs(sgC / sg0) <= DEFAULT_CAPPA) {
437       if (info[0] == null) {
438         info[0] = LineSearchResult.Success;
439       }
440       f0 = fC;
441       sg0 = sgC;
442       return f0;
443     }
444 
445     // If the interpolated point is better than at least one of the brackets,
446     // continue with further interpolation
447     if (fC <= fA || fC <= fB) {
448       double cubstp = min(abs(cube), abs(step - cube));
449 
450       // Continue interpolation if step size is reasonable and we haven't exceeded max iterations
451       if (cubstp >= DEFAULT_STEPMIN && interpolation < DEFAULT_INTMAX) {
452         if (sgA * sgB < 0.0) {
453           // If the current brackets have slopes of opposite sign,
454           // substitute the interpolated point for the bracket point
455           // with slope of same sign as the interpolated point
456           if (sgA * sgC < 0.0) {
457             fB = fC;
458             sgB = sgC;
459             step = step - cube;
460           } else {
461             fA = fC;
462             sgA = sgC;
463             step = cube;
464             aV1PlusV2(n, cube, s, 0, 1, x, 0, 1);
465           }
466         } else {
467           // If current brackets have slopes of same sign, then replace
468           // the far bracket if the interpolated point has a slope of
469           // the opposite sign or a lower function value than the near bracket
470           if (sgA * sgC < 0.0 || fA <= fC) {
471             fB = fC;
472             sgB = sgC;
473             step -= cube;
474           } else {
475             fA = fC;
476             sgA = sgC;
477             step = cube;
478             aV1PlusV2(n, cube, s, 0, 1, x, 0, 1);
479           }
480         }
481         return cubic();
482       }
483     }
484 
485     // Interpolation has failed; reset to the best current point
486     double f1 = min(fA, min(fB, fC));
487     double sg1;
488 
489     // Move to the best point
490     if (f1 == fA) {
491       sg1 = sgA;
492       aV1PlusV2(n, cube - step, s, 0, 1, x, 0, 1);
493     } else if (f1 == fB) {
494       sg1 = sgB;
495       aV1PlusV2(n, cube, s, 0, 1, x, 0, 1);
496     } else {
497       sg1 = sgC;
498     }
499 
500     // If the best point is worse than the initial point, return to the initial point
501     if (f1 > f0) {
502       functionEvaluations[0]++;
503       f0 = optimizationSystem.energyAndGradient(x, g);
504       sg0 = v1DotV2(n, s, 0, 1, g, 0, 1);
505       info[0] = LineSearchResult.IntplnErr;
506       return f0;
507     }
508 
509     // Update with the best point found
510     f0 = f1;
511     sg0 = sg1;
512 
513     // If the gradient at the best point is positive, reverse the search direction
514     if (sg1 > 0.0) {
515       for (int i = 0; i < n; i++) {
516         s[i] *= -1.0;
517       }
518       sg0 = -sg1;
519     }
520 
521     // Reduce the step size
522     step = max(cube, step - cube) / RESTART_STEP_SCALE;
523     step = max(step, DEFAULT_STEPMIN);
524 
525     // If already restarted once, then return with the best point
526     if (info[0] == LineSearchResult.ReSearch) {
527       functionEvaluations[0]++;
528       f0 = optimizationSystem.energyAndGradient(x, g);
529       sg0 = v1DotV2(n, s, 0, 1, g, 0, 1);
530       info[0] = LineSearchResult.BadIntpln;
531       return f0;
532     } else {
533       // Begin again
534       info[0] = LineSearchResult.ReSearch;
535       return begin();
536     }
537   }
538 
539   /**
540    * Enum representing the possible outcomes of a line search operation.
541    * <p>
542    * These status codes provide information about how the line search terminated,
543    * which can be useful for diagnosing optimization issues or understanding the
544    * behavior of the algorithm.
545    */
546   public enum LineSearchResult {
547     /**
548      * Successful line search.
549      * <p>
550      * The algorithm found a point with sufficient decrease in function value
551      * and a small enough gradient projection along the search direction.
552      */
553     Success,
554 
555     /**
556      * Angle between gradient and search direction is too wide.
557      * <p>
558      * The search direction makes an angle with the negative gradient that exceeds
559      * DEFAULT_ANGLEMAX (180 degrees). This usually indicates a problem with the
560      * search direction calculation in the optimization algorithm.
561      */
562     WideAngle,
563 
564     /**
565      * Step size was scaled down.
566      * <p>
567      * The initial step resulted in a gradient change that was too large
568      * (exceeding DEFAULT_SLOPEMAX), so the step size was reduced to ensure
569      * more stable convergence.
570      */
571     ScaleStep,
572 
573     /**
574      * Interpolation error occurred.
575      * <p>
576      * An error occurred during cubic interpolation, such as a negative discriminant
577      * or an invalid step size. The algorithm falls back to using the best point found.
578      */
579     IntplnErr,
580 
581     /**
582      * Search was restarted.
583      * <p>
584      * The line search was restarted with a smaller step size after interpolation
585      * failed to find a better point. This is an intermediate status that may lead
586      * to eventual success or failure.
587      */
588     ReSearch,
589 
590     /**
591      * Bad interpolation result.
592      * <p>
593      * Interpolation failed repeatedly, even after restarting the search. This
594      * typically indicates a difficult optimization landscape or numerical issues.
595      */
596     BadIntpln
597   }
598 }