use crate::linear_algebra::qr::{PivotedQr, enorm, max, min};
use crate::linear_algebra::{Matrix, Vector};
use crate::numerical_derivative::autodiff::AutoDiffMulti;
use crate::numerical_derivative::derivator::DerivatorMultiVariable;
use crate::numerical_derivative::jacobian::Jacobian;
use crate::optimization::trust_region::determine_lambda_and_parameter_update;
use crate::optimization::{MinimizationReport, TerminationReason, is_finite, report};
use crate::scalar::{Numeric, VectorFn};
use crate::utils::error_codes::CalcError;
const MAX_TRUST_REGION_TRIALS: usize = 100;
pub struct LevenbergMarquardt<D: DerivatorMultiVariable = AutoDiffMulti> {
derivator: D,
ftol: D::Scalar,
xtol: D::Scalar,
gtol: D::Scalar,
stepbound: D::Scalar,
patience: usize,
scale_diag: bool,
}
impl<D: DerivatorMultiVariable + Default> Default for LevenbergMarquardt<D> {
fn default() -> Self {
Self::from_derivator(D::default())
}
}
impl<D: DerivatorMultiVariable> LevenbergMarquardt<D> {
pub fn from_derivator(derivator: D) -> Self {
let tol = D::Scalar::EPSILON * D::Scalar::from_f64(30.0);
LevenbergMarquardt {
derivator,
ftol: tol,
xtol: tol,
gtol: tol,
stepbound: D::Scalar::from_f64(100.0),
patience: 100,
scale_diag: true,
}
}
#[must_use]
pub fn with_ftol(mut self, ftol: D::Scalar) -> Self {
self.ftol = ftol;
self
}
#[must_use]
pub fn with_xtol(mut self, xtol: D::Scalar) -> Self {
self.xtol = xtol;
self
}
#[must_use]
pub fn with_gtol(mut self, gtol: D::Scalar) -> Self {
self.gtol = gtol;
self
}
#[must_use]
pub fn with_stepbound(mut self, stepbound: D::Scalar) -> Self {
self.stepbound = stepbound;
self
}
#[must_use]
pub fn with_patience(mut self, patience: usize) -> Self {
self.patience = patience;
self
}
#[must_use]
pub fn with_scale_diag(mut self, scale_diag: bool) -> Self {
self.scale_diag = scale_diag;
self
}
pub fn minimize<F, const N: usize, const M: usize>(
&self,
f: &F,
x0: &[D::Scalar; N],
) -> Result<MinimizationReport<N, D::Scalar>, CalcError>
where
D: Clone,
F: VectorFn<N, M>,
{
let zero = D::Scalar::ZERO;
let one = D::Scalar::ONE;
let p1 = D::Scalar::from_f64(0.1);
let p25 = D::Scalar::from_f64(0.25);
let p5 = D::Scalar::HALF;
let p75 = D::Scalar::from_f64(0.75);
let p0001 = D::Scalar::from_f64(1.0e-4);
let jacobian = Jacobian::from_derivator(self.derivator.clone());
let mut x = *x0;
let mut residuals = f.eval(&x);
if !is_finite(&residuals) {
return Err(CalcError::NonFiniteValue);
}
let mut fnorm = enorm(&residuals);
let mut diag = [zero; N];
let mut delta = zero;
let mut xnorm = zero;
let mut par = zero;
let mut evaluations = 1usize;
let mut first = true;
for _ in 0..self.patience {
let jac = jacobian.get(f, &x)?;
if !jac.iter().all(is_finite) {
return Err(CalcError::NonFiniteValue);
}
let dls = PivotedQr::decompose(Matrix::from_fn(|r, c| jac[r][c]))?
.into_damped(Vector::new(residuals));
if first {
for (slot, &column_norm) in diag.iter_mut().zip(dls.column_norms.iter()) {
*slot = if column_norm == zero {
one
} else {
column_norm
};
}
xnorm = enorm(&core::array::from_fn::<_, N, _>(|j| diag[j] * x[j]));
delta = self.stepbound * xnorm;
if delta == zero {
delta = self.stepbound;
}
}
if dls.max_a_t_b_scaled(fnorm) <= self.gtol {
return Ok(report(x, fnorm, evaluations, TerminationReason::Gtol));
}
if self.scale_diag {
for (slot, &column_norm) in diag.iter_mut().zip(dls.column_norms.iter()) {
*slot = max(*slot, column_norm);
}
}
for _ in 0..MAX_TRUST_REGION_TRIALS {
let update = determine_lambda_and_parameter_update(&dls, &diag, delta, par);
par = update.lambda;
let p = update.step;
let pnorm = enorm(&core::array::from_fn::<_, N, _>(|j| diag[j] * p[j]));
if first {
delta = min(delta, pnorm);
first = false;
}
let x_new: [D::Scalar; N] = core::array::from_fn(|j| x[j] - p[j]);
let residuals_new = f.eval(&x_new);
evaluations += 1;
if !is_finite(&residuals_new) {
return Err(CalcError::NonFiniteValue);
}
let fnorm1 = enorm(&residuals_new);
let actred = if p1 * fnorm1 < fnorm {
one - (fnorm1 / fnorm) * (fnorm1 / fnorm)
} else {
-one
};
let temp1 = dls.a_x_norm(&p) / fnorm;
let temp2 = (par.sqrt() * pnorm) / fnorm;
let prered = temp1 * temp1 + (temp2 * temp2) / p5;
let dirder = -(temp1 * temp1 + temp2 * temp2);
let ratio = if prered != zero {
actred / prered
} else {
zero
};
if ratio <= p25 {
let mut temp = if actred >= zero {
p5
} else {
let denom = dirder + p5 * actred;
if denom != zero {
p5 * dirder / denom
} else {
p5
}
};
if p1 * fnorm1 >= fnorm || temp < p1 {
temp = p1;
}
delta = temp * min(delta, pnorm / p1);
par /= temp;
} else if par == zero || ratio >= p75 {
delta = pnorm / p5;
par = p5 * par;
}
let accepted = ratio >= p0001;
if accepted {
x = x_new;
residuals = residuals_new;
fnorm = fnorm1;
xnorm = enorm(&core::array::from_fn::<_, N, _>(|j| diag[j] * x[j]));
}
if actred.abs() <= self.ftol && prered <= self.ftol && p5 * ratio <= one {
return Ok(report(x, fnorm, evaluations, TerminationReason::Ftol));
}
if delta <= self.xtol * xnorm {
return Ok(report(x, fnorm, evaluations, TerminationReason::Xtol));
}
if accepted {
break;
}
}
}
Err(CalcError::DidNotConverge)
}
}