// Copyright Maksym Zhelyenzyakov 2025-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) #ifndef BOOST_MATH_OPTIMIZATION_LBFGS_HPP #define BOOST_MATH_OPTIMIZATION_LBFGS_HPP #include #include #include #include #include #include namespace boost { namespace math { namespace optimization { /** @brief> Helper struct for L-BFGS * * stores state of L-BFGS optimizer * @param> m = 10 -> history (how far back to look) * @param> S - > x_k - x_{k-1} ( m states ) * @param> Y - > g_k - g_{k-1} (m states) * @param> rho - > 1/(y^T y) * @param> g_prev, x_prev - > previous state of argument and gradient * @param -> f_prev - > previous function value * * https://en.wikipedia.org/wiki/Limited-memory_BFGS * * Jorge Nocedal and Stephen J. Wright, * Numerical Optimization, 2nd Edition, * Springer, 2006. * * pages 176-180 * algorithms 7.4/7.5 * */ template struct lbfgs_optimizer_state { size_t m = 10; // default history length std::deque> S, Y; std::deque rho; std::vector g_prev, x_prev; RealType f_prev = std::numeric_limits::quiet_NaN(); const RealType EPS = std::numeric_limits::epsilon(); template void update_state(ArgumentContainer& x, std::vector& g_k, RealType fk) { // iteration 0 if (g_prev.empty()) { g_prev.assign(g_k.begin(), g_k.end()); x_prev.resize(x.size()); std::transform(x.begin(), x.end(), x_prev.begin(), [](const auto& xi) { return static_cast(xi); }); f_prev = fk; return; } std::vector s_k(x.size()), y_k(g_k.size()); for (size_t i = 0; i < x.size(); ++i) { s_k[i] = static_cast(x[i]) - x_prev[i]; y_k[i] = g_k[i] - g_prev[i]; } RealType ys = dot(y_k, s_k); RealType sn = sqrt(dot(s_k, s_k)); RealType yn = sqrt(dot(y_k, y_k)); const RealType threshold = EPS * sn * yn; if (ys > threshold && ys > RealType(0)) { // check if curvature if non-zero if (S.size() == m) { // iteration > m S.pop_front(); Y.pop_front(); rho.pop_front(); } S.push_back(std::move(s_k)); Y.push_back(std::move(y_k)); rho.push_back(RealType(1) / ys); } g_prev.assign(g_k.begin(), g_k.end()); // safely cast to realtype std::transform(x.begin(), x.end(), x_prev.begin(), [](const auto& xi) { return static_cast(xi); }); f_prev = fk; } }; /** @brief> helper update for l-bfgs * x += alpha * search direction * */ template struct lbfgs_update_policy { template::value>::type> void operator()(ArgumentType& x, RealType pk, RealType alpha) { x.get_value() += alpha * pk; } template::value, int>::type = 0> void operator()(ArgumentType& x, RealType pk, RealType alpha) { x += alpha * pk; } }; /** * * @brief Limited-memory BFGS (L-BFGS) optimizer * * The `lbfgs` class implements the Limited-memory BFGS optimization algorithm, * a quasi-Newton method that approximates the inverse Hessian using a rolling * window of the last `m` updates. It is suitable for medium- to large-scale * optimization problems where full Hessian storage is infeasible. * * @tparam> ArgumentContainer: container type for parameters, e.g. * std::vector * @tparam> RealType scalar floating type (e.g. double, float) * @tparam> Objective: objective function. must support "f(x)" evaluation * @tparam> InitializationPolicy: policy for initializing x * @tparam> ObjectiveEvalPolicy: policy for computing the objective value * @tparam> GradEvalPolicy: policy for computing gradients * @tparam> LineaSearchPolicy: e.g. Armijo, StrongWolfe * * https://en.wikipedia.org/wiki/Limited-memory_BFGS */ template class lbfgs : public abstract_optimizer, lbfgs> { using base_opt = abstract_optimizer, lbfgs>; const RealType EPS = std::numeric_limits::epsilon(); lbfgs_optimizer_state state_; LineSearchPolicy line_search_; std::vector compute_direction(const std::vector& gk) { const size_t n = gk.size(); const size_t L = state_.S.size(); // since S changes when iter < m if (L == 0) { std::vector p(n); std::transform( gk.begin(), gk.end(), p.begin(), [](RealType gi) { return -gi; }); return p; } std::vector q = gk; std::vector alpha(L, RealType(0)); for (size_t t = 0; t < L; ++t) { const std::size_t i = L - 1 - t; // newest first const RealType sTq = dot(state_.S[i], q); alpha[i] = state_.rho[i] * sTq; axpy(-alpha[i], state_.Y[i], q); } const RealType sTy = dot(state_.S.back(), state_.Y.back()); const RealType yTy = dot(state_.Y.back(), state_.Y.back()); const RealType gamma = (yTy > RealType(0)) ? (sTy / yTy) : RealType(1); std::vector r = q; scale(r, gamma); for (std::size_t i = 0; i < L; ++i) { const RealType yTr = dot(state_.Y[i], r); const RealType beta = state_.rho[i] * yTr; axpy(alpha[i] - beta, state_.S[i], r); } scale(r, RealType{ -1 }); return r; } public: using base_opt::base_opt; lbfgs(Objective&& objective, ArgumentContainer& x, size_t m, InitializationPolicy&& ip, ObjectiveEvalPolicy&& oep, GradEvalPolicy&& gep, lbfgs_update_policy&& up, LineSearchPolicy&& lsp) : base_opt(std::forward(objective), x, std::forward(ip), std::forward(oep), std::forward(gep), std::forward>(up)) , line_search_(lsp) { state_.m = m; state_.S.clear(); state_.Y.clear(); state_.rho.clear(); state_.g_prev.clear(); state_.f_prev = std::numeric_limits::quiet_NaN(); } void step() { auto& x = this->arguments(); auto& g = this->gradients(); auto& obj = this->objective_value(); auto& obj_eval = this->obj_eval_; auto& grad_eval = this->grad_eval_; auto& objective = this->objective_; auto& update = this->update_; grad_eval(objective, x, obj_eval, obj, g); state_.update_state(x, g, obj); std::vector p = compute_direction(g); RealType alpha = line_search_(objective, obj_eval, grad_eval, x, g, p, obj); for (size_t i = 0; i < x.size(); ++i) { update(x[i], p[i], alpha); } } }; template auto make_lbfgs(Objective&& obj, ArgumentContainer& x, std::size_t m = 10) { using RealType = typename argument_container_t::type; return lbfgs, tape_initializer_rvar, reverse_mode_function_eval_policy, reverse_mode_gradient_evaluation_policy, strong_wolfe_line_search_policy>( std::forward(obj), x, m, tape_initializer_rvar{}, reverse_mode_function_eval_policy{}, reverse_mode_gradient_evaluation_policy{}, lbfgs_update_policy{}, strong_wolfe_line_search_policy{}); } template auto make_lbfgs(Objective&& obj, ArgumentContainer& x, std::size_t m, InitializationPolicy&& ip) { using RealType = typename argument_container_t::type; return lbfgs, InitializationPolicy, reverse_mode_function_eval_policy, reverse_mode_gradient_evaluation_policy, strong_wolfe_line_search_policy>( std::forward(obj), x, m, std::forward(ip), reverse_mode_function_eval_policy{}, reverse_mode_gradient_evaluation_policy{}, lbfgs_update_policy{}, strong_wolfe_line_search_policy{}); } template auto make_lbfgs(Objective&& obj, ArgumentContainer& x, std::size_t m, InitializationPolicy&& ip, LineSearchPolicy&& lsp) { using RealType = typename argument_container_t::type; return lbfgs, InitializationPolicy, reverse_mode_function_eval_policy, reverse_mode_gradient_evaluation_policy, LineSearchPolicy>( std::forward(obj), x, m, std::forward(ip), reverse_mode_function_eval_policy{}, reverse_mode_gradient_evaluation_policy{}, lbfgs_update_policy{}, std::forward(lsp)); } template auto make_lbfgs(Objective&& obj, ArgumentContainer& x, std::size_t m, InitializationPolicy&& ip, FunctionEvalPolicy&& fep, GradientEvalPolicy&& gep, LineSearchPolicy&& lsp) { using RealType = typename argument_container_t::type; return lbfgs, InitializationPolicy, FunctionEvalPolicy, GradientEvalPolicy, LineSearchPolicy>(std::forward(obj), x, m, std::forward(ip), std::forward(fep), std::forward(gep), lbfgs_update_policy{}, std::forward(lsp)); } } // namespace optimization } // namespace math } // namespace boost #endif