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 }