File indexing completed on 2025-04-26 08:38:01
0001
0002
0003
0004
0005
0006
0007 #ifndef BOOST_MATH_OPTIMIZATION_JSO_HPP
0008 #define BOOST_MATH_OPTIMIZATION_JSO_HPP
0009 #include <atomic>
0010 #include <boost/math/optimization/detail/common.hpp>
0011 #include <cmath>
0012 #include <iostream>
0013 #include <limits>
0014 #include <random>
0015 #include <sstream>
0016 #include <stdexcept>
0017 #include <thread>
0018 #include <type_traits>
0019 #include <utility>
0020 #include <vector>
0021
0022 namespace boost::math::optimization {
0023
0024 #ifndef BOOST_MATH_DEBUG_JSO
0025 #define BOOST_MATH_DEBUG_JSO 0
0026 #endif
0027
0028
0029
0030
0031
0032
0033
0034 template <typename ArgumentContainer> struct jso_parameters {
0035 using Real = typename ArgumentContainer::value_type;
0036 using DimensionlessReal = decltype(Real()/Real());
0037 ArgumentContainer lower_bounds;
0038 ArgumentContainer upper_bounds;
0039
0040
0041
0042
0043 size_t initial_population_size = 0;
0044
0045
0046
0047 size_t max_function_evaluations = 0;
0048 size_t threads = std::thread::hardware_concurrency();
0049 ArgumentContainer const *initial_guess = nullptr;
0050 };
0051
0052 template <typename ArgumentContainer>
0053 void validate_jso_parameters(jso_parameters<ArgumentContainer> &jso_params) {
0054 using std::isfinite;
0055 using std::isnan;
0056 std::ostringstream oss;
0057 if (jso_params.threads == 0) {
0058 oss << __FILE__ << ":" << __LINE__ << ":" << __func__;
0059 oss << ": Requested zero threads to perform the calculation, but at least "
0060 "1 is required.";
0061 throw std::invalid_argument(oss.str());
0062 }
0063 detail::validate_bounds(jso_params.lower_bounds, jso_params.upper_bounds);
0064 auto const dimension = jso_params.lower_bounds.size();
0065 if (jso_params.initial_population_size == 0) {
0066
0067
0068 jso_params.initial_population_size = static_cast<size_t>(
0069 std::ceil(25 * std::log(dimension + 1.0) * sqrt(dimension)));
0070 }
0071 if (jso_params.max_function_evaluations == 0) {
0072
0073 jso_params.max_function_evaluations = 10000 * dimension;
0074 }
0075 if (jso_params.initial_population_size < 4) {
0076 oss << __FILE__ << ":" << __LINE__ << ":" << __func__;
0077 oss << ": The population size must be at least 4, but requested population "
0078 "size of "
0079 << jso_params.initial_population_size << ".";
0080 throw std::invalid_argument(oss.str());
0081 }
0082 if (jso_params.threads > jso_params.initial_population_size) {
0083 oss << __FILE__ << ":" << __LINE__ << ":" << __func__;
0084 oss << ": There must be more individuals in the population than threads.";
0085 throw std::invalid_argument(oss.str());
0086 }
0087 if (jso_params.initial_guess) {
0088 detail::validate_initial_guess(*jso_params.initial_guess,
0089 jso_params.lower_bounds,
0090 jso_params.upper_bounds);
0091 }
0092 }
0093
0094 template <typename ArgumentContainer, class Func, class URBG>
0095 ArgumentContainer
0096 jso(const Func cost_function, jso_parameters<ArgumentContainer> &jso_params,
0097 URBG &gen,
0098 std::invoke_result_t<Func, ArgumentContainer> target_value =
0099 std::numeric_limits<
0100 std::invoke_result_t<Func, ArgumentContainer>>::quiet_NaN(),
0101 std::atomic<bool> *cancellation = nullptr,
0102 std::atomic<std::invoke_result_t<Func, ArgumentContainer>>
0103 *current_minimum_cost = nullptr,
0104 std::vector<std::pair<ArgumentContainer,
0105 std::invoke_result_t<Func, ArgumentContainer>>>
0106 *queries = nullptr) {
0107 using Real = typename ArgumentContainer::value_type;
0108 using DimensionlessReal = decltype(Real()/Real());
0109 validate_jso_parameters(jso_params);
0110
0111 using ResultType = std::invoke_result_t<Func, ArgumentContainer>;
0112 using std::abs;
0113 using std::cauchy_distribution;
0114 using std::clamp;
0115 using std::isnan;
0116 using std::max;
0117 using std::round;
0118 using std::isfinite;
0119 using std::uniform_real_distribution;
0120
0121
0122
0123 std::vector<ArgumentContainer> archive;
0124
0125
0126
0127 auto population = detail::random_initial_population(
0128 jso_params.lower_bounds, jso_params.upper_bounds,
0129 jso_params.initial_population_size, gen);
0130 if (jso_params.initial_guess) {
0131 population[0] = *jso_params.initial_guess;
0132 }
0133 size_t dimension = jso_params.lower_bounds.size();
0134
0135 if (current_minimum_cost) {
0136 *current_minimum_cost = std::numeric_limits<ResultType>::infinity();
0137 }
0138 std::atomic<bool> target_attained = false;
0139 std::vector<ResultType> cost(jso_params.initial_population_size,
0140 std::numeric_limits<ResultType>::quiet_NaN());
0141 for (size_t i = 0; i < cost.size(); ++i) {
0142 cost[i] = cost_function(population[i]);
0143 if (!isnan(target_value) && cost[i] <= target_value) {
0144 target_attained = true;
0145 }
0146 if (current_minimum_cost && cost[i] < *current_minimum_cost) {
0147 *current_minimum_cost = cost[i];
0148 }
0149 if (queries) {
0150 queries->push_back(std::make_pair(population[i], cost[i]));
0151 }
0152 }
0153 std::vector<size_t> indices = detail::best_indices(cost);
0154 std::atomic<size_t> function_evaluations = population.size();
0155 #if BOOST_MATH_DEBUG_JSO
0156 {
0157 auto const &min_cost = cost[indices[0]];
0158 auto const &location = population[indices[0]];
0159 std::cout << __FILE__ << ":" << __LINE__ << ":" << __func__;
0160 std::cout << "\n\tMinimum cost after evaluation of initial random "
0161 "population of size "
0162 << population.size() << " is " << min_cost << "."
0163 << "\n\tLocation {";
0164 for (size_t i = 0; i < location.size() - 1; ++i) {
0165 std::cout << location[i] << ", ";
0166 }
0167 std::cout << location.back() << "}.\n";
0168 }
0169 #endif
0170
0171
0172
0173
0174
0175 size_t H = 5;
0176 std::vector<DimensionlessReal> M_F(H, static_cast<DimensionlessReal>(0.3));
0177
0178
0179 std::vector<DimensionlessReal> M_CR(H, static_cast<DimensionlessReal>(0.8));
0180
0181 std::uniform_real_distribution<DimensionlessReal> unif01(DimensionlessReal(0), DimensionlessReal(1));
0182 bool keep_going = !target_attained;
0183 if (cancellation && *cancellation) {
0184 keep_going = false;
0185 }
0186
0187
0188
0189 size_t k = 0;
0190 size_t minimum_population_size = (max)(size_t(4), size_t(jso_params.threads));
0191 while (keep_going) {
0192
0193 std::vector<DimensionlessReal> S_CR;
0194 std::vector<DimensionlessReal> S_F;
0195
0196 std::vector<ResultType> delta_f;
0197 for (size_t i = 0; i < population.size(); ++i) {
0198
0199 auto ri = gen() % H;
0200
0201
0202
0203
0204
0205
0206 DimensionlessReal mu_F = static_cast<DimensionlessReal>(0.9);
0207 DimensionlessReal mu_CR = static_cast<DimensionlessReal>(0.9);
0208 if (ri != H - 1) {
0209 mu_F = M_F[ri];
0210 mu_CR = M_CR[ri];
0211 }
0212
0213 DimensionlessReal crossover_probability = static_cast<DimensionlessReal>(0);
0214 if (mu_CR >= 0) {
0215 using std::normal_distribution;
0216 normal_distribution<DimensionlessReal> normal(mu_CR, static_cast<DimensionlessReal>(0.1));
0217 crossover_probability = normal(gen);
0218
0219 crossover_probability = clamp(crossover_probability, DimensionlessReal(0), DimensionlessReal(1));
0220 }
0221
0222
0223
0224
0225
0226 if (4 * function_evaluations < jso_params.max_function_evaluations) {
0227 crossover_probability = (max)(crossover_probability, DimensionlessReal(0.7));
0228 } else if (2 * function_evaluations <
0229 jso_params.max_function_evaluations) {
0230 crossover_probability = (max)(crossover_probability, DimensionlessReal(0.6));
0231 }
0232
0233
0234
0235
0236 cauchy_distribution<DimensionlessReal> cauchy(mu_F, static_cast<DimensionlessReal>(0.1));
0237 DimensionlessReal F;
0238 do {
0239 F = cauchy(gen);
0240 if (F > 1) {
0241 F = 1;
0242 }
0243 } while (F <= 0);
0244 DimensionlessReal threshold = static_cast<DimensionlessReal>(7) / static_cast<DimensionlessReal>(10);
0245 if ((10 * function_evaluations <
0246 6 * jso_params.max_function_evaluations) &&
0247 (F > threshold)) {
0248 F = threshold;
0249 }
0250
0251
0252
0253 DimensionlessReal p = DimensionlessReal(0.25) * (1 - static_cast<DimensionlessReal>(function_evaluations) /
0254 (2 * jso_params.max_function_evaluations));
0255
0256 DimensionlessReal Fw = static_cast<DimensionlessReal>(1.2) * F;
0257 if (10 * function_evaluations < 4 * jso_params.max_function_evaluations) {
0258 if (10 * function_evaluations <
0259 2 * jso_params.max_function_evaluations) {
0260 Fw = static_cast<DimensionlessReal>(0.7) * F;
0261 } else {
0262 Fw = static_cast<DimensionlessReal>(0.8) * F;
0263 }
0264 }
0265
0266
0267
0268
0269
0270
0271
0272
0273
0274
0275
0276
0277
0278
0279
0280 size_t max_p_index = static_cast<size_t>(std::ceil(p * indices.size()));
0281 size_t p_best_idx = gen() % max_p_index;
0282
0283 size_t r1;
0284 do {
0285 r1 = gen() % population.size();
0286 } while (r1 == i);
0287 size_t r2;
0288 do {
0289 r2 = gen() % (population.size() + archive.size());
0290 } while (r2 == r1 || r2 == i);
0291
0292 ArgumentContainer trial_vector;
0293 if constexpr (detail::has_resize_v<ArgumentContainer>) {
0294 trial_vector.resize(dimension);
0295 }
0296 auto const &xi = population[i];
0297 auto const &xr1 = population[r1];
0298 ArgumentContainer xr2;
0299 if (r2 < population.size()) {
0300 xr2 = population[r2];
0301 } else {
0302 xr2 = archive[r2 - population.size()];
0303 }
0304 auto const &x_p_best = population[p_best_idx];
0305 for (size_t j = 0; j < dimension; ++j) {
0306 auto guaranteed_changed_idx = gen() % dimension;
0307 if (unif01(gen) < crossover_probability ||
0308 j == guaranteed_changed_idx) {
0309 auto tmp = xi[j] + Fw * (x_p_best[j] - xi[j]) + F * (xr1[j] - xr2[j]);
0310 auto const &lb = jso_params.lower_bounds[j];
0311 auto const &ub = jso_params.upper_bounds[j];
0312
0313
0314
0315
0316
0317 trial_vector[j] = clamp(tmp, lb, ub);
0318 } else {
0319 trial_vector[j] = population[i][j];
0320 }
0321 }
0322 auto trial_cost = cost_function(trial_vector);
0323 function_evaluations++;
0324 if (isnan(trial_cost)) {
0325 continue;
0326 }
0327 if (queries) {
0328 queries->push_back(std::make_pair(trial_vector, trial_cost));
0329 }
0330
0331
0332 if (trial_cost < cost[i] || isnan(cost[i])) {
0333 if (!isnan(target_value) && trial_cost <= target_value) {
0334 target_attained = true;
0335 }
0336 if (current_minimum_cost && trial_cost < *current_minimum_cost) {
0337 *current_minimum_cost = trial_cost;
0338 }
0339
0340 if (!isnan(cost[i])) {
0341 if (crossover_probability > 1 || crossover_probability < 0) {
0342 throw std::domain_error("Crossover probability is weird.");
0343 }
0344 if (F > 1 || F < 0) {
0345 throw std::domain_error("Scale factor (F) is weird.");
0346 }
0347 S_CR.push_back(crossover_probability);
0348 S_F.push_back(F);
0349 delta_f.push_back(abs(cost[i] - trial_cost));
0350 }
0351
0352 if (archive.size() < cost.size()) {
0353 archive.push_back(trial_vector);
0354 } else {
0355
0356 archive.resize(cost.size());
0357 auto idx = gen() % archive.size();
0358 archive[idx] = trial_vector;
0359 }
0360 cost[i] = trial_cost;
0361 population[i] = trial_vector;
0362 }
0363 }
0364
0365 indices = detail::best_indices(cost);
0366
0367
0368
0369 if (S_CR.size() > 0) {
0370 std::vector<DimensionlessReal> weights(S_CR.size(),
0371 std::numeric_limits<DimensionlessReal>::quiet_NaN());
0372 ResultType delta_sum = static_cast<ResultType>(0);
0373 for (auto const &delta : delta_f) {
0374 delta_sum += delta;
0375 }
0376 if (delta_sum <= 0 || !isfinite(delta_sum)) {
0377 std::ostringstream oss;
0378 oss << __FILE__ << ":" << __LINE__ << ":" << __func__;
0379 oss << "\n\tYou've hit a bug: The sum of improvements must be strictly "
0380 "positive, but got "
0381 << delta_sum << " improvement from " << S_CR.size()
0382 << " successful updates.\n";
0383 oss << "\tImprovements: {" << std::hexfloat;
0384 for (size_t l = 0; l < delta_f.size() -1; ++l) {
0385 oss << delta_f[l] << ", ";
0386 }
0387 oss << delta_f.back() << "}.\n";
0388 throw std::logic_error(oss.str());
0389 }
0390 for (size_t i = 0; i < weights.size(); ++i) {
0391 weights[i] = delta_f[i] / delta_sum;
0392 }
0393
0394 M_CR[k] = detail::weighted_lehmer_mean(S_CR, weights);
0395 M_F[k] = detail::weighted_lehmer_mean(S_F, weights);
0396 }
0397 k++;
0398 if (k == M_F.size()) {
0399 k = 0;
0400 }
0401 if (target_attained) {
0402 break;
0403 }
0404 if (cancellation && *cancellation) {
0405 break;
0406 }
0407 if (function_evaluations >= jso_params.max_function_evaluations) {
0408 break;
0409 }
0410
0411 size_t new_population_size = static_cast<size_t>(round(
0412 -double(jso_params.initial_population_size - minimum_population_size) *
0413 function_evaluations /
0414 static_cast<double>(jso_params.max_function_evaluations) +
0415 jso_params.initial_population_size));
0416 size_t num_erased = population.size() - new_population_size;
0417 std::vector<size_t> indices_to_erase(num_erased);
0418 size_t j = 0;
0419 for (size_t i = population.size() - 1; i >= new_population_size; --i) {
0420 indices_to_erase[j++] = indices[i];
0421 }
0422 std::sort(indices_to_erase.rbegin(), indices_to_erase.rend());
0423 for (auto const &idx : indices_to_erase) {
0424 population.erase(population.begin() + idx);
0425 cost.erase(cost.begin() + idx);
0426 }
0427 indices.resize(new_population_size);
0428 }
0429
0430 #if BOOST_MATH_DEBUG_JSO
0431 {
0432 auto const &min_cost = cost[indices[0]];
0433 auto const &location = population[indices[0]];
0434 std::cout << __FILE__ << ":" << __LINE__ << ":" << __func__;
0435 std::cout << "\n\tMinimum cost after completion is " << min_cost
0436 << ".\n\tLocation: {";
0437 for (size_t i = 0; i < location.size() - 1; ++i) {
0438 std::cout << location[i] << ", ";
0439 }
0440 std::cout << location.back() << "}.\n";
0441 std::cout << "\tRequired " << function_evaluations
0442 << " function calls out of "
0443 << jso_params.max_function_evaluations << " allowed.\n";
0444 if (target_attained) {
0445 std::cout << "\tReason for return: Target value attained.\n";
0446 }
0447 std::cout << "\tHistorical crossover probabilities (M_CR): {";
0448 for (size_t i = 0; i < M_CR.size() - 1; ++i) {
0449 std::cout << M_CR[i] << ", ";
0450 }
0451 std::cout << M_CR.back() << "}.\n";
0452 std::cout << "\tHistorical scale factors (M_F): {";
0453 for (size_t i = 0; i < M_F.size() - 1; ++i) {
0454 std::cout << M_F[i] << ", ";
0455 }
0456 std::cout << M_F.back() << "}.\n";
0457 std::cout << "\tFinal population size: " << population.size() << ".\n";
0458 }
0459 #endif
0460 return population[indices[0]];
0461 }
0462
0463 }
0464 #endif