use crate::core::math::{
AddDiagonalVectorInPlace, ComponentDivAssign, ComponentMaxAssign,
ComponentMulAssign, Dot, FactorizePivotedQr, FloorZerosInPlace, GramMatrix,
LinearSolveSpd, MatDiagonal, MatTransposeVec, NegInPlace, NormInfinity,
NormSquared, QrSolveError, RegularizedQrSolve, Scalar, ScaleInPlace,
ScaledAdd,
};
use crate::core::problem::{Jacobian, Problem, Residual};
use crate::core::solver::Solver;
use crate::core::state::NllsState;
use crate::core::termination::TerminationReason;
pub struct LevenbergMarquardt<V, M, F = f64> {
tol_grad: F,
tol_grad_rel: F,
tol_cost_rel: F,
tol_step_rel: F,
tau: F,
max_inner_attempts: u32,
mu: Option<F>,
nu: F,
diag: Option<V>,
r_cache: Option<V>,
model_cache: Option<Result<M, QrSolveError>>,
jtr_cache: Option<V>,
}
impl<V, M> Default for LevenbergMarquardt<V, M> {
fn default() -> Self {
Self::new()
}
}
impl<V, M> LevenbergMarquardt<V, M> {
pub fn new() -> Self {
Self::defaults()
}
}
impl<V, M, F: Scalar> LevenbergMarquardt<V, M, F> {
fn defaults() -> Self {
Self {
tol_grad: F::from_f64(1e-8).unwrap(),
tol_grad_rel: F::zero(),
tol_cost_rel: F::zero(),
tol_step_rel: F::zero(),
tau: F::from_f64(1e-3).unwrap(),
max_inner_attempts: 50,
mu: None,
nu: F::from_f64(2.0).unwrap(),
diag: None,
r_cache: None,
model_cache: None,
jtr_cache: None,
}
}
pub fn with_tol_grad(mut self, tol: F) -> Self {
assert!(tol >= F::zero(), "tol_grad must be ≥ 0");
self.tol_grad = tol;
self
}
pub fn with_tol_grad_rel(mut self, tol: F) -> Self {
assert!(tol >= F::zero(), "tol_grad_rel must be ≥ 0");
self.tol_grad_rel = tol;
self
}
pub fn with_tol_cost_rel(mut self, tol: F) -> Self {
assert!(tol >= F::zero(), "tol_cost_rel must be ≥ 0");
self.tol_cost_rel = tol;
self
}
pub fn with_tol_step_rel(mut self, tol: F) -> Self {
assert!(tol >= F::zero(), "tol_step_rel must be ≥ 0");
self.tol_step_rel = tol;
self
}
pub fn with_tau(mut self, tau: F) -> Self {
assert!(tau > F::zero(), "tau must be > 0");
self.tau = tau;
self
}
pub fn with_max_inner_attempts(mut self, n: u32) -> Self {
assert!(n > 0, "max_inner_attempts must be > 0");
self.max_inner_attempts = n;
self
}
}
impl<P, V, M, F> Solver<P, NllsState<V, F>> for LevenbergMarquardt<V, M, F>
where
F: Scalar,
P: Residual<Param = V, Output = V> + Jacobian<Jacobian = M>,
V: ScaledAdd<F>
+ NormSquared<F>
+ NormInfinity<F>
+ NegInPlace
+ Dot<F>
+ ScaleInPlace<F>
+ ComponentMulAssign
+ ComponentDivAssign
+ ComponentMaxAssign
+ FloorZerosInPlace<F>
+ Clone,
M: GramMatrix
+ MatTransposeVec<V>
+ LinearSolveSpd<V>
+ AddDiagonalVectorInPlace<V>
+ MatDiagonal<V>
+ Clone,
{
type Error = <P as Residual>::Error;
fn init(
&mut self,
problem: &mut Problem<P>,
state: NllsState<V, F>,
) -> Result<NllsState<V, F>, Self::Error> {
self.init_model::<P, M, NormalEquations>(problem, state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
state: NllsState<V, F>,
) -> Result<(NllsState<V, F>, Option<TerminationReason>), Self::Error> {
self.next_iter_model::<P, M, NormalEquations>(problem, state, None)
}
}
impl<V, C, F: Scalar> LevenbergMarquardt<V, C, F> {
fn init_model<P, M, Model>(
&mut self,
problem: &mut Problem<P>,
mut state: NllsState<V, F>,
) -> Result<NllsState<V, F>, <P as Residual>::Error>
where
P: Residual<Param = V, Output = V> + Jacobian<Jacobian = M>,
M: MatTransposeVec<V>,
Model: LinearModel<M, V, F, Cache = C>,
V: ScaledAdd<F>
+ NormSquared<F>
+ NormInfinity<F>
+ NegInPlace
+ Dot<F>
+ ScaleInPlace<F>
+ ComponentMulAssign
+ ComponentDivAssign
+ ComponentMaxAssign
+ FloorZerosInPlace<F>
+ Clone,
{
let (r, j) = problem.residual_and_jacobian(&state.param)?;
state.cost = Some(F::from_f64(0.5).unwrap() * r.norm_squared());
let a = Model::prepare(&j, &r);
self.diag = a.as_ref().ok().map(|a| {
let mut d = Model::diagonal(a);
d.floor_zeros_in_place(F::one());
d
});
self.mu = Some(self.tau);
self.nu = F::from_f64(2.0).unwrap();
self.jtr_cache = Some(j.mat_transpose_vec(&r));
self.model_cache = Some(a);
self.r_cache = Some(r);
Ok(state)
}
fn next_iter_model<P, M, Model>(
&mut self,
problem: &mut Problem<P>,
mut state: NllsState<V, F>,
rank_tolerance: Option<F>,
) -> LmStep<V, F, <P as Residual>::Error>
where
P: Residual<Param = V, Output = V> + Jacobian<Jacobian = M>,
M: MatTransposeVec<V>,
Model: LinearModel<M, V, F, Cache = C>,
V: ScaledAdd<F>
+ NormSquared<F>
+ NormInfinity<F>
+ NegInPlace
+ Dot<F>
+ ScaleInPlace<F>
+ ComponentMulAssign
+ ComponentDivAssign
+ ComponentMaxAssign
+ FloorZerosInPlace<F>
+ Clone,
{
let r = match self.r_cache.take() {
Some(r) => r,
None => problem.residual(&state.param)?,
};
let (a, g) = match (self.model_cache.take(), self.jtr_cache.take()) {
(Some(a), Some(g)) => (a, g),
_ => {
let j = problem.jacobian(&state.param)?;
(Model::prepare(&j, &r), j.mat_transpose_vec(&r))
}
};
let a = match a {
Ok(a) => a,
Err(error) => {
self.model_cache = Some(Err(error));
self.r_cache = Some(r);
self.jtr_cache = Some(g);
return Ok((state, Some(TerminationReason::SolverFailed)));
}
};
if Model::CHECK_FINITE
&& (!r.norm_squared().is_finite() || !g.norm_infinity().is_finite())
{
self.model_cache = Some(Ok(a));
self.r_cache = Some(r);
self.jtr_cache = Some(g);
return Ok((state, Some(TerminationReason::SolverFailed)));
}
let diag_cur = Model::diagonal(&a);
let abs_converged =
self.tol_grad > F::zero() && g.norm_infinity() <= self.tol_grad;
let rel_converged = self.tol_grad_rel > F::zero() && {
let mut cos_sq = g.clone();
cos_sq.component_mul_assign(&g);
let mut denom = diag_cur.clone();
denom.floor_zeros_in_place(F::one());
cos_sq.component_div_assign(&denom);
cos_sq.norm_infinity()
<= self.tol_grad_rel * self.tol_grad_rel * r.norm_squared()
};
if abs_converged || rel_converged {
self.r_cache = Some(r);
self.model_cache = Some(Ok(a));
self.jtr_cache = Some(g);
return Ok((state, Some(TerminationReason::SolverConverged)));
}
let mut d = self
.diag
.take()
.expect("diag not set: Solver::init must run before next_iter");
d.component_max_assign(&diag_cur);
let mut mu = self
.mu
.expect("mu not set: Solver::init must run before next_iter");
let mut nu = self.nu;
let two = F::from_f64(2.0).unwrap();
let half = F::from_f64(0.5).unwrap();
let one_third = F::from_f64(1.0 / 3.0).unwrap();
let h;
let mut attempts: u32 = 0;
loop {
match Model::solve(&a, &g, &d, mu, rank_tolerance) {
Ok(step) => {
h = step;
break;
}
Err(failure) => {
attempts += 1;
if failure == ModelSolveError::Failed
|| attempts >= self.max_inner_attempts
|| !mu.is_finite()
{
self.mu = Some(mu);
self.nu = nu;
self.diag = Some(d);
self.r_cache = Some(r);
self.model_cache = Some(Ok(a));
self.jtr_cache = Some(g);
return Ok((
state,
Some(TerminationReason::SolverFailed),
));
}
mu = mu * nu;
nu = nu * two;
}
}
}
let mut dh = d.clone();
dh.component_mul_assign(&h);
let l_diff = half * (mu * h.dot(&dh) - h.dot(&g));
let mut x_trial = state.param.clone();
x_trial.scaled_add(F::one(), &h);
let r_trial = problem.residual(&x_trial)?;
state.cost_evals += 1;
let f_trial = half * r_trial.norm_squared();
let prev_cost = state
.cost
.expect("cost not set: Solver::init must run before next_iter");
let actual_diff = prev_cost - f_trial;
let rho = if l_diff > F::zero() {
actual_diff / l_diff
} else {
F::zero()
};
if rho > F::zero() {
state.param = x_trial;
state.cost = Some(f_trial);
let factor = F::one() - (two * rho - F::one()).powi(3);
mu = mu * factor.max(one_third);
nu = two;
self.r_cache = Some(r_trial);
self.model_cache = None;
self.jtr_cache = None;
} else {
mu = mu * nu;
nu = nu * two;
self.r_cache = Some(r);
self.model_cache = Some(Ok(a));
self.jtr_cache = Some(g);
}
self.mu = Some(mu);
self.nu = nu;
self.diag = Some(d);
let cost_rel_converged = self.tol_cost_rel > F::zero()
&& actual_diff.abs() <= self.tol_cost_rel * prev_cost
&& l_diff <= self.tol_cost_rel * prev_cost
&& rho <= two;
let step_rel_converged = self.tol_step_rel > F::zero()
&& h.norm_squared()
<= self.tol_step_rel
* self.tol_step_rel
* state.param.norm_squared();
if cost_rel_converged || step_rel_converged {
return Ok((state, Some(TerminationReason::SolverConverged)));
}
Ok((state, None))
}
}
type LmStep<V, F, E> = Result<(NllsState<V, F>, Option<TerminationReason>), E>;
#[derive(PartialEq)]
enum ModelSolveError {
Retry,
Failed,
}
trait LinearModel<M, V, F: Scalar> {
type Cache;
const CHECK_FINITE: bool;
fn prepare(j: &M, r: &V) -> Result<Self::Cache, QrSolveError>;
fn diagonal(cache: &Self::Cache) -> V;
fn solve(
cache: &Self::Cache,
g: &V,
d: &V,
mu: F,
tolerance: Option<F>,
) -> Result<V, ModelSolveError>;
}
struct NormalEquations;
impl<M, V, F: Scalar> LinearModel<M, V, F> for NormalEquations
where
M: GramMatrix
+ MatDiagonal<V>
+ LinearSolveSpd<V>
+ AddDiagonalVectorInPlace<V>
+ Clone,
V: Clone + NegInPlace + ScaleInPlace<F>,
{
type Cache = M;
const CHECK_FINITE: bool = false;
fn prepare(j: &M, _: &V) -> Result<M, QrSolveError> {
Ok(j.gram())
}
fn diagonal(cache: &M) -> V {
cache.diagonal()
}
fn solve(
cache: &M,
g: &V,
d: &V,
mu: F,
_: Option<F>,
) -> Result<V, ModelSolveError> {
let mut a = cache.clone();
let mut diagonal = d.clone();
diagonal.scale_in_place(mu);
a.add_diagonal_vector_in_place(&diagonal);
let mut rhs = g.clone();
rhs.neg_in_place();
a.solve_spd(&rhs).map_err(|_| ModelSolveError::Retry)
}
}
struct PivotedQr;
impl<M, V, F: Scalar> LinearModel<M, V, F> for PivotedQr
where
M: FactorizePivotedQr<V, F>,
V: Clone + NegInPlace,
{
type Cache = M::Factorization;
const CHECK_FINITE: bool = true;
fn prepare(j: &M, r: &V) -> Result<Self::Cache, QrSolveError> {
let mut rhs = r.clone();
rhs.neg_in_place();
j.factorize_pivoted_qr(&rhs)
}
fn diagonal(cache: &Self::Cache) -> V {
cache.column_norms_squared()
}
fn solve(
cache: &Self::Cache,
_: &V,
d: &V,
mu: F,
tolerance: Option<F>,
) -> Result<V, ModelSolveError> {
cache.solve_regularized(mu, d, tolerance).map_err(|e| {
if e == QrSolveError::RankDeficient {
ModelSolveError::Retry
} else {
ModelSolveError::Failed
}
})
}
}
pub struct LevenbergMarquardtQr<V, M, F: Scalar = f64>
where
M: FactorizePivotedQr<V, F>,
{
inner: LevenbergMarquardt<V, M::Factorization, F>,
rank_tolerance: Option<F>,
}
impl<V, M, F: Scalar> LevenbergMarquardt<V, M, F> {
pub fn with_pivoted_qr(self) -> LevenbergMarquardtQr<V, M, F>
where
M: FactorizePivotedQr<V, F>,
{
LevenbergMarquardtQr {
inner: LevenbergMarquardt {
tol_grad: self.tol_grad,
tol_grad_rel: self.tol_grad_rel,
tol_cost_rel: self.tol_cost_rel,
tol_step_rel: self.tol_step_rel,
tau: self.tau,
max_inner_attempts: self.max_inner_attempts,
..LevenbergMarquardt::defaults()
},
rank_tolerance: None,
}
}
}
impl<V, M, F: Scalar> Default for LevenbergMarquardtQr<V, M, F>
where
M: FactorizePivotedQr<V, F>,
{
fn default() -> Self {
Self::new()
}
}
impl<V, M, F: Scalar> LevenbergMarquardtQr<V, M, F>
where
M: FactorizePivotedQr<V, F>,
{
pub fn new() -> Self {
Self {
inner: LevenbergMarquardt::defaults(),
rank_tolerance: None,
}
}
pub fn with_rank_tolerance(mut self, tol: F) -> Self {
assert!(
tol.is_finite() && tol >= F::zero() && tol < F::one(),
"rank tolerance must be finite and in [0,1)"
);
self.rank_tolerance = Some(tol);
self
}
pub fn with_tol_grad(mut self, value: F) -> Self {
self.inner = self.inner.with_tol_grad(value);
self
}
pub fn with_tol_grad_rel(mut self, value: F) -> Self {
self.inner = self.inner.with_tol_grad_rel(value);
self
}
pub fn with_tol_cost_rel(mut self, value: F) -> Self {
self.inner = self.inner.with_tol_cost_rel(value);
self
}
pub fn with_tol_step_rel(mut self, value: F) -> Self {
self.inner = self.inner.with_tol_step_rel(value);
self
}
pub fn with_tau(mut self, value: F) -> Self {
self.inner = self.inner.with_tau(value);
self
}
pub fn with_max_inner_attempts(mut self, value: u32) -> Self {
self.inner = self.inner.with_max_inner_attempts(value);
self
}
}
impl<P, V, M, F> Solver<P, NllsState<V, F>> for LevenbergMarquardtQr<V, M, F>
where
F: Scalar,
P: Residual<Param = V, Output = V> + Jacobian<Jacobian = M>,
V: ScaledAdd<F>
+ NormSquared<F>
+ NormInfinity<F>
+ NegInPlace
+ Dot<F>
+ ScaleInPlace<F>
+ ComponentMulAssign
+ ComponentDivAssign
+ ComponentMaxAssign
+ FloorZerosInPlace<F>
+ Clone,
M: FactorizePivotedQr<V, F> + MatTransposeVec<V>,
{
type Error = <P as Residual>::Error;
fn init(
&mut self,
problem: &mut Problem<P>,
state: NllsState<V, F>,
) -> Result<NllsState<V, F>, Self::Error> {
self.inner.init_model::<P, M, PivotedQr>(problem, state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
state: NllsState<V, F>,
) -> Result<(NllsState<V, F>, Option<TerminationReason>), Self::Error> {
self.inner.next_iter_model::<P, M, PivotedQr>(
problem,
state,
self.rank_tolerance,
)
}
}
impl<V: Clone, M, F: Scalar> crate::core::inner::InitialState<V>
for LevenbergMarquardtQr<V, M, F>
where
M: FactorizePivotedQr<V, F>,
{
type State = NllsState<V, F>;
fn seed(&self, x: &V) -> Self::State {
NllsState::new(x.clone())
}
}
impl<V: Clone, M, F: Scalar> crate::core::inner::WarmStart<V>
for LevenbergMarquardtQr<V, M, F>
where
M: FactorizePivotedQr<V, F>,
{
}
impl<V: Clone, M, F: Scalar> super::cma_inject::MemeticInner<V, F>
for LevenbergMarquardtQr<V, M, F>
where
M: FactorizePivotedQr<V, F>,
{
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{DenseMatrix, Executor};
#[derive(Clone)]
struct CholeskyOnly(DenseMatrix);
impl GramMatrix for CholeskyOnly {
fn gram(&self) -> Self {
Self(self.0.gram())
}
}
impl MatDiagonal<Vec<f64>> for CholeskyOnly {
fn diagonal(&self) -> Vec<f64> {
self.0.diagonal()
}
}
impl MatTransposeVec<Vec<f64>> for CholeskyOnly {
fn mat_transpose_vec(&self, v: &Vec<f64>) -> Vec<f64> {
self.0.mat_transpose_vec(v)
}
}
impl AddDiagonalVectorInPlace<Vec<f64>> for CholeskyOnly {
fn add_diagonal_vector_in_place(&mut self, d: &Vec<f64>) {
self.0.add_diagonal_vector_in_place(d);
}
}
impl LinearSolveSpd<Vec<f64>> for CholeskyOnly {
fn solve_spd(
&self,
b: &Vec<f64>,
) -> Result<Vec<f64>, crate::LinearSolveError> {
self.0.solve_spd(b)
}
}
struct Fit;
impl Residual for Fit {
type Param = Vec<f64>;
type Output = Vec<f64>;
type Error = std::convert::Infallible;
fn residual(&self, x: &Vec<f64>) -> Result<Vec<f64>, Self::Error> {
Ok(vec![x[0] - 1.])
}
}
impl Jacobian for Fit {
type Jacobian = CholeskyOnly;
fn jacobian(&self, _: &Vec<f64>) -> Result<CholeskyOnly, Self::Error> {
Ok(CholeskyOnly(DenseMatrix::from_row_slice(1, 1, &[1.])))
}
}
#[test]
fn legacy_annotations_and_cholesky_only_capabilities_still_work() {
let solver: LevenbergMarquardt<Vec<f64>, CholeskyOnly> =
LevenbergMarquardt::new();
let result = Executor::from_start(Fit, solver, vec![0.])
.max_iter(50)
.run()
.unwrap();
assert_eq!(result.reason, TerminationReason::SolverConverged);
assert!((result.param()[0] - 1.).abs() < 1e-8);
}
}