use super::least_squares_diagnostics::{
LeastSquaresCheck, LeastSquaresDiagnostics, LeastSquaresObservation, Trace,
};
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::Problem;
use crate::core::solver::Solver;
use crate::core::state::{PointState, State};
use crate::core::termination::Termination;
use crate::{VectorIndex, VectorLen};
pub struct GaussNewton<V, M, F: Scalar = f64> {
tol_grad: Option<F>,
r_cache: Option<V>,
j_cache: Option<M>,
model_r_cache: Option<V>,
failed: bool,
trace: Option<Trace<F>>,
trace_vector: Option<fn(&V) -> Vec<F>>,
}
impl<V, M, F: Scalar> LeastSquaresDiagnostics<F> for GaussNewton<V, M, F> {
fn least_squares_observations(&self) -> &[LeastSquaresObservation<F>] {
self.trace.as_ref().map_or(&[], |trace| &trace.observations)
}
}
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,
trace: None,
trace_vector: None,
}
}
}
impl<V, M> GaussNewton<V, M> {
pub fn new() -> Self {
Self::default()
}
}
impl<V, M, F: Scalar> GaussNewton<V, M, F> {
pub fn with_trial_diagnostics(mut self, enabled: bool) -> Self
where
V: VectorLen + VectorIndex<F>,
{
self.trace = enabled.then(Trace::default);
self.trace_vector = enabled.then_some(|v: &V| {
(0..v.vec_len()).map(|i| v.get_scalar(i)).collect()
});
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 = PointState<V, F>;
fn seed(&self, x: &V) -> Self::State {
PointState::new(x.clone())
}
}
impl<P, V, M, F, E> Solver<P, PointState<V, F>> for GaussNewton<V, M, F>
where
F: Scalar,
Problem<P>: Evaluation<V, M, F, Error = E>,
V: ScaledAdd<F> + NormSquared<F> + NormInfinity<F> + NegInPlace + Clone,
M: GramMatrix + MatTransposeVec<V> + LinearSolveSpd<V>,
{
type Error = E;
fn init(
&mut self,
problem: &mut Problem<P>,
state: PointState<V, F>,
) -> Result<PointState<V, F>, Self::Error> {
self.init_evaluated(problem, state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
state: PointState<V, F>,
) -> Result<crate::SolverStep<PointState<V, F>>, 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: PointState<V, F>,
) -> Result<PointState<V, F>, E::Error> {
state.reset();
if let Some(trace) = &mut self.trace {
trace.reset();
}
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.replace(
state.param().clone(),
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;
if let Some(trace) = &mut self.trace {
trace.event(state.cost(), "construction", "invalid_model");
}
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: PointState<V, F>,
) -> NllsStep<V, F, E::Error> {
if let Some(trace) = &mut self.trace {
trace.clear();
}
if self.failed {
if let Some(trace) = &mut self.trace {
trace.event(state.cost(), "construction", "invalid_model");
}
return Ok(crate::SolverStep::from((
state,
Some(Termination::numerical_failure(
"Gauss-Newton could not initialize a finite residual model.",
)),
)));
}
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;
if let Some(trace) = &mut self.trace {
trace.event(
state.cost(),
"construction",
"invalid_model",
);
}
return Ok(crate::SolverStep::from((
state,
Some(Termination::numerical_failure(
"Gauss-Newton could not construct the residual model at the current point.",
)),
)));
};
model
}
};
let g = j.mat_transpose_vec(model_r.as_ref().unwrap_or(&r));
if !problem.valid_vector(&g) {
self.failed = true;
if let Some(trace) = &mut self.trace {
let index = trace.event(
state.cost(),
"construction",
"invalid_gradient",
);
trace.observations[index].gradient =
self.trace_vector.unwrap()(&g);
}
return Ok(crate::SolverStep::from((
state,
Some(Termination::numerical_failure(
"The Gauss-Newton model gradient is invalid.",
)),
)));
}
let gradient_norm = g.norm_infinity();
if let Some(trace) = &mut self.trace {
let mut row = LeastSquaresObservation::model(state.cost());
row.gradient = self.trace_vector.unwrap()(&g);
row.checks.push(LeastSquaresCheck {
name: "absolute_gradient",
tolerance: self.tol_grad,
passed: self.tol_grad.map(|tol| gradient_norm <= tol),
evidence: self.tol_grad.map(|tolerance| {
crate::ConvergenceEvidence::UpperBound {
value: gradient_norm,
bound: tolerance,
tolerance,
reference: None,
}
}),
});
trace.push(row);
}
if self.tol_grad.is_some_and(|tol| gradient_norm <= tol) {
self.r_cache = Some(r);
self.j_cache = Some(j);
self.model_r_cache = model_r;
return Ok(crate::SolverStep::from((
state,
Some(Termination::upper_bound(
crate::ConvergenceTest::AbsoluteGradientInfinity,
gradient_norm,
self.tol_grad.unwrap(),
self.tol_grad.unwrap(),
None,
)),
)));
}
let gram = j.gram();
let mut neg_g = g;
neg_g.neg_in_place();
let solved = gram.solve_spd(&neg_g);
if let Some(trace) = &mut self.trace {
let mut row = LeastSquaresObservation::model(state.cost());
row.event = "subproblem";
row.outcome = Some(if solved.is_ok() {
"solved"
} else {
"factorization_failed"
});
row.model_solves = 1;
row.gradient = self.trace_vector.unwrap()(&neg_g)
.into_iter()
.map(|v| -v)
.collect();
if let Ok(delta) = &solved {
row.step = self.trace_vector.unwrap()(delta);
row.step_norm = Some(delta.norm_squared().sqrt());
}
trace.push(row);
}
let delta = match solved {
Ok(d) => d,
Err(_) => {
self.r_cache = Some(r);
self.j_cache = Some(j);
self.model_r_cache = model_r;
return Ok(crate::SolverStep::from((
state,
Some(Termination::numerical_failure(
"The Gauss-Newton normal-equation Cholesky solve failed.",
)),
)));
}
};
let mut param = state.param().clone();
param.scaled_add(F::one(), &delta);
let trial = self.trace.as_mut().map(|trace| {
let mut row = LeastSquaresObservation::model(state.cost());
row.event = "trial";
row.outcome = Some("callback_pending");
row.trial = true;
row.step = self.trace_vector.unwrap()(&delta);
row.gradient = self.trace_vector.unwrap()(&neg_g)
.into_iter()
.map(|v| -v)
.collect();
row.step_norm = Some(delta.norm_squared().sqrt());
row.predicted_reduction = Some(
-row.gradient
.iter()
.zip(&row.step)
.fold(F::zero(), |sum, (g, d)| sum + *g * *d)
/ F::from_f64(2.).unwrap(),
);
trace.push(row)
});
let r_new = problem.residual(¶m).inspect_err(|_| {
if let Some(index) = trial {
self.trace.as_mut().unwrap().observations[index].outcome =
Some("callback_failed");
}
})?;
let trial_cost = problem
.cost(&r_new, |r| F::from_f64(0.5).unwrap() * r.norm_squared());
if let Some(index) = trial {
let row = &mut self.trace.as_mut().unwrap().observations[index];
row.outcome = Some("unconditional_full_step");
row.trial_cost = Some(trial_cost);
row.actual_reduction = Some(row.base_cost - trial_cost);
row.accepted = Some(true);
}
state.replace(param, trial_cost);
self.r_cache = Some(r_new);
self.j_cache = None;
let failed = E::ROBUST && !state.cost().is_finite();
self.failed = failed;
Ok(crate::SolverStep::from((
state,
failed.then(|| Termination::numerical_failure("Gauss-Newton trial produced a non-finite robust objective.")),
)))
}
}