random_search.hpp 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. /*
  2. * Copyright Nick Thompson, 2024
  3. * Use, modification and distribution are subject to the
  4. * Boost Software License, Version 1.0. (See accompanying file
  5. * LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
  6. */
  7. #ifndef BOOST_MATH_OPTIMIZATION_RANDOM_SEARCH_HPP
  8. #define BOOST_MATH_OPTIMIZATION_RANDOM_SEARCH_HPP
  9. #include <atomic>
  10. #include <cmath>
  11. #include <limits>
  12. #include <mutex>
  13. #include <random>
  14. #include <sstream>
  15. #include <stdexcept>
  16. #include <thread>
  17. #include <utility>
  18. #include <vector>
  19. #include <boost/math/optimization/detail/common.hpp>
  20. namespace boost::math::optimization {
  21. template <typename ArgumentContainer> struct random_search_parameters {
  22. using Real = typename ArgumentContainer::value_type;
  23. ArgumentContainer lower_bounds;
  24. ArgumentContainer upper_bounds;
  25. size_t max_function_calls = 10000*std::thread::hardware_concurrency();
  26. ArgumentContainer const *initial_guess = nullptr;
  27. unsigned threads = std::thread::hardware_concurrency();
  28. };
  29. template <typename ArgumentContainer>
  30. void validate_random_search_parameters(random_search_parameters<ArgumentContainer> const &params) {
  31. using std::isfinite;
  32. using std::isnan;
  33. std::ostringstream oss;
  34. detail::validate_bounds(params.lower_bounds, params.upper_bounds);
  35. if (params.initial_guess) {
  36. detail::validate_initial_guess(*params.initial_guess, params.lower_bounds, params.upper_bounds);
  37. }
  38. if (params.threads == 0) {
  39. oss << __FILE__ << ":" << __LINE__ << ":" << __func__;
  40. oss << ": There must be at least one thread.";
  41. throw std::invalid_argument(oss.str());
  42. }
  43. }
  44. template <typename ArgumentContainer, class Func, class URBG>
  45. ArgumentContainer random_search(
  46. const Func cost_function,
  47. random_search_parameters<ArgumentContainer> const &params,
  48. URBG &gen,
  49. std::invoke_result_t<Func, ArgumentContainer> target_value = std::numeric_limits<std::invoke_result_t<Func, ArgumentContainer>>::quiet_NaN(),
  50. std::atomic<bool> *cancellation = nullptr,
  51. std::atomic<std::invoke_result_t<Func, ArgumentContainer>> *current_minimum_cost = nullptr,
  52. std::vector<std::pair<ArgumentContainer, std::invoke_result_t<Func, ArgumentContainer>>> *queries = nullptr)
  53. {
  54. using Real = typename ArgumentContainer::value_type;
  55. using DimensionlessReal = decltype(Real()/Real());
  56. using ResultType = std::invoke_result_t<Func, ArgumentContainer>;
  57. using std::isnan;
  58. using std::uniform_real_distribution;
  59. validate_random_search_parameters(params);
  60. const size_t dimension = params.lower_bounds.size();
  61. std::atomic<bool> target_attained = false;
  62. // Unfortunately, the "minimum_cost" variable can either be passed
  63. // (for observability) or not (if the user doesn't care).
  64. // That makes this a bit awkward . . .
  65. std::atomic<ResultType> lowest_cost = std::numeric_limits<ResultType>::infinity();
  66. ArgumentContainer best_vector;
  67. if constexpr (detail::has_resize_v<ArgumentContainer>) {
  68. best_vector.resize(dimension, std::numeric_limits<Real>::quiet_NaN());
  69. }
  70. if (params.initial_guess) {
  71. auto initial_cost = cost_function(*params.initial_guess);
  72. if (!isnan(initial_cost)) {
  73. lowest_cost = initial_cost;
  74. best_vector = *params.initial_guess;
  75. if (current_minimum_cost) {
  76. *current_minimum_cost = initial_cost;
  77. }
  78. }
  79. }
  80. std::mutex mt;
  81. std::vector<std::thread> thread_pool;
  82. std::atomic<size_t> function_calls = 0;
  83. for (unsigned j = 0; j < params.threads; ++j) {
  84. auto seed = gen();
  85. thread_pool.emplace_back([&, seed]() {
  86. URBG g(seed);
  87. ArgumentContainer trial_vector;
  88. // This vector is empty unless the user requests the queries be stored:
  89. std::vector<std::pair<ArgumentContainer, std::invoke_result_t<Func, ArgumentContainer>>> local_queries;
  90. if constexpr (detail::has_resize_v<ArgumentContainer>) {
  91. trial_vector.resize(dimension, std::numeric_limits<Real>::quiet_NaN());
  92. }
  93. while (function_calls < params.max_function_calls) {
  94. if (cancellation && *cancellation) {
  95. break;
  96. }
  97. if (target_attained) {
  98. break;
  99. }
  100. // Fill trial vector:
  101. uniform_real_distribution<DimensionlessReal> unif01(DimensionlessReal(0), DimensionlessReal(1));
  102. for (size_t i = 0; i < dimension; ++i) {
  103. trial_vector[i] = params.lower_bounds[i] + (params.upper_bounds[i] - params.lower_bounds[i])*unif01(g);
  104. }
  105. ResultType trial_cost = cost_function(trial_vector);
  106. ++function_calls;
  107. if (isnan(trial_cost)) {
  108. continue;
  109. }
  110. if (trial_cost < lowest_cost) {
  111. lowest_cost = trial_cost;
  112. if (current_minimum_cost) {
  113. *current_minimum_cost = trial_cost;
  114. }
  115. // We expect to need to acquire this lock with decreasing frequency
  116. // as the computation proceeds:
  117. std::scoped_lock lock(mt);
  118. best_vector = trial_vector;
  119. }
  120. if (queries) {
  121. local_queries.push_back(std::make_pair(trial_vector, trial_cost));
  122. }
  123. if (!isnan(target_value) && trial_cost <= target_value) {
  124. target_attained = true;
  125. }
  126. }
  127. if (queries) {
  128. std::scoped_lock lock(mt);
  129. queries->insert(queries->begin(), local_queries.begin(), local_queries.end());
  130. }
  131. });
  132. }
  133. for (auto &thread : thread_pool) {
  134. thread.join();
  135. }
  136. return best_vector;
  137. }
  138. } // namespace boost::math::optimization
  139. #endif