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 }