// Copyright (c) 2025 OPEN CASCADE SAS // // This file is part of Open CASCADE Technology software library. // // This library is free software; you can redistribute it and/or modify it under // the terms of the GNU Lesser General Public License version 2.1 as published // by the Free Software Foundation, with special exception defined in the file // OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT // distribution for complete text of the license and disclaimer of any warranty. // // Alternatively, this file may be used under the terms of Open CASCADE // commercial license or contractual agreement. #ifndef _MathOpt_BFGS_HeaderFile #define _MathOpt_BFGS_HeaderFile #include "MathUtils_Types.hxx" #include "MathUtils_Config.hxx" #include "MathUtils_Core.hxx" #include "MathUtils_LineSearch.hxx" #include "MathUtils_Deriv.hxx" #include "NCollection_DynamicArray.hxx" #include namespace MathOpt { using namespace MathUtils; //! BFGS (Broyden-Fletcher-Goldfarb-Shanno) quasi-Newton method. //! One of the most effective algorithms for smooth unconstrained optimization. //! //! Algorithm: //! 1. Start with initial Hessian approximation H (usually identity) //! 2. Compute search direction p = -H * gradient //! 3. Perform line search to find step size alpha satisfying Wolfe conditions //! 4. Update x = x + alpha * p //! 5. Update Hessian approximation using BFGS formula //! 6. Repeat until convergence //! //! The BFGS update maintains positive definiteness of H if started with //! a positive definite matrix and using proper line search. //! //! Advantages: //! - Superlinear convergence near minimum //! - Self-correcting Hessian approximation //! - No need to compute actual Hessian //! //! @tparam Function type with: //! - Value(const math_Vector&, double&) for function value //! - Gradient(const math_Vector&, math_Vector&) for gradient //! @param theFunc function object with value and gradient //! @param theStartingPoint initial guess //! @param theConfig solver configuration //! @return result containing minimum location and value template VectorResult BFGS(Function& theFunc, const math_Vector& theStartingPoint, const Config& theConfig = Config()) { VectorResult aResult; const int aLower = theStartingPoint.Lower(); const int aUpper = theStartingPoint.Upper(); const int aN = aUpper - aLower + 1; // Current point and function value math_Vector aX(aLower, aUpper); aX = theStartingPoint; double aFx = 0.0; if (!theFunc.Value(aX, aFx)) { aResult.Status = Status::NumericalError; return aResult; } // Gradient at current point math_Vector aGrad(aLower, aUpper); if (!theFunc.Gradient(aX, aGrad)) { aResult.Status = Status::NumericalError; return aResult; } // Check if already at minimum (gradient near zero) double aGradNorm = 0.0; for (int i = aLower; i <= aUpper; ++i) { aGradNorm += MathUtils::Sqr(aGrad(i)); } aGradNorm = std::sqrt(aGradNorm); if (aGradNorm < theConfig.FTolerance) { aResult.Status = Status::OK; aResult.Solution = aX; aResult.Value = aFx; aResult.Gradient = aGrad; return aResult; } // Initialize inverse Hessian approximation to identity math_Matrix aH(1, aN, 1, aN, 0.0); for (int i = 1; i <= aN; ++i) { aH(i, i) = 1.0; } // Working vectors math_Vector aDir(aLower, aUpper); // Search direction math_Vector aXNew(aLower, aUpper); // New point math_Vector aGradNew(aLower, aUpper); // New gradient math_Vector aS(1, aN); // Step: x_new - x math_Vector aY(1, aN); // Gradient difference: grad_new - grad for (int anIter = 0; anIter < theConfig.MaxIterations; ++anIter) { aResult.NbIterations = anIter + 1; // Compute search direction: p = -H * grad for (int i = 1; i <= aN; ++i) { double aSum = 0.0; for (int j = 1; j <= aN; ++j) { aSum += aH(i, j) * aGrad(aLower + j - 1); } aDir(aLower + i - 1) = -aSum; } // Line search with Wolfe conditions MathUtils::LineSearchResult aLineResult = MathUtils::ArmijoBacktrack(theFunc, aX, aDir, aGrad, aFx, 1.0, 1.0e-4, 0.5, 50); if (!aLineResult.IsValid || aLineResult.Alpha < MathUtils::THE_EPSILON) { // Line search failed, try steepest descent direction for (int i = aLower; i <= aUpper; ++i) { aDir(i) = -aGrad(i); } aLineResult = MathUtils::ArmijoBacktrack(theFunc, aX, aDir, aGrad, aFx, 1.0, 1.0e-4, 0.5, 50); if (!aLineResult.IsValid) { // Both BFGS and steepest descent failed aResult.Status = Status::NotConverged; aResult.Solution = aX; aResult.Value = aFx; aResult.Gradient = aGrad; return aResult; } // Reset Hessian to identity after steepest descent step for (int i = 1; i <= aN; ++i) { for (int j = 1; j <= aN; ++j) { aH(i, j) = (i == j) ? 1.0 : 0.0; } } } // Compute new point for (int i = aLower; i <= aUpper; ++i) { aXNew(i) = aX(i) + aLineResult.Alpha * aDir(i); } // Compute s = x_new - x for (int i = 1; i <= aN; ++i) { aS(i) = aXNew(aLower + i - 1) - aX(aLower + i - 1); } // Evaluate gradient at new point if (!theFunc.Gradient(aXNew, aGradNew)) { aResult.Status = Status::NumericalError; aResult.Solution = aX; aResult.Value = aFx; return aResult; } // Check gradient convergence aGradNorm = 0.0; for (int i = aLower; i <= aUpper; ++i) { aGradNorm += MathUtils::Sqr(aGradNew(i)); } aGradNorm = std::sqrt(aGradNorm); if (aGradNorm < theConfig.FTolerance) { aResult.Status = Status::OK; aResult.Solution = aXNew; aResult.Value = aLineResult.FNew; aResult.Gradient = aGradNew; return aResult; } // Check X convergence double aMaxDiff = 0.0; for (int i = aLower; i <= aUpper; ++i) { aMaxDiff = std::max(aMaxDiff, std::abs(aXNew(i) - aX(i))); } if (aMaxDiff < theConfig.XTolerance) { aResult.Status = Status::OK; aResult.Solution = aXNew; aResult.Value = aLineResult.FNew; aResult.Gradient = aGradNew; return aResult; } // Compute y = grad_new - grad for (int i = 1; i <= aN; ++i) { aY(i) = aGradNew(aLower + i - 1) - aGrad(aLower + i - 1); } // Compute s^T * y (curvature condition) double aSY = 0.0; for (int i = 1; i <= aN; ++i) { aSY += aS(i) * aY(i); } // Skip update if curvature condition is not satisfied if (aSY > MathUtils::THE_ZERO_TOL) { // BFGS update: H_new = (I - rho*sy^T) H (I - rho*ys^T) + rho*ss^T // where rho = 1 / (s^T y) const double aRho = 1.0 / aSY; // Compute H * y math_Vector aHy(1, aN, 0.0); for (int i = 1; i <= aN; ++i) { for (int j = 1; j <= aN; ++j) { aHy(i) += aH(i, j) * aY(j); } } // Compute y^T * H * y double aYHy = 0.0; for (int i = 1; i <= aN; ++i) { aYHy += aY(i) * aHy(i); } // Update H using the formula: // H_new = H - (Hy*s^T + s*y^T*H)/(s^T*y) + (1 + y^T*H*y/(s^T*y)) * s*s^T/(s^T*y) const double aFactor = 1.0 + aRho * aYHy; for (int i = 1; i <= aN; ++i) { for (int j = 1; j <= aN; ++j) { aH(i, j) = aH(i, j) - aRho * (aHy(i) * aS(j) + aS(i) * aHy(j)) + aFactor * aRho * aS(i) * aS(j); } } } // Update for next iteration aX = aXNew; aGrad = aGradNew; aFx = aLineResult.FNew; } // Maximum iterations reached aResult.Status = Status::MaxIterations; aResult.Solution = aX; aResult.Value = aFx; aResult.Gradient = aGrad; return aResult; } //! BFGS with numerical gradient. //! Uses central differences to approximate gradient when analytical //! gradient is not available. //! //! @tparam Function type with Value(const math_Vector&, double&) method only //! @param theFunc function object //! @param theStartingPoint initial guess //! @param theGradStep step size for numerical gradient (default 1e-8) //! @param theConfig solver configuration //! @return result containing minimum location and value template VectorResult BFGSNumerical(Function& theFunc, const math_Vector& theStartingPoint, double theGradStep = 1.0e-8, const Config& theConfig = Config()) { // Wrapper that adds numerical gradient class FuncWithGradient { public: FuncWithGradient(Function& theF, double theStep) : myFunc(theF), myStep(theStep) { } bool Value(const math_Vector& theX, double& theF) { return myFunc.Value(theX, theF); } bool Gradient(const math_Vector& theX, math_Vector& theGrad) { math_Vector aXMod = theX; // Make mutable copy return MathUtils::NumericalGradientAdaptive(myFunc, aXMod, theGrad, myStep); } private: Function& myFunc; double myStep; }; FuncWithGradient aWrapper(theFunc, theGradStep); return BFGS(aWrapper, theStartingPoint, theConfig); } //! L-BFGS (Limited-memory BFGS) for large-scale optimization. //! Uses only the m most recent {s, y} pairs instead of full Hessian. //! Memory: O(m*n) instead of O(n^2). //! //! @tparam Function type with Value and Gradient methods //! @param theFunc function object //! @param theStartingPoint initial guess //! @param theMemorySize number of gradient pairs to store (default 10) //! @param theConfig solver configuration //! @return result containing minimum location and value template VectorResult LBFGS(Function& theFunc, const math_Vector& theStartingPoint, int theMemorySize = 10, const Config& theConfig = Config()) { VectorResult aResult; const int aLower = theStartingPoint.Lower(); const int aUpper = theStartingPoint.Upper(); const int aN = aUpper - aLower + 1; const int aM = theMemorySize; math_Vector aX(aLower, aUpper); aX = theStartingPoint; double aFx = 0.0; if (!theFunc.Value(aX, aFx)) { aResult.Status = Status::NumericalError; return aResult; } math_Vector aGrad(aLower, aUpper); if (!theFunc.Gradient(aX, aGrad)) { aResult.Status = Status::NumericalError; return aResult; } // Storage for {s, y} pairs (circular buffer) NCollection_DynamicArray aSVec; NCollection_DynamicArray aYVec; math_Vector aRhoVec(0, aM - 1, 0.0); for (int i = 0; i < aM; ++i) { aSVec.Append(math_Vector(1, aN)); aYVec.Append(math_Vector(1, aN)); } int aHead = 0; // Index of oldest entry int aCount = 0; // Number of stored pairs math_Vector aDir(aLower, aUpper); math_Vector aXNew(aLower, aUpper); math_Vector aGradNew(aLower, aUpper); math_Vector aQ(1, aN); math_Vector aAlphaVec(0, aM - 1); for (int anIter = 0; anIter < theConfig.MaxIterations; ++anIter) { aResult.NbIterations = anIter + 1; // Check gradient convergence double aGradNorm = 0.0; for (int i = aLower; i <= aUpper; ++i) { aGradNorm += MathUtils::Sqr(aGrad(i)); } if (std::sqrt(aGradNorm) < theConfig.FTolerance) { aResult.Status = Status::OK; aResult.Solution = aX; aResult.Value = aFx; aResult.Gradient = aGrad; return aResult; } // L-BFGS two-loop recursion to compute search direction // q = gradient for (int i = 1; i <= aN; ++i) { aQ(i) = aGrad(aLower + i - 1); } // First loop (backward) for (int k = aCount - 1; k >= 0; --k) { const int aIdx = (aHead + k) % aM; aAlphaVec(aIdx) = aRhoVec(aIdx) * MathUtils::DotProduct(aSVec.Value(aIdx), aQ); for (int i = 1; i <= aN; ++i) { aQ(i) -= aAlphaVec(aIdx) * aYVec.Value(aIdx)(i); } } // Initial Hessian: H0 = gamma*I where gamma = s^T y / y^T y double aGamma = 1.0; if (aCount > 0) { const int aLastIdx = (aHead + aCount - 1) % aM; double aYY = MathUtils::DotProduct(aYVec.Value(aLastIdx), aYVec.Value(aLastIdx)); if (aYY > MathUtils::THE_ZERO_TOL) { aGamma = 1.0 / (aRhoVec(aLastIdx) * aYY); } } // r = H0 * q = gamma * q math_Vector aR(1, aN); for (int i = 1; i <= aN; ++i) { aR(i) = aGamma * aQ(i); } // Second loop (forward) for (int k = 0; k < aCount; ++k) { const int aIdx = (aHead + k) % aM; const double aBeta = aRhoVec(aIdx) * MathUtils::DotProduct(aYVec.Value(aIdx), aR); for (int i = 1; i <= aN; ++i) { aR(i) += (aAlphaVec(aIdx) - aBeta) * aSVec.Value(aIdx)(i); } } // Search direction: p = -r for (int i = aLower; i <= aUpper; ++i) { aDir(i) = -aR(i - aLower + 1); } // Line search MathUtils::LineSearchResult aLineResult = MathUtils::ArmijoBacktrack(theFunc, aX, aDir, aGrad, aFx, 1.0, 1.0e-4, 0.5, 50); if (!aLineResult.IsValid) { // Fall back to steepest descent for (int i = aLower; i <= aUpper; ++i) { aDir(i) = -aGrad(i); } aLineResult = MathUtils::ArmijoBacktrack(theFunc, aX, aDir, aGrad, aFx, 1.0, 1.0e-4, 0.5, 50); if (!aLineResult.IsValid) { aResult.Status = Status::NotConverged; aResult.Solution = aX; aResult.Value = aFx; return aResult; } // Reset history after steepest descent aCount = 0; } // Compute new point for (int i = aLower; i <= aUpper; ++i) { aXNew(i) = aX(i) + aLineResult.Alpha * aDir(i); } // Check X convergence double aMaxDiff = 0.0; for (int i = aLower; i <= aUpper; ++i) { aMaxDiff = std::max(aMaxDiff, std::abs(aXNew(i) - aX(i))); } if (aMaxDiff < theConfig.XTolerance) { if (!theFunc.Gradient(aXNew, aGradNew)) { aGradNew = aGrad; } aResult.Status = Status::OK; aResult.Solution = aXNew; aResult.Value = aLineResult.FNew; aResult.Gradient = aGradNew; return aResult; } // Evaluate gradient at new point if (!theFunc.Gradient(aXNew, aGradNew)) { aResult.Status = Status::NumericalError; aResult.Solution = aX; aResult.Value = aFx; return aResult; } // Store new {s, y} pair const int aNewIdx = (aHead + aCount) % aM; for (int i = 1; i <= aN; ++i) { aSVec.ChangeValue(aNewIdx)(i) = aXNew(aLower + i - 1) - aX(aLower + i - 1); aYVec.ChangeValue(aNewIdx)(i) = aGradNew(aLower + i - 1) - aGrad(aLower + i - 1); } double aSY = MathUtils::DotProduct(aSVec.Value(aNewIdx), aYVec.Value(aNewIdx)); if (aSY > MathUtils::THE_ZERO_TOL) { aRhoVec(aNewIdx) = 1.0 / aSY; if (aCount < aM) { ++aCount; } else { aHead = (aHead + 1) % aM; } } // Update for next iteration aX = aXNew; aGrad = aGradNew; aFx = aLineResult.FNew; } aResult.Status = Status::MaxIterations; aResult.Solution = aX; aResult.Value = aFx; aResult.Gradient = aGrad; return aResult; } //! BFGS with bound constraints (box constraints). //! //! Minimizes f(x) subject to theLowerBounds <= x <= theUpperBounds. //! Uses projected gradient approach: //! - After each step, clamp x to bounds //! - Zero gradient components at bounds where they point outward //! //! @tparam Function type with Value and Gradient methods //! @param theFunc function object //! @param theStartingPoint initial guess //! @param theLowerBounds lower bounds for each variable //! @param theUpperBounds upper bounds for each variable //! @param theConfig solver configuration //! @return result containing minimum location and value template VectorResult BFGSBounded(Function& theFunc, const math_Vector& theStartingPoint, const math_Vector& theLowerBounds, const math_Vector& theUpperBounds, const Config& theConfig = Config()) { VectorResult aResult; const int aLower = theStartingPoint.Lower(); const int aUpper = theStartingPoint.Upper(); const int aN = aUpper - aLower + 1; // Check dimensions if (theLowerBounds.Length() != aN || theUpperBounds.Length() != aN) { aResult.Status = Status::InvalidInput; return aResult; } // Lambda to clamp a point to bounds auto ClampToBounds = [&](math_Vector& theX) { for (int i = aLower; i <= aUpper; ++i) { const int aBndIdx = theLowerBounds.Lower() + (i - aLower); if (theX(i) < theLowerBounds(aBndIdx)) { theX(i) = theLowerBounds(aBndIdx); } if (theX(i) > theUpperBounds(aBndIdx)) { theX(i) = theUpperBounds(aBndIdx); } } }; // Lambda to project gradient (zero components at active bounds) auto ProjectGradient = [&](const math_Vector& theX, math_Vector& theGrad) { for (int i = aLower; i <= aUpper; ++i) { const int aBndIdx = theLowerBounds.Lower() + (i - aLower); const double aTol = MathUtils::THE_EPSILON * std::max(1.0, std::abs(theX(i))); // At lower bound and gradient points downward -> zero gradient if (theX(i) - theLowerBounds(aBndIdx) < aTol && theGrad(i) > 0.0) { theGrad(i) = 0.0; } // At upper bound and gradient points upward -> zero gradient if (theUpperBounds(aBndIdx) - theX(i) < aTol && theGrad(i) < 0.0) { theGrad(i) = 0.0; } } }; // Current point and function value math_Vector aX(aLower, aUpper); aX = theStartingPoint; ClampToBounds(aX); double aFx = 0.0; if (!theFunc.Value(aX, aFx)) { aResult.Status = Status::NumericalError; return aResult; } // Gradient at current point math_Vector aGrad(aLower, aUpper); if (!theFunc.Gradient(aX, aGrad)) { aResult.Status = Status::NumericalError; return aResult; } ProjectGradient(aX, aGrad); // Check if already at minimum double aGradNorm = 0.0; for (int i = aLower; i <= aUpper; ++i) { aGradNorm += MathUtils::Sqr(aGrad(i)); } aGradNorm = std::sqrt(aGradNorm); if (aGradNorm < theConfig.FTolerance) { aResult.Status = Status::OK; aResult.Solution = aX; aResult.Value = aFx; aResult.Gradient = aGrad; return aResult; } // Initialize inverse Hessian approximation to identity math_Matrix aH(1, aN, 1, aN, 0.0); for (int i = 1; i <= aN; ++i) { aH(i, i) = 1.0; } // Working vectors math_Vector aDir(aLower, aUpper); math_Vector aXNew(aLower, aUpper); math_Vector aGradNew(aLower, aUpper); math_Vector aS(1, aN); math_Vector aY(1, aN); for (int anIter = 0; anIter < theConfig.MaxIterations; ++anIter) { aResult.NbIterations = anIter + 1; // Compute search direction: p = -H * grad for (int i = 1; i <= aN; ++i) { double aSum = 0.0; for (int j = 1; j <= aN; ++j) { aSum += aH(i, j) * aGrad(aLower + j - 1); } aDir(aLower + i - 1) = -aSum; } // Line search with bounds-aware step double aAlphaMax = 1.0; for (int i = aLower; i <= aUpper; ++i) { const int aBndIdx = theLowerBounds.Lower() + (i - aLower); if (aDir(i) < -MathUtils::THE_EPSILON) { // Moving toward lower bound double aMaxStep = (theLowerBounds(aBndIdx) - aX(i)) / aDir(i); aAlphaMax = std::min(aAlphaMax, aMaxStep); } else if (aDir(i) > MathUtils::THE_EPSILON) { // Moving toward upper bound double aMaxStep = (theUpperBounds(aBndIdx) - aX(i)) / aDir(i); aAlphaMax = std::min(aAlphaMax, aMaxStep); } } aAlphaMax = std::max(aAlphaMax, MathUtils::THE_EPSILON); MathUtils::LineSearchResult aLineResult = MathUtils::ArmijoBacktrack(theFunc, aX, aDir, aGrad, aFx, aAlphaMax, 1.0e-4, 0.5, 50); if (!aLineResult.IsValid || aLineResult.Alpha < MathUtils::THE_EPSILON) { // Fall back to projected steepest descent for (int i = aLower; i <= aUpper; ++i) { aDir(i) = -aGrad(i); } // Recompute alpha max for steepest descent aAlphaMax = 1.0; for (int i = aLower; i <= aUpper; ++i) { const int aBndIdx = theLowerBounds.Lower() + (i - aLower); if (aDir(i) < -MathUtils::THE_EPSILON) { aAlphaMax = std::min(aAlphaMax, (theLowerBounds(aBndIdx) - aX(i)) / aDir(i)); } else if (aDir(i) > MathUtils::THE_EPSILON) { aAlphaMax = std::min(aAlphaMax, (theUpperBounds(aBndIdx) - aX(i)) / aDir(i)); } } aAlphaMax = std::max(aAlphaMax, MathUtils::THE_EPSILON); aLineResult = MathUtils::ArmijoBacktrack(theFunc, aX, aDir, aGrad, aFx, aAlphaMax, 1.0e-4, 0.5, 50); if (!aLineResult.IsValid) { aResult.Status = Status::NotConverged; aResult.Solution = aX; aResult.Value = aFx; aResult.Gradient = aGrad; return aResult; } // Reset Hessian for (int i = 1; i <= aN; ++i) { for (int j = 1; j <= aN; ++j) { aH(i, j) = (i == j) ? 1.0 : 0.0; } } } // Compute and clamp new point for (int i = aLower; i <= aUpper; ++i) { aXNew(i) = aX(i) + aLineResult.Alpha * aDir(i); } ClampToBounds(aXNew); // Compute s = x_new - x for (int i = 1; i <= aN; ++i) { aS(i) = aXNew(aLower + i - 1) - aX(aLower + i - 1); } // Evaluate at new point double aFxNew = 0.0; if (!theFunc.Value(aXNew, aFxNew)) { aResult.Status = Status::NumericalError; aResult.Solution = aX; aResult.Value = aFx; return aResult; } if (!theFunc.Gradient(aXNew, aGradNew)) { aResult.Status = Status::NumericalError; aResult.Solution = aX; aResult.Value = aFx; return aResult; } ProjectGradient(aXNew, aGradNew); // Check gradient convergence aGradNorm = 0.0; for (int i = aLower; i <= aUpper; ++i) { aGradNorm += MathUtils::Sqr(aGradNew(i)); } aGradNorm = std::sqrt(aGradNorm); if (aGradNorm < theConfig.FTolerance) { aResult.Status = Status::OK; aResult.Solution = aXNew; aResult.Value = aFxNew; aResult.Gradient = aGradNew; return aResult; } // Check X convergence double aMaxDiff = 0.0; for (int i = aLower; i <= aUpper; ++i) { aMaxDiff = std::max(aMaxDiff, std::abs(aXNew(i) - aX(i))); } if (aMaxDiff < theConfig.XTolerance) { aResult.Status = Status::OK; aResult.Solution = aXNew; aResult.Value = aFxNew; aResult.Gradient = aGradNew; return aResult; } // Compute y = grad_new - grad for (int i = 1; i <= aN; ++i) { aY(i) = aGradNew(aLower + i - 1) - aGrad(aLower + i - 1); } // Curvature condition double aSY = 0.0; for (int i = 1; i <= aN; ++i) { aSY += aS(i) * aY(i); } if (aSY > MathUtils::THE_ZERO_TOL) { const double aRho = 1.0 / aSY; math_Vector aHy(1, aN, 0.0); for (int i = 1; i <= aN; ++i) { for (int j = 1; j <= aN; ++j) { aHy(i) += aH(i, j) * aY(j); } } double aYHy = 0.0; for (int i = 1; i <= aN; ++i) { aYHy += aY(i) * aHy(i); } const double aFactor = 1.0 + aRho * aYHy; for (int i = 1; i <= aN; ++i) { for (int j = 1; j <= aN; ++j) { aH(i, j) = aH(i, j) - aRho * (aHy(i) * aS(j) + aS(i) * aHy(j)) + aFactor * aRho * aS(i) * aS(j); } } } aX = aXNew; aGrad = aGradNew; aFx = aFxNew; } aResult.Status = Status::MaxIterations; aResult.Solution = aX; aResult.Value = aFx; aResult.Gradient = aGrad; return aResult; } } // namespace MathOpt #endif // _MathOpt_BFGS_HeaderFile