use crate::{AlgorithmError, Result};
pub struct LevenbergMarquardt {
max_iterations: usize,
tolerance: f64,
lambda_initial: f64,
lambda_factor: f64,
epsilon: f64,
}
impl Default for LevenbergMarquardt {
fn default() -> Self {
LevenbergMarquardt {
max_iterations: 100,
tolerance: 1e-8,
lambda_initial: 1e-3,
lambda_factor: 10.0,
epsilon: 1e-8,
}
}
}
impl LevenbergMarquardt {
pub fn new() -> Self {
Self::default()
}
pub fn with_max_iterations(mut self, max_iter: usize) -> Self {
self.max_iterations = max_iter;
self
}
pub fn with_tolerance(mut self, tol: f64) -> Self {
self.tolerance = tol;
self
}
pub fn fit<F>(
&self,
residual_fn: F,
initial_params: &[f64],
n_data: usize,
) -> Result<FitResult>
where
F: Fn(&[f64], usize) -> f64,
{
let n_params = initial_params.len();
let mut params = initial_params.to_vec();
let mut lambda = self.lambda_initial;
let mut prev_cost = self.compute_cost(&residual_fn, ¶ms, n_data);
for iteration in 0..self.max_iterations {
let (jacobian, residuals) = self.compute_jacobian_and_residuals(
&residual_fn,
¶ms,
n_data,
);
let jtj = self.multiply_jt_j(&jacobian);
let jtr = self.multiply_jt_r(&jacobian, &residuals);
let mut jtj_lambda = jtj.clone();
for i in 0..n_params {
jtj_lambda[i][i] += lambda * (1.0 + jtj[i][i]);
}
let delta = match self.solve_linear_system(&jtj_lambda, &jtr) {
Ok(d) => d,
Err(_) => {
lambda *= self.lambda_factor;
continue;
}
};
let mut new_params = Vec::with_capacity(n_params);
for i in 0..n_params {
new_params.push(params[i] - delta[i]);
}
let new_cost = self.compute_cost(&residual_fn, &new_params, n_data);
if new_cost < prev_cost {
let improvement = (prev_cost - new_cost) / prev_cost;
params = new_params;
prev_cost = new_cost;
lambda /= self.lambda_factor;
if improvement < self.tolerance {
return Ok(FitResult {
parameters: params,
cost: prev_cost,
iterations: iteration + 1,
converged: true,
});
}
} else {
lambda *= self.lambda_factor;
}
}
Ok(FitResult {
parameters: params,
cost: prev_cost,
iterations: self.max_iterations,
converged: false,
})
}
fn compute_cost<F>(&self, residual_fn: &F, params: &[f64], n_data: usize) -> f64
where
F: Fn(&[f64], usize) -> f64,
{
let mut sum = 0.0;
for i in 0..n_data {
let r = residual_fn(params, i);
sum += r * r;
}
sum / 2.0
}
fn compute_jacobian_and_residuals<F>(
&self,
residual_fn: &F,
params: &[f64],
n_data: usize,
) -> (Vec<Vec<f64>>, Vec<f64>)
where
F: Fn(&[f64], usize) -> f64,
{
let n_params = params.len();
let mut jacobian = vec![vec![0.0; n_params]; n_data];
let mut residuals = vec![0.0; n_data];
for i in 0..n_data {
residuals[i] = residual_fn(params, i);
for j in 0..n_params {
let mut params_plus = params.to_vec();
params_plus[j] += self.epsilon;
let r_plus = residual_fn(¶ms_plus, i);
jacobian[i][j] = (r_plus - residuals[i]) / self.epsilon;
}
}
(jacobian, residuals)
}
fn multiply_jt_j(&self, jacobian: &[Vec<f64>]) -> Vec<Vec<f64>> {
let n_data = jacobian.len();
let n_params = jacobian[0].len();
let mut result = vec![vec![0.0; n_params]; n_params];
for i in 0..n_params {
for j in 0..n_params {
#[allow(clippy::needless_range_loop)]
for k in 0..n_data {
result[i][j] += jacobian[k][i] * jacobian[k][j];
}
}
}
result
}
fn multiply_jt_r(&self, jacobian: &[Vec<f64>], residuals: &[f64]) -> Vec<f64> {
let n_data = jacobian.len();
let n_params = jacobian[0].len();
let mut result = vec![0.0; n_params];
for i in 0..n_params {
for j in 0..n_data {
result[i] += jacobian[j][i] * residuals[j];
}
}
result
}
fn solve_linear_system(&self, a: &[Vec<f64>], b: &[f64]) -> Result<Vec<f64>> {
let n = a.len();
let mut a = a.to_vec();
let mut b = b.to_vec();
for k in 0..n - 1 {
let mut max_idx = k;
for i in k + 1..n {
if a[i][k].abs() > a[max_idx][k].abs() {
max_idx = i;
}
}
if max_idx != k {
a.swap(k, max_idx);
b.swap(k, max_idx);
}
if a[k][k].abs() < 1e-14 {
return Err(AlgorithmError::NumericalInstability(
"Matrix is singular".to_string()
));
}
for i in k + 1..n {
let factor = a[i][k] / a[k][k];
#[allow(clippy::needless_range_loop)]
for j in k..n {
a[i][j] -= factor * a[k][j];
}
b[i] -= factor * b[k];
}
}
let mut x = vec![0.0; n];
for i in (0..n).rev() {
if a[i][i].abs() < 1e-14 {
return Err(AlgorithmError::NumericalInstability(
"Matrix is singular".to_string()
));
}
let mut sum = b[i];
for j in i + 1..n {
sum -= a[i][j] * x[j];
}
x[i] = sum / a[i][i];
}
Ok(x)
}
}
#[derive(Debug, Clone)]
pub struct FitResult {
pub parameters: Vec<f64>,
pub cost: f64,
pub iterations: usize,
pub converged: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_linear_fit() {
let x_data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let y_data = vec![2.01, 3.98, 6.02, 7.97, 10.03];
let residual = |params: &[f64], i: usize| {
let prediction = params[0] * x_data[i] + params[1];
y_data[i] - prediction
};
let lm = LevenbergMarquardt::new();
let result = lm.fit(residual, &[1.0, 0.0], x_data.len()).unwrap();
assert!(result.converged);
assert!((result.parameters[0] - 2.0).abs() < 0.1); assert!(result.parameters[1].abs() < 0.1); }
#[test]
fn test_exponential_fit() {
let x_data = vec![0.0, 1.0, 2.0, 3.0];
let y_data = vec![1.0, 2.7183, 7.3891, 20.0855];
let residual = |params: &[f64], i: usize| {
let prediction = params[0] * (params[1] * x_data[i]).exp();
y_data[i] - prediction
};
let lm = LevenbergMarquardt::new().with_max_iterations(200);
let result = lm.fit(residual, &[1.0, 1.0], x_data.len()).unwrap();
assert!((result.parameters[0] - 1.0).abs() < 0.1);
assert!((result.parameters[1] - 1.0).abs() < 0.1);
}
}