use crate::error::SolveError;
use crate::linear_algebra::Vector;
use crate::linear_algebra::{PivotedQr, enorm, max};
use crate::numerical_derivative::AutoDiffMulti;
use crate::numerical_derivative::DerivatorMultiVariable;
use crate::numerical_derivative::Jacobian;
use crate::optimization::{MinimizationReport, TerminationReason, is_finite, report};
use crate::scalar::{Numeric, Primal, VectorFn};
const MAX_BACKTRACK: usize = 20;
pub struct GaussNewton<D: DerivatorMultiVariable = AutoDiffMulti> {
derivator: D,
ftol: D::Scalar,
xtol: D::Scalar,
gtol: D::Scalar,
patience: usize,
backtracking: bool,
}
impl<D: DerivatorMultiVariable + Default> Default for GaussNewton<D> {
fn default() -> Self {
Self::from_derivator(D::default())
}
}
impl GaussNewton<AutoDiffMulti> {
#[inline]
pub const fn new() -> Self {
Self::from_derivator(AutoDiffMulti::new())
}
}
impl<D: DerivatorMultiVariable> GaussNewton<D> {
pub const fn from_derivator(derivator: D) -> Self {
let tol = D::Scalar::EPSILON_X30;
GaussNewton {
derivator,
ftol: tol,
xtol: tol,
gtol: tol,
patience: 100,
backtracking: false,
}
}
#[must_use]
pub const fn with_ftol(mut self, ftol: D::Scalar) -> Self {
self.ftol = ftol;
self
}
#[must_use]
pub const fn with_xtol(mut self, xtol: D::Scalar) -> Self {
self.xtol = xtol;
self
}
#[must_use]
pub const fn with_gtol(mut self, gtol: D::Scalar) -> Self {
self.gtol = gtol;
self
}
#[must_use]
pub const fn with_patience(mut self, patience: usize) -> Self {
self.patience = patience;
self
}
#[must_use]
pub const fn with_backtracking(mut self, backtracking: bool) -> Self {
self.backtracking = backtracking;
self
}
pub fn minimize<F, const N: usize, const M: usize>(
&self,
f: &F,
x0: &[D::Scalar; N],
) -> Result<MinimizationReport<N, D::Scalar>, SolveError>
where
D: Clone,
D::Scalar: Primal,
F: VectorFn<N, M>,
{
let zero = D::Scalar::ZERO;
let half = D::Scalar::HALF;
let jacobian = Jacobian::from_derivator(self.derivator.clone());
let mut x = *x0;
let mut residuals = f.eval(&x);
if !is_finite(&residuals) {
return Err(SolveError::NonFinite);
}
let mut fnorm = enorm(&residuals);
let mut evaluations = 1usize;
for _ in 0..self.patience {
let j = jacobian.evaluate(f, &x)?;
if !j.is_finite() {
return Err(SolveError::NonFinite);
}
let qr = PivotedQr::decompose(j)?;
if fnorm == zero {
return Ok(report(x, fnorm, evaluations, TerminationReason::Gtol));
}
let gradient = j.transpose() * Vector::new(residuals);
let ga = *gradient.as_array();
let mut gnorm = zero;
for (c, &column_norm) in qr.column_norms.iter().enumerate() {
if column_norm != zero {
gnorm = max(gnorm, (ga[c] / (fnorm * column_norm)).abs());
}
}
if gnorm <= self.gtol {
return Ok(report(x, fnorm, evaluations, TerminationReason::Gtol));
}
let p = qr.solve_least_squares(Vector::new(residuals))?;
let pa = *p.as_array();
let mut alpha = D::Scalar::ONE;
let mut tries = 0;
let (x_new, residuals_new, fnorm_new) = loop {
let candidate: [D::Scalar; N] = core::array::from_fn(|k| x[k] - alpha * pa[k]);
let trial = f.eval(&candidate);
evaluations += 1;
let finite = is_finite(&trial);
let candidate_fnorm = if finite {
enorm(&trial)
} else {
D::Scalar::INFINITY
};
if !self.backtracking {
if !finite {
return Err(SolveError::NonFinite);
}
break (candidate, trial, candidate_fnorm);
}
if (finite && candidate_fnorm < fnorm) || tries >= MAX_BACKTRACK {
if !finite {
return Err(SolveError::NonFinite);
}
break (candidate, trial, candidate_fnorm);
}
alpha *= half;
tries += 1;
};
let step_norm = alpha * enorm(p.as_array());
let previous_fnorm = fnorm;
x = x_new;
residuals = residuals_new;
fnorm = fnorm_new;
let xnorm = enorm(&x);
if step_norm <= self.xtol * xnorm {
return Ok(report(x, fnorm, evaluations, TerminationReason::Xtol));
}
if (previous_fnorm - fnorm).abs() <= self.ftol * previous_fnorm {
return Ok(report(x, fnorm, evaluations, TerminationReason::Ftol));
}
}
Err(SolveError::DidNotConverge {
iters: evaluations,
residual: fnorm.to_f64(),
})
}
}