50 using Objective = std::function<double(
const std::vector<double>&)>;
51 using Callback = std::function<bool(
const std::vector<double>&, std::size_t)>;
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)),
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),
72 initialize_latin_hypercube();
83 calculate_initial_energies();
84 promote_lowest_energy();
87 for (std::size_t nit = 1; nit <= max_iterations_; ++nit) {
90 if (callback_ && callback_(best(), nit)) stopped =
true;
92 if (stopped || converged())
break;
95 if (result.
nit == max_iterations_ && !converged()) stopped =
true;
97 result.
fun = energies_[0];
98 result.
nfev = evaluations_;
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");
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]);
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()));
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;
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];
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));
155 std::vector<double> best()
const {
return scale_parameters(population_[0]); }
157 void calculate_initial_energies() {
158 for (std::size_t i = 0; i < members_; ++i) {
159 energies_[i] = objective_(scale_parameters(population_[i]));
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());
168 std::swap(energies_[0], energies_[best_i]);
169 std::swap(population_[0], population_[best_i]);
173 bool any_infinite()
const {
174 return std::any_of(energies_.begin(), energies_.end(),
175 [](
double x) { return std::isinf(x); });
178 bool converged()
const {
179 if (any_infinite())
return false;
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);
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;
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]);
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]);
220 b[j] = population_[0][j] +
221 scale_ * (population_[s[0]][j] - population_[s[1]][j]);
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];
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);
244 for (
double& value : trial)
245 if (value < 0.0 || value > 1.0) value = replacements[k++];
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));
255 if (energy <= energies_[candidate]) {
256 population_[candidate] = std::move(trial);
257 energies_[candidate] = energy;
258 if (energy <= energies_[0]) promote_lowest_energy();
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_;
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_;