#![allow(clippy::needless_range_loop, clippy::too_many_arguments)]
pub(crate) mod backend;
pub(crate) mod cauchy;
pub(crate) mod compact;
pub mod diagnostics;
pub(crate) mod formk;
pub(crate) mod history;
pub(crate) mod subsm;
mod workspace;
use diagnostics::{ModelFailure, ModelObservation, ModelPhase, Trace};
use core::marker::PhantomData;
use self::workspace::{History, LbfgsbWork};
use crate::core::constraint::evaluation::BoxProblem;
use crate::core::math::{Dot, Scalar, ScaledAdd, VectorLen};
use crate::core::problem::{CostFunction, Gradient, Problem};
use crate::core::solver::Solver;
use crate::core::state::{FirstOrderState, State};
use crate::core::termination::Termination;
use crate::line_search::{
LineSearch, LineSearchBounds, LineSearchOutcome, LineSearchResult,
MoreThuente,
};
pub 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;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Lbfgs<V, F: Scalar = f64, Mode = Bounded, S = MoreThuente<F>> {
history: Option<History<V, F>>,
#[cfg_attr(feature = "serde", serde(skip))]
trace: Trace<F>,
line_search: S,
epsilon: F,
tol_pg: Option<F>,
#[cfg_attr(feature = "serde", serde(default))]
tol_cost_reduction: Option<F>,
#[cfg_attr(feature = "serde", serde(default))]
previous_cost: Option<F>,
m_capacity: usize,
_mode: PhantomData<fn() -> Mode>,
}
pub struct Bounded;
pub struct Unbounded;
pub type Lbfgsb<V, F = f64, S = MoreThuente<F>> = Lbfgs<V, F, Bounded, S>;
impl<V, F: Scalar> Default for Lbfgs<V, F> {
fn default() -> Self {
Self::new()
}
}
impl<V, F: Scalar> Default for Lbfgs<V, F, Unbounded> {
fn default() -> Self {
Lbfgs::new().unbounded()
}
}
impl<V, F: Scalar> Lbfgs<V, F> {
pub fn new() -> Self {
Self::with_line_search(MoreThuente::new())
}
}
impl<V, S, F: Scalar> Lbfgs<V, F, Bounded, S> {
pub fn with_line_search(line_search: S) -> Self {
let double_precision =
F::epsilon() == F::from_f64(f64::EPSILON).unwrap();
Self {
line_search,
epsilon: F::epsilon(),
tol_pg: Some(
F::from_f64(if double_precision { 1e-9 } else { 1e-10 })
.unwrap(),
),
tol_cost_reduction: double_precision
.then(|| F::from_f64(10.).unwrap() * F::epsilon()),
previous_cost: None,
m_capacity: 10,
history: None,
trace: Trace::default(),
_mode: PhantomData,
}
}
pub fn with_absolute_projected_gradient_tolerance(
mut self,
value: impl Into<Option<F>>,
) -> Self {
self.tol_pg = crate::core::convergence::optional_tolerance(value);
self
}
pub fn with_relative_cost_reduction_tolerance(
mut self,
value: impl Into<Option<F>>,
) -> Self {
self.tol_cost_reduction =
crate::core::convergence::optional_tolerance(value);
self
}
pub fn unbounded(self) -> Lbfgs<V, F, Unbounded, S> {
Lbfgs {
line_search: self.line_search,
epsilon: self.epsilon,
tol_pg: self.tol_pg,
tol_cost_reduction: self.tol_cost_reduction,
previous_cost: None,
m_capacity: self.m_capacity,
history: None,
trace: Trace {
enabled: self.trace.enabled,
..Trace::default()
},
_mode: PhantomData,
}
}
}
impl<V, S, F: Scalar> Lbfgs<V, F, Unbounded, S> {
pub fn bounded(self) -> Lbfgs<V, F, Bounded, S> {
Lbfgs {
line_search: self.line_search,
epsilon: self.epsilon,
tol_pg: self.tol_pg,
tol_cost_reduction: self.tol_cost_reduction,
previous_cost: None,
m_capacity: self.m_capacity,
history: None,
trace: Trace {
enabled: self.trace.enabled,
..Trace::default()
},
_mode: PhantomData,
}
}
}
impl<V, Mode, S, F: Scalar> Lbfgs<V, F, Mode, S> {
pub fn with_trial_diagnostics(mut self, enabled: bool) -> Self {
self.trace.enabled = enabled;
self.trace.observations.clear();
self
}
pub fn model_observations(&self) -> &[ModelObservation<F>] {
&self.trace.observations
}
pub fn line_search(&self) -> &S {
&self.line_search
}
pub fn with_relative_curvature_tolerance(mut self, epsilon: F) -> Self {
assert!(epsilon >= F::zero(), "epsilon must be ≥ 0");
self.epsilon = epsilon;
self
}
pub fn with_m_capacity(mut self, m_capacity: usize) -> Self {
assert!(m_capacity >= 1, "m_capacity must be ≥ 1");
self.m_capacity = m_capacity;
self
}
pub fn history_len(&self) -> usize {
self.history.as_ref().map_or(0, History::col)
}
pub fn history_capacity(&self) -> usize {
self.history
.as_ref()
.map_or(self.m_capacity, |history| history.m_capacity)
}
}
impl<P, V, S, F> Solver<P, FirstOrderState<V, F>> for Lbfgs<V, F, Bounded, S>
where
F: Scalar,
P: CostFunction<Param = V, Output = F>
+ Gradient<Gradient = V>
+ BoxProblem<Param = V>,
V: AsFloatSliceMut<F> + Clone + Dot<F> + ScaledAdd<F> + VectorLen,
S: LineSearch<P, V, F, Error = P::Error>,
{
type Error = crate::ConstraintSolveError<P::Error>;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: FirstOrderState<V, F>,
) -> Result<FirstOrderState<V, F>, Self::Error> {
let (lower, upper) = problem
.inner()
.box_bounds()
.map_err(crate::core::constraint::evaluation::invalid)?;
crate::core::constraint::evaluation::validate_bound_slices(
lower.as_float_slice(),
upper.as_float_slice(),
state.param().vec_len(),
false,
)
.map_err(crate::core::constraint::evaluation::invalid)?;
state.reset();
self.previous_cost = None;
self.trace.observations.clear();
self.trace.sequence = 0;
self.line_search.reset();
self.history = Some(History::new(self.m_capacity));
let history = self.history.as_mut().unwrap();
let n = state.param().as_float_slice().len();
let m = history.m_capacity;
let mut work = Box::new(LbfgsbWork::<F>::new(n, m));
active_init(
state.seed_param_mut().as_float_slice_mut(),
problem.inner().lower().as_float_slice(),
problem.inner().upper().as_float_slice(),
&mut work.iwhere,
&mut work.cnstnd,
&mut work.boxed,
);
let (cost, grad) = problem.cost_and_gradient(state.param())?;
state
.set_evaluation(cost, grad)
.expect("gradient dimension differs from parameter");
history.work = Some(work);
Ok(state)
}
fn reset_convergence(&mut self) {
self.previous_cost = None;
}
fn check_convergence(
&mut self,
problem: &Problem<P>,
state: &FirstOrderState<V, F>,
) -> Option<Termination<F>> {
let gradient = crate::GradientState::gradient(state)
.expect("L-BFGS-B requires initialized progress");
if !finite_evaluation(state.param(), state.cost(), gradient) {
return Some(Termination::numerical_failure(
"L-BFGS-B requires a finite point, cost, and gradient.",
));
}
let tolerance = self.tol_cost_reduction?;
let norm = projected_gradient_norm(
state.param().as_float_slice(),
gradient.as_float_slice(),
problem.inner().lower().as_float_slice(),
problem.inner().upper().as_float_slice(),
);
if projected_gradient_converged(norm, self.tol_pg) {
return None;
}
let previous = self.previous_cost?;
let current = state.cost();
let reference = previous.abs().max(current.abs()).max(F::one());
let reduction = previous - current;
(previous.is_finite() && reduction <= tolerance * reference).then(
|| {
Termination::upper_bound(
crate::ConvergenceTest::RelativeCostReduction,
reduction,
tolerance * reference,
tolerance,
Some(reference),
)
},
)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: FirstOrderState<V, F>,
) -> Result<crate::SolverStep<FirstOrderState<V, F>>, Self::Error> {
self.trace.observations.clear();
self.previous_cost = None;
let (f_old, g_v) = state
.take_evaluation()
.expect("L-BFGS requires initialized progress");
if !finite_evaluation(state.param(), f_old, &g_v) {
state
.set_evaluation(f_old, g_v)
.expect("gradient dimension differs from parameter");
return Ok(crate::SolverStep::stopped(
state,
Termination::numerical_failure(
"L-BFGS-B requires a finite point, cost, and gradient.",
),
));
}
if self.trace.enabled {
self.trace.context = [
state.param().as_float_slice().to_vec(),
g_v.as_float_slice().to_vec(),
problem.inner().lower().as_float_slice().to_vec(),
problem.inner().upper().as_float_slice().to_vec(),
];
}
let history = self
.history
.as_mut()
.expect("L-BFGS history is not initialized");
let n = state.param().as_float_slice().len();
let m = history.m_capacity;
let mut restart_budget = 1u8;
loop {
let work = history.work.as_mut().expect("work missing");
let sbgnrm = projected_gradient_norm(
state.param().as_float_slice(),
g_v.as_float_slice(),
problem.inner().lower().as_float_slice(),
problem.inner().upper().as_float_slice(),
);
if projected_gradient_converged(sbgnrm, self.tol_pg) {
state
.set_evaluation(f_old, g_v)
.expect("gradient dimension differs from parameter");
let tolerance = self.tol_pg.unwrap();
let stop = Termination::upper_bound(
crate::ConvergenceTest::BoundClippedGradient,
sbgnrm,
tolerance,
tolerance,
None,
);
return Ok(crate::SolverStep::from((state, Some(stop))));
}
let col = history.ws.len();
let theta = history.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 cauchy_res = cauchy(
state.param().as_float_slice(),
problem.inner().lower().as_float_slice(),
problem.inner().upper().as_float_slice(),
g_v.as_float_slice(),
&history.ws,
&history.wy,
&history.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,
);
self.trace.record(
ModelPhase::Cauchy,
cauchy_res.is_ok(),
cauchy_res
.as_ref()
.err()
.map(|_| ModelFailure::SingularMiddleMatrix),
restart_budget,
*problem.counts(),
m,
&history.ws,
&history.wy,
&history.sy,
&history.ss,
history.theta,
work,
None,
);
if cauchy_res.is_err() {
if try_restart(history, &mut restart_budget) {
continue;
} else {
state.set_evaluation(f_old, g_v).expect(
"gradient dimension differs from parameter",
);
return Ok(crate::SolverStep::from((
state,
Some(Termination::numerical_failure(
"L-BFGS-B Cauchy-point construction failed after exhausting restarts.",
)),
)));
}
}
}
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 {
let factor_result = if wrk {
formk(
&mut work.wn,
&mut work.wn1,
m,
col,
theta,
&history.sy,
&history.ws,
&history.wy,
nfree,
&work.index,
nenter,
ileave,
&work.indx2,
work.iupdat,
updatd,
)
} else {
Ok(())
};
if wrk {
self.trace.record(
ModelPhase::FactorK,
factor_result.is_ok(),
factor_result.as_ref().err().map(|e| match e {
self::formk::FormkError::NotPositiveDefiniteFirst => ModelFailure::NotPositiveDefiniteFirst,
self::formk::FormkError::NotPositiveDefiniteSecond => ModelFailure::NotPositiveDefiniteSecond,
}),
restart_budget,
*problem.counts(),
m,
&history.ws,
&history.wy,
&history.sy,
&history.ss,
history.theta,
work,
None,
);
}
if factor_result.is_err() {
if try_restart(history, &mut restart_budget) {
continue;
} else {
state.set_evaluation(f_old, g_v).expect(
"gradient dimension differs from parameter",
);
return Ok(crate::SolverStep::from((
state,
Some(Termination::numerical_failure(
"L-BFGS-B compact subspace factorization failed after exhausting restarts.",
)),
)));
}
}
let reduced_result = cmprlb(
state.param().as_float_slice(),
g_v.as_float_slice(),
&work.z,
&mut work.r,
&mut work.wa_c,
&mut work.wa_p,
&history.sy,
&work.wt,
&history.ws,
&history.wy,
&work.index,
nfree,
cnstnd,
col,
theta,
m,
);
self.trace.record(
ModelPhase::ReducedGradient,
reduced_result.is_ok(),
reduced_result
.as_ref()
.err()
.map(|_| ModelFailure::SingularMiddleMatrix),
restart_budget,
*problem.counts(),
m,
&history.ws,
&history.wy,
&history.sy,
&history.ss,
history.theta,
work,
None,
);
if reduced_result.is_err() {
if try_restart(history, &mut restart_budget) {
continue;
} else {
state.set_evaluation(f_old, g_v).expect(
"gradient dimension differs from parameter",
);
return Ok(crate::SolverStep::from((
state,
Some(Termination::numerical_failure(
"L-BFGS-B reduced-gradient construction failed after exhausting restarts.",
)),
)));
}
}
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.inner().lower().as_float_slice(),
problem.inner().upper().as_float_slice(),
&history.ws,
&history.wy,
&work.wn,
&mut work.wa_v,
m,
col,
theta,
);
self.trace.record(
ModelPhase::Subspace,
subsm_res.is_ok(),
subsm_res.as_ref().err().map(|_| ModelFailure::SingularK),
restart_budget,
*problem.counts(),
m,
&history.ws,
&history.wy,
&history.sy,
&history.ss,
history.theta,
work,
subsm_res.as_ref().ok().map(|s| {
*s == self::subsm::SubsmStatus::BoundEncountered
}),
);
if subsm_res.is_err() {
if try_restart(history, &mut restart_budget) {
continue;
} else {
state.set_evaluation(f_old, g_v).expect(
"gradient dimension differs from parameter",
);
return Ok(crate::SolverStep::from((
state,
Some(Termination::numerical_failure(
"L-BFGS-B subspace minimization failed after exhausting restarts.",
)),
)));
}
}
}
for i in 0..n {
work.d[i] = work.z[i] - state.param().as_float_slice()[i];
}
let dtd: F =
work.d.iter().fold(F::zero(), |acc, x| acc + (*x) * (*x));
let dnorm = dtd.sqrt();
work.dnorm = dnorm;
let stpmx = if cnstnd {
if state.iter() == 0 {
F::one()
} else {
feasible_step_cap(
state.param().as_float_slice(),
problem.inner().lower().as_float_slice(),
problem.inner().upper().as_float_slice(),
&work.d,
)
}
} else {
F::from_f64(1.0e10).unwrap()
};
let mut d_v = state.param().clone();
d_v.as_float_slice_mut().copy_from_slice(&work.d);
let stpmx = safeguard_step_cap(
state.param(),
problem.inner().lower().as_float_slice(),
problem.inner().upper().as_float_slice(),
&d_v,
stpmx,
);
let alpha_init = if state.iter() == 0 && !boxed {
(F::one() / dnorm).min(stpmx)
} else {
F::one().min(stpmx)
};
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())
.fold(F::zero(), |acc, (a, b)| acc + (*a) * (*b));
let line_search_result = if alpha_init.is_finite()
&& alpha_init > F::zero()
&& stpmx.is_finite()
{
self.line_search.next_with_bounds(
problem,
state.param(),
f_old,
&g_v,
&d_v,
LineSearchBounds::new(alpha_init, stpmx),
)?
} else {
LineSearchResult::new(LineSearchOutcome::Failed)
};
let stp = match line_search_result.outcome {
LineSearchOutcome::Step(stp) => stp,
LineSearchOutcome::Failed => F::zero(),
};
let accepted = if stp.is_finite() && stp > F::zero() && stp <= stpmx
{
let (param, evaluation) =
if let Some(evaluation) = line_search_result.evaluation {
(
evaluation.param,
Some((evaluation.cost, evaluation.gradient)),
)
} else {
let mut param = state.param().clone();
param.scaled_add(stp, &d_v);
(param, None)
};
if inside_box(
param.as_float_slice(),
problem.inner().lower().as_float_slice(),
problem.inner().upper().as_float_slice(),
) {
let (cost, gradient) = if let Some(evaluation) = evaluation
{
evaluation
} else {
problem.cost_and_gradient(¶m)?
};
finite_evaluation(¶m, cost, &gradient)
.then_some((param, cost, gradient))
} else {
None
}
} else {
None
};
self.trace.record(
ModelPhase::SelectedPoint,
accepted.is_some(),
accepted
.is_none()
.then_some(ModelFailure::UnusableSelectedPoint),
restart_budget,
*problem.counts(),
m,
&history.ws,
&history.wy,
&history.sy,
&history.ss,
history.theta,
work,
None,
);
let Some((param_new, f_new, g_new)) = accepted else {
if history.ws.is_empty() {
state
.set_evaluation(f_old, g_v)
.expect("gradient dimension differs from parameter");
return Ok(crate::SolverStep::from((
state,
Some(Termination::numerical_failure(
"L-BFGS-B line search failed with empty curvature history.",
)),
)));
}
if try_restart(history, &mut restart_budget) {
continue;
} else {
state
.set_evaluation(f_old, g_v)
.expect("gradient dimension differs from parameter");
return Ok(crate::SolverStep::from((
state,
Some(Termination::numerical_failure(
"L-BFGS-B line search failed after exhausting restarts.",
)),
)));
}
};
let work = history.work.as_mut().unwrap();
let g_new_slice = g_new.as_float_slice();
let g_old_slice = work.r.as_slice();
let mut dr = F::zero();
for i in 0..n {
let yi = g_new_slice[i] - g_old_slice[i];
let si = stp * work.d[i];
dr = dr + yi * si;
}
let ddum = -work.gdold * stp;
if dr > self.epsilon * ddum.abs() {
let mut s_v = d_v;
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_v;
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 = history.append_pair(s_v, y_v);
if appended {
let work = history.work.as_mut().unwrap();
work.updatd = true;
work.iupdat = work.iupdat.saturating_add(1);
let new_col = history.ws.len();
let factor_result = formt(
history.theta,
&history.sy,
&history.ss,
new_col,
m,
&mut work.wt,
);
self.trace.record(
ModelPhase::FactorT,
factor_result.is_ok(),
factor_result
.as_ref()
.err()
.map(|_| ModelFailure::NotPositiveDefiniteT),
restart_budget,
*problem.counts(),
m,
&history.ws,
&history.wy,
&history.sy,
&history.ss,
history.theta,
work,
None,
);
if factor_result.is_err() {
history.ws.clear();
history.wy.clear();
for v in history.sy.iter_mut() {
*v = F::zero();
}
for v in history.ss.iter_mut() {
*v = F::zero();
}
history.theta = F::one();
work.reset_history();
}
} else {
let work = history.work.as_mut().unwrap();
work.updatd = false;
}
} else {
let work = history.work.as_mut().unwrap();
work.updatd = false;
}
state
.replace(param_new, f_new, g_new)
.expect("gradient dimension differs from parameter");
self.previous_cost = Some(f_old);
return Ok(crate::SolverStep::from((state, None)));
}
}
}
impl<P, V, S, F> Solver<P, FirstOrderState<V, F>> for Lbfgs<V, F, Unbounded, S>
where
F: Scalar,
P: CostFunction<Param = V, Output = F> + Gradient<Gradient = V>,
V: AsFloatSliceMut<F> + Clone + Dot<F> + ScaledAdd<F> + VectorLen,
S: LineSearch<P, V, F, Error = P::Error>,
{
type Error = P::Error;
fn check_convergence(
&mut self,
_problem: &Problem<P>,
state: &FirstOrderState<V, F>,
) -> Option<Termination<F>> {
let gradient = crate::GradientState::gradient(state)
.expect("L-BFGS requires initialized progress");
(!finite_evaluation(state.param(), state.cost(), gradient)).then(|| {
Termination::numerical_failure(
"L-BFGS requires a finite point, cost, and gradient.",
)
})
}
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: FirstOrderState<V, F>,
) -> Result<FirstOrderState<V, F>, Self::Error> {
state.reset();
self.trace.observations.clear();
self.trace.sequence = 0;
self.line_search.reset();
self.history = Some(History::new(self.m_capacity));
let (cost, grad) = problem.cost_and_gradient(state.param())?;
state
.set_evaluation(cost, grad)
.expect("gradient dimension differs from parameter");
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: FirstOrderState<V, F>,
) -> Result<crate::SolverStep<FirstOrderState<V, F>>, Self::Error> {
let (f_old, g_v) = state
.take_evaluation()
.expect("L-BFGS requires initialized progress");
if !finite_evaluation(state.param(), f_old, &g_v) {
state
.set_evaluation(f_old, g_v)
.expect("gradient dimension differs from parameter");
return Ok(crate::SolverStep::stopped(
state,
Termination::numerical_failure(
"L-BFGS requires a finite point, cost, and gradient.",
),
));
}
let history = self
.history
.as_mut()
.expect("L-BFGS history is not initialized");
let n = state.param().as_float_slice().len();
let m = history.m_capacity;
let col = history.ws.len();
let theta = history.theta;
let mut d_v = state.param().clone();
{
let d_slice = d_v.as_float_slice_mut();
let g_slice = g_v.as_float_slice();
d_slice.copy_from_slice(g_slice);
if col > 0 {
let mut alpha = vec![F::zero(); col];
for i in (0..col).rev() {
let rho_i = F::one() / history.sy[i * m + i];
let s_i = history.ws[i].as_float_slice();
let y_i = history.wy[i].as_float_slice();
let mut s_dot_q = F::zero();
for k in 0..n {
s_dot_q = s_dot_q + s_i[k] * d_slice[k];
}
let a = rho_i * s_dot_q;
alpha[i] = a;
for k in 0..n {
d_slice[k] = d_slice[k] - a * y_i[k];
}
}
let inv_theta = F::one() / theta;
for k in 0..n {
d_slice[k] = d_slice[k] * inv_theta;
}
for i in 0..col {
let rho_i = F::one() / history.sy[i * m + i];
let s_i = history.ws[i].as_float_slice();
let y_i = history.wy[i].as_float_slice();
let mut y_dot_r = F::zero();
for k in 0..n {
y_dot_r = y_dot_r + y_i[k] * d_slice[k];
}
let beta = rho_i * y_dot_r;
let coef = alpha[i] - beta;
for k in 0..n {
d_slice[k] = d_slice[k] + coef * s_i[k];
}
}
}
for k in 0..n {
d_slice[k] = -d_slice[k];
}
}
let gdold: F = {
let g_slice = g_v.as_float_slice();
let d_slice = d_v.as_float_slice();
let mut acc = F::zero();
for i in 0..n {
acc = acc + g_slice[i] * d_slice[i];
}
acc
};
let line_search_result = self.line_search.next_with_evaluation(
problem,
state.param(),
f_old,
&g_v,
&d_v,
)?;
let stp = match line_search_result.outcome {
LineSearchOutcome::Step(stp) => stp,
LineSearchOutcome::Failed => F::zero(),
};
if !(stp.is_finite() && stp > F::zero()) {
state
.set_evaluation(f_old, g_v)
.expect("gradient dimension differs from parameter");
return Ok(crate::SolverStep::from((
state,
Some(Termination::numerical_failure(
"L-BFGS line search returned no finite positive step.",
)),
)));
}
let (param_new, f_new, g_new) =
if let Some(evaluation) = line_search_result.evaluation {
(evaluation.param, evaluation.cost, evaluation.gradient)
} else {
let mut param = state.param().clone();
param.scaled_add(stp, &d_v);
let (cost, gradient) = problem.cost_and_gradient(¶m)?;
(param, cost, gradient)
};
if !finite_evaluation(¶m_new, f_new, &g_new) {
state
.set_evaluation(f_old, g_v)
.expect("gradient dimension differs from parameter");
return Ok(crate::SolverStep::stopped(
state,
Termination::numerical_failure(
"L-BFGS line search returned a nonfinite evaluation.",
),
));
}
let g_new_slice = g_new.as_float_slice();
let g_old_slice = g_v.as_float_slice();
let d_slice = d_v.as_float_slice();
let mut dr = F::zero();
for i in 0..n {
let yi = g_new_slice[i] - g_old_slice[i];
let si = stp * d_slice[i];
dr = dr + yi * si;
}
let ddum = -gdold * stp;
if dr > self.epsilon * ddum.abs() {
let mut s_v = param_new.clone();
{
let s_slice = s_v.as_float_slice_mut();
for i in 0..n {
s_slice[i] = stp * d_slice[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];
}
}
history.append_pair(s_v, y_v);
}
state
.replace(param_new, f_new, g_new)
.expect("gradient dimension differs from parameter");
Ok(crate::SolverStep::from((state, None)))
}
}
fn active_init<F: Scalar>(
x: &mut [F],
l: &[F],
u: &[F],
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 finite_evaluation<V: AsFloatSlice<F>, F: Scalar>(
x: &V,
cost: F,
gradient: &V,
) -> bool {
cost.is_finite()
&& x.as_float_slice().len() == gradient.as_float_slice().len()
&& x.as_float_slice()
.iter()
.chain(gradient.as_float_slice())
.all(|v| v.is_finite())
}
fn projected_gradient_converged<F: Scalar>(
norm: F,
tolerance: Option<F>,
) -> bool {
tolerance.is_some_and(|tolerance| norm.is_finite() && norm <= tolerance)
}
fn projected_gradient_norm<F: Scalar>(x: &[F], g: &[F], l: &[F], u: &[F]) -> F {
let zero = F::zero();
let mut sbgnrm = zero;
for i in 0..x.len() {
let mut gi = g[i];
if !gi.is_finite() {
return F::infinity();
}
let lower_finite = l[i].is_finite();
let upper_finite = u[i].is_finite();
if lower_finite || upper_finite {
if gi < zero {
if upper_finite {
gi = (x[i] - u[i]).max(gi);
}
} else if lower_finite {
gi = (x[i] - l[i]).min(gi);
}
}
let abs_gi = gi.abs();
if abs_gi > sbgnrm {
sbgnrm = abs_gi;
}
}
sbgnrm
}
fn feasible_step_cap<F: Scalar>(x: &[F], l: &[F], u: &[F], d: &[F]) -> F {
let zero = F::zero();
let mut stpmx = F::from_f64(1.0e10).unwrap();
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 < zero && lower_finite {
let gap = l[i] - x[i];
if gap >= zero {
stpmx = zero;
} else if di * stpmx < gap {
stpmx = gap / di;
}
} else if di > zero && upper_finite {
let gap = u[i] - x[i];
if gap <= zero {
stpmx = zero;
} else if di * stpmx > gap {
stpmx = gap / di;
}
}
}
stpmx
}
fn safeguard_step_cap<V, F>(x: &V, l: &[F], u: &[F], d: &V, mut cap: F) -> F
where
F: Scalar,
V: AsFloatSlice<F> + ScaledAdd<F> + Clone,
{
let mut trial = x.clone();
for _ in 0..8 {
trial.clone_from(x);
trial.scaled_add(cap, d);
if inside_box(trial.as_float_slice(), l, u) {
return cap;
}
cap = cap * (F::one() - F::epsilon());
}
F::zero()
}
fn inside_box<F: Scalar>(x: &[F], l: &[F], u: &[F]) -> bool {
x.len() == l.len()
&& x.len() == u.len()
&& x.iter()
.enumerate()
.all(|(i, &xi)| xi.is_finite() && l[i] <= xi && xi <= u[i])
}
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, F: Scalar>(
x: &[F],
g: &[F],
z: &[F],
r: &mut [F],
wa_c: &mut [F],
wa_p: &mut [F],
sy: &[F],
wt: &[F],
ws: &[V],
wy: &[V],
index: &[usize],
nfree: usize,
cnstnd: bool,
col: usize,
theta: F,
m: usize,
) -> Result<(), ()>
where
V: AsFloatSlice<F>,
{
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] = r[i] + wy_j[k] * a1 + ws_j[k] * a2;
}
}
Ok(())
}
fn try_restart<V, F: Scalar>(
history: &mut History<V, F>,
restart_budget: &mut u8,
) -> bool {
if *restart_budget == 0 {
return false;
}
*restart_budget -= 1;
history.ws.clear();
history.wy.clear();
history.sy.fill(F::zero());
history.ss.fill(F::zero());
history.theta = F::one();
if let Some(work) = history.work.as_mut() {
work.reset_history();
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::executor::Executor;
#[test]
fn shifted_quadratic_in_box_converges_to_clamp() {
use crate::core::problem::{CostFunction, Gradient};
struct Quad {
c: Vec<f64>,
l: Vec<f64>,
u: Vec<f64>,
}
impl crate::Constraints for Quad {
type Set = crate::ConstraintSet<crate::BoxBounds<Vec<f64>>>;
fn constraints(
&self,
) -> <Self::Set as crate::ConstraintDescription>::Borrowed<'_>
{
crate::ConstraintSet::new().with_bounds(crate::BoxBounds::new(
Some(&self.l),
Some(&self.u),
))
}
}
impl CostFunction for Quad {
type Param = Vec<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &Vec<f64>) -> Result<f64, Self::Error> {
Ok(x.iter().zip(&self.c).map(|(a, b)| (a - b).powi(2)).sum())
}
}
impl Gradient for Quad {
type Gradient = Vec<f64>;
fn gradient(&self, x: &Vec<f64>) -> Result<Vec<f64>, Self::Error> {
Ok(x.iter().zip(&self.c).map(|(a, b)| 2.0 * (a - b)).collect())
}
}
let problem = Quad {
c: vec![3.0, -1.0],
l: vec![0.0, 0.0],
u: vec![2.0, 2.0],
};
let state = FirstOrderState::new(vec![1.0, 1.0]);
let solver = Lbfgsb::new().with_m_capacity(5);
let result = Executor::new(
problem,
(solver).with_absolute_projected_gradient_tolerance(1e-10),
state,
)
.max_iter(50)
.run()
.unwrap();
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::problem::{CostFunction, Gradient};
struct Rosen {
l: Vec<f64>,
u: Vec<f64>,
}
impl crate::Constraints for Rosen {
type Set = crate::ConstraintSet<crate::BoxBounds<Vec<f64>>>;
fn constraints(
&self,
) -> <Self::Set as crate::ConstraintDescription>::Borrowed<'_>
{
crate::ConstraintSet::new().with_bounds(crate::BoxBounds::new(
Some(&self.l),
Some(&self.u),
))
}
}
impl CostFunction for Rosen {
type Param = Vec<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &Vec<f64>) -> Result<f64, Self::Error> {
Ok((1.0 - x[0]).powi(2) + 100.0 * (x[1] - x[0] * x[0]).powi(2))
}
}
impl Gradient for Rosen {
type Gradient = Vec<f64>;
fn gradient(&self, x: &Vec<f64>) -> Result<Vec<f64>, Self::Error> {
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]);
Ok(vec![dfdx0, dfdx1])
}
}
let problem = Rosen {
l: vec![f64::NEG_INFINITY; 2],
u: vec![f64::INFINITY; 2],
};
let state = FirstOrderState::new(vec![-1.2, 1.0]);
let solver = Lbfgsb::new().with_m_capacity(5);
let result = Executor::new(
problem,
(solver).with_absolute_projected_gradient_tolerance(1e-8),
state,
)
.max_iter(200)
.run()
.unwrap();
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
);
}
}