use crate::core::inner::InitialState;
use crate::core::least_squares::evaluation::{Evaluation, NllsStep};
use crate::core::math::{
GramMatrix, LinearSolveSpd, MatTransposeVec, NegInPlace, NormInfinity,
NormSquared, Scalar, ScaledAdd,
};
use crate::core::problem::{Jacobian, Problem, Residual};
use crate::core::solver::Solver;
use crate::core::state::NllsState;
use crate::core::termination::TerminationReason;
use crate::{
LossFunction, RobustLeastSquares, ScaleRowsInPlace, VectorIndex, VectorLen,
};
pub struct GaussNewton<V, M, F = f64> {
tol_grad: Option<F>,
r_cache: Option<V>,
j_cache: Option<M>,
model_r_cache: Option<V>,
failed: bool,
}
impl<V, M, F: Scalar> Default for GaussNewton<V, M, F> {
fn default() -> Self {
Self {
tol_grad: Some(F::from_f64(1e-8).unwrap()),
r_cache: None,
j_cache: None,
model_r_cache: None,
failed: false,
}
}
}
impl<V, M> GaussNewton<V, M> {
pub fn new() -> Self {
Self::default()
}
}
impl<V, M, F: Scalar> GaussNewton<V, M, F> {
#[deprecated(
note = "use `with_absolute_gradient_tolerance`; removal scheduled for Basin 2.0"
)]
pub fn with_tol_grad(mut self, tol: F) -> Self {
assert!(tol >= F::zero(), "tol_grad must be ≥ 0");
self.tol_grad = (tol > F::zero()).then_some(tol);
self
}
pub fn with_absolute_gradient_tolerance(
mut self,
value: impl Into<Option<F>>,
) -> Self {
self.tol_grad = crate::core::convergence::optional_tolerance(value);
self
}
}
impl<V, M, F> InitialState<V> for GaussNewton<V, M, F>
where
F: Scalar,
V: Clone,
{
type State = NllsState<V, F>;
fn seed(&self, x: &V) -> Self::State {
NllsState::new(x.clone())
}
}
impl<P, V, M, F> Solver<P, NllsState<V, F>> for GaussNewton<V, M, F>
where
F: Scalar,
P: Residual<Param = V, Output = V> + Jacobian<Jacobian = M>,
V: ScaledAdd<F> + NormSquared<F> + NormInfinity<F> + NegInPlace + Clone,
M: GramMatrix + MatTransposeVec<V> + LinearSolveSpd<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.init_evaluated(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_evaluated(problem, state)
}
}
impl<P, L, V, M, F> Solver<RobustLeastSquares<P, L, F>, NllsState<V, F>>
for GaussNewton<V, M, F>
where
F: Scalar,
P: Residual<Param = V, Output = V> + Jacobian<Jacobian = M>,
V: ScaledAdd<F> + NormSquared<F> + NormInfinity<F> + NegInPlace + Clone,
M: GramMatrix + MatTransposeVec<V> + LinearSolveSpd<V>,
L: LossFunction<F>,
V: VectorLen + VectorIndex<F>,
M: ScaleRowsInPlace<F>,
{
type Error = <P as Residual>::Error;
fn init(
&mut self,
problem: &mut Problem<RobustLeastSquares<P, L, F>>,
state: NllsState<V, F>,
) -> Result<NllsState<V, F>, Self::Error> {
self.init_evaluated(problem, state)
}
fn next_iter(
&mut self,
problem: &mut Problem<RobustLeastSquares<P, L, F>>,
state: NllsState<V, F>,
) -> Result<(NllsState<V, F>, Option<TerminationReason>), Self::Error> {
self.next_evaluated(problem, state)
}
}
impl<V, M, F: Scalar> GaussNewton<V, M, F>
where
V: ScaledAdd<F> + NormSquared<F> + NormInfinity<F> + NegInPlace + Clone,
M: GramMatrix + MatTransposeVec<V> + LinearSolveSpd<V>,
{
fn init_evaluated<E: Evaluation<V, M, F>>(
&mut self,
problem: &mut E,
mut state: NllsState<V, F>,
) -> Result<NllsState<V, F>, E::Error> {
self.failed = false;
self.r_cache = None;
self.j_cache = None;
self.model_r_cache = None;
let (r, j) = problem.residual_and_jacobian(&state.param)?;
state.cost = Some(
problem.cost(&r, |r| F::from_f64(0.5).unwrap() * r.norm_squared()),
);
let Some((model_r, j)) = problem.model(&r, j) else {
self.failed = true;
return Ok(state);
};
self.r_cache = Some(r);
self.j_cache = Some(j);
self.model_r_cache = model_r;
Ok(state)
}
fn next_evaluated<E: Evaluation<V, M, F>>(
&mut self,
problem: &mut E,
mut state: NllsState<V, F>,
) -> NllsStep<V, F, E::Error> {
if self.failed {
return Ok((state, Some(TerminationReason::SolverFailed)));
}
let r = match self.r_cache.take() {
Some(r) => r,
None => problem.residual(&state.param)?,
};
let (model_r, j) = match self.j_cache.take() {
Some(j) => (self.model_r_cache.take(), j),
None => {
let j = problem.jacobian(&state.param)?;
let Some(model) = problem.model(&r, j) else {
self.failed = true;
return Ok((state, Some(TerminationReason::SolverFailed)));
};
model
}
};
let g = j.mat_transpose_vec(model_r.as_ref().unwrap_or(&r));
if !problem.valid_vector(&g) {
self.failed = true;
return Ok((state, Some(TerminationReason::SolverFailed)));
}
if self.tol_grad.is_some_and(|tol| g.norm_infinity() <= tol) {
self.r_cache = Some(r);
self.j_cache = Some(j);
self.model_r_cache = model_r;
return Ok((state, Some(TerminationReason::SolverConverged)));
}
let gram = j.gram();
let mut neg_g = g;
neg_g.neg_in_place();
let delta = match gram.solve_spd(&neg_g) {
Ok(d) => d,
Err(_) => {
self.r_cache = Some(r);
self.j_cache = Some(j);
self.model_r_cache = model_r;
return Ok((state, Some(TerminationReason::SolverFailed)));
}
};
state.param.scaled_add(F::one(), &delta);
let r_new = problem.residual(&state.param)?;
state.cost =
Some(problem.cost(&r_new, |r| {
F::from_f64(0.5).unwrap() * r.norm_squared()
}));
self.r_cache = Some(r_new);
self.j_cache = None;
let failed = E::ROBUST && !state.cost.unwrap().is_finite();
self.failed = failed;
Ok((state, failed.then_some(TerminationReason::SolverFailed)))
}
}