use crate::core::inner::{InitialState, WarmStart};
use crate::core::math::{
DenseBackend, Dot, GeneralRankOneUpdate, MatVec, MatrixIdentity,
MatrixIndex, NegInPlace, NormSquared, Scalar, ScaleInPlace, 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, LineSearchOutcome, Wolfe};
mod initial_inverse_hessian;
pub use initial_inverse_hessian::{
FixedInverseHessian, IdentityInverseHessian, InitialInverseHessian,
};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Bfgs<
V: DenseBackend<F>,
F: Scalar = f64,
M = <V as DenseBackend<F>>::Matrix,
S = Wolfe<F>,
I = IdentityInverseHessian,
> {
line_search: S,
epsilon: F,
inverse_hessian: Option<M>,
initial_scaling_done: bool,
initial_inverse_hessian: I,
param: std::marker::PhantomData<V>,
}
impl<V: DenseBackend<F>, F: Scalar> Default for Bfgs<V, F> {
fn default() -> Self {
Self::new()
}
}
impl<V: DenseBackend<F>, F: Scalar> Bfgs<V, F> {
pub fn new() -> Self {
Self::with_line_search(Wolfe::new())
}
}
impl<V: DenseBackend<F>, F: Scalar, S>
Bfgs<V, F, <V as DenseBackend<F>>::Matrix, S>
{
pub fn with_line_search(line_search: S) -> Self {
Self::with_matrix_and_line_search(line_search)
}
}
impl<V: DenseBackend<F>, F: Scalar, M, S> Bfgs<V, F, M, S> {
pub fn with_matrix_and_line_search(line_search: S) -> Self {
Self {
line_search,
epsilon: F::from_f64(1e-10).unwrap(),
inverse_hessian: None,
initial_scaling_done: false,
initial_inverse_hessian: IdentityInverseHessian,
param: std::marker::PhantomData,
}
}
}
impl<V: DenseBackend<F>, F: Scalar, M, S, I> Bfgs<V, F, M, S, I> {
pub fn inverse_hessian(&self) -> Option<&M> {
self.inverse_hessian.as_ref()
}
pub fn with_initial_inverse_hessian(
self,
matrix: M,
) -> Bfgs<V, F, M, S, FixedInverseHessian<M>>
where
V: VectorLen,
M: Clone + MatrixIndex<F>,
{
self.with_initial_inverse_hessian_initializer(FixedInverseHessian(
matrix,
))
}
pub fn with_initial_inverse_hessian_initializer<Init>(
self,
initializer: Init,
) -> Bfgs<V, F, M, S, Init>
where
Init: InitialInverseHessian<V, M, F>,
{
Bfgs {
line_search: self.line_search,
epsilon: self.epsilon,
inverse_hessian: self.inverse_hessian,
initial_scaling_done: self.initial_scaling_done,
initial_inverse_hessian: initializer,
param: self.param,
}
}
pub fn with_relative_curvature_tolerance(mut self, epsilon: F) -> Self {
assert!(epsilon >= F::zero(), "epsilon must be ≥ 0");
self.epsilon = epsilon;
self
}
}
impl<P, S, V, M, F, I> Solver<P, FirstOrderState<V, F>> for Bfgs<V, F, M, S, I>
where
F: Scalar,
P: CostFunction<Param = V, Output = F> + Gradient<Gradient = V>,
S: LineSearch<P, V, F, Error = P::Error>,
I: InitialInverseHessian<V, M, F>,
V: DenseBackend<F>
+ Clone
+ Dot<F>
+ NormSquared<F>
+ ScaledAdd<F>
+ ScaleInPlace<F>
+ NegInPlace
+ VectorLen,
M: MatVec<V>
+ MatrixIdentity
+ ScaleInPlace<F>
+ GeneralRankOneUpdate<V, F>,
P: crate::ProblemConstraints<Constraints = crate::NoConstraints>,
{
type Error = P::Error;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: FirstOrderState<V, F>,
) -> Result<FirstOrderState<V, F>, Self::Error> {
state.reset();
self.line_search.reset();
self.inverse_hessian = None;
self.initial_scaling_done =
!self.initial_inverse_hessian.rescale_on_first_update();
let param = state.param().clone();
let (cost, grad) = problem.cost_and_gradient(¶m)?;
state
.replace(param, cost, grad)
.expect("gradient dimension differs from parameter");
self.inverse_hessian = Some(self.initial_inverse_hessian.initialize(
state.param(),
state.current().expect("initialized BFGS progress").2,
));
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 (param, cost_old, g) =
state.current().expect("BFGS requires initialized progress");
let mut param = param.clone();
let h = self
.inverse_hessian
.as_mut()
.expect("BFGS model is not initialized");
let mut direction = (*h).matvec(g);
direction.neg_in_place();
let alpha = match self
.line_search
.next_with_outcome(problem, ¶m, cost_old, g, &direction)?
{
LineSearchOutcome::Step(alpha) => alpha,
LineSearchOutcome::Failed => {
return Ok(crate::SolverStep::from((
state,
Some(Termination::numerical_failure(
"BFGS line search failed.",
)),
)));
}
};
if !(alpha.is_finite() && alpha > F::zero()) {
return Ok(crate::SolverStep::from((
state,
Some(if alpha.is_finite() {
Termination::numerical_stall(
"The line search returned a nonpositive step without establishing stationarity.",
)
} else {
Termination::numerical_failure(
"The line search returned a non-finite step.",
)
}),
)));
}
let mut s = direction;
s.scale_in_place(alpha);
param.scaled_add(F::one(), &s);
let (cost_new, g_new) = problem.cost_and_gradient(¶m)?;
let mut y = g_new.clone();
y.scaled_add(-F::one(), 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 !self.initial_scaling_done {
let yy = y.dot(&y);
if yy > F::zero() {
let scale = sy / yy;
let n = param.vec_len();
let mut h0 = M::identity(n);
h0.scale_in_place(scale);
(*h) = h0;
}
self.initial_scaling_done = true;
}
let rho = F::one() / sy;
let hy = (*h).matvec(&y);
let yhy = y.dot(&hy);
let coef = rho * (F::one() + rho * yhy);
(*h).general_rank_one_update(coef, &s, &s);
(*h).general_rank_one_update(-rho, &s, &hy);
(*h).general_rank_one_update(-rho, &hy, &s);
}
state
.replace(param, cost_new, g_new)
.expect("gradient dimension differs from parameter");
Ok(crate::SolverStep::from((state, None)))
}
}
impl<V: DenseBackend<F> + Clone, F: Scalar, M, S, I> InitialState<V>
for Bfgs<V, F, M, S, I>
{
type State = FirstOrderState<V, F>;
fn seed(&self, x: &V) -> Self::State {
FirstOrderState::new(x.clone())
}
}
impl<V: DenseBackend<F> + Clone, F: Scalar, M, S, I> WarmStart<V>
for Bfgs<V, F, M, S, I>
{
}