use crate::primitives::Vector;
use super::line_search::WolfeLineSearch;
use super::{ConvergenceStatus, OptimizationResult, OptimizationResultF64, Optimizer};
pub(crate) trait LbfgsFloat:
Copy
+ PartialOrd
+ core::fmt::Debug
+ core::ops::Add<Output = Self>
+ core::ops::Sub<Output = Self>
+ core::ops::Mul<Output = Self>
+ core::ops::Div<Output = Self>
+ core::ops::Neg<Output = Self>
+ core::ops::AddAssign
+ core::ops::SubAssign
+ core::ops::MulAssign
{
const ZERO: Self;
const ONE: Self;
const TWO: Self;
const INF: Self;
const NAN_SIGNAL: Self;
const WOLFE_C1: Self;
const WOLFE_C2: Self;
const CURVATURE_EPS: Self;
const STALL_EPS: Self;
fn sqrt(self) -> Self;
fn abs(self) -> Self;
fn is_finite(self) -> bool;
fn midpoint(self, other: Self) -> Self;
}
impl LbfgsFloat for f32 {
const ZERO: Self = 0.0;
const ONE: Self = 1.0;
const TWO: Self = 2.0;
const INF: Self = f32::INFINITY;
const NAN_SIGNAL: Self = f32::NAN;
const WOLFE_C1: Self = 1e-4;
const WOLFE_C2: Self = 0.9;
const CURVATURE_EPS: Self = 1e-10;
const STALL_EPS: Self = 1e-12;
fn sqrt(self) -> Self {
f32::sqrt(self)
}
fn abs(self) -> Self {
f32::abs(self)
}
fn is_finite(self) -> bool {
f32::is_finite(self)
}
fn midpoint(self, other: Self) -> Self {
f32::midpoint(self, other)
}
}
impl LbfgsFloat for f64 {
const ZERO: Self = 0.0;
const ONE: Self = 1.0;
const TWO: Self = 2.0;
const INF: Self = f64::INFINITY;
const NAN_SIGNAL: Self = f64::NAN;
const WOLFE_C1: Self = 1e-4;
const WOLFE_C2: Self = 0.9;
const CURVATURE_EPS: Self = 1e-10;
const STALL_EPS: Self = 1e-12;
fn sqrt(self) -> Self {
f64::sqrt(self)
}
fn abs(self) -> Self {
f64::abs(self)
}
fn is_finite(self) -> bool {
f64::is_finite(self)
}
fn midpoint(self, other: Self) -> Self {
f64::midpoint(self, other)
}
}
fn zeros<T: LbfgsFloat>(n: usize) -> Vector<T> {
Vector::from_vec(vec![T::ZERO; n])
}
fn is_all_finite<T: LbfgsFloat>(v: &Vector<T>) -> bool {
(0..v.len()).all(|i| v[i].is_finite())
}
#[derive(Debug, Clone)]
struct WolfeSearch<T: LbfgsFloat> {
c1: T,
c2: T,
max_iter: usize,
}
impl<T: LbfgsFloat> WolfeSearch<T> {
fn new(c1: T, c2: T, max_iter: usize) -> Self {
Self { c1, c2, max_iter }
}
fn search<F, G>(&self, f: &F, grad: &G, x: &Vector<T>, d: &Vector<T>) -> T
where
F: Fn(&Vector<T>) -> T,
G: Fn(&Vector<T>) -> Vector<T>,
{
let fx = f(x);
let grad_x = grad(x);
let mut dir_deriv = T::ZERO;
for i in 0..x.len() {
dir_deriv += grad_x[i] * d[i];
}
if !fx.is_finite() || !dir_deriv.is_finite() {
return T::NAN_SIGNAL;
}
let mut alpha = T::ONE;
let mut alpha_lo = T::ZERO;
let mut alpha_hi = T::INF;
let mut x_new = zeros::<T>(x.len());
for _ in 0..self.max_iter {
for i in 0..x.len() {
x_new[i] = x[i] + alpha * d[i];
}
let fx_new = f(&x_new);
let grad_new = grad(&x_new);
let mut dir_deriv_new = T::ZERO;
for i in 0..x.len() {
dir_deriv_new += grad_new[i] * d[i];
}
if !fx_new.is_finite() || !dir_deriv_new.is_finite() || !is_all_finite(&x_new) {
return T::NAN_SIGNAL;
}
if fx_new > fx + self.c1 * alpha * dir_deriv {
alpha_hi = alpha;
alpha = alpha_lo.midpoint(alpha_hi);
continue;
}
if dir_deriv_new.abs() <= self.c2 * dir_deriv.abs() {
return alpha;
}
if dir_deriv_new > T::ZERO {
alpha_hi = alpha;
} else {
alpha_lo = alpha;
}
if alpha_hi.is_finite() {
alpha = alpha_lo.midpoint(alpha_hi);
} else {
alpha *= T::TWO;
}
}
alpha
}
}
#[derive(Debug, Clone)]
struct LbfgsOutcome<T: LbfgsFloat> {
solution: Vector<T>,
objective_value: T,
iterations: usize,
status: ConvergenceStatus,
gradient_norm: T,
elapsed_time: std::time::Duration,
}
#[derive(Debug, Clone)]
struct LbfgsImpl<T: LbfgsFloat> {
max_iter: usize,
tol: T,
m: usize,
line_search: WolfeSearch<T>,
s_history: Vec<Vector<T>>,
y_history: Vec<Vector<T>>,
}
impl<T: LbfgsFloat> LbfgsImpl<T> {
fn new(max_iter: usize, tol: T, m: usize, line_search: WolfeSearch<T>) -> Self {
Self {
max_iter,
tol,
m,
line_search,
s_history: Vec::with_capacity(m),
y_history: Vec::with_capacity(m),
}
}
#[provable_contracts_macros::contract("lbfgs-kernel-v1", equation = "two_loop_recursion")]
fn compute_direction(&self, grad: &Vector<T>) -> Vector<T> {
let n = grad.len();
let k = self.s_history.len();
if k == 0 {
let mut d = zeros::<T>(n);
for i in 0..n {
d[i] = -grad[i];
}
return d;
}
let mut q = zeros::<T>(n);
for i in 0..n {
q[i] = -grad[i];
}
let mut alpha = vec![T::ZERO; k];
let mut rho = vec![T::ZERO; k];
for i in (0..k).rev() {
let s = &self.s_history[i];
let y = &self.y_history[i];
let mut y_dot_s = T::ZERO;
for j in 0..n {
y_dot_s += y[j] * s[j];
}
rho[i] = T::ONE / y_dot_s;
let mut s_dot_q = T::ZERO;
for j in 0..n {
s_dot_q += s[j] * q[j];
}
alpha[i] = rho[i] * s_dot_q;
for j in 0..n {
q[j] -= alpha[i] * y[j];
}
}
let s_last = &self.s_history[k - 1];
let y_last = &self.y_history[k - 1];
let mut s_dot_y = T::ZERO;
let mut y_dot_y = T::ZERO;
for i in 0..n {
s_dot_y += s_last[i] * y_last[i];
y_dot_y += y_last[i] * y_last[i];
}
let gamma = s_dot_y / y_dot_y;
let mut r = zeros::<T>(n);
for i in 0..n {
r[i] = gamma * q[i];
}
for i in 0..k {
let s = &self.s_history[i];
let y = &self.y_history[i];
let mut y_dot_r = T::ZERO;
for j in 0..n {
y_dot_r += y[j] * r[j];
}
let beta = rho[i] * y_dot_r;
for j in 0..n {
r[j] += s[j] * (alpha[i] - beta);
}
}
r
}
fn norm(v: &Vector<T>) -> T {
let mut sum = T::ZERO;
for i in 0..v.len() {
sum += v[i] * v[i];
}
sum.sqrt()
}
fn outcome(
solution: Vector<T>,
objective_value: T,
iterations: usize,
status: ConvergenceStatus,
gradient_norm: T,
elapsed_time: std::time::Duration,
) -> LbfgsOutcome<T> {
LbfgsOutcome {
solution,
objective_value,
iterations,
status,
gradient_norm,
elapsed_time,
}
}
#[provable_contracts_macros::contract("lbfgs-kernel-v1", equation = "nonfinite_input_status")]
fn minimize<F, G>(&mut self, objective: F, gradient: G, x0: Vector<T>) -> LbfgsOutcome<T>
where
F: Fn(&Vector<T>) -> T,
G: Fn(&Vector<T>) -> Vector<T>,
{
let start_time = std::time::Instant::now();
let n = x0.len();
self.s_history.clear();
self.y_history.clear();
let mut x = x0;
let mut fx = objective(&x);
let mut grad = gradient(&x);
let mut grad_norm = Self::norm(&grad);
if !is_all_finite(&x) || !fx.is_finite() || !is_all_finite(&grad) || !grad_norm.is_finite()
{
return Self::outcome(
x,
fx,
0,
ConvergenceStatus::NumericalError,
grad_norm,
start_time.elapsed(),
);
}
for iter in 0..self.max_iter {
if grad_norm < self.tol {
return Self::outcome(
x,
fx,
iter,
ConvergenceStatus::Converged,
grad_norm,
start_time.elapsed(),
);
}
let d = self.compute_direction(&grad);
let alpha = self.line_search.search(&objective, &gradient, &x, &d);
if !alpha.is_finite() {
return Self::outcome(
x,
fx,
iter,
ConvergenceStatus::NumericalError,
grad_norm,
start_time.elapsed(),
);
}
if alpha < T::STALL_EPS {
return Self::outcome(
x,
fx,
iter,
ConvergenceStatus::Stalled,
grad_norm,
start_time.elapsed(),
);
}
let mut x_new = zeros::<T>(n);
for i in 0..n {
x_new[i] = x[i] + alpha * d[i];
}
let fx_new = objective(&x_new);
let grad_new = gradient(&x_new);
if !fx_new.is_finite() || !is_all_finite(&grad_new) || !is_all_finite(&x_new) {
return Self::outcome(
x,
fx,
iter,
ConvergenceStatus::NumericalError,
grad_norm,
start_time.elapsed(),
);
}
let mut s_k = zeros::<T>(n);
let mut y_k = zeros::<T>(n);
for i in 0..n {
s_k[i] = x_new[i] - x[i];
y_k[i] = grad_new[i] - grad[i];
}
let mut y_dot_s = T::ZERO;
for i in 0..n {
y_dot_s += y_k[i] * s_k[i];
}
if y_dot_s > T::CURVATURE_EPS {
if self.s_history.len() >= self.m {
self.s_history.remove(0);
self.y_history.remove(0);
}
self.s_history.push(s_k);
self.y_history.push(y_k);
}
x = x_new;
fx = fx_new;
grad = grad_new;
grad_norm = Self::norm(&grad);
}
Self::outcome(
x,
fx,
self.max_iter,
ConvergenceStatus::MaxIterations,
grad_norm,
start_time.elapsed(),
)
}
}
#[derive(Debug, Clone)]
pub struct LBFGS {
pub(crate) max_iter: usize,
pub(crate) tol: f32,
pub(crate) m: usize,
line_search: WolfeLineSearch,
pub(crate) s_history: Vec<Vector<f32>>,
pub(crate) y_history: Vec<Vector<f32>>,
}
impl LBFGS {
#[must_use]
pub fn new(max_iter: usize, tol: f32, m: usize) -> Self {
Self {
max_iter,
tol,
m,
line_search: WolfeLineSearch::new(1e-4, 0.9, 50),
s_history: Vec::with_capacity(m),
y_history: Vec::with_capacity(m),
}
}
fn core(&self) -> LbfgsImpl<f32> {
LbfgsImpl::new(
self.max_iter,
self.tol,
self.m,
WolfeSearch::new(
self.line_search.c1,
self.line_search.c2,
self.line_search.max_iter,
),
)
}
}
#[cfg(test)]
impl LBFGS {
fn compute_direction(&self, grad: &Vector<f32>) -> Vector<f32> {
let mut core = self.core();
core.s_history.clone_from(&self.s_history);
core.y_history.clone_from(&self.y_history);
core.compute_direction(grad)
}
fn norm(v: &Vector<f32>) -> f32 {
LbfgsImpl::<f32>::norm(v)
}
}
impl Optimizer for LBFGS {
fn step(&mut self, _params: &mut Vector<f32>, _gradients: &Vector<f32>) {
panic!(
"L-BFGS does not support stochastic updates (step). Use minimize() for batch optimization."
)
}
fn minimize<F, G>(&mut self, objective: F, gradient: G, x0: Vector<f32>) -> OptimizationResult
where
F: Fn(&Vector<f32>) -> f32,
G: Fn(&Vector<f32>) -> Vector<f32>,
{
let mut core = self.core();
let outcome = core.minimize(objective, gradient, x0);
self.s_history = core.s_history;
self.y_history = core.y_history;
OptimizationResult {
solution: outcome.solution,
objective_value: outcome.objective_value,
iterations: outcome.iterations,
status: outcome.status,
gradient_norm: outcome.gradient_norm,
constraint_violation: 0.0,
elapsed_time: outcome.elapsed_time,
}
}
fn reset(&mut self) {
self.s_history.clear();
self.y_history.clear();
}
}
#[derive(Debug, Clone)]
pub struct LbfgsF64 {
pub(crate) max_iter: usize,
pub(crate) tol: f64,
pub(crate) m: usize,
pub(crate) s_history: Vec<Vector<f64>>,
pub(crate) y_history: Vec<Vector<f64>>,
}
impl LbfgsF64 {
#[must_use]
pub fn new(max_iter: usize, tol: f64, m: usize) -> Self {
Self {
max_iter,
tol,
m,
s_history: Vec::with_capacity(m),
y_history: Vec::with_capacity(m),
}
}
pub fn minimize<F, G>(
&mut self,
objective: F,
gradient: G,
x0: &Vector<f64>,
) -> OptimizationResultF64
where
F: Fn(&Vector<f64>) -> f64,
G: Fn(&Vector<f64>) -> Vector<f64>,
{
let mut core = LbfgsImpl::new(
self.max_iter,
self.tol,
self.m,
WolfeSearch::new(f64::WOLFE_C1, f64::WOLFE_C2, 50),
);
let outcome = core.minimize(objective, gradient, x0.clone());
self.s_history = core.s_history;
self.y_history = core.y_history;
OptimizationResultF64 {
solution: outcome.solution,
objective_value: outcome.objective_value,
iterations: outcome.iterations,
status: outcome.status,
gradient_norm: outcome.gradient_norm,
}
}
pub fn reset(&mut self) {
self.s_history.clear();
self.y_history.clear();
}
}
#[cfg(test)]
#[path = "lbfgs_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "tests_lbfgs_contract.rs"]
mod tests_lbfgs_contract;