#![allow(clippy::needless_range_loop, clippy::too_many_arguments)]
pub(crate) mod backend;
pub(crate) mod cauchy;
pub(crate) mod compact;
pub(crate) mod formk;
pub(crate) mod subsm;
use crate::core::constraint::BoxConstrained;
use crate::core::math::{Dot, ScaledAdd};
use crate::core::problem::{CostFunction, Gradient};
use crate::core::solver::Solver;
use crate::core::state::lbfgs::{LbfgsState, LbfgsbWork};
use crate::core::termination::TerminationReason;
use crate::line_search::{LineSearch, MoreThuente};
use self::backend::{AsFloatSlice, AsFloatSliceMut};
use self::cauchy::{cauchy, iwhere as iwh};
use self::compact::{bmv, formt};
use self::formk::formk;
use self::subsm::subsm;
pub struct LBFGSB<S = MoreThuente> {
line_search: S,
epsilon: f64,
tol_pg: f64,
pub(crate) m_capacity: usize,
}
impl Default for LBFGSB<MoreThuente> {
fn default() -> Self {
Self::new()
}
}
impl LBFGSB<MoreThuente> {
pub fn new() -> Self {
Self {
line_search: MoreThuente::new(),
epsilon: f64::EPSILON,
tol_pg: 1e-10,
m_capacity: 10,
}
}
}
impl<S> LBFGSB<S> {
pub fn with_line_search(line_search: S) -> Self {
Self {
line_search,
epsilon: f64::EPSILON,
tol_pg: 1e-10,
m_capacity: 10,
}
}
pub fn epsilon(mut self, epsilon: f64) -> Self {
assert!(epsilon >= 0.0, "epsilon must be ≥ 0");
self.epsilon = epsilon;
self
}
pub fn tol_pg(mut self, tol_pg: f64) -> Self {
assert!(tol_pg >= 0.0, "tol_pg must be ≥ 0");
self.tol_pg = tol_pg;
self
}
pub fn m_capacity(mut self, m_capacity: usize) -> Self {
assert!(m_capacity >= 1, "m_capacity must be ≥ 1");
self.m_capacity = m_capacity;
self
}
}
impl<P, V, S> Solver<P, LbfgsState<V>> for LBFGSB<S>
where
P: CostFunction<Param = V, Output = f64> + Gradient<Param = V, Gradient = V> + BoxConstrained,
V: AsFloatSliceMut + Clone + Dot + ScaledAdd<f64>,
S: LineSearch<P, V>,
{
fn init(&mut self, problem: &P, mut state: LbfgsState<V>) -> LbfgsState<V> {
let n = state.param.as_float_slice().len();
let m = state.m_capacity;
let mut work = LbfgsbWork::new(n, m);
active_init(
state.param.as_float_slice_mut(),
problem.lower().as_float_slice(),
problem.upper().as_float_slice(),
&mut work.iwhere,
&mut work.cnstnd,
&mut work.boxed,
);
state.cost = Some(problem.cost(&state.param));
state.gradient = Some(problem.gradient(&state.param));
state.cost_evals += 1;
state.gradient_evals += 1;
state.work = Some(work);
state
}
fn next_iter(
&mut self,
problem: &P,
mut state: LbfgsState<V>,
) -> (LbfgsState<V>, Option<TerminationReason>) {
let g_v = state
.gradient
.take()
.expect("gradient not set: Solver::init must run before next_iter");
let f_old = state
.cost
.expect("cost not set: Solver::init must run before next_iter");
let n = state.param.as_float_slice().len();
let m = state.m_capacity;
let mut restart_budget = 1u8;
loop {
let work = state.work.as_mut().expect("work missing");
let sbgnrm = projected_gradient_norm(
state.param.as_float_slice(),
g_v.as_float_slice(),
problem.lower().as_float_slice(),
problem.upper().as_float_slice(),
);
if sbgnrm <= self.tol_pg {
state.gradient = Some(g_v);
state.cost = Some(f_old);
return (state, Some(TerminationReason::SolverConverged));
}
let col = state.ws.len();
let theta = state.theta;
let cnstnd = work.cnstnd;
let boxed = work.boxed;
let updatd = work.updatd;
let mut wrk = updatd;
if !cnstnd && col > 0 {
work.z.copy_from_slice(state.param.as_float_slice());
} else {
let ws_cols: Vec<&[f64]> = state.ws.iter().map(|v| v.as_float_slice()).collect();
let wy_cols: Vec<&[f64]> = state.wy.iter().map(|v| v.as_float_slice()).collect();
let cauchy_res = cauchy(
state.param.as_float_slice(),
problem.lower().as_float_slice(),
problem.upper().as_float_slice(),
g_v.as_float_slice(),
&ws_cols,
&wy_cols,
&state.sy,
&work.wt,
m,
theta,
sbgnrm,
&mut work.z,
&mut work.d,
&mut work.t_buf,
&mut work.iwhere,
&mut work.indx2,
&mut work.wa_p,
&mut work.wa_c,
&mut work.wa_wbp,
&mut work.wa_v,
);
if cauchy_res.is_err() {
if try_restart(&mut state, &g_v, f_old, &mut restart_budget) {
continue;
} else {
return (state, Some(TerminationReason::SolverFailed));
}
}
}
let (nfree, nenter, ileave) = freev(
n,
&work.iwhere,
&mut work.index,
&mut work.indx2,
work.nfree,
state.iter,
cnstnd,
);
work.nfree = nfree;
let wrk_local = (ileave < n) || (nenter > 0) || wrk;
wrk = wrk_local;
if nfree > 0 && col > 0 {
if wrk {
let ws_cols: Vec<&[f64]> =
state.ws.iter().map(|v| v.as_float_slice()).collect();
let wy_cols: Vec<&[f64]> =
state.wy.iter().map(|v| v.as_float_slice()).collect();
if formk(
&mut work.wn,
&mut work.wn1,
m,
col,
theta,
&state.sy,
&ws_cols,
&wy_cols,
nfree,
&work.index,
nenter,
ileave,
&work.indx2,
work.iupdat,
updatd,
)
.is_err()
{
if try_restart(&mut state, &g_v, f_old, &mut restart_budget) {
continue;
} else {
return (state, Some(TerminationReason::SolverFailed));
}
}
}
if cmprlb(
state.param.as_float_slice(),
g_v.as_float_slice(),
&work.z,
&mut work.r,
&mut work.wa_c,
&mut work.wa_p,
&state.sy,
&work.wt,
&state.ws,
&state.wy,
&work.index,
nfree,
cnstnd,
col,
theta,
m,
)
.is_err()
{
if try_restart(&mut state, &g_v, f_old, &mut restart_budget) {
continue;
} else {
return (state, Some(TerminationReason::SolverFailed));
}
}
let ws_cols: Vec<&[f64]> = state.ws.iter().map(|v| v.as_float_slice()).collect();
let wy_cols: Vec<&[f64]> = state.wy.iter().map(|v| v.as_float_slice()).collect();
let subsm_res = subsm(
&mut work.z,
&mut work.r,
&mut work.xp,
state.param.as_float_slice(),
g_v.as_float_slice(),
&work.index[0..nfree],
problem.lower().as_float_slice(),
problem.upper().as_float_slice(),
&ws_cols,
&wy_cols,
&work.wn,
&mut work.wa_v,
m,
col,
theta,
);
if subsm_res.is_err() {
if try_restart(&mut state, &g_v, f_old, &mut restart_budget) {
continue;
} else {
return (state, Some(TerminationReason::SolverFailed));
}
}
}
for i in 0..n {
work.d[i] = work.z[i] - state.param.as_float_slice()[i];
}
let dtd: f64 = work.d.iter().map(|x| x * x).sum();
let dnorm = dtd.sqrt();
work.dnorm = dnorm;
let stpmx = if cnstnd {
if state.iter == 0 {
1.0
} else {
feasible_step_cap(
state.param.as_float_slice(),
problem.lower().as_float_slice(),
problem.upper().as_float_slice(),
&work.d,
)
}
} else {
1.0e10
};
let alpha_init = if state.iter == 0 && !boxed {
(1.0_f64 / dnorm).min(stpmx)
} else {
1.0
};
let mut d_v = state.param.clone();
d_v.as_float_slice_mut().copy_from_slice(&work.d);
work.t_buf.copy_from_slice(state.param.as_float_slice());
work.r.copy_from_slice(g_v.as_float_slice());
work.gdold = work
.d
.iter()
.zip(g_v.as_float_slice())
.map(|(a, b)| a * b)
.sum();
let _ = (alpha_init, stpmx);
let ls_result = self
.line_search
.next(problem, &state.param, f_old, &g_v, &d_v);
state.cost_evals += ls_result.cost_evals;
state.gradient_evals += ls_result.gradient_evals;
let stp = ls_result.alpha;
if !(stp.is_finite() && stp > 0.0) {
state.gradient = Some(g_v.clone());
state.cost = Some(f_old);
if state.ws.is_empty() {
return (state, Some(TerminationReason::SolverFailed));
}
if try_restart_after_lnsrch(&mut state, &mut restart_budget) {
continue;
} else {
return (state, Some(TerminationReason::SolverFailed));
}
}
state.param.scaled_add(stp, &d_v);
let f_new = problem.cost(&state.param);
let g_new = problem.gradient(&state.param);
state.cost_evals += 1;
state.gradient_evals += 1;
let work = state.work.as_mut().unwrap();
let g_new_slice = g_new.as_float_slice();
let g_old_slice = work.r.as_slice();
let mut dr = 0.0;
for i in 0..n {
let yi = g_new_slice[i] - g_old_slice[i];
let si = stp * work.d[i];
dr += yi * si;
}
let ddum = -work.gdold * stp;
if dr > self.epsilon * ddum.abs() {
let mut s_v = state.param.clone();
let s_slice = s_v.as_float_slice_mut();
for i in 0..n {
s_slice[i] = stp * work.d[i];
}
let mut y_v = g_new.clone();
let y_slice = y_v.as_float_slice_mut();
for i in 0..n {
y_slice[i] = g_new_slice[i] - g_old_slice[i];
}
let appended = state.append_pair(s_v, y_v);
if appended {
let work = state.work.as_mut().unwrap();
work.updatd = true;
work.iupdat = work.iupdat.saturating_add(1);
let new_col = state.ws.len();
if formt(state.theta, &state.sy, &state.ss, new_col, m, &mut work.wt).is_err() {
state.ws.clear();
state.wy.clear();
for v in state.sy.iter_mut() {
*v = 0.0;
}
for v in state.ss.iter_mut() {
*v = 0.0;
}
state.theta = 1.0;
work.reset_history();
}
} else {
let work = state.work.as_mut().unwrap();
work.updatd = false;
}
} else {
let work = state.work.as_mut().unwrap();
work.updatd = false;
}
state.cost = Some(f_new);
state.gradient = Some(g_new);
return (state, None);
}
}
}
fn active_init(
x: &mut [f64],
l: &[f64],
u: &[f64],
iwhere: &mut [i8],
cnstnd: &mut bool,
boxed: &mut bool,
) {
let n = x.len();
*cnstnd = false;
*boxed = true;
for i in 0..n {
let lo = l[i];
let hi = u[i];
let lower_finite = lo.is_finite();
let upper_finite = hi.is_finite();
if lower_finite && x[i] < lo {
x[i] = lo;
}
if upper_finite && x[i] > hi {
x[i] = hi;
}
if !(lower_finite && upper_finite) {
*boxed = false;
}
if !lower_finite && !upper_finite {
iwhere[i] = iwh::ALWAYS_FREE;
} else {
*cnstnd = true;
if lower_finite && upper_finite && lo == hi {
iwhere[i] = iwh::ALWAYS_FIXED;
} else {
iwhere[i] = iwh::FREE_MOVED;
}
}
}
}
fn projected_gradient_norm(x: &[f64], g: &[f64], l: &[f64], u: &[f64]) -> f64 {
let mut sbgnrm = 0.0_f64;
for i in 0..x.len() {
let mut gi = g[i];
let lower_finite = l[i].is_finite();
let upper_finite = u[i].is_finite();
if lower_finite || upper_finite {
if gi < 0.0 {
if upper_finite {
gi = (x[i] - u[i]).max(gi);
}
} else if lower_finite {
gi = (x[i] - l[i]).min(gi);
}
}
sbgnrm = sbgnrm.max(gi.abs());
}
sbgnrm
}
fn feasible_step_cap(x: &[f64], l: &[f64], u: &[f64], d: &[f64]) -> f64 {
let mut stpmx = 1.0e10_f64;
for i in 0..x.len() {
let di = d[i];
let lower_finite = l[i].is_finite();
let upper_finite = u[i].is_finite();
if di < 0.0 && lower_finite {
let gap = l[i] - x[i];
if gap >= 0.0 {
stpmx = 0.0;
} else if di * stpmx < gap {
stpmx = gap / di;
}
} else if di > 0.0 && upper_finite {
let gap = u[i] - x[i];
if gap <= 0.0 {
stpmx = 0.0;
} else if di * stpmx > gap {
stpmx = gap / di;
}
}
}
stpmx
}
fn freev(
n: usize,
iwhere: &[i8],
index: &mut [usize],
indx2: &mut [usize],
prev_nfree: usize,
iter: u64,
cnstnd: bool,
) -> (usize, usize, usize) {
let mut nenter = 0usize;
let mut ileave = n;
if iter > 0 && cnstnd {
for i in 0..prev_nfree {
let k = index[i];
if iwhere[k] > 0 {
ileave -= 1;
indx2[ileave] = k;
}
}
for i in prev_nfree..n {
let k = index[i];
if iwhere[k] <= 0 {
indx2[nenter] = k;
nenter += 1;
}
}
}
let mut nfree = 0usize;
let mut iact = n;
for i in 0..n {
if iwhere[i] <= 0 {
index[nfree] = i;
nfree += 1;
} else {
iact -= 1;
index[iact] = i;
}
}
(nfree, nenter, ileave)
}
#[allow(clippy::too_many_arguments)]
fn cmprlb<V>(
x: &[f64],
g: &[f64],
z: &[f64],
r: &mut [f64],
wa_c: &mut [f64],
wa_p: &mut [f64],
sy: &[f64],
wt: &[f64],
ws: &[V],
wy: &[V],
index: &[usize],
nfree: usize,
cnstnd: bool,
col: usize,
theta: f64,
m: usize,
) -> Result<(), ()>
where
V: AsFloatSlice,
{
if !cnstnd && col > 0 {
for i in 0..x.len() {
r[i] = -g[i];
}
return Ok(());
}
for i in 0..nfree {
let k = index[i];
r[i] = -theta * (z[k] - x[k]) - g[k];
}
bmv(sy, wt, col, m, wa_c, wa_p).map_err(|_| ())?;
for j in 0..col {
let a1 = wa_p[j];
let a2 = theta * wa_p[col + j];
let wy_j = wy[j].as_float_slice();
let ws_j = ws[j].as_float_slice();
for i in 0..nfree {
let k = index[i];
r[i] += wy_j[k] * a1 + ws_j[k] * a2;
}
}
Ok(())
}
fn try_restart<V>(state: &mut LbfgsState<V>, g_v: &V, f_old: f64, restart_budget: &mut u8) -> bool
where
V: Clone,
{
if *restart_budget == 0 {
state.gradient = Some(g_v.clone());
state.cost = Some(f_old);
return false;
}
*restart_budget -= 1;
state.ws.clear();
state.wy.clear();
for v in state.sy.iter_mut() {
*v = 0.0;
}
for v in state.ss.iter_mut() {
*v = 0.0;
}
state.theta = 1.0;
if let Some(work) = state.work.as_mut() {
work.reset_history();
}
true
}
fn try_restart_after_lnsrch<V>(state: &mut LbfgsState<V>, restart_budget: &mut u8) -> bool {
if *restart_budget == 0 {
return false;
}
*restart_budget -= 1;
state.ws.clear();
state.wy.clear();
for v in state.sy.iter_mut() {
*v = 0.0;
}
for v in state.ss.iter_mut() {
*v = 0.0;
}
state.theta = 1.0;
if let Some(work) = state.work.as_mut() {
work.reset_history();
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::executor::Executor;
use crate::core::termination::{MaxIter, ProjectedGradientTolerance};
#[test]
fn shifted_quadratic_in_box_converges_to_clamp() {
use crate::core::constraint::BoxConstrained;
use crate::core::problem::{CostFunction, Gradient};
struct Quad {
c: Vec<f64>,
l: Vec<f64>,
u: Vec<f64>,
}
impl CostFunction for Quad {
type Param = Vec<f64>;
type Output = f64;
fn cost(&self, x: &Vec<f64>) -> f64 {
x.iter().zip(&self.c).map(|(a, b)| (a - b).powi(2)).sum()
}
}
impl Gradient for Quad {
type Param = Vec<f64>;
type Gradient = Vec<f64>;
fn gradient(&self, x: &Vec<f64>) -> Vec<f64> {
x.iter().zip(&self.c).map(|(a, b)| 2.0 * (a - b)).collect()
}
}
impl BoxConstrained for Quad {
fn lower(&self) -> &Vec<f64> {
&self.l
}
fn upper(&self) -> &Vec<f64> {
&self.u
}
}
let problem = Quad {
c: vec![3.0, -1.0],
l: vec![0.0, 0.0],
u: vec![2.0, 2.0],
};
let state = LbfgsState::new(vec![1.0, 1.0], 5);
let solver = LBFGSB::new();
let lower = problem.lower().clone();
let upper = problem.upper().clone();
let result = Executor::new(problem, solver, state)
.terminate_on(MaxIter(50))
.terminate_on(ProjectedGradientTolerance::new(lower, upper, 1e-10))
.run();
let final_x = result.state.param.clone();
assert!((final_x[0] - 2.0).abs() < 1e-6, "x0 = {}", final_x[0]);
assert!(final_x[1].abs() < 1e-6, "x1 = {}", final_x[1]);
}
#[test]
fn unbounded_rosenbrock_2d_converges() {
use crate::core::constraint::BoxConstrained;
use crate::core::problem::{CostFunction, Gradient};
struct Rosen {
l: Vec<f64>,
u: Vec<f64>,
}
impl CostFunction for Rosen {
type Param = Vec<f64>;
type Output = f64;
fn cost(&self, x: &Vec<f64>) -> f64 {
(1.0 - x[0]).powi(2) + 100.0 * (x[1] - x[0] * x[0]).powi(2)
}
}
impl Gradient for Rosen {
type Param = Vec<f64>;
type Gradient = Vec<f64>;
fn gradient(&self, x: &Vec<f64>) -> Vec<f64> {
let dfdx0 = -2.0 * (1.0 - x[0]) - 400.0 * x[0] * (x[1] - x[0] * x[0]);
let dfdx1 = 200.0 * (x[1] - x[0] * x[0]);
vec![dfdx0, dfdx1]
}
}
impl BoxConstrained for Rosen {
fn lower(&self) -> &Vec<f64> {
&self.l
}
fn upper(&self) -> &Vec<f64> {
&self.u
}
}
let problem = Rosen {
l: vec![f64::NEG_INFINITY; 2],
u: vec![f64::INFINITY; 2],
};
let state = LbfgsState::new(vec![-1.2, 1.0], 5);
let solver = LBFGSB::new();
let lower = problem.lower().clone();
let upper = problem.upper().clone();
let result = Executor::new(problem, solver, state)
.terminate_on(MaxIter(200))
.terminate_on(ProjectedGradientTolerance::new(lower, upper, 1e-8))
.run();
let final_x = result.state.param.clone();
assert!(
(final_x[0] - 1.0).abs() < 1e-3 && (final_x[1] - 1.0).abs() < 1e-3,
"x = {:?}",
final_x
);
}
}