146 lines
5.3 KiB
C++
146 lines
5.3 KiB
C++
/*
|
|
* Copyright Nick Thompson, 2024
|
|
* Use, modification and distribution are subject to the
|
|
* Boost Software License, Version 1.0. (See accompanying file
|
|
* LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
|
*/
|
|
#ifndef BOOST_MATH_OPTIMIZATION_RANDOM_SEARCH_HPP
|
|
#define BOOST_MATH_OPTIMIZATION_RANDOM_SEARCH_HPP
|
|
#include <atomic>
|
|
#include <cmath>
|
|
#include <limits>
|
|
#include <mutex>
|
|
#include <random>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
#include <thread>
|
|
#include <utility>
|
|
#include <vector>
|
|
#include <boost/math/optimization/detail/common.hpp>
|
|
|
|
namespace boost::math::optimization {
|
|
|
|
template <typename ArgumentContainer> struct random_search_parameters {
|
|
using Real = typename ArgumentContainer::value_type;
|
|
ArgumentContainer lower_bounds;
|
|
ArgumentContainer upper_bounds;
|
|
size_t max_function_calls = 10000*std::thread::hardware_concurrency();
|
|
ArgumentContainer const *initial_guess = nullptr;
|
|
unsigned threads = std::thread::hardware_concurrency();
|
|
};
|
|
|
|
template <typename ArgumentContainer>
|
|
void validate_random_search_parameters(random_search_parameters<ArgumentContainer> const ¶ms) {
|
|
using std::isfinite;
|
|
using std::isnan;
|
|
std::ostringstream oss;
|
|
detail::validate_bounds(params.lower_bounds, params.upper_bounds);
|
|
if (params.initial_guess) {
|
|
detail::validate_initial_guess(*params.initial_guess, params.lower_bounds, params.upper_bounds);
|
|
}
|
|
if (params.threads == 0) {
|
|
oss << __FILE__ << ":" << __LINE__ << ":" << __func__;
|
|
oss << ": There must be at least one thread.";
|
|
throw std::invalid_argument(oss.str());
|
|
}
|
|
}
|
|
|
|
template <typename ArgumentContainer, class Func, class URBG>
|
|
ArgumentContainer random_search(
|
|
const Func cost_function,
|
|
random_search_parameters<ArgumentContainer> const ¶ms,
|
|
URBG &gen,
|
|
std::invoke_result_t<Func, ArgumentContainer> target_value = std::numeric_limits<std::invoke_result_t<Func, ArgumentContainer>>::quiet_NaN(),
|
|
std::atomic<bool> *cancellation = nullptr,
|
|
std::atomic<std::invoke_result_t<Func, ArgumentContainer>> *current_minimum_cost = nullptr,
|
|
std::vector<std::pair<ArgumentContainer, std::invoke_result_t<Func, ArgumentContainer>>> *queries = nullptr)
|
|
{
|
|
using Real = typename ArgumentContainer::value_type;
|
|
using DimensionlessReal = decltype(Real()/Real());
|
|
using ResultType = std::invoke_result_t<Func, ArgumentContainer>;
|
|
using std::isnan;
|
|
using std::uniform_real_distribution;
|
|
validate_random_search_parameters(params);
|
|
const size_t dimension = params.lower_bounds.size();
|
|
std::atomic<bool> target_attained = false;
|
|
// Unfortunately, the "minimum_cost" variable can either be passed
|
|
// (for observability) or not (if the user doesn't care).
|
|
// That makes this a bit awkward . . .
|
|
std::atomic<ResultType> lowest_cost = std::numeric_limits<ResultType>::infinity();
|
|
|
|
ArgumentContainer best_vector;
|
|
if constexpr (detail::has_resize_v<ArgumentContainer>) {
|
|
best_vector.resize(dimension, std::numeric_limits<Real>::quiet_NaN());
|
|
}
|
|
if (params.initial_guess) {
|
|
auto initial_cost = cost_function(*params.initial_guess);
|
|
if (!isnan(initial_cost)) {
|
|
lowest_cost = initial_cost;
|
|
best_vector = *params.initial_guess;
|
|
if (current_minimum_cost) {
|
|
*current_minimum_cost = initial_cost;
|
|
}
|
|
}
|
|
}
|
|
std::mutex mt;
|
|
std::vector<std::thread> thread_pool;
|
|
std::atomic<size_t> function_calls = 0;
|
|
for (unsigned j = 0; j < params.threads; ++j) {
|
|
auto seed = gen();
|
|
thread_pool.emplace_back([&, seed]() {
|
|
URBG g(seed);
|
|
ArgumentContainer trial_vector;
|
|
// This vector is empty unless the user requests the queries be stored:
|
|
std::vector<std::pair<ArgumentContainer, std::invoke_result_t<Func, ArgumentContainer>>> local_queries;
|
|
if constexpr (detail::has_resize_v<ArgumentContainer>) {
|
|
trial_vector.resize(dimension, std::numeric_limits<Real>::quiet_NaN());
|
|
}
|
|
while (function_calls < params.max_function_calls) {
|
|
if (cancellation && *cancellation) {
|
|
break;
|
|
}
|
|
if (target_attained) {
|
|
break;
|
|
}
|
|
// Fill trial vector:
|
|
uniform_real_distribution<DimensionlessReal> unif01(DimensionlessReal(0), DimensionlessReal(1));
|
|
for (size_t i = 0; i < dimension; ++i) {
|
|
trial_vector[i] = params.lower_bounds[i] + (params.upper_bounds[i] - params.lower_bounds[i])*unif01(g);
|
|
}
|
|
ResultType trial_cost = cost_function(trial_vector);
|
|
++function_calls;
|
|
if (isnan(trial_cost)) {
|
|
continue;
|
|
}
|
|
if (trial_cost < lowest_cost) {
|
|
lowest_cost = trial_cost;
|
|
if (current_minimum_cost) {
|
|
*current_minimum_cost = trial_cost;
|
|
}
|
|
// We expect to need to acquire this lock with decreasing frequency
|
|
// as the computation proceeds:
|
|
std::scoped_lock lock(mt);
|
|
best_vector = trial_vector;
|
|
}
|
|
if (queries) {
|
|
local_queries.push_back(std::make_pair(trial_vector, trial_cost));
|
|
}
|
|
if (!isnan(target_value) && trial_cost <= target_value) {
|
|
target_attained = true;
|
|
}
|
|
}
|
|
if (queries) {
|
|
std::scoped_lock lock(mt);
|
|
queries->insert(queries->begin(), local_queries.begin(), local_queries.end());
|
|
}
|
|
});
|
|
}
|
|
for (auto &thread : thread_pool) {
|
|
thread.join();
|
|
}
|
|
return best_vector;
|
|
}
|
|
|
|
} // namespace boost::math::optimization
|
|
#endif
|