use super::NR::solve_linear_system;
use nalgebra::{DMatrix, DVector};
pub fn Powell_dogleg_method(
Jy: DMatrix<f64>,
Fy: DVector<f64>,
scaling: DVector<f64>,
delta: f64,
solver: String,
) -> DVector<f64> {
let p_gauss: DVector<f64> =
solve_linear_system(solver, &Jy, &(-Fy.clone())).expect("Failed to solve linear system"); assert_eq!(p_gauss.len(), scaling.len(), "length of vectors differ");
let scaled_norm = (scaling.clone().component_mul(&p_gauss.clone())).norm();
if scaled_norm <= delta {
println!("scaled norm of gauss step <= delta => return gauss step");
return p_gauss; }
let gradient: DVector<f64> = Jy.transpose() * Fy;
let p_steepest = -gradient.clone();
let gradient_scaled: DVector<f64> = gradient.component_div(&scaling);
let alpha_optimal = (gradient_scaled.norm() / (Jy * gradient_scaled).norm()).powf(2.0);
let p_dl: DVector<f64>;
if p_gauss.norm() < delta {
println!("scaled norm of gauss step <= delta => return gauss step");
p_dl = p_gauss;
} else if p_steepest.norm() * alpha_optimal > delta {
println!(
" norm of stepest descent step*alpha > delta => return p_steepest*(delta/||p_steepest||)"
);
p_dl = p_steepest.clone() * (delta / p_steepest.norm());
} else {
println!(
"||p_steepest|| {}, alpha {}, delta {}",
p_steepest.norm(),
alpha_optimal,
delta
);
let a = alpha_optimal * p_steepest.clone();
let b = p_gauss.clone();
let c = a.clone().transpose().dot(&(b.clone() - a.clone()));
let b_min_a = (b.clone() - a.clone()).norm();
let L = (c.powf(2.0) + b_min_a.powf(2.0) * (delta.powf(2.0) - a.norm().powf(2.0))).sqrt();
let beta: f64;
if c <= 0.0 {
beta = (-c + L) / b_min_a.powf(2.0);
} else {
beta = (delta.powf(2.0) - a.norm().powf(2.0)) / (c + L);
}
println!("beta = {}", beta);
p_dl = alpha_optimal * p_steepest + beta * p_gauss;
}
return p_dl;
}
#[derive(Debug)]
pub enum DoglegError {
DimensionMismatch,
SingularMatrix,
NumericalError,
AllocationError,
}
pub struct DoglegState {
n: usize,
p: usize,
dx_gn: DVector<f64>,
dx_sd: DVector<f64>,
norm_dgn: f64,
norm_dsd: f64,
norm_dinvg: f64,
norm_jdinv2g: f64,
workp: DVector<f64>,
workn: DVector<f64>,
gn_computed: bool,
}
impl DoglegState {
pub fn new(n: usize, p: usize) -> Result<Self, DoglegError> {
Ok(DoglegState {
n,
p,
dx_gn: DVector::zeros(p),
dx_sd: DVector::zeros(p),
norm_dgn: -1.0, norm_dsd: 0.0,
norm_dinvg: 0.0,
norm_jdinv2g: 0.0,
workp: DVector::zeros(p),
workn: DVector::zeros(n),
gn_computed: false,
})
}
pub fn preloop(
&mut self,
jacobian: &DMatrix<f64>,
residual: &DVector<f64>,
gradient: &DVector<f64>,
diag: &DVector<f64>,
) -> Result<(), DoglegError> {
if jacobian.nrows() != self.n || jacobian.ncols() != self.p {
return Err(DoglegError::DimensionMismatch);
}
if gradient.len() != self.p || diag.len() != self.p {
return Err(DoglegError::DimensionMismatch);
}
for i in 0..self.p {
if diag[i] == 0.0 {
return Err(DoglegError::SingularMatrix);
}
self.workp[i] = gradient[i] / diag[i];
}
self.norm_dinvg = self.workp.norm();
for i in 0..self.p {
self.workp[i] /= diag[i];
}
self.workn = jacobian * &self.workp;
self.norm_jdinv2g = self.workn.norm();
if self.norm_jdinv2g == 0.0 {
return Err(DoglegError::NumericalError);
}
let u = self.norm_dinvg / self.norm_jdinv2g;
let alpha = u * u;
self.dx_sd = -alpha * &self.workp;
self.norm_dsd = self.scaled_norm(&self.dx_sd, diag);
self.gn_computed = false;
self.norm_dgn = -1.0;
Ok(())
}
pub fn step(
&mut self,
jacobian: &DMatrix<f64>,
residual: &DVector<f64>,
diag: &DVector<f64>,
delta: f64,
) -> Result<DVector<f64>, DoglegError> {
if self.norm_dsd >= delta {
let dx = (delta / self.norm_dsd) * &self.dx_sd;
return Ok(dx);
}
if !self.gn_computed {
self.compute_gauss_newton_step(jacobian, residual)?;
self.norm_dgn = self.scaled_norm(&self.dx_gn, diag);
self.gn_computed = true;
}
if self.norm_dgn <= delta {
println!("gauss newton step norm <= delta: return gauss newton step");
return Ok(self.dx_gn.clone());
}
let beta = self.compute_dogleg_beta(1.0, delta, diag)?;
println!("beta = {}", beta);
let dx_diff = &self.dx_gn - &self.dx_sd;
let dx = &self.dx_sd + beta * dx_diff;
Ok(dx)
}
pub fn double_step(
&mut self,
jacobian: &DMatrix<f64>,
residual: &DVector<f64>,
gradient: &DVector<f64>,
diag: &DVector<f64>,
delta: f64,
) -> Result<DVector<f64>, DoglegError> {
const ALPHA_FAC: f64 = 0.8;
if self.norm_dsd >= delta {
let dx = (delta / self.norm_dsd) * &self.dx_sd;
return Ok(dx);
}
if !self.gn_computed {
self.compute_gauss_newton_step(jacobian, residual)?;
self.norm_dgn = self.scaled_norm(&self.dx_gn, diag);
self.gn_computed = true;
}
if self.norm_dgn <= delta {
return Ok(self.dx_gn.clone());
}
let v_ratio = self.norm_dinvg / self.norm_jdinv2g;
let u = v_ratio * v_ratio;
let v = gradient.dot(&self.dx_gn);
let c = u * (self.norm_dinvg / v.abs()) * self.norm_dinvg;
let t = 1.0 - ALPHA_FAC * (1.0 - c);
if t * self.norm_dgn <= delta {
let dx = (delta / self.norm_dgn) * &self.dx_gn;
return Ok(dx);
} else {
let beta = self.compute_dogleg_beta(t, delta, diag)?;
let t_dx_gn = t * &self.dx_gn;
let dx_diff = t_dx_gn - &self.dx_sd;
let dx = &self.dx_sd + beta * dx_diff;
return Ok(dx);
}
}
pub fn predicted_reduction(
&mut self,
jacobian: &DMatrix<f64>,
residual: &DVector<f64>,
gradient: &DVector<f64>,
dx: &DVector<f64>,
) -> Result<f64, DoglegError> {
let linear_term = -gradient.dot(dx);
self.workn = jacobian * dx;
let quadratic_term = -0.5 * dx.dot(&(jacobian.transpose() * &self.workn));
Ok(linear_term + quadratic_term)
}
fn compute_gauss_newton_step(
&mut self,
jacobian: &DMatrix<f64>,
residual: &DVector<f64>,
) -> Result<(), DoglegError> {
let jtj = jacobian.transpose() * jacobian;
let jtf = jacobian.transpose() * residual;
let rhs = -jtf;
match jtj.lu().solve(&rhs) {
Some(solution) => {
self.dx_gn = solution;
Ok(())
}
None => Err(DoglegError::SingularMatrix),
}
}
fn compute_dogleg_beta(
&mut self,
t: f64,
delta: f64,
diag: &DVector<f64>,
) -> Result<f64, DoglegError> {
self.workp = t * &self.dx_gn - &self.dx_sd;
let a = self.scaled_norm(&self.workp, diag).powi(2);
for i in 0..self.p {
self.workp[i] *= diag[i] * diag[i];
}
let b = 2.0 * self.dx_sd.dot(&self.workp);
let c = (self.norm_dsd + delta) * (self.norm_dsd - delta);
let discriminant = b * b - 4.0 * a * c;
if discriminant < 0.0 {
return Err(DoglegError::NumericalError);
}
let beta = if b > 0.0 {
(-2.0 * c) / (b + discriminant.sqrt())
} else {
(-b + discriminant.sqrt()) / (2.0 * a)
};
Ok(beta.max(0.0).min(1.0))
}
fn scaled_norm(&self, x: &DVector<f64>, diag: &DVector<f64>) -> f64 {
let mut sum = 0.0;
for i in 0..x.len() {
let scaled = diag[i] * x[i];
sum += scaled * scaled;
}
sum.sqrt()
}
}
pub struct DoglegSolver {
state: DoglegState,
use_double_dogleg: bool,
}
impl DoglegSolver {
pub fn new(n: usize, p: usize, use_double_dogleg: bool) -> Result<Self, DoglegError> {
Ok(DoglegSolver {
state: DoglegState::new(n, p)?,
use_double_dogleg,
})
}
pub fn initialize(
&mut self,
jacobian: &DMatrix<f64>,
residual: &DVector<f64>,
gradient: &DVector<f64>,
diag: &DVector<f64>,
) -> Result<(), DoglegError> {
self.state.preloop(jacobian, residual, gradient, diag)
}
pub fn solve_step(
&mut self,
jacobian: &DMatrix<f64>,
residual: &DVector<f64>,
gradient: &DVector<f64>,
diag: &DVector<f64>,
delta: f64,
) -> Result<DVector<f64>, DoglegError> {
if self.use_double_dogleg {
self.state
.double_step(jacobian, residual, gradient, diag, delta)
} else {
self.state.step(jacobian, residual, diag, delta)
}
}
pub fn predicted_reduction(
&mut self,
jacobian: &DMatrix<f64>,
residual: &DVector<f64>,
gradient: &DVector<f64>,
dx: &DVector<f64>,
) -> Result<f64, DoglegError> {
self.state
.predicted_reduction(jacobian, residual, gradient, dx)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use nalgebra::{DMatrix, DVector};
fn create_test_problem() -> (DMatrix<f64>, DVector<f64>, DVector<f64>, DVector<f64>) {
let jacobian = DMatrix::from_row_slice(2, 2, &[2.0, 1.0, 1.0, 3.0]);
let residual = DVector::from_vec(vec![1.0, 2.0]);
let gradient = jacobian.transpose() * &residual; let diag = DVector::from_vec(vec![1.0, 1.0]);
(jacobian, residual, gradient, diag)
}
fn create_complex_test_problem() -> (DMatrix<f64>, DVector<f64>, DVector<f64>, DVector<f64>) {
let jacobian = DMatrix::from_row_slice(3, 2, &[1.0, 2.0, 3.0, 1.0, 2.0, 2.0]);
let residual = DVector::from_vec(vec![1.0, -1.0, 0.5]);
let gradient = jacobian.transpose() * &residual;
let diag = DVector::from_vec(vec![2.0, 1.5]);
(jacobian, residual, gradient, diag)
}
#[test]
fn test_dogleg_state_creation() {
let state = DoglegState::new(3, 2);
assert!(state.is_ok());
let state = state.unwrap();
assert_eq!(state.n, 3);
assert_eq!(state.p, 2);
assert_eq!(state.dx_gn.len(), 2);
assert_eq!(state.dx_sd.len(), 2);
assert_eq!(state.workp.len(), 2);
assert_eq!(state.workn.len(), 3);
assert!(!state.gn_computed);
assert_eq!(state.norm_dgn, -1.0);
}
#[test]
fn test_dogleg_state_preloop() {
let (jacobian, residual, gradient, diag) = create_test_problem();
let mut state = DoglegState::new(2, 2).unwrap();
let result = state.preloop(&jacobian, &residual, &gradient, &diag);
assert!(result.is_ok());
assert!(state.norm_dsd > 0.0);
assert!(state.norm_dinvg > 0.0);
assert!(state.norm_jdinv2g > 0.0);
let expected_direction = -gradient.component_div(&diag).component_div(&diag);
let actual_direction = state.dx_sd.normalize();
let expected_direction_norm = expected_direction.normalize();
let dot_product = actual_direction.dot(&expected_direction_norm).abs();
assert!(dot_product > 0.9, "Steepest descent direction incorrect");
}
#[test]
fn test_dogleg_state_preloop_dimension_mismatch() {
let (jacobian, residual, gradient, diag) = create_test_problem();
let mut state = DoglegState::new(3, 2).unwrap();
let result = state.preloop(&jacobian, &residual, &gradient, &diag);
assert!(matches!(result, Err(DoglegError::DimensionMismatch)));
}
#[test]
fn test_dogleg_state_preloop_singular_matrix() {
let jacobian = DMatrix::from_row_slice(2, 2, &[1.0, 2.0, 3.0, 1.0]);
let residual = DVector::from_vec(vec![1.0, 2.0]);
let gradient = jacobian.transpose() * &residual;
let diag = DVector::from_vec(vec![0.0, 1.0]);
let mut state = DoglegState::new(2, 2).unwrap();
let result = state.preloop(&jacobian, &residual, &gradient, &diag);
assert!(matches!(result, Err(DoglegError::SingularMatrix)));
}
#[test]
fn test_dogleg_step_steepest_descent_case() {
let (jacobian, residual, gradient, diag) = create_test_problem();
let mut state = DoglegState::new(2, 2).unwrap();
state
.preloop(&jacobian, &residual, &gradient, &diag)
.unwrap();
let delta = state.norm_dsd * 0.5;
let step = state.step(&jacobian, &residual, &diag, delta).unwrap();
let scaled_norm = state.scaled_norm(&step, &diag);
assert_relative_eq!(scaled_norm, delta, epsilon = 1e-10);
let step_direction = step.normalize();
let sd_direction = state.dx_sd.normalize();
let dot_product = step_direction.dot(&sd_direction);
assert!(
dot_product > 0.99,
"Step should be in steepest descent direction"
);
}
#[test]
fn test_dogleg_step_gauss_newton_case() {
let (jacobian, residual, gradient, diag) = create_test_problem();
let mut state = DoglegState::new(2, 2).unwrap();
state
.preloop(&jacobian, &residual, &gradient, &diag)
.unwrap();
let delta = 100.0;
let step = state.step(&jacobian, &residual, &diag, delta).unwrap();
assert!(state.gn_computed);
let diff = (&step - &state.dx_gn).norm();
assert!(
diff < 1e-10,
"Step should be Gauss-Newton step for large trust region"
);
}
#[test]
fn test_dogleg_step_interpolation_case() {
let (jacobian, residual, gradient, diag) = create_complex_test_problem();
let mut state = DoglegState::new(3, 2).unwrap();
state
.preloop(&jacobian, &residual, &gradient, &diag)
.unwrap();
state
.compute_gauss_newton_step(&jacobian, &residual)
.unwrap();
state.norm_dgn = state.scaled_norm(&state.dx_gn, &diag);
state.gn_computed = true;
let delta = (state.norm_dsd + state.norm_dgn) * 0.5;
let step = state.step(&jacobian, &residual, &diag, delta).unwrap();
let scaled_norm = state.scaled_norm(&step, &diag);
assert_relative_eq!(scaled_norm, delta, epsilon = 1e-8);
let is_between_sd_gn = {
let step_minus_sd = &step - &state.dx_sd;
let gn_minus_sd = &state.dx_gn - &state.dx_sd;
let projection = step_minus_sd.dot(&gn_minus_sd) / gn_minus_sd.norm_squared();
projection >= 0.0 && projection <= 1.0
};
assert!(
is_between_sd_gn,
"Step should be on dogleg path between SD and GN"
);
}
#[test]
fn test_dogleg_solver_creation() {
let solver = DoglegSolver::new(3, 2, false);
assert!(solver.is_ok());
let solver = solver.unwrap();
assert_eq!(solver.state.n, 3);
assert_eq!(solver.state.p, 2);
assert!(!solver.use_double_dogleg);
let double_solver = DoglegSolver::new(3, 2, true);
assert!(double_solver.is_ok());
assert!(double_solver.unwrap().use_double_dogleg);
}
#[test]
fn test_dogleg_solver_initialize() {
let (jacobian, residual, gradient, diag) = create_test_problem();
let mut solver = DoglegSolver::new(2, 2, false).unwrap();
let result = solver.initialize(&jacobian, &residual, &gradient, &diag);
assert!(result.is_ok());
assert!(solver.state.norm_dsd > 0.0);
assert!(solver.state.norm_dinvg > 0.0);
}
#[test]
fn test_dogleg_solver_solve_step() {
let (jacobian, residual, gradient, diag) = create_test_problem();
let mut solver = DoglegSolver::new(2, 2, false).unwrap();
solver
.initialize(&jacobian, &residual, &gradient, &diag)
.unwrap();
let delta = 1.0;
let step = solver.solve_step(&jacobian, &residual, &gradient, &diag, delta);
assert!(step.is_ok());
let step = step.unwrap();
assert_eq!(step.len(), 2);
let scaled_norm = solver.state.scaled_norm(&step, &diag);
assert!(
scaled_norm <= delta + 1e-10,
"Step should satisfy trust region constraint"
);
}
#[test]
fn test_dogleg_solver_double_dogleg() {
let (jacobian, residual, gradient, diag) = create_complex_test_problem();
let mut solver = DoglegSolver::new(3, 2, true).unwrap();
solver
.initialize(&jacobian, &residual, &gradient, &diag)
.unwrap();
let delta = 0.5;
let step = solver.solve_step(&jacobian, &residual, &gradient, &diag, delta);
assert!(step.is_ok());
let step = step.unwrap();
let scaled_norm = solver.state.scaled_norm(&step, &diag);
assert!(
scaled_norm <= delta + 1e-10,
"Double dogleg step should satisfy trust region"
);
}
#[test]
fn test_dogleg_solver_predicted_reduction() {
let (jacobian, residual, gradient, diag) = create_test_problem();
let mut solver = DoglegSolver::new(2, 2, false).unwrap();
solver
.initialize(&jacobian, &residual, &gradient, &diag)
.unwrap();
let delta = 1.0;
let step = solver
.solve_step(&jacobian, &residual, &gradient, &diag, delta)
.unwrap();
let pred_reduction = solver.predicted_reduction(&jacobian, &residual, &gradient, &step);
assert!(pred_reduction.is_ok());
let pred_reduction = pred_reduction.unwrap();
assert!(
pred_reduction > 0.0,
"Predicted reduction should be positive for descent step"
);
}
#[test]
fn test_dogleg_beta_computation() {
let (jacobian, residual, gradient, diag) = create_test_problem();
let mut state = DoglegState::new(2, 2).unwrap();
state
.preloop(&jacobian, &residual, &gradient, &diag)
.unwrap();
state
.compute_gauss_newton_step(&jacobian, &residual)
.unwrap();
state.gn_computed = true;
let delta = (state.norm_dsd + 1.0) * 0.7; let beta = state.compute_dogleg_beta(1.0, delta, &diag);
assert!(beta.is_ok());
let beta = beta.unwrap();
assert!(beta >= 0.0 && beta <= 1.0, "Beta should be in [0,1]");
}
#[test]
fn test_problem_solve() {
let (jacobian, residual, gradient, diag) = create_test_problem();
let mut solver = DoglegSolver::new(2, 2, false).unwrap();
println!(
"residual {:?} ,\n gradient {:?} \n, diag {:?}",
residual, gradient, diag
);
solver
.initialize(&jacobian, &residual, &gradient, &diag)
.unwrap();
let delta = 100.0;
let step = solver.solve_step(&jacobian, &residual, &gradient, &diag, delta);
assert!(step.is_ok());
println!("Step: {}", step.unwrap());
let step1 = Powell_dogleg_method(jacobian, residual, diag, delta, "lu".to_owned());
println!("step1: {}", step1);
}
}