| /* |
| * 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 |