use nalgebra::{DMatrix, DVector};
use crate::numerical::Nonlinear_systems::engine::{
IterationState, LinearSolverKind, NonlinearMethod, RuntimeDiagnostics, SolveOptions,
StepOutcome, scaled_norm, scaling_vector, solve_linear_system,
};
use crate::numerical::Nonlinear_systems::error::{SolveError, TerminationReason};
use crate::numerical::Nonlinear_systems::problem::JacobianProvider;
use crate::numerical::Nonlinear_systems::trust_region_LM::solve_trust_region_subproblem;
#[derive(Debug, Clone, Copy)]
pub struct LevenbergMarquardtMethod {
pub lambda_init: f64,
pub diag_scaling: bool,
pub increase_factor: f64,
pub decrease_factor: f64,
pub min_lambda: f64,
pub max_lambda: f64,
}
impl Default for LevenbergMarquardtMethod {
fn default() -> Self {
Self {
lambda_init: 1e-3,
diag_scaling: true,
increase_factor: 3.0,
decrease_factor: 10.0,
min_lambda: 1e-6,
max_lambda: 1e3,
}
}
}
#[derive(Debug, Clone)]
pub struct LevenbergMarquardtState {
lambda: f64,
}
impl NonlinearMethod for LevenbergMarquardtMethod {
type MethodState = LevenbergMarquardtState;
fn init<P: JacobianProvider>(
&self,
_problem: &P,
_x0: &DVector<f64>,
_options: &SolveOptions,
_residual: &DVector<f64>,
_jacobian: &DMatrix<f64>,
) -> Result<Self::MethodState, SolveError> {
if self.lambda_init <= 0.0 {
return Err(SolveError::InvalidConfig(
"lambda_init must be positive".to_string(),
));
}
if self.increase_factor <= 1.0 || self.decrease_factor <= 1.0 {
return Err(SolveError::InvalidConfig(
"increase_factor and decrease_factor must be greater than 1".to_string(),
));
}
if self.min_lambda <= 0.0 || self.max_lambda < self.min_lambda {
return Err(SolveError::InvalidConfig(
"invalid lambda bounds".to_string(),
));
}
Ok(LevenbergMarquardtState {
lambda: self.lambda_init,
})
}
fn step<P: JacobianProvider>(
&self,
problem: &P,
state: &IterationState,
method_state: &mut Self::MethodState,
options: &SolveOptions,
runtime: &mut RuntimeDiagnostics,
) -> Result<StepOutcome, SolveError> {
let jtj = state.jacobian.transpose() * &state.jacobian;
let scaling = if self.diag_scaling {
DMatrix::from_diagonal(&jtj.diagonal())
} else {
DMatrix::identity(jtj.nrows(), jtj.ncols())
};
runtime.linear_solves += 1;
let step = solve_linear_system(
options.linear_solver,
&(&jtj + method_state.lambda * scaling),
&(-state.jacobian.transpose() * &state.residual),
)?;
if step.norm() < options.tolerance {
return Ok(StepOutcome::Terminated(TerminationReason::StepTooSmall));
}
let trial_x = if let Some(bounds) = &options.bounds {
bounds.project(&(&state.x + &step))
} else {
&state.x + &step
};
let trial_residual = problem.residual(&trial_x)?;
let actual = state.residual.norm_squared() - trial_residual.norm_squared();
let predicted = state.residual.norm_squared()
- (&state.residual + &state.jacobian * &step).norm_squared();
let rho = if predicted.abs() > 1e-12 {
actual / predicted
} else {
0.0
};
if rho > 0.0 {
method_state.lambda = (method_state.lambda / self.decrease_factor).max(self.min_lambda);
runtime.accepted_steps += 1;
Ok(StepOutcome::Continue {
next_x: trial_x,
accepted: true,
})
} else {
method_state.lambda = (method_state.lambda * self.increase_factor).min(self.max_lambda);
runtime.rejected_steps += 1;
if method_state.lambda >= self.max_lambda {
return Ok(StepOutcome::Terminated(TerminationReason::Stagnation));
}
Ok(StepOutcome::Continue {
next_x: state.x.clone(),
accepted: false,
})
}
}
}
#[derive(Debug, Clone)]
pub struct LevenbergMarquardtMinpack {
pub ftol: f64,
pub xtol: f64,
pub gtol: f64,
pub maxfev: usize,
pub mode: i32,
pub factor: f64,
pub diag: Option<DVector<f64>>,
}
impl Default for LevenbergMarquardtMinpack {
fn default() -> Self {
Self {
ftol: 1e-8,
xtol: 1e-8,
gtol: 1e-8,
maxfev: 1000,
mode: 1,
factor: 100.0,
diag: None,
}
}
}
#[derive(Debug, Clone)]
pub struct LMMinpackState {
par: f64,
delta: f64,
diag: DVector<f64>,
nfev: usize,
njev: usize,
}
impl LevenbergMarquardtMinpack {
fn build_augmented(&self, jtj: &DMatrix<f64>, par: f64, diag: &DVector<f64>) -> DMatrix<f64> {
let n = jtj.nrows();
let mut mat = jtj.clone();
for i in 0..n {
mat[(i, i)] += par * diag[i] * diag[i];
}
mat
}
fn find_par(
&self,
solver: LinearSolverKind,
jtj: &DMatrix<f64>,
g: &DVector<f64>,
diag: &DVector<f64>,
delta: f64,
) -> Result<(DVector<f64>, f64), SolveError> {
let mut par = 0.0;
let mut p = solve_linear_system(solver, &self.build_augmented(jtj, par, diag), &(-g))?;
let mut pnorm = scaled_norm(diag, &p);
if pnorm <= delta {
return Ok((p, par));
}
let mut par_lo = 0.0;
let mut par_hi: f64 = 1.0;
for _ in 0..60 {
let mat = self.build_augmented(jtj, par_hi, diag);
match solve_linear_system(solver, &mat, &(-g)) {
Ok(p_try) => {
let pn = scaled_norm(diag, &p_try);
if pn <= delta {
p = p_try;
pnorm = pn;
par = par_hi;
break;
} else {
par_hi *= 10.0;
}
}
Err(_) => {
par_hi *= 10.0;
}
}
}
if pnorm > delta {
for _ in 0..80 {
let par_mid = 0.5 * (par_lo + par_hi);
let mat = self.build_augmented(jtj, par_mid, diag);
let p_try = solve_linear_system(solver, &mat, &(-g))?;
let pn = scaled_norm(diag, &p_try);
if pn <= delta {
par_hi = par_mid;
p = p_try;
pnorm = pn;
} else {
par_lo = par_mid;
}
if (par_hi - par_lo).abs() < 1e-18 {
break;
}
}
par = par_hi;
}
Ok((p, par))
}
}
impl NonlinearMethod for LevenbergMarquardtMinpack {
type MethodState = LMMinpackState;
fn init<P: JacobianProvider>(
&self,
_problem: &P,
x0: &DVector<f64>,
_options: &SolveOptions,
_residual: &DVector<f64>,
jacobian: &DMatrix<f64>,
) -> Result<Self::MethodState, SolveError> {
let n = jacobian.ncols();
let diag = if self.mode == 2 {
if let Some(d) = &self.diag {
if d.len() != n {
return Err(SolveError::DimensionMismatch {
expected: n,
actual: d.len(),
context: "lm diag",
});
}
d.clone()
} else {
return Err(SolveError::InvalidConfig(
"mode=2 requires diag to be set".to_string(),
));
}
} else {
scaling_vector(jacobian, true)
};
let xscaled = diag.component_mul(x0);
let xnorm = xscaled.norm();
let delta = self.factor * if xnorm > 0.0 { xnorm } else { 1.0 };
Ok(LMMinpackState {
par: 0.0,
delta,
diag,
nfev: 0,
njev: 0,
})
}
fn step<P: JacobianProvider>(
&self,
problem: &P,
state: &IterationState,
method_state: &mut Self::MethodState,
options: &SolveOptions,
runtime: &mut RuntimeDiagnostics,
) -> Result<StepOutcome, SolveError> {
let p1 = 0.1;
let p5 = 0.5;
let p25 = 0.25;
let p75 = 0.75;
let p0001 = 1e-4;
let epsmch = f64::EPSILON;
let n = state.jacobian.ncols();
let fnorm = state.residual.norm();
let j = &state.jacobian;
let jtj = j.transpose() * j;
let g = j.transpose() * &state.residual;
let mut gnorm: f64 = 0.0;
if fnorm > 0.0 {
for jcol in 0..n {
let colnorm = j.column(jcol).norm();
if colnorm > 0.0 {
let mut sum = 0.0;
for i in 0..n {
sum += jtj[(i, jcol)] * (g[i] / fnorm);
}
let denom = if self.mode == 2 {
method_state.diag[jcol]
} else {
colnorm
};
if denom != 0.0 {
gnorm = gnorm.max((sum.abs()) / denom);
}
}
}
}
if gnorm <= self.gtol {
return Ok(StepOutcome::Terminated(TerminationReason::Converged));
}
if self.mode != 2 {
for jcol in 0..n {
let colnorm = j.column(jcol).norm();
method_state.diag[jcol] = method_state.diag[jcol].max(colnorm);
if method_state.diag[jcol] == 0.0 {
method_state.diag[jcol] = 1.0;
}
}
}
let _solver_kind = options.linear_solver;
runtime.linear_solves += 1;
let subproblem = solve_trust_region_subproblem(
&state.jacobian,
&state.residual,
&method_state.diag,
method_state.delta,
method_state.par,
)
.map_err(|message| SolveError::LinearSolveFailure(message.to_string()))?;
let pvec = subproblem.step;
let par = subproblem.lambda;
method_state.par = par;
let pnorm = scaled_norm(&method_state.diag, &pvec);
let xscaled = method_state.diag.component_mul(&state.x);
let xnorm = xscaled.norm();
if state.iteration == 0 {
method_state.delta = method_state.delta.min(pnorm);
}
let mut trial_x = &state.x - &pvec;
if let Some(bounds) = &options.bounds {
trial_x = bounds.project(&trial_x);
}
let trial_residual = problem
.residual(&trial_x)
.map_err(|e| SolveError::ResidualEvaluation(format!("{:?}", e)))?;
method_state.nfev += 1;
let fnorm1 = trial_residual.norm();
let actred = if p1 * fnorm1 < fnorm {
1.0 - (fnorm1 / fnorm).powi(2)
} else {
-1.0
};
let j_p = &state.jacobian * &pvec;
let temp1 = j_p.norm() / fnorm.max(1e-300);
let temp2 = (par.sqrt() * pnorm) / fnorm.max(1e-300);
let prered = temp1 * temp1 + (temp2 * temp2) / p5;
let dirder = -(temp1 * temp1 + temp2 * temp2);
let ratio = if prered != 0.0 { actred / prered } else { 0.0 };
if ratio > p25 {
if par == 0.0 || ratio < p75 {
method_state.delta = pnorm / p5;
method_state.par = 0.5 * par;
} else {
method_state.delta = pnorm / p5;
}
} else {
let mut temp = p5;
if actred < 0.0 {
temp = p5 * dirder / (dirder + p5 * actred);
}
if p1 * fnorm1 >= fnorm || temp < p1 {
temp = p1;
}
method_state.delta = temp * method_state.delta.min(pnorm / p1);
if par != 0.0 {
method_state.par = par / temp;
}
}
if ratio > p0001 {
runtime.accepted_steps += 1;
return Ok(StepOutcome::Continue {
next_x: trial_x,
accepted: true,
});
} else {
runtime.rejected_steps += 1;
}
if method_state.nfev >= self.maxfev {
return Ok(StepOutcome::Terminated(TerminationReason::MaxIterations));
}
if actred.abs() <= epsmch && prered <= epsmch && p5 * ratio <= 1.0 {
return Ok(StepOutcome::Terminated(TerminationReason::Stagnation));
}
if method_state.delta <= epsmch * xnorm {
return Ok(StepOutcome::Terminated(TerminationReason::Stagnation));
}
Ok(StepOutcome::Continue {
next_x: state.x.clone(),
accepted: false,
})
}
}
#[cfg(test)]
mod lm_minpack_tests {
use super::*;
use crate::numerical::Nonlinear_systems::engine::{SolveOptions, SolverEngine};
use crate::numerical::Nonlinear_systems::error::TerminationReason;
use approx::assert_relative_eq;
use nalgebra::{DMatrix, DVector};
struct ScalarQuadratic; impl crate::numerical::Nonlinear_systems::problem::NonlinearProblem for ScalarQuadratic {
fn dimension(&self) -> usize {
1
}
fn residual(&self, x: &DVector<f64>) -> Result<DVector<f64>, SolveError> {
Ok(DVector::from_vec(vec![x[0] * x[0] - 2.0]))
}
}
impl crate::numerical::Nonlinear_systems::problem::JacobianProvider for ScalarQuadratic {
fn jacobian(&self, x: &DVector<f64>) -> Result<DMatrix<f64>, SolveError> {
Ok(DMatrix::from_row_slice(1, 1, &[2.0 * x[0]]))
}
}
#[test]
fn lm_minpack_scalar_quadratic_converges() {
let method = LevenbergMarquardtMinpack {
ftol: 1e-10,
xtol: 1e-10,
gtol: 1e-10,
maxfev: 200,
mode: 1,
factor: 10.0,
diag: None,
};
let options = SolveOptions {
tolerance: 1e-8,
max_iterations: 50,
..Default::default()
};
let engine = SolverEngine::new(method, options);
let x0 = DVector::from_vec(vec![1.5]);
let res = engine.solve(&ScalarQuadratic, x0).expect("solve failed");
assert_eq!(res.termination, TerminationReason::Converged);
let root = res.x[0];
assert!(
(root - 2f64.sqrt()).abs() < 1e-6,
"root ~= sqrt(2): got {}",
root
);
}
struct TwoEq;
impl crate::numerical::Nonlinear_systems::problem::NonlinearProblem for TwoEq {
fn dimension(&self) -> usize {
2
}
fn residual(&self, x: &DVector<f64>) -> Result<DVector<f64>, SolveError> {
Ok(DVector::from_vec(vec![
x[0] * x[0] + x[1] * x[1] - 10.0,
x[0] - x[1] - 4.0,
]))
}
}
impl crate::numerical::Nonlinear_systems::problem::JacobianProvider for TwoEq {
fn jacobian(&self, x: &DVector<f64>) -> Result<DMatrix<f64>, SolveError> {
Ok(DMatrix::from_row_slice(
2,
2,
&[2.0 * x[0], 2.0 * x[1], 1.0, -1.0],
))
}
}
#[test]
fn lm_minpack_two_eq_converges() {
let method = LevenbergMarquardtMinpack::default();
let options = SolveOptions {
tolerance: 1e-8,
max_iterations: 100,
..Default::default()
};
let engine = SolverEngine::new(method, options);
let x0 = DVector::from_vec(vec![1.0, 1.0]);
let res = engine.solve(&TwoEq, x0).expect("solve failed");
assert_eq!(res.termination, TerminationReason::Converged);
let x = res.x;
assert!((x[0] - 3.0).abs() < 1e-4, "x[0] close to 3: got {}", x[0]);
assert!((x[1] + 1.0).abs() < 1e-4, "x[1] close to -1: got {}", x[1]);
}
struct Coupled;
impl crate::numerical::Nonlinear_systems::problem::NonlinearProblem for Coupled {
fn dimension(&self) -> usize {
2
}
fn residual(&self, x: &DVector<f64>) -> Result<DVector<f64>, SolveError> {
Ok(DVector::from_vec(vec![
x[0] * x[0] + x[1] - 37.0,
x[0] - x[1] * x[1] - 5.0,
]))
}
}
impl crate::numerical::Nonlinear_systems::problem::JacobianProvider for Coupled {
fn jacobian(&self, x: &DVector<f64>) -> Result<DMatrix<f64>, SolveError> {
Ok(DMatrix::from_row_slice(
2,
2,
&[2.0 * x[0], 1.0, 1.0, -2.0 * x[1]],
))
}
}
#[test]
fn lm_minpack_coupled_converges() {
let method = LevenbergMarquardtMinpack {
ftol: 1e-10,
xtol: 1e-10,
gtol: 1e-10,
maxfev: 500,
mode: 1,
factor: 10.0,
diag: None,
};
let options = SolveOptions {
tolerance: 1e-8,
max_iterations: 200,
..Default::default()
};
let engine = SolverEngine::new(method, options);
let x0 = DVector::from_vec(vec![6.0, 1.0]);
let res = engine.solve(&Coupled, x0).expect("solve failed");
assert_eq!(res.termination, TerminationReason::Converged);
let x = res.x;
println!("x = {:?}", x);
assert_relative_eq!(x[0], 6.0, epsilon = 1e-6);
assert_relative_eq!(x[1], 1.0, epsilon = 1e-6);
assert!((x[0] - 6.0).abs() < 1e-4, "x[0] close to 6: got {}", x[0]);
}
}