Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-28 09:20:51

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 _MathOpt_PSO_HeaderFile
0015 #define _MathOpt_PSO_HeaderFile
0016 
0017 #include <MathUtils_Types.hxx>
0018 #include <MathUtils_Config.hxx>
0019 #include <MathUtils_Core.hxx>
0020 #include <MathUtils_LineSearch.hxx>
0021 #include <MathUtils_Random.hxx>
0022 
0023 #include <NCollection_DynamicArray.hxx>
0024 
0025 #include <cmath>
0026 #include <optional>
0027 
0028 namespace MathOpt
0029 {
0030 using namespace MathUtils;
0031 
0032 //! Initialization mode for PSO particles.
0033 enum class PSOInitMode
0034 {
0035   RandomOnly,      //!< All particles randomly initialized (default)
0036   SeededOnly,      //!< Initialize from seed particles only
0037   SeededPlusRandom //!< Seed particles + remaining random
0038 };
0039 
0040 //! Boundary handling mode for particles leaving the search space.
0041 enum class PSOBoundaryMode
0042 {
0043   Clamp,   //!< Clamp to bound, reverse velocity with -0.5 damping (default)
0044   Reflect, //!< Reflect coordinate, damp velocity by -0.5
0045   Wrap     //!< Periodic wrap into bounds
0046 };
0047 
0048 //! Inertia weight schedule.
0049 enum class PSOInertiaSchedule
0050 {
0051   Constant,   //!< Fixed omega (default)
0052   LinearDecay //!< Linear decay from Omega to OmegaMin over iterations
0053 };
0054 
0055 //! Seed particle for PSO initialization.
0056 struct PSOSeedParticle
0057 {
0058   math_Vector                Position; //!< Initial position (will be clamped to bounds)
0059   std::optional<double>      Value;    //!< Known function value (nullopt = will be evaluated)
0060   std::optional<math_Vector> Velocity; //!< Initial velocity (nullopt = generate bounded random)
0061 
0062   PSOSeedParticle(const math_Vector& thePos)
0063       : Position(thePos)
0064   {
0065   }
0066 
0067   PSOSeedParticle(const math_Vector& thePos, const double theValue)
0068       : Position(thePos),
0069         Value(theValue)
0070   {
0071   }
0072 };
0073 
0074 //! Statistics collected during PSO execution.
0075 struct PSOStats
0076 {
0077   int    NbFunctionEvals       = 0;                        //!< Total function evaluations
0078   int    NbIterations          = 0;                        //!< Iterations performed
0079   int    NbBoundaryCorrections = 0;                        //!< Boundary corrections applied
0080   int    NbStagnationEvents    = 0;                        //!< Times stagnation was detected
0081   int    NbRestarts            = 0;                        //!< Restarts performed
0082   double InitialBest = std::numeric_limits<double>::max(); //!< Best value after initialization
0083   double FinalBest   = std::numeric_limits<double>::max(); //!< Best value at termination
0084 };
0085 
0086 //! Configuration for Particle Swarm Optimization.
0087 struct PSOConfig : NDimConfig
0088 {
0089   int          NbParticles   = 40;  //!< Number of particles in the swarm
0090   double       Omega         = 0.7; //!< Inertia weight (velocity decay)
0091   double       PhiPersonal   = 1.5; //!< Personal best attraction coefficient
0092   double       PhiGlobal     = 1.5; //!< Global best attraction coefficient
0093   double       VelocityClamp = 0.5; //!< Max velocity as fraction of search space
0094   unsigned int Seed          = 6;   //!< Random seed for reproducibility
0095 
0096   PSOInitMode           InitMode        = PSOInitMode::RandomOnly;
0097   PSOBoundaryMode       BoundaryMode    = PSOBoundaryMode::Clamp;
0098   PSOInertiaSchedule    InertiaSchedule = PSOInertiaSchedule::Constant;
0099   double                OmegaMin        = 0.4; //!< Min inertia for LinearDecay
0100   int                   MinIterations   = 0;   //!< Minimum iterations before convergence
0101   std::optional<double> TargetValue;          //!< Early stop if best <= target (nullopt = disabled)
0102   double                NoImproveTol   = 0.0; //!< Stagnation tolerance (0 = use Tolerance)
0103   int                   NoImproveIters = 10;  //!< Stagnation iteration threshold
0104   double RestartFraction    = 0.0; //!< Fraction of particles to reinitialize (0 = no restarts)
0105   int    MaxRestarts        = 0;   //!< Maximum restart count (0 = unlimited when fraction > 0)
0106   int    PolishBudgetPerDim = 50;  //!< Max polishing evals per dimension (0 = no polishing)
0107 
0108   //! Default constructor.
0109   PSOConfig()
0110       : NDimConfig(1.0e-8, 100, true)
0111   {
0112   }
0113 
0114   //! Constructor with parameters.
0115   PSOConfig(int theNbParticles, int theMaxIter = 100, double theTolerance = 1.0e-8)
0116       : NDimConfig(theTolerance, theMaxIter, true),
0117         NbParticles(theNbParticles)
0118   {
0119   }
0120 };
0121 
0122 //! Coordinate-wise polishing using Brent's 1D minimization.
0123 //! For each dimension, performs Brent's method directly on the single coordinate,
0124 //! modifying only one element of the position vector per evaluation (zero-allocation).
0125 //!
0126 //! @tparam Function type with Value(const math_Vector&, double&) method
0127 //! @param theFunc function to minimize
0128 //! @param thePosition current best position (updated in place)
0129 //! @param theValue current best value (updated in place)
0130 //! @param theLowerBounds lower bounds for each variable
0131 //! @param theUpperBounds upper bounds for each variable
0132 //! @param theTolerance convergence tolerance for Brent's method
0133 //! @param theMaxPolishEvals maximum total function evaluations for polishing
0134 //! @param theEvalCount output: number of function evaluations used
0135 template <typename Function>
0136 void PolishCoordinateWise(Function&          theFunc,
0137                           math_Vector&       thePosition,
0138                           double&            theValue,
0139                           const math_Vector& theLowerBounds,
0140                           const math_Vector& theUpperBounds,
0141                           double             theTolerance,
0142                           int                theMaxPolishEvals,
0143                           int&               theEvalCount)
0144 {
0145   theEvalCount = 0;
0146 
0147   const int aLower  = thePosition.Lower();
0148   const int aUpper  = thePosition.Upper();
0149   const int aNbDims = aUpper - aLower + 1;
0150 
0151   // Fair per-coordinate cap: total budget split across dimensions and passes
0152   const int aNbPasses      = (aNbDims > 1) ? 2 : 1;
0153   const int aPerCoordLimit = std::max(1, theMaxPolishEvals / (aNbDims * aNbPasses));
0154 
0155   for (int aDimIdx = aLower; aDimIdx <= aUpper; ++aDimIdx)
0156   {
0157     if (theEvalCount >= theMaxPolishEvals)
0158     {
0159       break;
0160     }
0161 
0162     const int aMaxIter = std::min(aPerCoordLimit, theMaxPolishEvals - theEvalCount);
0163     BrentAlongCoordinate(theFunc,
0164                          thePosition,
0165                          aDimIdx,
0166                          theLowerBounds(aDimIdx),
0167                          theUpperBounds(aDimIdx),
0168                          theValue,
0169                          theTolerance,
0170                          aMaxIter,
0171                          theEvalCount);
0172   }
0173 
0174   // Second pass for non-separable functions - dimensions may interact
0175   if (theEvalCount < theMaxPolishEvals && aNbDims > 1)
0176   {
0177     for (int aDimIdx = aLower; aDimIdx <= aUpper; ++aDimIdx)
0178     {
0179       if (theEvalCount >= theMaxPolishEvals)
0180       {
0181         break;
0182       }
0183 
0184       const int aMaxIter = std::min(aPerCoordLimit, theMaxPolishEvals - theEvalCount);
0185       BrentAlongCoordinate(theFunc,
0186                            thePosition,
0187                            aDimIdx,
0188                            theLowerBounds(aDimIdx),
0189                            theUpperBounds(aDimIdx),
0190                            theValue,
0191                            theTolerance,
0192                            aMaxIter,
0193                            theEvalCount);
0194     }
0195   }
0196 }
0197 
0198 //! Particle Swarm Optimization for global minimization.
0199 //!
0200 //! PSO is a stochastic, population-based optimization algorithm inspired
0201 //! by social behavior of bird flocking or fish schooling.
0202 //!
0203 //! Algorithm:
0204 //! 1. Initialize particles with random positions and velocities
0205 //! 2. Evaluate fitness (function value) for each particle
0206 //! 3. Update each particle's personal best if current position is better
0207 //! 4. Update global best across all particles
0208 //! 5. Update velocities: v = omega*v + c1*r1*(pbest-x) + c2*r2*(gbest-x)
0209 //! 6. Update positions: x = x + v
0210 //! 7. Repeat until convergence or max iterations
0211 //!
0212 //! Properties:
0213 //! - Gradient-free: does not require derivatives
0214 //! - Global: can escape local minima
0215 //! - Stochastic: results depend on random initialization
0216 //! - Bound-constrained: handles box constraints naturally
0217 //!
0218 //! @tparam Function type with Value(const math_Vector&, double&) method
0219 //! @param theFunc function to minimize
0220 //! @param theLowerBounds lower bounds for each variable
0221 //! @param theUpperBounds upper bounds for each variable
0222 //! @param theConfig PSO configuration
0223 //! @param theSeeds optional seed particles for initialization
0224 //! @param theStats optional output statistics
0225 //! @return result containing best solution found
0226 template <typename Function>
0227 VectorResult PSO(Function&                                        theFunc,
0228                  const math_Vector&                               theLowerBounds,
0229                  const math_Vector&                               theUpperBounds,
0230                  const PSOConfig&                                 theConfig,
0231                  const NCollection_DynamicArray<PSOSeedParticle>* theSeeds,
0232                  PSOStats*                                        theStats = nullptr)
0233 {
0234   VectorResult aResult;
0235 
0236   const int aLower  = theLowerBounds.Lower();
0237   const int aUpper  = theLowerBounds.Upper();
0238   const int aNbDims = aUpper - aLower + 1;
0239 
0240   // Check dimensions
0241   if (theUpperBounds.Length() != aNbDims)
0242   {
0243     aResult.Status = Status::InvalidInput;
0244     return aResult;
0245   }
0246 
0247   // Check bounds
0248   for (int aDimIdx = aLower; aDimIdx <= aUpper; ++aDimIdx)
0249   {
0250     if (theLowerBounds(aDimIdx) >= theUpperBounds(aDimIdx))
0251     {
0252       aResult.Status = Status::InvalidInput;
0253       return aResult;
0254     }
0255   }
0256 
0257   const int aNbParticles = theConfig.NbParticles;
0258   if (aNbParticles <= 0)
0259   {
0260     aResult.Status = Status::InvalidInput;
0261     return aResult;
0262   }
0263 
0264   // Initialize stats
0265   PSOStats aLocalStats;
0266 
0267   // Particle data
0268   struct Particle
0269   {
0270     math_Vector Position;
0271     math_Vector Velocity;
0272     math_Vector BestPosition;
0273     double      BestValue;
0274     double      CurrentValue;
0275 
0276     Particle(int theLower, int theUpper)
0277         : Position(theLower, theUpper),
0278           Velocity(theLower, theUpper),
0279           BestPosition(theLower, theUpper),
0280           BestValue(std::numeric_limits<double>::max()),
0281           CurrentValue(std::numeric_limits<double>::max())
0282     {
0283     }
0284   };
0285 
0286   NCollection_DynamicArray<Particle> aSwarm;
0287   for (int aPartIdx = 0; aPartIdx < aNbParticles; ++aPartIdx)
0288   {
0289     aSwarm.Append(Particle(aLower, aUpper));
0290   }
0291 
0292   // Random number generator
0293   MathUtils::RandomGenerator aRNG(theConfig.Seed);
0294 
0295   // Compute velocity limits
0296   math_Vector aVelMax(aLower, aUpper);
0297   math_Vector aRange(aLower, aUpper);
0298   for (int aDimIdx = aLower; aDimIdx <= aUpper; ++aDimIdx)
0299   {
0300     aRange(aDimIdx)  = theUpperBounds(aDimIdx) - theLowerBounds(aDimIdx);
0301     aVelMax(aDimIdx) = theConfig.VelocityClamp * aRange(aDimIdx);
0302   }
0303 
0304   // Initialize particles
0305   math_Vector aGlobalBest(aLower, aUpper);
0306   double      aGlobalBestValue = std::numeric_limits<double>::max();
0307 
0308   // Determine how many particles are seeded
0309   const int aNbSeeds = (theSeeds != nullptr) ? theSeeds->Length() : 0;
0310   int       aSeeded  = 0;
0311 
0312   // Validate seed dimensions and index ranges
0313   if (aNbSeeds > 0)
0314   {
0315     for (int aSeedIdx = 0; aSeedIdx < aNbSeeds; ++aSeedIdx)
0316     {
0317       const PSOSeedParticle& aSeed = theSeeds->Value(aSeedIdx);
0318       if (aSeed.Position.Lower() != aLower || aSeed.Position.Upper() != aUpper)
0319       {
0320         aResult.Status = Status::InvalidInput;
0321         return aResult;
0322       }
0323       if (aSeed.Velocity.has_value()
0324           && (aSeed.Velocity->Lower() != aLower || aSeed.Velocity->Upper() != aUpper))
0325       {
0326         aResult.Status = Status::InvalidInput;
0327         return aResult;
0328       }
0329     }
0330   }
0331 
0332   // Validate SeededOnly requires at least one seed
0333   if (theConfig.InitMode == PSOInitMode::SeededOnly && aNbSeeds == 0)
0334   {
0335     aResult.Status = Status::InvalidInput;
0336     return aResult;
0337   }
0338 
0339   if (aNbSeeds > 0
0340       && (theConfig.InitMode == PSOInitMode::SeededPlusRandom
0341           || theConfig.InitMode == PSOInitMode::SeededOnly))
0342   {
0343     // Initialize from seeds
0344     const int aNbDirect = std::min(aNbSeeds, aNbParticles);
0345     for (int aSeedIdx = 0; aSeedIdx < aNbDirect; ++aSeedIdx)
0346     {
0347       Particle&              aParticle = aSwarm.ChangeValue(aSeedIdx);
0348       const PSOSeedParticle& aSeed     = theSeeds->Value(aSeedIdx);
0349 
0350       // Clamp seed position to bounds, track if any coordinate was clamped
0351       bool aWasClamped = false;
0352       for (int aDimIdx = aLower; aDimIdx <= aUpper; ++aDimIdx)
0353       {
0354         const double aClamped = MathUtils::Clamp(aSeed.Position(aDimIdx),
0355                                                  theLowerBounds(aDimIdx),
0356                                                  theUpperBounds(aDimIdx));
0357         if (aClamped != aSeed.Position(aDimIdx))
0358         {
0359           aWasClamped = true;
0360         }
0361         aParticle.Position(aDimIdx) = aClamped;
0362       }
0363 
0364       // Use seed velocity or generate random
0365       if (aSeed.Velocity.has_value())
0366       {
0367         const math_Vector& aSeedVel = *aSeed.Velocity;
0368         for (int aDimIdx = aLower; aDimIdx <= aUpper; ++aDimIdx)
0369         {
0370           aParticle.Velocity(aDimIdx) =
0371             MathUtils::Clamp(aSeedVel(aDimIdx), -aVelMax(aDimIdx), aVelMax(aDimIdx));
0372         }
0373       }
0374       else
0375       {
0376         for (int aDimIdx = aLower; aDimIdx <= aUpper; ++aDimIdx)
0377         {
0378           aParticle.Velocity(aDimIdx) = (2.0 * aRNG.NextReal() - 1.0) * aVelMax(aDimIdx);
0379         }
0380       }
0381 
0382       // Use known value only if position was not clamped; otherwise re-evaluate
0383       if (aSeed.Value.has_value() && !aWasClamped)
0384       {
0385         aParticle.CurrentValue = *aSeed.Value;
0386       }
0387       else
0388       {
0389         if (!theFunc.Value(aParticle.Position, aParticle.CurrentValue))
0390         {
0391           aParticle.CurrentValue = std::numeric_limits<double>::max();
0392         }
0393         ++aLocalStats.NbFunctionEvals;
0394       }
0395 
0396       aParticle.BestPosition = aParticle.Position;
0397       aParticle.BestValue    = aParticle.CurrentValue;
0398 
0399       if (aParticle.BestValue < aGlobalBestValue)
0400       {
0401         aGlobalBestValue = aParticle.BestValue;
0402         aGlobalBest      = aParticle.BestPosition;
0403       }
0404     }
0405     aSeeded = aNbDirect;
0406 
0407     // For SeededOnly: if seeds < NbParticles, jitter best seeds to fill
0408     if (theConfig.InitMode == PSOInitMode::SeededOnly && aSeeded < aNbParticles)
0409     {
0410       for (int aPartIdx = aSeeded; aPartIdx < aNbParticles; ++aPartIdx)
0411       {
0412         Particle& aParticle = aSwarm.ChangeValue(aPartIdx);
0413         // Pick a seed to jitter (round-robin)
0414         const int              aSrcIdx = aPartIdx % aSeeded;
0415         const PSOSeedParticle& aSrc    = theSeeds->Value(aSrcIdx);
0416 
0417         for (int aDimIdx = aLower; aDimIdx <= aUpper; ++aDimIdx)
0418         {
0419           double aJitter              = (2.0 * aRNG.NextReal() - 1.0) * 0.1 * aRange(aDimIdx);
0420           aParticle.Position(aDimIdx) = MathUtils::Clamp(aSrc.Position(aDimIdx) + aJitter,
0421                                                          theLowerBounds(aDimIdx),
0422                                                          theUpperBounds(aDimIdx));
0423           aParticle.Velocity(aDimIdx) = (2.0 * aRNG.NextReal() - 1.0) * aVelMax(aDimIdx);
0424         }
0425 
0426         if (!theFunc.Value(aParticle.Position, aParticle.CurrentValue))
0427         {
0428           aParticle.CurrentValue = std::numeric_limits<double>::max();
0429         }
0430         ++aLocalStats.NbFunctionEvals;
0431 
0432         aParticle.BestPosition = aParticle.Position;
0433         aParticle.BestValue    = aParticle.CurrentValue;
0434 
0435         if (aParticle.BestValue < aGlobalBestValue)
0436         {
0437           aGlobalBestValue = aParticle.BestValue;
0438           aGlobalBest      = aParticle.BestPosition;
0439         }
0440       }
0441       aSeeded = aNbParticles;
0442     }
0443   }
0444 
0445   // Fill remaining particles randomly
0446   for (int aPartIdx = aSeeded; aPartIdx < aNbParticles; ++aPartIdx)
0447   {
0448     Particle& aParticle = aSwarm.ChangeValue(aPartIdx);
0449     for (int aDimIdx = aLower; aDimIdx <= aUpper; ++aDimIdx)
0450     {
0451       aParticle.Position(aDimIdx) = theLowerBounds(aDimIdx) + aRNG.NextReal() * aRange(aDimIdx);
0452       aParticle.Velocity(aDimIdx) = (2.0 * aRNG.NextReal() - 1.0) * aVelMax(aDimIdx);
0453     }
0454 
0455     if (!theFunc.Value(aParticle.Position, aParticle.CurrentValue))
0456     {
0457       aParticle.CurrentValue = std::numeric_limits<double>::max();
0458     }
0459     ++aLocalStats.NbFunctionEvals;
0460 
0461     aParticle.BestPosition = aParticle.Position;
0462     aParticle.BestValue    = aParticle.CurrentValue;
0463 
0464     if (aParticle.BestValue < aGlobalBestValue)
0465     {
0466       aGlobalBestValue = aParticle.BestValue;
0467       aGlobalBest      = aParticle.BestPosition;
0468     }
0469   }
0470 
0471   aLocalStats.InitialBest = aGlobalBestValue;
0472 
0473   // Main PSO loop
0474   double       aPrevBest        = aGlobalBestValue;
0475   int          aStagnationCount = 0;
0476   const double aStagnTol =
0477     (theConfig.NoImproveTol > 0.0) ? theConfig.NoImproveTol : theConfig.Tolerance;
0478 
0479   for (int anIter = 0; anIter < theConfig.MaxIterations; ++anIter)
0480   {
0481     aResult.NbIterations = anIter + 1;
0482 
0483     // Compute inertia weight
0484     double anOmega = theConfig.Omega;
0485     if (theConfig.InertiaSchedule == PSOInertiaSchedule::LinearDecay)
0486     {
0487       anOmega = theConfig.Omega
0488                 - (theConfig.Omega - theConfig.OmegaMin) * static_cast<double>(anIter)
0489                     / static_cast<double>(theConfig.MaxIterations);
0490     }
0491 
0492     // Update each particle
0493     for (int aPartIdx = 0; aPartIdx < aNbParticles; ++aPartIdx)
0494     {
0495       Particle& aParticle = aSwarm.ChangeValue(aPartIdx);
0496 
0497       // Update velocity
0498       for (int aDimIdx = aLower; aDimIdx <= aUpper; ++aDimIdx)
0499       {
0500         const double aRand1 = aRNG.NextReal();
0501         const double aRand2 = aRNG.NextReal();
0502 
0503         double aVnew =
0504           anOmega * aParticle.Velocity(aDimIdx)
0505           + theConfig.PhiPersonal * aRand1
0506               * (aParticle.BestPosition(aDimIdx) - aParticle.Position(aDimIdx))
0507           + theConfig.PhiGlobal * aRand2 * (aGlobalBest(aDimIdx) - aParticle.Position(aDimIdx));
0508 
0509         // Clamp velocity
0510         aVnew                       = MathUtils::Clamp(aVnew, -aVelMax(aDimIdx), aVelMax(aDimIdx));
0511         aParticle.Velocity(aDimIdx) = aVnew;
0512       }
0513 
0514       // Update position with boundary handling
0515       for (int aDimIdx = aLower; aDimIdx <= aUpper; ++aDimIdx)
0516       {
0517         double aXnew = aParticle.Position(aDimIdx) + aParticle.Velocity(aDimIdx);
0518 
0519         if (aXnew < theLowerBounds(aDimIdx) || aXnew > theUpperBounds(aDimIdx))
0520         {
0521           ++aLocalStats.NbBoundaryCorrections;
0522 
0523           switch (theConfig.BoundaryMode)
0524           {
0525             case PSOBoundaryMode::Clamp: {
0526               aXnew = MathUtils::Clamp(aXnew, theLowerBounds(aDimIdx), theUpperBounds(aDimIdx));
0527               aParticle.Velocity(aDimIdx) = -0.5 * aParticle.Velocity(aDimIdx);
0528               break;
0529             }
0530             case PSOBoundaryMode::Reflect: {
0531               if (aXnew < theLowerBounds(aDimIdx))
0532               {
0533                 aXnew = 2.0 * theLowerBounds(aDimIdx) - aXnew;
0534               }
0535               else
0536               {
0537                 aXnew = 2.0 * theUpperBounds(aDimIdx) - aXnew;
0538               }
0539               // Re-clamp in case reflection overshoots the other bound
0540               aXnew = MathUtils::Clamp(aXnew, theLowerBounds(aDimIdx), theUpperBounds(aDimIdx));
0541               aParticle.Velocity(aDimIdx) = -0.5 * aParticle.Velocity(aDimIdx);
0542               break;
0543             }
0544             case PSOBoundaryMode::Wrap: {
0545               const double aRangeSize = aRange(aDimIdx);
0546               aXnew                   = aXnew - theLowerBounds(aDimIdx);
0547               aXnew                   = std::fmod(aXnew, aRangeSize);
0548               if (aXnew < 0.0)
0549               {
0550                 aXnew += aRangeSize;
0551               }
0552               aXnew += theLowerBounds(aDimIdx);
0553               break;
0554             }
0555           }
0556         }
0557 
0558         aParticle.Position(aDimIdx) = aXnew;
0559       }
0560 
0561       // Evaluate fitness
0562       if (!theFunc.Value(aParticle.Position, aParticle.CurrentValue))
0563       {
0564         aParticle.CurrentValue = std::numeric_limits<double>::max();
0565       }
0566       ++aLocalStats.NbFunctionEvals;
0567 
0568       // Update personal best
0569       if (aParticle.CurrentValue < aParticle.BestValue)
0570       {
0571         aParticle.BestValue    = aParticle.CurrentValue;
0572         aParticle.BestPosition = aParticle.Position;
0573       }
0574 
0575       // Update global best
0576       if (aParticle.BestValue < aGlobalBestValue)
0577       {
0578         aGlobalBestValue = aParticle.BestValue;
0579         aGlobalBest      = aParticle.BestPosition;
0580       }
0581     }
0582 
0583     // Check for target value early stop
0584     if (theConfig.TargetValue.has_value() && aGlobalBestValue <= *theConfig.TargetValue)
0585     {
0586       break;
0587     }
0588 
0589     // Check for convergence (stagnation) after minimum iterations
0590     if (anIter >= theConfig.MinIterations)
0591     {
0592       if (std::abs(aGlobalBestValue - aPrevBest) < aStagnTol * (1.0 + std::abs(aGlobalBestValue)))
0593       {
0594         ++aStagnationCount;
0595         if (aStagnationCount >= theConfig.NoImproveIters)
0596         {
0597           ++aLocalStats.NbStagnationEvents;
0598 
0599           // Restart logic
0600           if (theConfig.RestartFraction > 0.0
0601               && (theConfig.MaxRestarts == 0 || aLocalStats.NbRestarts < theConfig.MaxRestarts))
0602           {
0603             ++aLocalStats.NbRestarts;
0604             aStagnationCount = 0;
0605 
0606             // Reinitialize worst particles
0607             int aNbRestart = static_cast<int>(std::ceil(theConfig.RestartFraction * aNbParticles));
0608             aNbRestart     = std::min(aNbRestart, aNbParticles - 1); // keep at least the best
0609 
0610             // Find best particle index
0611             int aBestIdx = 0;
0612             for (int aFindIdx = 1; aFindIdx < aNbParticles; ++aFindIdx)
0613             {
0614               if (aSwarm.Value(aFindIdx).BestValue < aSwarm.Value(aBestIdx).BestValue)
0615               {
0616                 aBestIdx = aFindIdx;
0617               }
0618             }
0619 
0620             // Sort by fitness descending (simple selection of worst)
0621             // Reinitialize aNbRestart worst particles
0622             NCollection_DynamicArray<int> aWorstIndices;
0623             for (int aCollIdx = 0; aCollIdx < aNbParticles; ++aCollIdx)
0624             {
0625               if (aCollIdx != aBestIdx)
0626               {
0627                 aWorstIndices.Append(aCollIdx);
0628               }
0629             }
0630             // Simple approach: sort by BestValue descending, take first aNbRestart
0631             for (int aSortOuter = 0; aSortOuter < aWorstIndices.Length() - 1; ++aSortOuter)
0632             {
0633               for (int aSortInner = aSortOuter + 1; aSortInner < aWorstIndices.Length();
0634                    ++aSortInner)
0635               {
0636                 if (aSwarm.Value(aWorstIndices.Value(aSortOuter)).BestValue
0637                     < aSwarm.Value(aWorstIndices.Value(aSortInner)).BestValue)
0638                 {
0639                   const int aTmp                        = aWorstIndices.Value(aSortOuter);
0640                   aWorstIndices.ChangeValue(aSortOuter) = aWorstIndices.Value(aSortInner);
0641                   aWorstIndices.ChangeValue(aSortInner) = aTmp;
0642                 }
0643               }
0644             }
0645 
0646             const int aNbToRestart = std::min(aNbRestart, aWorstIndices.Length());
0647             for (int aRestIdx = 0; aRestIdx < aNbToRestart; ++aRestIdx)
0648             {
0649               Particle& aRestartPart = aSwarm.ChangeValue(aWorstIndices.Value(aRestIdx));
0650               for (int aDimIdx = aLower; aDimIdx <= aUpper; ++aDimIdx)
0651               {
0652                 aRestartPart.Position(aDimIdx) =
0653                   theLowerBounds(aDimIdx) + aRNG.NextReal() * aRange(aDimIdx);
0654                 aRestartPart.Velocity(aDimIdx) = (2.0 * aRNG.NextReal() - 1.0) * aVelMax(aDimIdx);
0655               }
0656 
0657               if (!theFunc.Value(aRestartPart.Position, aRestartPart.CurrentValue))
0658               {
0659                 aRestartPart.CurrentValue = std::numeric_limits<double>::max();
0660               }
0661               ++aLocalStats.NbFunctionEvals;
0662 
0663               aRestartPart.BestPosition = aRestartPart.Position;
0664               aRestartPart.BestValue    = aRestartPart.CurrentValue;
0665 
0666               if (aRestartPart.BestValue < aGlobalBestValue)
0667               {
0668                 aGlobalBestValue = aRestartPart.BestValue;
0669                 aGlobalBest      = aRestartPart.BestPosition;
0670               }
0671             }
0672           }
0673           else
0674           {
0675             // No restart budget - converged
0676             break;
0677           }
0678         }
0679       }
0680       else
0681       {
0682         aStagnationCount = 0;
0683       }
0684     }
0685     aPrevBest = aGlobalBestValue;
0686   }
0687 
0688   // Polish the global best using coordinate-wise Brent's method
0689   int aPolishEvals = 0;
0690   if (theConfig.PolishBudgetPerDim > 0)
0691   {
0692     PolishCoordinateWise(theFunc,
0693                          aGlobalBest,
0694                          aGlobalBestValue,
0695                          theLowerBounds,
0696                          theUpperBounds,
0697                          theConfig.Tolerance,
0698                          theConfig.PolishBudgetPerDim * aNbDims,
0699                          aPolishEvals);
0700     aLocalStats.NbFunctionEvals += aPolishEvals;
0701   }
0702 
0703   aLocalStats.NbIterations = static_cast<int>(aResult.NbIterations);
0704   aLocalStats.FinalBest    = aGlobalBestValue;
0705 
0706   if (theStats != nullptr)
0707   {
0708     *theStats = aLocalStats;
0709   }
0710 
0711   aResult.Status   = Status::OK;
0712   aResult.Solution = aGlobalBest;
0713   aResult.Value    = aGlobalBestValue;
0714   return aResult;
0715 }
0716 
0717 //! Particle Swarm Optimization for global minimization (basic overload).
0718 //!
0719 //! @tparam Function type with Value(const math_Vector&, double&) method
0720 //! @param theFunc function to minimize
0721 //! @param theLowerBounds lower bounds for each variable
0722 //! @param theUpperBounds upper bounds for each variable
0723 //! @param theConfig PSO configuration
0724 //! @return result containing best solution found
0725 template <typename Function>
0726 VectorResult PSO(Function&          theFunc,
0727                  const math_Vector& theLowerBounds,
0728                  const math_Vector& theUpperBounds,
0729                  const PSOConfig&   theConfig = PSOConfig())
0730 {
0731   return PSO(theFunc, theLowerBounds, theUpperBounds, theConfig, nullptr, nullptr);
0732 }
0733 
0734 } // namespace MathOpt
0735 
0736 #endif // _MathOpt_PSO_HeaderFile