use crate::linear_algebra::qr::{PivotedQr, enorm, max};
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::{MinimizationReport, TerminationReason, is_finite, report};
use crate::scalar::{Numeric, VectorFn};
use crate::utils::error_codes::CalcError;
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<D: DerivatorMultiVariable> GaussNewton<D> {
pub fn from_derivator(derivator: D) -> Self {
let tol = D::Scalar::EPSILON * D::Scalar::from_f64(30.0);
GaussNewton {
derivator,
ftol: tol,
xtol: tol,
gtol: tol,
patience: 100,
backtracking: false,
}
}
#[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_patience(mut self, patience: usize) -> Self {
self.patience = patience;
self
}
#[must_use]
pub 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>, CalcError>
where
D: Clone,
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(CalcError::NonFiniteValue);
}
let mut fnorm = enorm(&residuals);
let mut evaluations = 1usize;
for _ in 0..self.patience {
let jac = jacobian.get(f, &x)?;
if !jac.iter().all(is_finite) {
return Err(CalcError::NonFiniteValue);
}
let j: Matrix<M, N, D::Scalar> = Matrix::from_fn(|r, c| jac[r][c]);
let qr = PivotedQr::decompose(j)?;
if fnorm == zero {
return Ok(report(x, fnorm, evaluations, TerminationReason::Gtol));
}
let gradient = j.transpose() * Vector::new(residuals);
let mut gnorm = zero;
for (c, &column_norm) in qr.column_norms.iter().enumerate() {
if column_norm != zero {
gnorm = max(gnorm, (gradient[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 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 * p[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(CalcError::NonFiniteValue);
}
break (candidate, trial, candidate_fnorm);
}
if (finite && candidate_fnorm < fnorm) || tries >= MAX_BACKTRACK {
if !finite {
return Err(CalcError::NonFiniteValue);
}
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(CalcError::DidNotConverge)
}
}