// 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_Newton3D_HeaderFile #define _MathSys_Newton3D_HeaderFile #include "MathSys_NewtonTypes.hxx" #include "gp_Pnt.hxx" #include "gp_Vec.hxx" #include #include #include //! @file MathSys_Newton3D.hxx //! @brief Optimized 3D Newton-Raphson solver for systems of 3 equations in 3 unknowns. //! //! This solver is specifically optimized for 3D problems like: //! - Curve-surface extrema (find t, u, v where distance is extremal) //! - Curve intersection with surface plus a constraint //! - Point-surface with additional constraint //! //! Optimizations compared to general Newton: //! - No math_Vector/math_Matrix allocation overhead //! - Cramer's rule with precomputed cofactors for 3x3 system //! - Squared norm comparisons (avoid sqrt in convergence check) //! - Step limiting to prevent wild oscillations //! - Gradient descent fallback for singular Jacobian namespace MathSys { namespace detail { //! Check that NewtonBoundsN<3> has valid (min <= max) ranges. inline bool IsBoundsValid3D(const NewtonBoundsN<3>& theBounds) { if (!theBounds.HasBounds) { return true; } return theBounds.Min[0] <= theBounds.Max[0] && theBounds.Min[1] <= theBounds.Max[1] && theBounds.Min[2] <= theBounds.Max[2]; } //! Check that NewtonOptions fields are positive and valid. inline bool IsOptionsValid3D(const NewtonOptions& theOptions) { return theOptions.FTolerance > 0.0 && theOptions.XTolerance > 0.0 && theOptions.MaxIterations > 0 && theOptions.MaxStepRatio > 0.0 && theOptions.SoftBoundsExtension >= 0.0; } //! Return the largest domain extent across all 3 dimensions (min 1.0). inline double MaxDomainSize3D(const NewtonBoundsN<3>& theBounds) { if (!theBounds.HasBounds) { return 1.0; } const double aDX = theBounds.Max[0] - theBounds.Min[0]; const double aDY = theBounds.Max[1] - theBounds.Min[1]; const double aDZ = theBounds.Max[2] - theBounds.Min[2]; return std::max(1.0, std::max(aDX, std::max(aDY, aDZ))); } //! Clamp solution array to bounds, optionally extending by soft-bounds ratio. inline void Clamp3D(std::array& theX, const NewtonBoundsN<3>& theBounds, bool theUseSoftBounds, double theSoftExtRatio) { if (!theBounds.HasBounds) { return; } const double aExtX = theUseSoftBounds ? (theBounds.Max[0] - theBounds.Min[0]) * theSoftExtRatio : 0.0; const double aExtY = theUseSoftBounds ? (theBounds.Max[1] - theBounds.Min[1]) * theSoftExtRatio : 0.0; const double aExtZ = theUseSoftBounds ? (theBounds.Max[2] - theBounds.Min[2]) * theSoftExtRatio : 0.0; theX[0] = std::clamp(theX[0], theBounds.Min[0] - aExtX, theBounds.Max[0] + aExtX); theX[1] = std::clamp(theX[1], theBounds.Min[1] - aExtY, theBounds.Max[1] + aExtY); theX[2] = std::clamp(theX[2], theBounds.Min[2] - aExtZ, theBounds.Max[2] + aExtZ); } //! Solve 3x3 linear system J*x = -F using Cramer's rule with cofactor expansion. //! @param[in] theJ 3x3 Jacobian matrix //! @param[in] theF 3-element right-hand side //! @param[out] theDelta 3-element solution vector //! @return true if system was solved successfully (non-singular) inline bool Solve3x3(const double theJ[3][3], const double theF[3], double theDelta[3]) { // Compute cofactors for first row (used for determinant and inverse) const double aCof00 = theJ[1][1] * theJ[2][2] - theJ[1][2] * theJ[2][1]; const double aCof01 = theJ[1][2] * theJ[2][0] - theJ[1][0] * theJ[2][2]; const double aCof02 = theJ[1][0] * theJ[2][1] - theJ[1][1] * theJ[2][0]; // Determinant by first row expansion const double aDet = theJ[0][0] * aCof00 + theJ[0][1] * aCof01 + theJ[0][2] * aCof02; // Check for singularity if (std::abs(aDet) < 1.0e-30) { return false; } const double aInvDet = 1.0 / aDet; // Remaining cofactors const double aCof10 = theJ[0][2] * theJ[2][1] - theJ[0][1] * theJ[2][2]; const double aCof11 = theJ[0][0] * theJ[2][2] - theJ[0][2] * theJ[2][0]; const double aCof12 = theJ[0][1] * theJ[2][0] - theJ[0][0] * theJ[2][1]; const double aCof20 = theJ[0][1] * theJ[1][2] - theJ[0][2] * theJ[1][1]; const double aCof21 = theJ[0][2] * theJ[1][0] - theJ[0][0] * theJ[1][2]; const double aCof22 = theJ[0][0] * theJ[1][1] - theJ[0][1] * theJ[1][0]; // Solve: delta = -J^(-1) * F = -adjugate(J)^T * F / det theDelta[0] = -(aCof00 * theF[0] + aCof10 * theF[1] + aCof20 * theF[2]) * aInvDet; theDelta[1] = -(aCof01 * theF[0] + aCof11 * theF[1] + aCof21 * theF[2]) * aInvDet; theDelta[2] = -(aCof02 * theF[0] + aCof12 * theF[1] + aCof22 * theF[2]) * aInvDet; return true; } } // namespace detail //! Solve a 3x3 nonlinear system by Newton iteration with bounds. //! //! Solves the system [F1, F2, F3] = [0, 0, 0] using Newton-Raphson iteration //! with Cramer's rule for the 3x3 linear system at each step. //! //! The function type must be callable with signature: //! @code //! bool operator()(double theX1, double theX2, double theX3, //! double theF[3], double theJ[3][3]) const; //! @endcode //! where theF is the function values and theJ is the 3x3 Jacobian matrix. //! //! @tparam Function callable type (functor, lambda, or function pointer) //! @param[in] theFunc function to solve (provides F and Jacobian) //! @param[in] theX0 initial guess {x1, x2, x3} //! @param[in] theBounds box bounds for each variable //! @param[in] theOptions solver options (tolerances, max iterations, etc.) //! @return NewtonResultN<3> containing solution, status, and diagnostics template NewtonResultN<3> Solve3D(const Function& theFunc, const std::array& theX0, const NewtonBoundsN<3>& theBounds, const NewtonOptions& theOptions = NewtonOptions()) { NewtonResultN<3> aRes; aRes.X = theX0; if (!detail::IsOptionsValid3D(theOptions) || !detail::IsBoundsValid3D(theBounds)) { aRes.Status = MathUtils::Status::InvalidInput; return aRes; } detail::Clamp3D(aRes.X, theBounds, theOptions.AllowSoftBounds, theOptions.SoftBoundsExtension); const double aTolSq = theOptions.FTolerance * theOptions.FTolerance; const double aMaxStep = theOptions.MaxStepRatio * detail::MaxDomainSize3D(theBounds); for (int anIter = 0; anIter < theOptions.MaxIterations; ++anIter) { aRes.NbIterations = static_cast(anIter + 1); double aF[3]; double aJ[3][3]; if (!theFunc(aRes.X[0], aRes.X[1], aRes.X[2], aF, aJ)) { aRes.Status = MathUtils::Status::NumericalError; return aRes; } // Check convergence using squared norm (avoid sqrt) const double aFNormSq = aF[0] * aF[0] + aF[1] * aF[1] + aF[2] * aF[2]; aRes.ResidualNorm = std::sqrt(aFNormSq); if (aFNormSq <= aTolSq) { aRes.Status = MathUtils::Status::OK; return aRes; } // Solve 3x3 linear system: J * delta = -F double aDelta[3]; if (!detail::Solve3x3(aJ, aF, aDelta)) { // Singular Jacobian - try steepest descent direction: -J^T * F const double aGradX = aJ[0][0] * aF[0] + aJ[1][0] * aF[1] + aJ[2][0] * aF[2]; const double aGradY = aJ[0][1] * aF[0] + aJ[1][1] * aF[1] + aJ[2][1] * aF[2]; const double aGradZ = aJ[0][2] * aF[0] + aJ[1][2] * aF[1] + aJ[2][2] * aF[2]; const double aGradSq = aGradX * aGradX + aGradY * aGradY + aGradZ * aGradZ; if (aGradSq < 1.0e-60) { aRes.Status = MathUtils::Status::Singular; return aRes; } const double aAlpha = std::min(1.0, std::sqrt(aFNormSq / aGradSq) * 0.1); aDelta[0] = -aAlpha * aGradX; aDelta[1] = -aAlpha * aGradY; aDelta[2] = -aAlpha * aGradZ; } // Limit step size to prevent wild oscillations const double aStepNormSq = aDelta[0] * aDelta[0] + aDelta[1] * aDelta[1] + aDelta[2] * aDelta[2]; const double aStepNorm = std::sqrt(aStepNormSq); if (aStepNorm > aMaxStep) { const double aScale = aMaxStep / aStepNorm; aDelta[0] *= aScale; aDelta[1] *= aScale; aDelta[2] *= aScale; } // Update and clamp to bounds std::array aNewX = {aRes.X[0] + aDelta[0], aRes.X[1] + aDelta[1], aRes.X[2] + aDelta[2]}; detail::Clamp3D(aNewX, theBounds, theOptions.AllowSoftBounds, theOptions.SoftBoundsExtension); aRes.StepNorm = std::sqrt((aNewX[0] - aRes.X[0]) * (aNewX[0] - aRes.X[0]) + (aNewX[1] - aRes.X[1]) * (aNewX[1] - aRes.X[1]) + (aNewX[2] - aRes.X[2]) * (aNewX[2] - aRes.X[2])); aRes.X = aNewX; const double aScaleRef = std::max(1.0, std::max(std::abs(aRes.X[0]), std::max(std::abs(aRes.X[1]), std::abs(aRes.X[2])))); if (aRes.StepNorm <= theOptions.XTolerance * aScaleRef) { double aCheckF[3]; double aCheckJ[3][3]; if (!theFunc(aRes.X[0], aRes.X[1], aRes.X[2], aCheckF, aCheckJ)) { aRes.Status = MathUtils::Status::NumericalError; return aRes; } aRes.ResidualNorm = std::sqrt(aCheckF[0] * aCheckF[0] + aCheckF[1] * aCheckF[1] + aCheckF[2] * aCheckF[2]); aRes.Status = (aRes.ResidualNorm <= theOptions.FTolerance) ? MathUtils::Status::OK : MathUtils::Status::MaxIterations; return aRes; } } // Final convergence check after max iterations double aF[3]; double aJ[3][3]; if (!theFunc(aRes.X[0], aRes.X[1], aRes.X[2], aF, aJ)) { aRes.Status = MathUtils::Status::NumericalError; return aRes; } aRes.ResidualNorm = std::sqrt(aF[0] * aF[0] + aF[1] * aF[1] + aF[2] * aF[2]); aRes.Status = (aRes.ResidualNorm <= theOptions.FTolerance) ? MathUtils::Status::OK : MathUtils::Status::MaxIterations; return aRes; } //! Optimized 3D Newton solver for curve-surface extrema. //! //! Specialized version for finding extrema between a curve C(t) and surface S(u,v). //! The function values are the gradient components of the squared distance: //! - F1 = (C-S) . dC/dt //! - F2 = (S-C) . dS/du //! - F3 = (S-C) . dS/dv //! //! @tparam CurveEvaluator type providing D2(t, P, D1, D2) evaluation //! @tparam SurfaceEvaluator type providing D2(u, v, P, D1U, D1V, D2UU, D2VV, D2UV) evaluation //! @param[in] theCurve curve evaluator //! @param[in] theSurface surface evaluator //! @param[in] theX0 initial guess {t, u, v} //! @param[in] theBounds box bounds for {t, u, v} //! @param[in] theOptions solver options //! @return NewtonResultN<3> with t, u, v stored in X[0], X[1], X[2] template NewtonResultN<3> SolveCurveSurfaceExtrema3D(const CurveEvaluator& theCurve, const SurfaceEvaluator& theSurface, const std::array& theX0, const NewtonBoundsN<3>& theBounds, const NewtonOptions& theOptions = NewtonOptions()) { auto aFunc = [&theCurve, &theSurface](double theT, double theU, double theV, double theF[3], double theJ[3][3]) -> bool { gp_Pnt aPtC, aPtS; gp_Vec aD1C, aD2C; gp_Vec aD1U, aD1V, aD2UU, aD2VV, aD2UV; theCurve.D2(theT, aPtC, aD1C, aD2C); theSurface.D2(theU, theV, aPtS, aD1U, aD1V, aD2UU, aD2VV, aD2UV); // D = C - S (vector from surface point to curve point) const gp_Vec aD(aPtS, aPtC); // Function values: gradient of ||C - S||^2 (factor 2 dropped) theF[0] = aD.Dot(aD1C); theF[1] = -aD.Dot(aD1U); theF[2] = -aD.Dot(aD1V); // Jacobian: Hessian of ||C - S||^2 (without factor 2) // J[0][0] = dF1/dt = dC/dt . dC/dt + D . d2C/dt2 theJ[0][0] = aD1C.Dot(aD1C) + aD.Dot(aD2C); // J[0][1] = dF1/du = -dS/du . dC/dt theJ[0][1] = -aD1U.Dot(aD1C); // J[0][2] = dF1/dv = -dS/dv . dC/dt theJ[0][2] = -aD1V.Dot(aD1C); // J[1][0] = dF2/dt (symmetric to J[0][1]) theJ[1][0] = theJ[0][1]; // J[1][1] = dF2/du = dS/du . dS/du - D . d2S/du2 theJ[1][1] = aD1U.Dot(aD1U) - aD.Dot(aD2UU); // J[1][2] = dF2/dv = dS/dv . dS/du - D . d2S/dudv theJ[1][2] = aD1V.Dot(aD1U) - aD.Dot(aD2UV); // J[2][0] = dF3/dt (symmetric to J[0][2]) theJ[2][0] = theJ[0][2]; // J[2][1] = dF3/du (symmetric to J[1][2]) theJ[2][1] = theJ[1][2]; // J[2][2] = dF3/dv = dS/dv . dS/dv - D . d2S/dv2 theJ[2][2] = aD1V.Dot(aD1V) - aD.Dot(aD2VV); return true; }; return Solve3D(aFunc, theX0, theBounds, theOptions); } } // namespace MathSys #endif // _MathSys_Newton3D_HeaderFile