// Copyright Maksym Zhelyenzyakov 2025-2026. // Copyright Matt Borland 2026. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) // // Module build check for boost::math::optimization L-BFGS. It exercises the // exported make_lbfgs / minimize entry points when built against the boost.math // module. The reverse-mode-autodiff paths of test_lbfgs.cpp are not usable here // (autodiff_reverse is not part of the module export surface), so this test // drives the fully analytic path: minimizing the isotropic quadratic // f(x) = sum x_i^2 whose minimizer is the origin with value 0. #ifndef BOOST_MATH_BUILD_MODULE #include #include #else import boost.math; #endif #ifndef BOOST_MATH_BUILD_MODULE #include #include #endif #include "math_unit_test.hpp" namespace bopt = boost::math::optimization; // Objective: isotropic quadratic, minimum 0 at the origin. template Real quadratic(std::vector& x) { Real res{ 0 }; for (auto& item : x) { res += item * item; } return res; } // No-op initializer: keep the caller-supplied starting point. template struct identity_init_policy { void operator()(std::vector&) const noexcept {} }; // Objective evaluation policy: just call the objective. template struct analytic_objective_eval_policy { template Real operator()(Objective&& objective, ArgumentContainer& x) { return objective(x); } }; // Gradient evaluation policy: analytic gradient of sum x_i^2 is 2*x. template struct analytic_gradient_eval_policy { template void operator()(Objective&& obj_f, ArgumentContainer& x, FunctionEvaluationPolicy&& f_eval_pol, Real& obj_v, std::vector& grad_container) { obj_v = f_eval_pol(obj_f, x); grad_container.resize(x.size()); for (std::size_t i{ 0 }; i < x.size(); ++i) { grad_container[i] = 2 * x[i]; } } }; // Self-contained backtracking Armijo line search. The library's own // armijo_line_search_policy and its dot/axpy helpers are not exported by the // module, so the search is reimplemented here with plain loops. template struct armijo_line_search_policy { template Real operator()(Objective& objective, ObjectiveEvalPolicy& obj_eval, GradientEvalPolicy& /*grad_eval*/, ArgumentContainer& x, const std::vector& g, const std::vector& p, Real f_x) const { const Real c{ static_cast(1e-4) }; const Real rho{ static_cast(0.5) }; Real gTp{ 0 }; for (std::size_t i{ 0 }; i < g.size(); ++i) { gTp += g[i] * p[i]; } Real alpha{ 1 }; for (int iter{ 0 }; iter < 20; ++iter) { ArgumentContainer x_trial{ x }; for (std::size_t i{ 0 }; i < x_trial.size(); ++i) { x_trial[i] += alpha * p[i]; } const Real f_trial{ obj_eval(objective, x_trial) }; if (f_trial <= f_x + c * alpha * gTp) { return alpha; } alpha *= rho; } return alpha; } }; template void test_quadratic_minimization() { constexpr std::size_t M{ 10 }; const Real tol{ static_cast(1e-3) }; // Non-trivial, distinct starting coordinates. std::vector x{ static_cast(2), static_cast(-1.5), static_cast(3) }; auto opt = bopt::make_lbfgs(&quadratic, // Objective x, // Arguments M, // History size identity_init_policy{}, // Initialization analytic_objective_eval_policy{}, // Function eval analytic_gradient_eval_policy{}, // Gradient eval armijo_line_search_policy{}); // Line search auto result = bopt::minimize(opt); // The minimizer of sum x_i^2 is the origin; each coordinate converges to 0. for (auto& xi : x) { CHECK_ABSOLUTE_ERROR(static_cast(0), xi, tol); } // Objective value at the minimum is 0, and the run reports convergence. CHECK_ABSOLUTE_ERROR(static_cast(0), result.objective_value, tol); CHECK_EQUAL(true, result.converged); } int main() { test_quadratic_minimization(); test_quadratic_minimization(); return boost::math::test::report_errors(); }