Coverage Report

Created: 2025-10-14 06:32

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/quantlib/ql/experimental/math/particleswarmoptimization.hpp
Line
Count
Source
1
/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
3
/*
4
Copyright (C) 2015 Andres Hernandez
5
6
This file is part of QuantLib, a free-software/open-source library
7
for financial quantitative analysts and developers - http://quantlib.org/
8
9
QuantLib is free software: you can redistribute it and/or modify it
10
under the terms of the QuantLib license.  You should have received a
11
copy of the license along with this program; if not, please email
12
<quantlib-dev@lists.sf.net>. The license is also available online at
13
<https://www.quantlib.org/license.shtml>.
14
15
This program is distributed in the hope that it will be useful, but WITHOUT
16
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17
FOR A PARTICULAR PURPOSE.  See the license for more details.
18
*/
19
20
/*! \file particleswarmoptimization.hpp
21
\brief Implementation based on:
22
Clerc, M., Kennedy, J. (2002) The particle swarm-explosion, stability and
23
convergence in a multidimensional complex space. IEEE Transactions on Evolutionary
24
Computation, 6(2): 58–73.
25
*/
26
27
#ifndef quantlib_optimization_particleswarmoptimization_hpp
28
#define quantlib_optimization_particleswarmoptimization_hpp
29
30
#include <ql/math/optimization/problem.hpp>
31
#include <ql/math/optimization/constraint.hpp>
32
#include <ql/math/randomnumbers/mt19937uniformrng.hpp>
33
#include <ql/experimental/math/isotropicrandomwalk.hpp>
34
#include <ql/experimental/math/levyflightdistribution.hpp>
35
#include <ql/math/randomnumbers/seedgenerator.hpp>
36
37
#include <random>
38
39
namespace QuantLib {
40
41
    /*! The process is as follows:
42
    M individuals are used to explore the N-dimensional parameter space:
43
    \f$ X_{i}^k = (X_{i, 1}^k, X_{i, 2}^k, \ldots, X_{i, N}^k) \f$ is the kth-iteration for the ith-individual.
44
45
    X is updated via the rule
46
    \f[
47
    X_{i, j}^{k+1} = X_{i, j}^k + V_{i, j}^{k+1}
48
    \f]
49
    with V being the "velocity" that updates the position:
50
    \f[
51
    V_{i, j}^{k+1} = \chi\left(V_{i, j}^k + c_1 r_{i, j}^k (P_{i, j}^k - X_{i, j}^k)
52
    + c_2 R_{i, j}^k (G_{i, j}^k - X_{i, j}^k)\right)
53
    \f]
54
    where c are constants, r and R are uniformly distributed random numbers in the range [0, 1], and
55
    \f$ P_{i, j} \f$ is the personal best parameter set for individual i up to iteration k
56
    \f$ G_{i, j} \f$ is the global best parameter set for the swarm up to iteration k.
57
    \f$ c_1 \f$ is the self recognition coefficient
58
    \f$ c_2 \f$ is the social recognition coefficient
59
60
    This version is known as the PSO with constriction factor (PSO-Co).
61
    PSO with inertia factor (PSO-In) updates the velocity according to:
62
    \f[
63
    V_{i, j}^{k+1} = \omega V_{i, j}^k + \hat{c}_1 r_{i, j}^k (P_{i, j}^k - X_{i, j}^k)
64
    + \hat{c}_2 R_{i, j}^k (G_{i, j}^k - X_{i, j}^k)
65
    \f]
66
    and is accessible from PSO-Co by setting \f$ \omega = \chi \f$,
67
    and \f$ \hat{c}_{1,2} = \chi c_{1,2} \f$.
68
69
    These two versions of PSO are normally referred to as canonical PSO.
70
71
    Convergence of PSO-Co is improved if \f$ \chi \f$ is chosen as
72
    \f$ \chi = \frac{2}{\vert 2-\phi-\sqrt{\phi^2 - 4\phi}\vert} \f$,
73
    with \f$ \phi = c_1 + c_2 \f$.
74
    Stable convergence is achieved if \f$ \phi >= 4 \f$. Clerc and Kennedy recommend
75
    \f$ c_1 = c_2 = 2.05 \f$ and \f$ \phi = 4.1 \f$.
76
77
    Different topologies can be chosen for G, e.g. instead of it being the best
78
    of the swarm, it is the best of the nearest neighbours, or some other form.
79
80
    In the canonical PSO, the inertia function is trivial. It is simply a
81
    constant (the inertia) multiplying the previous iteration's velocity. The
82
    value of the inertia constant determines the weight of a global search over
83
    local search. Like in the case of the topology, other possibilities for the
84
    inertia function are also possible, e.g. a function that interpolates between a
85
    high inertia at the beginning of the optimization (hence prioritizing a global
86
    search) and a low inertia towards the end of the optimization (hence prioritizing
87
    a local search).
88
89
    The optimization stops either because the number of iterations has been reached
90
    or because the stationary function value limit has been reached.
91
    */
92
    class ParticleSwarmOptimization : public OptimizationMethod {
93
      public:
94
        class Inertia;
95
        class Topology;
96
        ParticleSwarmOptimization(Size M,
97
                                  ext::shared_ptr<Topology> topology,
98
                                  ext::shared_ptr<Inertia> inertia,
99
                                  Real c1 = 2.05,
100
                                  Real c2 = 2.05,
101
                                  unsigned long seed = SeedGenerator::instance().get());
102
        explicit ParticleSwarmOptimization(Size M,
103
                                           ext::shared_ptr<Topology> topology,
104
                                           ext::shared_ptr<Inertia> inertia,
105
                                           Real omega,
106
                                           Real c1,
107
                                           Real c2,
108
                                           unsigned long seed = SeedGenerator::instance().get());
109
        void startState(Problem &P, const EndCriteria &endCriteria);
110
        EndCriteria::Type minimize(Problem& P, const EndCriteria& endCriteria) override;
111
112
      protected:
113
        std::vector<Array> X_, V_, pBX_, gBX_;
114
        Array pBF_, gBF_;
115
        Array lX_, uX_;
116
        Size M_, N_;
117
        Real c0_, c1_, c2_;
118
        MersenneTwisterUniformRng rng_;
119
        ext::shared_ptr<Topology> topology_;
120
        ext::shared_ptr<Inertia> inertia_;
121
    };
122
123
    //! Base inertia class used to alter the PSO state
124
    /*! This pure virtual base class provides the access to the PSO state
125
    which the particular inertia algorithm will change upon each iteration.
126
    */
127
    class ParticleSwarmOptimization::Inertia {
128
        friend class ParticleSwarmOptimization;
129
      public:
130
0
        virtual ~Inertia() = default;
131
        //! initialize state for current problem
132
        virtual void setSize(Size M, Size N, Real c0, const EndCriteria &endCriteria) = 0;
133
        //! produce changes to PSO state for current iteration
134
        virtual void setValues() = 0;
135
      protected:
136
        ParticleSwarmOptimization *pso_;
137
        std::vector<Array> *X_, *V_, *pBX_, *gBX_;
138
        Array *pBF_, *gBF_;
139
        Array *lX_, *uX_;
140
141
0
        virtual void init(ParticleSwarmOptimization *pso) {
142
0
            pso_ = pso;
143
0
            X_ = &pso_->X_;
144
0
            V_ = &pso_->V_;
145
0
            pBX_ = &pso_->pBX_;
146
0
            gBX_ = &pso_->gBX_;
147
0
            pBF_ = &pso_->pBF_;
148
0
            gBF_ = &pso_->gBF_;
149
0
            lX_ = &pso_->lX_;
150
0
            uX_ = &pso_->uX_;
151
0
        }
152
    };
153
154
    //! Trivial Inertia
155
    /*     Inertia is a static value
156
    */
157
    class TrivialInertia : public ParticleSwarmOptimization::Inertia {
158
      public:
159
0
        void setSize(Size M, Size N, Real c0, const EndCriteria& endCriteria) override {
160
0
            c0_ = c0;
161
0
            M_ = M;
162
0
        }
163
0
        void setValues() override {
164
0
            for (Size i = 0; i < M_; i++) {
165
0
                (*V_)[i] *= c0_;
166
0
            }
167
0
        }
168
169
      private:
170
        Real c0_;
171
        Size M_;
172
    };
173
174
    //! Simple Random Inertia
175
    /*     Inertia value gets multiplied with a random number
176
    between (threshold, 1)
177
    */
178
    class SimpleRandomInertia : public ParticleSwarmOptimization::Inertia {
179
      public:
180
        SimpleRandomInertia(Real threshold = 0.5, unsigned long seed = SeedGenerator::instance().get())
181
0
            : threshold_(threshold), rng_(seed) {
182
0
            QL_REQUIRE(threshold_ >= 0.0 && threshold_ < 1.0, "Threshold must be a Real in [0, 1)");
183
0
        }
184
0
        void setSize(Size M, Size N, Real c0, const EndCriteria& endCriteria) override {
185
0
            M_ = M;
186
0
            c0_ = c0;
187
0
        }
188
0
        void setValues() override {
189
0
            for (Size i = 0; i < M_; i++) {
190
0
                Real val = c0_*(threshold_ + (1.0 - threshold_)*rng_.nextReal());
191
0
                (*V_)[i] *= val;
192
0
            }
193
0
        }
194
195
      private:
196
        Real c0_, threshold_;
197
        Size M_;
198
        MersenneTwisterUniformRng rng_;
199
    };
200
201
    //! Decreasing Inertia
202
    /*     Inertia value gets decreased every iteration until it reaches
203
    a value of threshold when iteration reaches the maximum level
204
    */
205
    class DecreasingInertia : public ParticleSwarmOptimization::Inertia {
206
      public:
207
        DecreasingInertia(Real threshold = 0.5)
208
0
            : threshold_(threshold) {
209
0
            QL_REQUIRE(threshold_ >= 0.0 && threshold_ < 1.0, "Threshold must be a Real in [0, 1)");
210
0
        }
211
0
        void setSize(Size M, Size N, Real c0, const EndCriteria& endCriteria) override {
212
0
            N_ = N;
213
0
            c0_ = c0;
214
0
            iteration_ = 0;
215
0
            maxIterations_ = endCriteria.maxIterations();
216
0
        }
217
0
        void setValues() override {
218
0
            Real c0 = c0_*(threshold_ + (1.0 - threshold_)*(maxIterations_ - iteration_) / maxIterations_);
219
0
            for (Size i = 0; i < M_; i++) {
220
0
                (*V_)[i] *= c0;
221
0
            }
222
0
        }
223
224
      private:
225
        Real c0_, threshold_;
226
        Size M_, N_, maxIterations_, iteration_;
227
    };
228
229
    //! AdaptiveInertia
230
    /*    Alen Lukic, Approximating Kinetic Parameters Using Particle
231
    Swarm Optimization.
232
    */
233
    class AdaptiveInertia : public ParticleSwarmOptimization::Inertia {
234
      public:
235
        AdaptiveInertia(Real minInertia, Real maxInertia, Size sh = 5, Size sl = 2)
236
            :minInertia_(minInertia), maxInertia_(maxInertia),
237
0
            sh_(sh), sl_(sl) {};
238
0
        void setSize(Size M, Size N, Real c0, const EndCriteria& endCriteria) override {
239
0
            M_ = M;
240
0
            c0_ = c0;
241
0
            adaptiveCounter = 0;
242
0
            best_ = QL_MAX_REAL;
243
0
            started_ = false;
244
0
        }
245
        void setValues() override;
246
247
      private:
248
        Real c0_, best_;
249
        Real minInertia_, maxInertia_;
250
        Size M_;
251
        Size sh_, sl_;
252
        Size adaptiveCounter;
253
        bool started_;
254
    };
255
256
    //! Levy Flight Inertia
257
    /*    As long as the particle keeps getting frequent updates to its
258
    personal best value, the inertia behaves like a SimpleRandomInertia,
259
    but after a number of iterations without improvement, the behaviour
260
    changes to that of a Levy flight ~ u^{-1/\alpha}
261
    */
262
    class LevyFlightInertia : public ParticleSwarmOptimization::Inertia {
263
      public:
264
        LevyFlightInertia(Real alpha, Size threshold,
265
                          unsigned long seed = SeedGenerator::instance().get())
266
            :rng_(seed), generator_(seed), flight_(generator_, LevyFlightDistribution(1.0, alpha),
267
                1, Array(1, 1.0), seed),
268
0
            threshold_(threshold) {};
269
0
        void setSize(Size M, Size N, Real c0, const EndCriteria& endCriteria) override {
270
0
            M_ = M;
271
0
            N_ = N;
272
0
            c0_ = c0;
273
0
            adaptiveCounter_ = std::vector<Size>(M_, 0);
274
0
        }
275
0
        void setValues() override {
276
0
            for (Size i = 0; i < M_; i++) {
277
0
                if ((*pBF_)[i] < personalBestF_[i]) {
278
0
                    personalBestF_[i] = (*pBF_)[i];
279
0
                    adaptiveCounter_[i] = 0;
280
0
                }
281
0
                else {
282
0
                    adaptiveCounter_[i]++;
283
0
                }
284
0
                if (adaptiveCounter_[i] <= threshold_) {
285
0
                    //Simple Random Inertia
286
0
                    (*V_)[i] *= c0_*(0.5 + 0.5*rng_.nextReal());
287
0
                }
288
0
                else {
289
0
                    //If particle has not found a new personal best after threshold_ iterations
290
0
                    //then trigger a Levy flight pattern for the speed
291
0
                    flight_.nextReal<Real *>(&(*V_)[i][0]);
292
0
                }
293
0
            }
294
0
        }
295
296
      protected:
297
0
        void init(ParticleSwarmOptimization* pso) override {
298
0
            ParticleSwarmOptimization::Inertia::init(pso);
299
0
            personalBestF_ = *pBF_;
300
0
            flight_.setDimension(N_, *lX_, *uX_);
301
0
        }
302
303
      private:
304
        MersenneTwisterUniformRng rng_;
305
        std::mt19937 generator_;
306
        IsotropicRandomWalk<LevyFlightDistribution, std::mt19937> flight_;
307
        Array personalBestF_;
308
        std::vector<Size> adaptiveCounter_;
309
        Real c0_;
310
        Size M_, N_;
311
        Size threshold_;
312
    };
313
314
    //! Base topology class used to determine the personal and global best
315
    /*! This pure virtual base class provides the access to the PSO state
316
    which the particular topology algorithm will change upon each iteration.
317
    */
318
    class ParticleSwarmOptimization::Topology {
319
        friend class ParticleSwarmOptimization;
320
      public:
321
0
        virtual ~Topology() = default;
322
        //! initialize state for current problem
323
        virtual void setSize(Size M) = 0;
324
        //! produce changes to PSO state for current iteration
325
        virtual void findSocialBest() = 0;
326
      protected:
327
        ParticleSwarmOptimization *pso_;
328
        std::vector<Array> *X_, *V_, *pBX_, *gBX_;
329
        Array *pBF_, *gBF_;
330
      private:
331
0
        void init(ParticleSwarmOptimization *pso) {
332
0
            pso_ = pso;
333
0
            X_ = &pso_->X_;
334
0
            V_ = &pso_->V_;
335
0
            pBX_ = &pso_->pBX_;
336
0
            gBX_ = &pso_->gBX_;
337
0
            pBF_ = &pso_->pBF_;
338
0
            gBF_ = &pso_->gBF_;
339
0
        }
340
    };
341
342
    //! Global Topology
343
    /*  The global best as seen by each particle is the best from amongst
344
    all particles
345
    */
346
    class GlobalTopology : public ParticleSwarmOptimization::Topology {
347
      public:
348
0
        void setSize(Size M) override { M_ = M; }
349
0
        void findSocialBest() override {
350
0
            Real bestF = (*pBF_)[0];
351
0
            Size bestP = 0;
352
0
            for (Size i = 1; i < M_; i++) {
353
0
                if (bestF < (*pBF_)[i]) {
354
0
                    bestF = (*pBF_)[i];
355
0
                    bestP = i;
356
0
                }
357
0
            }
358
0
            Array& x = (*pBX_)[bestP];
359
0
            for (Size i = 0; i < M_; i++) {
360
0
                if (i != bestP) {
361
0
                    (*gBX_)[i] = x;
362
0
                    (*gBF_)[i] = bestF;
363
0
                }
364
0
            }
365
0
        }
366
367
      private:
368
        Size M_;
369
    };
370
371
    //! K-Neighbor Topology
372
    /*  The global best as seen by each particle is the best from amongst
373
    the previous K and next K neighbors. For particle I, the best is
374
    then taken from amongst the [I - K, I + K] particles.
375
    */
376
    class KNeighbors : public ParticleSwarmOptimization::Topology {
377
      public:
378
0
        KNeighbors(Size K = 1) :K_(K) {
379
0
            QL_REQUIRE(K > 0, "Neighbors need to be larger than 0");
380
0
        }
381
0
        void setSize(Size M) override {
382
0
            M_ = M;
383
            QL_ENSURE(K_ < M, "Number of neighbors need to be smaller than total particles in swarm");
384
0
        }
385
        void findSocialBest() override;
386
387
      private:
388
        Size K_, M_;
389
    };
390
391
    //! Clubs Topology
392
    /*  H.M. Emara,  Adaptive Clubs-based Particle Swarm Optimization
393
    Each particle is originally assigned to a default number of clubs
394
    from among the total set. The best as seen by each particle is the
395
    best from amongst the clubs to which the particle belongs.
396
    Underperforming particles join more clubs randomly (up to a maximum
397
    number) to widen the particles that influence them, while
398
    overperforming particles leave clubs randomly (down to a minimum
399
    number) to avoid early convergence to local minima.
400
    */
401
    class ClubsTopology : public ParticleSwarmOptimization::Topology {
402
      public:
403
        ClubsTopology(Size defaultClubs, Size totalClubs,
404
            Size maxClubs, Size minClubs,
405
            Size resetIteration, unsigned long seed = SeedGenerator::instance().get());
406
        void setSize(Size M) override;
407
        void findSocialBest() override;
408
409
      private:
410
        Size totalClubs_, maxClubs_, minClubs_, defaultClubs_;
411
        Size iteration_ = 0, resetIteration_;
412
        Size M_;
413
        std::vector<std::vector<bool> > clubs4particles_;
414
        std::vector<std::vector<bool> > particles4clubs_;
415
        std::vector<Size> bestByClub_;
416
        std::vector<Size> worstByClub_;
417
        std::mt19937 generator_;
418
        std::uniform_int_distribution<QuantLib::Size> distribution_;
419
        using param_type = decltype(distribution_)::param_type;
420
421
        void leaveRandomClub(Size particle, Size currentClubs);
422
        void joinRandomClub(Size particle, Size currentClubs);
423
    };
424
425
}
426
427
#endif