#[cfg(feature = "nalgebra")]
use crate::core::inner::WarmStart;
use crate::core::math::{
Dot, GeneralRankOneUpdate, MatVec, MatrixIdentity, NegInPlace, NormSquared, ScaleInPlace,
ScaledAdd, VectorLen,
};
use crate::core::problem::{CostFunction, Gradient, Problem};
use crate::core::solver::Solver;
use crate::core::state::QuasiNewtonState;
use crate::core::termination::TerminationReason;
use crate::line_search::{LineSearch, Wolfe};
pub struct Bfgs<S = Wolfe> {
line_search: S,
epsilon: f64,
}
impl Default for Bfgs<Wolfe> {
fn default() -> Self {
Self::new()
}
}
impl Bfgs<Wolfe> {
pub fn new() -> Self {
Self {
line_search: Wolfe::new(),
epsilon: 1e-10,
}
}
}
impl<S> Bfgs<S> {
pub fn with_line_search(line_search: S) -> Self {
Self {
line_search,
epsilon: 1e-10,
}
}
pub fn epsilon(mut self, epsilon: f64) -> Self {
assert!(epsilon >= 0.0, "epsilon must be ≥ 0");
self.epsilon = epsilon;
self
}
}
impl<P, S, V, M> Solver<P, QuasiNewtonState<V, M>> for Bfgs<S>
where
P: CostFunction<Param = V, Output = f64> + Gradient<Gradient = V>,
S: LineSearch<P, V, Error = P::Error>,
V: Clone + Dot + NormSquared + ScaledAdd<f64> + ScaleInPlace + NegInPlace + VectorLen,
M: MatVec<V> + MatrixIdentity + ScaleInPlace + GeneralRankOneUpdate<V>,
{
type Error = P::Error;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: QuasiNewtonState<V, M>,
) -> Result<QuasiNewtonState<V, M>, Self::Error> {
let (cost, grad) = problem.cost_and_gradient(&state.param)?;
state.cost = Some(cost);
state.gradient = Some(grad);
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: QuasiNewtonState<V, M>,
) -> Result<(QuasiNewtonState<V, M>, Option<TerminationReason>), Self::Error> {
let g = state
.gradient
.take()
.expect("gradient not set: Solver::init must run before next_iter");
let cost_old = state
.cost
.expect("cost not set: Solver::init must run before next_iter");
let mut direction = state.inverse_hessian.matvec(&g);
direction.neg_in_place();
let alpha = self
.line_search
.next(problem, &state.param, cost_old, &g, &direction)?;
if !(alpha.is_finite() && alpha > 0.0) {
state.gradient = Some(g);
state.cost = Some(cost_old);
return Ok((state, Some(TerminationReason::SolverConverged)));
}
let mut s = direction;
s.scale_in_place(alpha);
state.param.scaled_add(1.0, &s);
let (cost_new, g_new) = problem.cost_and_gradient(&state.param)?;
let mut y = g_new.clone();
y.scaled_add(-1.0, &g);
let sy = s.dot(&y);
let s_norm = s.norm_squared().sqrt();
let y_norm = y.norm_squared().sqrt();
if sy > self.epsilon * s_norm * y_norm {
if !state.initial_scaling_done {
let yy = y.dot(&y);
if yy > 0.0 {
let scale = sy / yy;
let n = state.param.vec_len();
let mut h0 = M::identity(n);
h0.scale_in_place(scale);
state.inverse_hessian = h0;
}
state.initial_scaling_done = true;
}
let rho = 1.0 / sy;
let hy = state.inverse_hessian.matvec(&y);
let yhy = y.dot(&hy);
let coef = rho * (1.0 + rho * yhy);
state.inverse_hessian.general_rank_one_update(coef, &s, &s);
state.inverse_hessian.general_rank_one_update(-rho, &s, &hy);
state.inverse_hessian.general_rank_one_update(-rho, &hy, &s);
}
state.cost = Some(cost_new);
state.gradient = Some(g_new);
Ok((state, None))
}
}
#[cfg(feature = "nalgebra")]
impl<S> WarmStart<nalgebra::DVector<f64>> for Bfgs<S> {
type State = QuasiNewtonState<nalgebra::DVector<f64>, nalgebra::DMatrix<f64>>;
fn seed(&self, x: &nalgebra::DVector<f64>) -> Self::State {
QuasiNewtonState::<nalgebra::DVector<f64>, nalgebra::DMatrix<f64>>::new(x.clone())
}
}
#[deprecated(
since = "0.8.0",
note = "use `Bfgs` (PascalCase) — the all-caps alias will be removed in a future release"
)]
#[allow(non_camel_case_types)]
pub type BFGS<S = Wolfe> = Bfgs<S>;