use crate::common::IntegrateFloat;
use crate::error::{IntegrateError, IntegrateResult};
use crate::ode::utils::jacobian::JacobianManager;
use crate::ode::utils::linear_solvers::{auto_solve_linear_system, LinearSolverType};
use scirs2_core::ndarray::Array1;
#[derive(Debug, Clone)]
pub struct NewtonParameters<F: IntegrateFloat> {
pub max_iterations: usize,
pub abs_tolerance: F,
pub rel_tolerance: F,
pub jacobian_update_freq: usize,
pub damping_factor: F,
pub min_damping: F,
pub reuse_jacobian: bool,
pub force_jacobian_init: bool,
}
impl<F: IntegrateFloat> Default for NewtonParameters<F> {
fn default() -> Self {
NewtonParameters {
max_iterations: 10,
abs_tolerance: F::from_f64(1e-10).expect("Operation failed"),
rel_tolerance: F::from_f64(1e-8).expect("Operation failed"),
jacobian_update_freq: 1,
damping_factor: F::one(),
min_damping: F::from_f64(0.1).expect("Operation failed"),
reuse_jacobian: true,
force_jacobian_init: false,
}
}
}
#[derive(Debug, Clone)]
pub struct NewtonResult<F: IntegrateFloat> {
pub solution: Array1<F>,
pub residual: Array1<F>,
pub iterations: usize,
pub converged: bool,
pub error_estimate: F,
pub func_evals: usize,
pub jac_evals: usize,
pub linear_solves: usize,
}
#[allow(clippy::explicit_counter_loop)]
#[allow(dead_code)]
pub fn newton_solve<F, Func>(
f: Func,
x0: Array1<F>,
jac_manager: &mut JacobianManager<F>,
params: NewtonParameters<F>,
) -> IntegrateResult<NewtonResult<F>>
where
F: IntegrateFloat + std::default::Default,
Func: Fn(&Array1<F>) -> Array1<F>,
{
let _n = x0.len();
let mut x = x0.clone();
let mut residual = f(&x);
let mut func_evals = 1;
let mut jac_evals = 0;
let mut linear_solves = 0;
let mut error = calculate_error(&residual, ¶ms);
if error <= params.abs_tolerance {
return Ok(NewtonResult {
solution: x,
residual,
iterations: 0,
converged: true,
error_estimate: error,
func_evals,
jac_evals,
linear_solves,
});
}
let dummy_t = F::zero();
for iter in 0..params.max_iterations {
if iter == 0 && params.force_jacobian_init {
jac_manager.update_jacobian(dummy_t, &x, &|_t, y| f(&y.to_owned()), None)?;
jac_evals += 1;
} else if iter > 0 && iter % params.jacobian_update_freq == 0 {
jac_manager.update_jacobian(dummy_t, &x, &|_t, y| f(&y.to_owned()), None)?;
jac_evals += 1;
} else if jac_manager.jacobian().is_none() {
jac_manager.update_jacobian(dummy_t, &x, &|_t, y| f(&y.to_owned()), None)?;
jac_evals += 1;
}
let jacobian = jac_manager.jacobian().expect("Operation failed");
let neg_residual = residual.clone() * F::from_f64(-1.0).expect("Operation failed");
let dx = auto_solve_linear_system(
&jacobian.view(),
&neg_residual.view(),
LinearSolverType::Direct,
)?;
linear_solves += 1;
let mut damping = params.damping_factor;
let mut x_new = x.clone() + &dx * damping;
let mut residual_new = f(&x_new);
func_evals += 1;
let mut error_new = calculate_error(&residual_new, ¶ms);
if error_new > error && damping > params.min_damping {
let mut backtrack_count = 0;
while error_new > error && damping > params.min_damping && backtrack_count < 5 {
damping *= F::from_f64(0.5).expect("Operation failed");
x_new = x.clone() + &dx * damping;
residual_new = f(&x_new);
func_evals += 1;
error_new = calculate_error(&residual_new, ¶ms);
backtrack_count += 1;
}
}
x = x_new;
residual = residual_new;
error = error_new;
if error <= params.abs_tolerance || (error <= params.rel_tolerance * calculate_norm(&x)) {
return Ok(NewtonResult {
solution: x,
residual,
iterations: iter + 1,
converged: true,
error_estimate: error,
func_evals,
jac_evals,
linear_solves,
});
}
}
Err(IntegrateError::ConvergenceError(format!(
"Newton's method failed to converge in {} iterations (error = {:.2e})",
params.max_iterations, error,
)))
}
#[allow(dead_code)]
pub fn modified_newton_solve<F, Func>(
f: Func,
x0: Array1<F>,
jac_manager: &mut JacobianManager<F>,
params: NewtonParameters<F>,
) -> IntegrateResult<NewtonResult<F>>
where
F: IntegrateFloat + std::default::Default,
Func: Fn(&Array1<F>) -> Array1<F>,
{
let modified_params = NewtonParameters {
jacobian_update_freq: params.max_iterations + 1, ..params
};
newton_solve(f, x0, jac_manager, modified_params)
}
#[allow(dead_code)]
fn calculate_error<F: IntegrateFloat>(residual: &Array1<F>, params: &NewtonParameters<F>) -> F {
let mut max_abs = F::zero();
for &r in residual.iter() {
max_abs = max_abs.max(r.abs());
}
max_abs
}
#[allow(dead_code)]
fn calculate_norm<F: IntegrateFloat>(x: &Array1<F>) -> F {
let mut max_abs = F::zero();
for &val in x.iter() {
max_abs = max_abs.max(val.abs());
}
max_abs
}