Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-28 09:19:57

0001 // Copyright (c) 2025 OPEN CASCADE SAS
0002 //
0003 // This file is part of Open CASCADE Technology software library.
0004 //
0005 // This library is free software; you can redistribute it and/or modify it under
0006 // the terms of the GNU Lesser General Public License version 2.1 as published
0007 // by the Free Software Foundation, with special exception defined in the file
0008 // OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
0009 // distribution for complete text of the license and disclaimer of any warranty.
0010 //
0011 // Alternatively, this file may be used under the terms of Open CASCADE
0012 // commercial license or contractual agreement.
0013 
0014 #ifndef _ExtremaPC_GridEvaluator_HeaderFile
0015 #define _ExtremaPC_GridEvaluator_HeaderFile
0016 
0017 #include <Adaptor3d_Curve.hxx>
0018 #include <ExtremaPC.hxx>
0019 #include <ExtremaPC_DistanceFunction.hxx>
0020 #include <GeomGridEval.hxx>
0021 #include <math_Vector.hxx>
0022 #include <MathRoot_Newton.hxx>
0023 #include <MathUtils_Config.hxx>
0024 #include <NCollection_Array1.hxx>
0025 #include <NCollection_DynamicArray.hxx>
0026 
0027 #include <algorithm>
0028 #include <cmath>
0029 #include <limits>
0030 #include <utility>
0031 
0032 //! @brief Grid-based point-curve extrema computation class.
0033 //!
0034 //! Provides grid-based extrema finding algorithm with cached state for
0035 //! optimal performance on repeated queries. Used by BSpline, Bezier,
0036 //! Offset, and other curve evaluators.
0037 //!
0038 //! Algorithm:
0039 //! 1. Build grid of (parameter, point, D1) from GeomGridEval
0040 //! 2. Linear scan of grid to find candidate intervals (sign changes in F(u))
0041 //! 3. Newton refinement on each candidate
0042 //! 4. Optional endpoint handling
0043 //!
0044 //! All temporary vectors are stored as mutable fields and reused via Clear()
0045 //! to avoid repeated heap allocations.
0046 class ExtremaPC_GridEvaluator
0047 {
0048 public:
0049   //! Cached grid point with pre-computed data.
0050   struct GridPoint
0051   {
0052     double Param; //!< Parameter value
0053     gp_Pnt Point; //!< Curve point C(u)
0054     gp_Vec D1;    //!< First derivative C'(u)
0055   };
0056 
0057   //! Type of candidate extremum detected during grid scan.
0058   enum class CandidateType
0059   {
0060     SignChange, //!< F(u) changes sign between grid points
0061     NearZero    //!< F(u) is very small at grid point
0062   };
0063 
0064   //! Candidate interval for Newton refinement.
0065   struct Candidate
0066   {
0067     CandidateType Type;   //!< Type of candidate
0068     int           IdxLo;  //!< Lower grid index
0069     int           IdxHi;  //!< Upper grid index (same as IdxLo for NearZero)
0070     double        StartU; //!< Starting point for Newton
0071   };
0072 
0073   //! Default constructor.
0074   ExtremaPC_GridEvaluator() = default;
0075 
0076   //! @brief Build grid from GeomGridEval D1 results.
0077   //!
0078   //! @tparam GridEval type with EvaluateGridD1(params) method
0079   //! @param theEval grid evaluator
0080   //! @param theParams parameter values (math_Vector with Array1() accessor)
0081   template <typename GridEval>
0082   void BuildGrid(GridEval& theEval, const math_Vector& theParams)
0083   {
0084     // Use Array1() accessor to pass to GeomGridEval which expects NCollection_Array1
0085     NCollection_Array1<GeomGridEval::CurveD1> aD1Grid = theEval.EvaluateGridD1(theParams.Array1());
0086 
0087     const int aNbParams = theParams.Length();
0088 
0089     // Resize grid if needed
0090     if (myGrid.Length() != aNbParams)
0091     {
0092       myGrid = NCollection_Array1<GridPoint>(0, aNbParams - 1);
0093     }
0094 
0095     for (int i = 0; i < aNbParams; ++i)
0096     {
0097       const int aD1Idx = aD1Grid.Lower() + i;
0098 
0099       myGrid[i].Param = theParams(theParams.Lower() + i);
0100       myGrid[i].Point = aD1Grid.Value(aD1Idx).Point;
0101       myGrid[i].D1    = aD1Grid.Value(aD1Idx).D1;
0102     }
0103   }
0104 
0105   //! Returns the cached grid.
0106   const NCollection_Array1<GridPoint>& Grid() const { return myGrid; }
0107 
0108   //! Returns mutable reference to the result for post-processing.
0109   ExtremaPC::Result& Result() const { return myResult; }
0110 
0111   //! @brief Perform extrema computation using cached grid (interior only).
0112   //!
0113   //! @param theCurve curve adaptor
0114   //! @param theP query point
0115   //! @param theDomain parameter domain
0116   //! @param theTol tolerance
0117   //! @param theMode search mode
0118   //! @return const reference to result with interior extrema only
0119   [[nodiscard]] const ExtremaPC::Result& Perform(const Adaptor3d_Curve&     theCurve,
0120                                                  const gp_Pnt&              theP,
0121                                                  const ExtremaPC::Domain1D& theDomain,
0122                                                  double                     theTol,
0123                                                  ExtremaPC::SearchMode      theMode) const
0124   {
0125     myResult.Clear();
0126     scanGrid(theP, theTol, theMode);
0127     refineCandidates(theCurve, theP, theDomain, theTol, theMode);
0128 
0129     if (!myResult.Extrema.IsEmpty())
0130     {
0131       myResult.Status = ExtremaPC::Status::OK;
0132     }
0133     return myResult;
0134   }
0135 
0136   //! @brief Build uniform parameter grid.
0137   //! @return math_Vector with 1-based indexing
0138   static math_Vector BuildUniformParams(double theUMin, double theUMax, int theNbSamples)
0139   {
0140     math_Vector  aParams(1, theNbSamples);
0141     const double aStep = (theUMax - theUMin) / (theNbSamples - 1);
0142 
0143     for (int i = 1; i <= theNbSamples; ++i)
0144     {
0145       aParams(i) = theUMin + (i - 1) * aStep;
0146     }
0147     aParams(theNbSamples) = theUMax; // Ensure exact endpoint
0148 
0149     return aParams;
0150   }
0151 
0152 private:
0153   //! @brief Scan grid to find candidate intervals for extrema.
0154   void scanGrid(const gp_Pnt& theP, double theTol, ExtremaPC::SearchMode theMode) const
0155   {
0156     myCandidates.Clear();
0157     const int aNbGrid = myGrid.Length();
0158 
0159     if (aNbGrid < 2)
0160     {
0161       return;
0162     }
0163 
0164     // Resize processed array if needed
0165     if (myProcessed.Length() != aNbGrid)
0166     {
0167       myProcessed = NCollection_Array1<bool>(0, aNbGrid - 1);
0168     }
0169     myProcessed.Init(false);
0170 
0171     double aPrevF     = 0.0;
0172     double aPrevDist  = 0.0;
0173     bool   aPrevValid = false;
0174 
0175     for (int i = 0; i < aNbGrid; ++i)
0176     {
0177       const GridPoint& aGP = myGrid[i];
0178 
0179       // Compute distance function value: F(u) = (C(u) - P) . C'(u)
0180       gp_Vec aVec(theP, aGP.Point);
0181       double aF    = aVec.Dot(aGP.D1);
0182       double aDist = aVec.SquareMagnitude();
0183 
0184       // Check for sign change with previous point
0185       if (aPrevValid && aPrevF * aF < 0.0 && !myProcessed[i - 1])
0186       {
0187         Candidate aCand;
0188         aCand.Type  = CandidateType::SignChange;
0189         aCand.IdxLo = i - 1;
0190         aCand.IdxHi = i;
0191         // Use linear interpolation for better starting point (secant method)
0192         double aFLo  = aPrevF;
0193         double aFHi  = aF;
0194         double aULo  = myGrid[i - 1].Param;
0195         double aUHi  = aGP.Param;
0196         aCand.StartU = aULo - aFLo * (aUHi - aULo) / (aFHi - aFLo);
0197         myCandidates.Append(aCand);
0198         myProcessed[i - 1] = true;
0199         myProcessed[i]     = true;
0200       }
0201 
0202       // Check for near-zero F (direct hit on extremum)
0203       if (std::abs(aF) < theTol * ExtremaPC::THE_NEAR_ZERO_F_FACTOR && !myProcessed[i])
0204       {
0205         Candidate aCand;
0206         aCand.Type   = CandidateType::NearZero;
0207         aCand.IdxLo  = i;
0208         aCand.IdxHi  = i;
0209         aCand.StartU = aGP.Param;
0210         myCandidates.Append(aCand);
0211         myProcessed[i] = true;
0212       }
0213 
0214       // Check for local extremum by distance comparison (3-point test)
0215       if (i > 0 && i < aNbGrid - 1 && !myProcessed[i])
0216       {
0217         double aNextDist = theP.SquareDistance(myGrid[i + 1].Point);
0218 
0219         // Local minimum: distance decreases then increases
0220         bool aIsLocalMin = (aDist <= aPrevDist && aDist <= aNextDist);
0221         // Local maximum: distance increases then decreases
0222         bool aIsLocalMax = (aDist >= aPrevDist && aDist >= aNextDist);
0223 
0224         if ((theMode == ExtremaPC::SearchMode::Min || theMode == ExtremaPC::SearchMode::MinMax)
0225             && aIsLocalMin)
0226         {
0227           Candidate aCand;
0228           aCand.Type   = CandidateType::NearZero;
0229           aCand.IdxLo  = i;
0230           aCand.IdxHi  = i;
0231           aCand.StartU = aGP.Param;
0232           myCandidates.Append(aCand);
0233           myProcessed[i] = true;
0234         }
0235         else if ((theMode == ExtremaPC::SearchMode::Max || theMode == ExtremaPC::SearchMode::MinMax)
0236                  && aIsLocalMax && !aIsLocalMin)
0237         {
0238           Candidate aCand;
0239           aCand.Type   = CandidateType::NearZero;
0240           aCand.IdxLo  = i;
0241           aCand.IdxHi  = i;
0242           aCand.StartU = aGP.Param;
0243           myCandidates.Append(aCand);
0244           myProcessed[i] = true;
0245         }
0246       }
0247 
0248       aPrevF     = aF;
0249       aPrevDist  = aDist;
0250       aPrevValid = true;
0251     }
0252   }
0253 
0254   //! @brief Refine candidates using Newton's method.
0255   void refineCandidates(const Adaptor3d_Curve&     theCurve,
0256                         const gp_Pnt&              theP,
0257                         const ExtremaPC::Domain1D& theDomain,
0258                         double                     theTol,
0259                         ExtremaPC::SearchMode      theMode) const
0260   {
0261     myResult.Status = ExtremaPC::Status::OK;
0262     myFoundRoots.Clear();
0263     mySortedIndices.Clear();
0264 
0265     ExtremaPC_DistanceFunction aFunc(theCurve, theP);
0266 
0267     // Newton configuration
0268     MathUtils::Config aConfig;
0269     aConfig.XTolerance    = theTol * ExtremaPC::THE_NEWTON_XTOL_FACTOR;
0270     aConfig.FTolerance    = theTol * ExtremaPC::THE_NEWTON_FTOL_FACTOR;
0271     aConfig.MaxIterations = ExtremaPC::THE_MAX_NEWTON_ITERATIONS;
0272 
0273     // Build sorted indices by estimated distance
0274     for (int c = 0; c < myCandidates.Length(); ++c)
0275     {
0276       const Candidate& aCand     = myCandidates.Value(c);
0277       double           anEstDist = theP.SquareDistance(myGrid[aCand.IdxLo].Point);
0278       mySortedIndices.Append(std::make_pair(c, anEstDist));
0279     }
0280 
0281     // Sort by estimated distance for Min mode (ascending), Max mode (descending)
0282     if (theMode == ExtremaPC::SearchMode::Min)
0283     {
0284       std::sort(mySortedIndices.begin(),
0285                 mySortedIndices.end(),
0286                 [](const std::pair<int, double>& a, const std::pair<int, double>& b) {
0287                   return a.second < b.second;
0288                 });
0289     }
0290     else if (theMode == ExtremaPC::SearchMode::Max)
0291     {
0292       std::sort(mySortedIndices.begin(),
0293                 mySortedIndices.end(),
0294                 [](const std::pair<int, double>& a, const std::pair<int, double>& b) {
0295                   return a.second > b.second;
0296                 });
0297     }
0298 
0299     // Best distance found so far (for early termination)
0300     double aBestSqDist = (theMode == ExtremaPC::SearchMode::Min)
0301                            ? std::numeric_limits<double>::max()
0302                            : -std::numeric_limits<double>::max();
0303 
0304     for (int s = 0; s < mySortedIndices.Length(); ++s)
0305     {
0306       int              c         = mySortedIndices.Value(s).first;
0307       double           anEstDist = mySortedIndices.Value(s).second;
0308       const Candidate& aCand     = myCandidates.Value(c);
0309 
0310       // Early termination: skip candidates that are clearly worse than the best found.
0311       // For Min mode: skip if estimated distance > best * (2.0 - threshold), i.e., ~1.1x best.
0312       // For Max mode: skip if estimated distance < best * threshold, i.e., ~0.9x best.
0313       constexpr double aMinSkipThreshold = 2.0 - ExtremaPC::THE_MAX_SKIP_THRESHOLD;
0314       if (theMode == ExtremaPC::SearchMode::Min && anEstDist > aBestSqDist * aMinSkipThreshold)
0315       {
0316         break;
0317       }
0318       if (theMode == ExtremaPC::SearchMode::Max
0319           && anEstDist < aBestSqDist * ExtremaPC::THE_MAX_SKIP_THRESHOLD)
0320       {
0321         break;
0322       }
0323 
0324       // Skip if too close to already found root
0325       bool aSkip = false;
0326       for (int r = 0; r < myFoundRoots.Length(); ++r)
0327       {
0328         if (std::abs(aCand.StartU - myFoundRoots.Value(r)) < theTol)
0329         {
0330           aSkip = true;
0331           break;
0332         }
0333       }
0334       if (aSkip)
0335       {
0336         continue;
0337       }
0338 
0339       // Determine Newton bounds
0340       double aULo, aUHi;
0341       if (aCand.Type == CandidateType::SignChange)
0342       {
0343         aULo = myGrid[aCand.IdxLo].Param;
0344         aUHi = myGrid[aCand.IdxHi].Param;
0345       }
0346       else
0347       {
0348         double aExpand = (theDomain.Max - theDomain.Min) * ExtremaPC::THE_INTERVAL_EXPAND_RATIO;
0349         aULo           = std::max(theDomain.Min, aCand.StartU - aExpand);
0350         aUHi           = std::min(theDomain.Max, aCand.StartU + aExpand);
0351       }
0352 
0353       // Try Newton refinement
0354       MathUtils::ScalarResult aNewtonRes =
0355         MathRoot::NewtonBounded(aFunc, aCand.StartU, aULo, aUHi, aConfig);
0356 
0357       double aRootU     = 0.0;
0358       bool   aConverged = false;
0359 
0360       if (aNewtonRes.IsDone())
0361       {
0362         aRootU     = std::max(theDomain.Min, std::min(theDomain.Max, *aNewtonRes.Root));
0363         aConverged = true;
0364       }
0365       else
0366       {
0367         // Try iterative grid refinement as fallback
0368         double aBestU    = aCand.StartU;
0369         double aBestDist = std::numeric_limits<double>::max();
0370         double aRefUMin  = aULo;
0371         double aRefUMax  = aUHi;
0372 
0373         for (int aPass = 0; aPass < ExtremaPC::THE_REFINEMENT_NB_PASSES; ++aPass)
0374         {
0375           const int    aNbSamples = ExtremaPC::THE_REFINEMENT_NB_SAMPLES;
0376           const double aStep      = (aRefUMax - aRefUMin) / (aNbSamples - 1);
0377 
0378           for (int i = 0; i < aNbSamples; ++i)
0379           {
0380             double aU    = aRefUMin + i * aStep;
0381             gp_Pnt aPt   = theCurve.Value(aU);
0382             double aDist = theP.SquareDistance(aPt);
0383 
0384             if (aDist < aBestDist)
0385             {
0386               aBestDist = aDist;
0387               aBestU    = aU;
0388             }
0389           }
0390 
0391           // Narrow range
0392           double aRangeHalf = (aRefUMax - aRefUMin) * ExtremaPC::THE_RANGE_NARROWING_FACTOR * 0.5;
0393           aRefUMin          = std::max(theDomain.Min, aBestU - aRangeHalf);
0394           aRefUMax          = std::min(theDomain.Max, aBestU + aRangeHalf);
0395 
0396           // Try Newton with refined point
0397           MathUtils::ScalarResult aRetryRes =
0398             MathRoot::NewtonBounded(aFunc, aBestU, aRefUMin, aRefUMax, aConfig);
0399           if (aRetryRes.IsDone())
0400           {
0401             aRootU     = std::max(theDomain.Min, std::min(theDomain.Max, *aRetryRes.Root));
0402             aConverged = true;
0403             break;
0404           }
0405         }
0406 
0407         // Use best grid point as fallback
0408         if (!aConverged)
0409         {
0410           gp_Pnt aPt;
0411           gp_Vec aD1;
0412           theCurve.D1(aBestU, aPt, aD1);
0413           gp_Vec aVec(theP, aPt);
0414           double aF = aVec.Dot(aD1);
0415 
0416           if (std::abs(aF) < theTol * ExtremaPC::THE_FALLBACK_F_FACTOR)
0417           {
0418             aRootU     = aBestU;
0419             aConverged = true;
0420           }
0421         }
0422       }
0423 
0424       if (!aConverged)
0425         continue;
0426 
0427       // Check for duplicate
0428       bool aDuplicate = false;
0429       for (int r = 0; r < myFoundRoots.Length(); ++r)
0430       {
0431         if (std::abs(aRootU - myFoundRoots.Value(r)) < theTol)
0432         {
0433           aDuplicate = true;
0434           break;
0435         }
0436       }
0437       if (aDuplicate)
0438         continue;
0439 
0440       gp_Pnt aPt     = theCurve.Value(aRootU);
0441       double aSqDist = theP.SquareDistance(aPt);
0442 
0443       // Classify as min/max using neighbor sampling
0444       double aStep = (theDomain.Max - theDomain.Min) * ExtremaPC::THE_REFINEMENT_STEP_RATIO;
0445       double aDistPlus =
0446         theP.SquareDistance(theCurve.Value(std::min(theDomain.Max, aRootU + aStep)));
0447       double aDistMinus =
0448         theP.SquareDistance(theCurve.Value(std::max(theDomain.Min, aRootU - aStep)));
0449       bool aIsMin = (aSqDist <= aDistPlus) && (aSqDist <= aDistMinus);
0450 
0451       // Filter by mode
0452       bool aKeep = false;
0453       if (theMode == ExtremaPC::SearchMode::MinMax)
0454       {
0455         aKeep = true;
0456       }
0457       else if (theMode == ExtremaPC::SearchMode::Min && aIsMin)
0458       {
0459         aKeep = true;
0460       }
0461       else if (theMode == ExtremaPC::SearchMode::Max && !aIsMin)
0462       {
0463         aKeep = true;
0464       }
0465 
0466       if (aKeep)
0467       {
0468         ExtremaPC::ExtremumResult anExt;
0469         anExt.Parameter      = aRootU;
0470         anExt.Point          = aPt;
0471         anExt.SquareDistance = aSqDist;
0472         anExt.IsMinimum      = aIsMin;
0473         myResult.Extrema.Append(anExt);
0474 
0475         myFoundRoots.Append(aRootU);
0476 
0477         // Update best distance for early termination
0478         if (theMode == ExtremaPC::SearchMode::Min && aSqDist < aBestSqDist)
0479         {
0480           aBestSqDist = aSqDist;
0481         }
0482         else if (theMode == ExtremaPC::SearchMode::Max && aSqDist > aBestSqDist)
0483         {
0484           aBestSqDist = aSqDist;
0485         }
0486       }
0487     }
0488 
0489     if (myResult.Extrema.IsEmpty() && myCandidates.IsEmpty())
0490     {
0491       myResult.Status = ExtremaPC::Status::NoSolution;
0492     }
0493   }
0494 
0495 private:
0496   NCollection_Array1<GridPoint> myGrid; //!< Cached grid
0497 
0498   // Mutable cached temporaries (reused via Clear())
0499   mutable ExtremaPC::Result                   myResult;     //!< Reusable result
0500   mutable NCollection_DynamicArray<Candidate> myCandidates; //!< Candidates from grid scan
0501   mutable NCollection_DynamicArray<double>    myFoundRoots; //!< Found roots for dedup
0502   mutable NCollection_DynamicArray<std::pair<int, double>>
0503                                    mySortedIndices; //!< Sorted candidate indices
0504   mutable NCollection_Array1<bool> myProcessed;     //!< Processed flags for grid scan
0505 };
0506 
0507 #endif // _ExtremaPC_GridEvaluator_HeaderFile