LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
differential_evolution.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_OPT_DE_DIFFERENTIAL_EVOLUTION_H
6#define LINE_OPT_DE_DIFFERENTIAL_EVOLUTION_H
7
8/**
9 * @file
10 * @ingroup line_opt
11 * Port of `matlab/src/opt/+opt/+de/DifferentialEvolution.m`.
12 *
13 * This is the self-contained line-opt subset of SciPy Differential Evolution:
14 * binomial mutation strategies, immediate updating, dithered mutation,
15 * Latin-hypercube initialization, no polish, and objective-side constraint
16 * penalties. Together with NumpyRandomState it reproduces the reference
17 * trajectory for a fixed seed, including the data-dependent draw count of
18 * out-of-bounds repair.
19 */
20
21#include <algorithm>
22#include <cmath>
23#include <cstddef>
24#include <cstdint>
25#include <functional>
26#include <limits>
27#include <numeric>
28#include <string>
29#include <utility>
30#include <vector>
31
33#include "line/util/error.h"
34
35namespace line {
36namespace opt {
37namespace de {
38
40 std::vector<double> x;
41 double fun = std::numeric_limits<double>::infinity();
42 std::size_t nit = 0;
43 std::size_t nfev = 0;
44 bool success = false;
45 std::vector<std::vector<double>> best_per_generation;
46};
47
49public:
50 using Objective = std::function<double(const std::vector<double>&)>;
51 using Callback = std::function<bool(const std::vector<double>&, std::size_t)>;
52
53 DifferentialEvolution(Objective objective, std::vector<double> low,
54 std::vector<double> high, std::string strategy = "best1bin",
55 std::size_t popsize_multiplier = 15, std::size_t max_iterations = 100,
56 double mutation_low = 0.5, double mutation_high = 1.0,
57 double recombination = 0.7, double tolerance = 0.01,
58 std::uint64_t seed = 0)
59 : objective_(std::move(objective)),
60 low_(std::move(low)),
61 high_(std::move(high)),
62 strategy_(std::move(strategy)),
63 popsize_multiplier_(popsize_multiplier),
64 max_iterations_(max_iterations),
65 dither_low_(std::min(mutation_low, mutation_high)),
66 dither_high_(std::max(mutation_low, mutation_high)),
67 recombination_(recombination),
68 tolerance_(tolerance),
69 scale_(mutation_low),
70 rng_(seed) {
71 validate();
72 initialize_latin_hypercube();
73 }
74
75 void set_callback(Callback callback) { callback_ = std::move(callback); }
76 NumpyRandomState& random_state() { return rng_; }
77 const NumpyRandomState& random_state() const { return rng_; }
78
81 bool stopped = false;
82 if (any_infinite()) {
83 calculate_initial_energies();
84 promote_lowest_energy();
85 }
86
87 for (std::size_t nit = 1; nit <= max_iterations_; ++nit) {
88 next_generation();
89 result.nit = nit;
90 if (callback_ && callback_(best(), nit)) stopped = true;
91 result.best_per_generation.push_back(best());
92 if (stopped || converged()) break;
93 }
94
95 if (result.nit == max_iterations_ && !converged()) stopped = true;
96 result.x = best();
97 result.fun = energies_[0];
98 result.nfev = evaluations_;
99 result.success = !stopped;
100 return result;
101 }
102
103private:
104 void validate() const {
105 if (!objective_) throw InputError("DifferentialEvolution: the objective is empty");
106 if (low_.empty()) throw InputError("DifferentialEvolution: no parameters were supplied");
107 if (low_.size() != high_.size())
108 throw InputError("DifferentialEvolution: lower and upper bounds differ in size");
109 for (std::size_t j = 0; j < low_.size(); ++j)
110 if (!std::isfinite(low_[j]) || !std::isfinite(high_[j]) || low_[j] > high_[j])
111 throw InputError("DifferentialEvolution: every bound must be finite and low <= high");
112 if (popsize_multiplier_ == 0)
113 throw InputError("DifferentialEvolution: popsize multiplier must be positive");
114 if (!(recombination_ >= 0.0 && recombination_ <= 1.0))
115 throw InputError("DifferentialEvolution: recombination must lie in [0,1]");
116 if (tolerance_ < 0.0)
117 throw InputError("DifferentialEvolution: tolerance must be non-negative");
118 }
119
120 std::vector<double> scale_parameters(const std::vector<double>& trial) const {
121 std::vector<double> out(low_.size());
122 for (std::size_t j = 0; j < low_.size(); ++j) {
123 const double midpoint = 0.5 * (low_[j] + high_[j]);
124 out[j] = midpoint + (trial[j] - 0.5) * std::fabs(low_[j] - high_[j]);
125 }
126 return out;
127 }
128
129 void initialize_latin_hypercube() {
130 std::size_t equal = 0;
131 for (std::size_t j = 0; j < low_.size(); ++j)
132 if (low_[j] == high_[j]) ++equal;
133 members_ = std::max<std::size_t>(5, popsize_multiplier_ *
134 std::max<std::size_t>(1, low_.size() - equal));
135 const double segment = 1.0 / static_cast<double>(members_);
136 const std::vector<double> flat = rng_.uniform(0.0, 1.0, members_ * low_.size());
137 std::vector<std::vector<double>> samples(members_, std::vector<double>(low_.size()));
138 std::size_t p = 0;
139 for (std::size_t i = 0; i < members_; ++i) {
140 const double offset = static_cast<double>(i) / static_cast<double>(members_);
141 for (std::size_t j = 0; j < low_.size(); ++j)
142 samples[i][j] = segment * flat[p++] + offset;
143 }
144 population_.assign(members_, std::vector<double>(low_.size()));
145 for (std::size_t j = 0; j < low_.size(); ++j) {
146 const std::vector<std::size_t> order = rng_.permutation(members_);
147 for (std::size_t i = 0; i < members_; ++i) population_[i][j] = samples[order[i]][j];
148 }
149 energies_.assign(members_, std::numeric_limits<double>::infinity());
150 random_index_.resize(members_);
151 std::iota(random_index_.begin(), random_index_.end(), std::size_t(0));
152 evaluations_ = 0;
153 }
154
155 std::vector<double> best() const { return scale_parameters(population_[0]); }
156
157 void calculate_initial_energies() {
158 for (std::size_t i = 0; i < members_; ++i) {
159 energies_[i] = objective_(scale_parameters(population_[i]));
160 ++evaluations_;
161 }
162 }
163
164 void promote_lowest_energy() {
165 const auto pos = std::min_element(energies_.begin(), energies_.end());
166 const std::size_t best_i = static_cast<std::size_t>(pos - energies_.begin());
167 if (best_i != 0) {
168 std::swap(energies_[0], energies_[best_i]);
169 std::swap(population_[0], population_[best_i]);
170 }
171 }
172
173 bool any_infinite() const {
174 return std::any_of(energies_.begin(), energies_.end(),
175 [](double x) { return std::isinf(x); });
176 }
177
178 bool converged() const {
179 if (any_infinite()) return false;
180 const double mean =
181 std::accumulate(energies_.begin(), energies_.end(), 0.0) / energies_.size();
182 double variance = 0.0;
183 for (double value : energies_) variance += (value - mean) * (value - mean);
184 const double stddev = std::sqrt(variance / energies_.size());
185 return stddev <= tolerance_ * std::fabs(mean); // atol is zero in MATLAB
186 }
187
188 std::vector<std::size_t> select_samples(std::size_t candidate, std::size_t count) {
189 rng_.shuffle(random_index_);
190 std::vector<std::size_t> out;
191 out.reserve(count);
192 for (std::size_t i = 0; i < count + 1 && out.size() < count; ++i)
193 if (random_index_[i] != candidate) out.push_back(random_index_[i]);
194 return out;
195 }
196
197 std::vector<double> donor(std::size_t candidate, const std::vector<std::size_t>& s) const {
198 std::vector<double> b(low_.size());
199 for (std::size_t j = 0; j < low_.size(); ++j) {
200 if (strategy_ == "rand1bin" || strategy_ == "rand1exp")
201 b[j] = population_[s[0]][j] + scale_ *
202 (population_[s[1]][j] - population_[s[2]][j]);
203 else if (strategy_ == "randtobest1bin" || strategy_ == "randtobest1exp") {
204 b[j] = population_[s[0]][j];
205 b[j] += scale_ * (population_[0][j] - b[j]);
206 b[j] += scale_ * (population_[s[1]][j] - population_[s[2]][j]);
207 } else if (strategy_ == "currenttobest1bin" || strategy_ == "currenttobest1exp")
208 b[j] = population_[candidate][j] + scale_ *
209 (population_[0][j] - population_[candidate][j] +
210 population_[s[0]][j] - population_[s[1]][j]);
211 else if (strategy_ == "best2bin" || strategy_ == "best2exp")
212 b[j] = population_[0][j] + scale_ *
213 (population_[s[0]][j] + population_[s[1]][j] -
214 population_[s[2]][j] - population_[s[3]][j]);
215 else if (strategy_ == "rand2bin" || strategy_ == "rand2exp")
216 b[j] = population_[s[0]][j] + scale_ *
217 (population_[s[1]][j] + population_[s[2]][j] -
218 population_[s[3]][j] - population_[s[4]][j]);
219 else
220 b[j] = population_[0][j] +
221 scale_ * (population_[s[0]][j] - population_[s[1]][j]);
222 }
223 return b;
224 }
225
226 std::vector<double> mutate(std::size_t candidate) {
227 const std::size_t fill = static_cast<std::size_t>(rng_.randint(low_.size()));
228 const std::vector<std::size_t> samples = select_samples(candidate, 5);
229 const std::vector<double> b = donor(candidate, samples);
230 std::vector<double> trial = population_[candidate];
231 const std::vector<double> cross = rng_.uniform(0.0, 1.0, low_.size());
232 for (std::size_t j = 0; j < low_.size(); ++j)
233 if (cross[j] < recombination_ || j == fill) trial[j] = b[j];
234 return trial;
235 }
236
237 void repair_bounds(std::vector<double>& trial) {
238 std::size_t outside = 0;
239 for (double value : trial)
240 if (value < 0.0 || value > 1.0) ++outside;
241 if (outside == 0) return;
242 const std::vector<double> replacements = rng_.uniform(0.0, 1.0, outside);
243 std::size_t k = 0;
244 for (double& value : trial)
245 if (value < 0.0 || value > 1.0) value = replacements[k++];
246 }
247
248 void next_generation() {
249 scale_ = rng_.uniform(dither_low_, dither_high_);
250 for (std::size_t candidate = 0; candidate < members_; ++candidate) {
251 std::vector<double> trial = mutate(candidate);
252 repair_bounds(trial);
253 const double energy = objective_(scale_parameters(trial));
254 ++evaluations_;
255 if (energy <= energies_[candidate]) {
256 population_[candidate] = std::move(trial);
257 energies_[candidate] = energy;
258 if (energy <= energies_[0]) promote_lowest_energy();
259 }
260 }
261 }
262
263 Objective objective_;
264 std::vector<double> low_, high_;
265 std::string strategy_;
266 std::size_t popsize_multiplier_, max_iterations_;
267 double dither_low_, dither_high_, recombination_, tolerance_, scale_;
268 Callback callback_;
269 NumpyRandomState rng_;
270 std::size_t members_ = 0, evaluations_ = 0;
271 std::vector<std::vector<double>> population_;
272 std::vector<double> energies_;
273 std::vector<std::size_t> random_index_;
274};
275
276} // namespace de
277} // namespace opt
278} // namespace line
279
280#endif
InputError(const std::string &what)
Definition error.h:39
DifferentialEvolution(Objective objective, std::vector< double > low, std::vector< double > high, std::string strategy="best1bin", std::size_t popsize_multiplier=15, std::size_t max_iterations=100, double mutation_low=0.5, double mutation_high=1.0, double recombination=0.7, double tolerance=0.01, std::uint64_t seed=0)
std::function< bool(const std::vector< double > &, std::size_t)> Callback
std::function< double(const std::vector< double > &)> Objective
const NumpyRandomState & random_state() const
double uniform(double low=0.0, double high=1.0)
The exception types the port throws.
Bit-exact port of matlab/src/opt/+opt/+de/NumpyRandomState.m.
std::vector< std::vector< double > > best_per_generation