// 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 _MathSys_LevenbergMarquardt_HeaderFile #define _MathSys_LevenbergMarquardt_HeaderFile #include "MathUtils_Types.hxx" #include "MathUtils_Config.hxx" #include "MathLin_Gauss.hxx" #include "MathUtils_Core.hxx" #include namespace MathSys { using namespace MathUtils; //! Configuration for Levenberg-Marquardt algorithm. //! Extends base Config with damping parameter settings. struct LMConfig : Config { double LambdaInit = 1.0e-3; //!< Initial damping parameter double LambdaIncrease = 10.0; //!< Factor to increase lambda on rejected step double LambdaDecrease = 0.1; //!< Factor to decrease lambda on accepted step double LambdaMax = 1.0e10; //!< Maximum lambda value before failing double LambdaMin = 1.0e-12; //!< Minimum lambda value //! Default constructor. LMConfig() = default; //! Constructor with custom tolerance. //! @param theTolerance convergence tolerance //! @param theMaxIter maximum iterations explicit LMConfig(double theTolerance, int theMaxIter = 100) : Config(theTolerance, theMaxIter) { } }; //! Levenberg-Marquardt algorithm for nonlinear least squares. //! //! Minimizes ||F(X)||^2 where F is a vector function of vector X. //! Combines Gauss-Newton method (fast near minimum) with gradient //! descent (robust far from minimum) using adaptive damping. //! //! Algorithm: //! 1. Compute F(X) and Jacobian J at current point //! 2. Solve (J^T*J + lambda*I) * dX = -J^T*F for correction dX //! 3. If ||F(X+dX)||^2 < ||F(X)||^2: accept step, decrease lambda //! 4. Otherwise: reject step, increase lambda //! 5. Repeat until convergence //! //! @tparam FuncSetType type with NbVariables(), NbEquations(), //! Value(const math_Vector& X, math_Vector& F) and //! Derivatives(const math_Vector& X, math_Matrix& J) or //! Values(const math_Vector& X, math_Vector& F, math_Matrix& J) //! @param theFunc function set providing residuals and Jacobian //! @param theStart initial guess vector //! @param theConfig Levenberg-Marquardt configuration //! @return result containing solution vector if converged template VectorResult LevenbergMarquardt(FuncSetType& theFunc, const math_Vector& theStart, const LMConfig& theConfig = LMConfig()) { VectorResult aResult; const int aNbVars = theFunc.NbVariables(); const int aNbEqs = theFunc.NbEquations(); // Check dimensions if (theStart.Length() != aNbVars) { aResult.Status = Status::InvalidInput; return aResult; } const int aVarLower = theStart.Lower(); const int aVarUpper = theStart.Upper(); // Working vectors and matrices math_Vector aSol = theStart; math_Vector aF(1, aNbEqs); math_Vector aFNew(1, aNbEqs); math_Vector aDeltaX(aVarLower, aVarUpper); math_Vector aGrad(aVarLower, aVarUpper); math_Matrix aJac(1, aNbEqs, aVarLower, aVarUpper); math_Matrix aJtJ(aVarLower, aVarUpper, aVarLower, aVarUpper); math_Vector aJtF(aVarLower, aVarUpper); double aLambda = theConfig.LambdaInit; // Evaluate initial residual if (!theFunc.Value(aSol, aF)) { aResult.Status = Status::NumericalError; return aResult; } // Compute initial ||F||^2 double aChi2 = 0.0; for (int i = 1; i <= aNbEqs; ++i) { aChi2 += aF(i) * aF(i); } // Main iteration loop for (int anIter = 0; anIter < theConfig.MaxIterations; ++anIter) { aResult.NbIterations = anIter + 1; // Check F convergence bool aFConverged = true; for (int i = 1; i <= aNbEqs; ++i) { if (std::abs(aF(i)) > theConfig.FTolerance) { aFConverged = false; break; } } if (aFConverged) { aResult.Status = Status::OK; aResult.Solution = aSol; aResult.Value = aChi2; return aResult; } // Compute Jacobian if (!theFunc.Derivatives(aSol, aJac)) { aResult.Status = Status::NumericalError; return aResult; } // Compute J^T * J for (int i = aVarLower; i <= aVarUpper; ++i) { for (int j = aVarLower; j <= aVarUpper; ++j) { double aSum = 0.0; for (int k = 1; k <= aNbEqs; ++k) { aSum += aJac(k, i) * aJac(k, j); } aJtJ(i, j) = aSum; } } // Compute J^T * F (negative gradient of chi^2) for (int i = aVarLower; i <= aVarUpper; ++i) { double aSum = 0.0; for (int k = 1; k <= aNbEqs; ++k) { aSum += aJac(k, i) * aF(k); } aJtF(i) = aSum; aGrad(i) = 2.0 * aSum; // Gradient of chi^2 = 2 * J^T * F } // Check gradient convergence double aGradNorm = 0.0; for (int i = aVarLower; i <= aVarUpper; ++i) { aGradNorm += aGrad(i) * aGrad(i); } aGradNorm = std::sqrt(aGradNorm); if (aGradNorm < theConfig.Tolerance) { aResult.Status = Status::OK; aResult.Solution = aSol; aResult.Value = aChi2; aResult.Gradient = aGrad; return aResult; } // Inner loop: try to find an acceptable step bool aStepAccepted = false; for (int aLamIter = 0; aLamIter < 20 && !aStepAccepted; ++aLamIter) { // Add damping: J^T*J + lambda*I math_Matrix aDamped = aJtJ; for (int i = aVarLower; i <= aVarUpper; ++i) { aDamped(i, i) += aLambda; } // Solve (J^T*J + lambda*I) * dX = -J^T*F math_Vector aNegJtF(aVarLower, aVarUpper); for (int i = aVarLower; i <= aVarUpper; ++i) { aNegJtF(i) = -aJtF(i); } auto aLinResult = MathLin::Solve(aDamped, aNegJtF); if (!aLinResult.IsDone()) { // Matrix is singular, increase damping aLambda *= theConfig.LambdaIncrease; if (aLambda > theConfig.LambdaMax) { aResult.Status = Status::Singular; aResult.Solution = aSol; aResult.Value = aChi2; return aResult; } continue; } aDeltaX = *aLinResult.Solution; // Compute new solution math_Vector aSolNew(aVarLower, aVarUpper); for (int i = aVarLower; i <= aVarUpper; ++i) { aSolNew(i) = aSol(i) + aDeltaX(i); } // Evaluate new residual if (!theFunc.Value(aSolNew, aFNew)) { // Function evaluation failed, increase damping aLambda *= theConfig.LambdaIncrease; if (aLambda > theConfig.LambdaMax) { aResult.Status = Status::NumericalError; aResult.Solution = aSol; aResult.Value = aChi2; return aResult; } continue; } // Compute new ||F||^2 double aChi2New = 0.0; for (int i = 1; i <= aNbEqs; ++i) { aChi2New += aFNew(i) * aFNew(i); } // Accept or reject step if (aChi2New < aChi2) { // Step accepted aSol = aSolNew; aF = aFNew; aChi2 = aChi2New; aLambda *= theConfig.LambdaDecrease; if (aLambda < theConfig.LambdaMin) { aLambda = theConfig.LambdaMin; } aStepAccepted = true; // Check X convergence bool aXConverged = true; for (int i = aVarLower; i <= aVarUpper; ++i) { if (std::abs(aDeltaX(i)) > theConfig.XTolerance * (1.0 + std::abs(aSol(i)))) { aXConverged = false; break; } } if (aXConverged) { aResult.Status = Status::OK; aResult.Solution = aSol; aResult.Value = aChi2; return aResult; } } else { // Step rejected, increase damping aLambda *= theConfig.LambdaIncrease; if (aLambda > theConfig.LambdaMax) { aResult.Status = Status::NotConverged; aResult.Solution = aSol; aResult.Value = aChi2; return aResult; } } } if (!aStepAccepted) { // Failed to find acceptable step aResult.Status = Status::NotConverged; aResult.Solution = aSol; aResult.Value = aChi2; return aResult; } } // Max iterations reached aResult.Status = Status::MaxIterations; aResult.Solution = aSol; aResult.Value = aChi2; return aResult; } //! Levenberg-Marquardt with bounds constraints. //! //! Minimizes ||F(X)||^2 subject to theInfBound <= X <= theSupBound. //! Solution is clamped to bounds after each step. //! //! @param theFunc function set providing residuals and Jacobian //! @param theStart initial guess vector //! @param theInfBound lower bounds for solution //! @param theSupBound upper bounds for solution //! @param theConfig Levenberg-Marquardt configuration //! @return result containing solution vector if converged template VectorResult LevenbergMarquardtBounded(FuncSetType& theFunc, const math_Vector& theStart, const math_Vector& theInfBound, const math_Vector& theSupBound, const LMConfig& theConfig = LMConfig()) { VectorResult aResult; const int aNbVars = theFunc.NbVariables(); const int aNbEqs = theFunc.NbEquations(); // Check dimensions if (theStart.Length() != aNbVars || theInfBound.Length() != aNbVars || theSupBound.Length() != aNbVars) { aResult.Status = Status::InvalidInput; return aResult; } const int aVarLower = theStart.Lower(); const int aVarUpper = theStart.Upper(); // Working vectors and matrices math_Vector aSol = theStart; math_Vector aF(1, aNbEqs); math_Vector aFNew(1, aNbEqs); math_Vector aDeltaX(aVarLower, aVarUpper); math_Vector aGrad(aVarLower, aVarUpper); math_Matrix aJac(1, aNbEqs, aVarLower, aVarUpper); math_Matrix aJtJ(aVarLower, aVarUpper, aVarLower, aVarUpper); math_Vector aJtF(aVarLower, aVarUpper); // Clamp initial solution to bounds for (int i = aVarLower; i <= aVarUpper; ++i) { aSol(i) = MathUtils::Clamp(aSol(i), theInfBound(i), theSupBound(i)); } double aLambda = theConfig.LambdaInit; // Evaluate initial residual if (!theFunc.Value(aSol, aF)) { aResult.Status = Status::NumericalError; return aResult; } // Compute initial ||F||^2 double aChi2 = 0.0; for (int i = 1; i <= aNbEqs; ++i) { aChi2 += aF(i) * aF(i); } // Main iteration loop for (int anIter = 0; anIter < theConfig.MaxIterations; ++anIter) { aResult.NbIterations = anIter + 1; // Check F convergence bool aFConverged = true; for (int i = 1; i <= aNbEqs; ++i) { if (std::abs(aF(i)) > theConfig.FTolerance) { aFConverged = false; break; } } if (aFConverged) { aResult.Status = Status::OK; aResult.Solution = aSol; aResult.Value = aChi2; return aResult; } // Compute Jacobian if (!theFunc.Derivatives(aSol, aJac)) { aResult.Status = Status::NumericalError; return aResult; } // Compute J^T * J for (int i = aVarLower; i <= aVarUpper; ++i) { for (int j = aVarLower; j <= aVarUpper; ++j) { double aSum = 0.0; for (int k = 1; k <= aNbEqs; ++k) { aSum += aJac(k, i) * aJac(k, j); } aJtJ(i, j) = aSum; } } // Compute J^T * F for (int i = aVarLower; i <= aVarUpper; ++i) { double aSum = 0.0; for (int k = 1; k <= aNbEqs; ++k) { aSum += aJac(k, i) * aF(k); } aJtF(i) = aSum; aGrad(i) = 2.0 * aSum; } // Check gradient convergence double aGradNorm = 0.0; for (int i = aVarLower; i <= aVarUpper; ++i) { aGradNorm += aGrad(i) * aGrad(i); } aGradNorm = std::sqrt(aGradNorm); if (aGradNorm < theConfig.Tolerance) { aResult.Status = Status::OK; aResult.Solution = aSol; aResult.Value = aChi2; aResult.Gradient = aGrad; return aResult; } // Inner loop: try to find an acceptable step bool aStepAccepted = false; for (int aLamIter = 0; aLamIter < 20 && !aStepAccepted; ++aLamIter) { // Add damping math_Matrix aDamped = aJtJ; for (int i = aVarLower; i <= aVarUpper; ++i) { aDamped(i, i) += aLambda; } // Solve (J^T*J + lambda*I) * dX = -J^T*F math_Vector aNegJtF(aVarLower, aVarUpper); for (int i = aVarLower; i <= aVarUpper; ++i) { aNegJtF(i) = -aJtF(i); } auto aLinResult = MathLin::Solve(aDamped, aNegJtF); if (!aLinResult.IsDone()) { aLambda *= theConfig.LambdaIncrease; if (aLambda > theConfig.LambdaMax) { aResult.Status = Status::Singular; aResult.Solution = aSol; aResult.Value = aChi2; return aResult; } continue; } aDeltaX = *aLinResult.Solution; // Compute new solution with bounds clamping math_Vector aSolNew(aVarLower, aVarUpper); for (int i = aVarLower; i <= aVarUpper; ++i) { aSolNew(i) = MathUtils::Clamp(aSol(i) + aDeltaX(i), theInfBound(i), theSupBound(i)); } // Evaluate new residual if (!theFunc.Value(aSolNew, aFNew)) { aLambda *= theConfig.LambdaIncrease; if (aLambda > theConfig.LambdaMax) { aResult.Status = Status::NumericalError; aResult.Solution = aSol; aResult.Value = aChi2; return aResult; } continue; } // Compute new ||F||^2 double aChi2New = 0.0; for (int i = 1; i <= aNbEqs; ++i) { aChi2New += aFNew(i) * aFNew(i); } // Accept or reject step if (aChi2New < aChi2) { aSol = aSolNew; aF = aFNew; aChi2 = aChi2New; aLambda *= theConfig.LambdaDecrease; if (aLambda < theConfig.LambdaMin) { aLambda = theConfig.LambdaMin; } aStepAccepted = true; // Check X convergence bool aXConverged = true; for (int i = aVarLower; i <= aVarUpper; ++i) { if (std::abs(aDeltaX(i)) > theConfig.XTolerance * (1.0 + std::abs(aSol(i)))) { aXConverged = false; break; } } if (aXConverged) { aResult.Status = Status::OK; aResult.Solution = aSol; aResult.Value = aChi2; return aResult; } } else { aLambda *= theConfig.LambdaIncrease; if (aLambda > theConfig.LambdaMax) { aResult.Status = Status::NotConverged; aResult.Solution = aSol; aResult.Value = aChi2; return aResult; } } } if (!aStepAccepted) { aResult.Status = Status::NotConverged; aResult.Solution = aSol; aResult.Value = aChi2; return aResult; } } // Max iterations reached aResult.Status = Status::MaxIterations; aResult.Solution = aSol; aResult.Value = aChi2; return aResult; } } // namespace MathSys #endif // _MathSys_LevenbergMarquardt_HeaderFile