use crate::StrError;
use crate::{OdeSolverTrait, Params, System, Workspace};
use russell_lab::{vec_copy, vec_rms_scaled, vec_update, Vector};
use russell_sparse::{numerical_jacobian, CooMatrix, LinSolver};
pub(crate) struct EulerBackward<'a, A> {
params: Params,
system: System<'a, A>,
k: Vector,
w: Vector,
r: Vector,
dy: Vector,
kk: CooMatrix,
solver: LinSolver<'a>,
}
impl<'a, A> EulerBackward<'a, A> {
pub fn new(params: Params, system: System<'a, A>) -> Self {
let ndim = system.ndim;
let jac_nnz = if params.newton.use_numerical_jacobian {
ndim * ndim
} else {
system.jac_nnz
};
let nnz = jac_nnz + ndim; let sym = system.symmetric;
EulerBackward {
params,
system,
k: Vector::new(ndim),
w: Vector::new(ndim),
r: Vector::new(ndim),
dy: Vector::new(ndim),
kk: CooMatrix::new(ndim, ndim, nnz, sym).unwrap(),
solver: LinSolver::new(params.newton.genie).unwrap(),
}
}
}
impl<'a, A> OdeSolverTrait<A> for EulerBackward<'a, A> {
fn enable_dense_output(&mut self) -> Result<(), StrError> {
Err("dense output is not available for the BwEuler method")
}
fn step(&mut self, work: &mut Workspace, x: f64, y: &Vector, h: f64, args: &mut A) -> Result<(), StrError> {
let traditional_newton = !self.params.bweuler.use_modified_newton;
let ndim = self.system.ndim;
let x_new = x + h;
let y_new = &mut self.w;
vec_copy(y_new, &y).unwrap();
let mut success = false;
work.stats.n_iterations = 0;
for _ in 0..self.params.newton.n_iteration_max {
work.stats.n_iterations += 1;
work.stats.n_function += 1;
(self.system.function)(&mut self.k, x_new, y_new, args)?;
for i in 0..ndim {
self.r[i] = y_new[i] - y[i] - h * self.k[i];
}
let r_norm = vec_rms_scaled(&self.r, y, self.params.tol.abs, self.params.tol.rel);
if r_norm < self.params.tol.newton {
success = true;
break;
}
if traditional_newton || work.stats.n_accepted == 0 {
work.stats.sw_jacobian.reset();
work.stats.n_jacobian += 1;
let kk = &mut self.kk;
if self.params.newton.use_numerical_jacobian || self.system.jacobian.is_none() {
work.stats.n_function += ndim;
let w1 = &mut self.k; let w2 = &mut self.dy; numerical_jacobian(kk, h, x_new, y_new, w1, w2, args, self.system.function.as_ref())?;
} else {
(self.system.jacobian.as_ref().unwrap())(kk, h, x_new, y_new, args)?;
}
for i in 0..self.system.ndim {
kk.put(i, i, -1.0).unwrap();
}
work.stats.stop_sw_jacobian();
work.stats.sw_factor.reset();
work.stats.n_factor += 1;
self.solver.actual.factorize(kk, self.params.newton.lin_sol_params)?;
work.stats.stop_sw_factor();
}
work.stats.sw_lin_sol.reset();
work.stats.n_lin_sol += 1;
self.solver.actual.solve(&mut self.dy, &self.r, false)?;
work.stats.stop_sw_lin_sol();
vec_update(y_new, 1.0, &self.dy).unwrap(); }
work.stats.update_n_iterations_max();
if !success {
return Err("Newton-Raphson method did not complete successfully");
}
Ok(())
}
fn accept(
&mut self,
_work: &mut Workspace,
x: &mut f64,
y: &mut Vector,
h: f64,
_args: &mut A,
) -> Result<(), StrError> {
*x += h;
vec_copy(y, &self.w).unwrap();
Ok(())
}
fn reject(&mut self, _work: &mut Workspace, _h: f64) {}
fn dense_output(&self, _y_out: &mut Vector, _x_out: f64, _x: f64, _y: &Vector, _h: f64) {}
fn update_params(&mut self, params: Params) {
self.params = params;
}
}
#[cfg(test)]
mod tests {
use super::EulerBackward;
use crate::{Method, OdeSolverTrait, Params, Samples, System, Workspace};
use russell_lab::{array_approx_eq, Vector};
use russell_sparse::Sym;
const XX_MATH: [f64; 6] = [0.0, 0.4, 0.8, 1.2, 1.6, 2.0];
const YY0_MATH: [f64; 6] = [
2.0,
1.314285714285715,
1.350204081632653,
1.572431486880467,
1.861908204914619,
2.186254432081871,
];
const YY1_MATH: [f64; 6] = [
-10.0,
-1.714285714285714,
0.0897959183673469,
0.5555685131195336,
0.723691795085381,
0.810865567918129,
];
const ERR_Y0_MATH: [f64; 6] = [
0.0,
0.2256500293613408,
0.100539654887529,
0.07123113075591125,
0.06001157438478888,
0.05091914678410436,
];
const ERR_Y1_MATH: [f64; 6] = [
0.0,
1.860809279362733,
0.4575204912364065,
0.1431758328447311,
0.07441056156821646,
0.05379912823372179,
];
#[test]
fn euler_backward_works() {
let (system, x0, y0, mut args, y_fn_x) = Samples::kreyszig_ex4_page920();
let ndim = system.ndim;
let params = Params::new(Method::BwEuler);
let mut solver = EulerBackward::new(params, system);
let mut work = Workspace::new(Method::BwEuler);
assert_eq!(
solver.enable_dense_output().err(),
Some("dense output is not available for the BwEuler method")
);
let h = 0.4;
let mut x = x0;
let mut y = y0.clone();
let mut xx = vec![x];
let mut yy0_num = vec![y[0]];
let mut yy1_num = vec![y[1]];
let mut y_ana = Vector::new(ndim);
y_fn_x(&mut y_ana, x, &mut args);
let mut err_y0 = vec![f64::abs(yy0_num[0] - y_ana[0])];
let mut err_y1 = vec![f64::abs(yy1_num[0] - y_ana[1])];
for n in 0..5 {
solver.step(&mut work, x, &y, h, &mut args).unwrap();
assert_eq!(work.stats.n_iterations, 2);
assert_eq!(work.stats.n_function, (n + 1) * 2);
assert_eq!(work.stats.n_jacobian, (n + 1));
solver.accept(&mut work, &mut x, &mut y, h, &mut args).unwrap();
xx.push(x);
yy0_num.push(y[0]);
yy1_num.push(y[1]);
y_fn_x(&mut y_ana, x, &mut args);
err_y0.push(f64::abs(yy0_num.last().unwrap() - y_ana[0]));
err_y1.push(f64::abs(yy1_num.last().unwrap() - y_ana[1]));
}
array_approx_eq(&xx, &XX_MATH, 1e-15);
array_approx_eq(&yy0_num, &YY0_MATH, 1e-15);
array_approx_eq(&yy1_num, &YY1_MATH, 1e-14);
array_approx_eq(&err_y0, &ERR_Y0_MATH, 1e-15);
array_approx_eq(&err_y1, &ERR_Y1_MATH, 1e-14);
}
#[test]
fn euler_backward_works_num_jacobian() {
let (system, x0, y0, mut args, y_fn_x) = Samples::kreyszig_ex4_page920();
let ndim = system.ndim;
let mut params = Params::new(Method::BwEuler);
params.newton.use_numerical_jacobian = true;
let mut solver = EulerBackward::new(params, system);
let mut work = Workspace::new(Method::BwEuler);
let h = 0.4;
let mut x = x0;
let mut y = y0.clone();
let mut xx = vec![x];
let mut yy0_num = vec![y[0]];
let mut yy1_num = vec![y[1]];
let mut y_ana = Vector::new(ndim);
y_fn_x(&mut y_ana, x, &mut args);
let mut err_y0 = vec![f64::abs(yy0_num[0] - y_ana[0])];
let mut err_y1 = vec![f64::abs(yy1_num[0] - y_ana[1])];
for n in 0..5 {
solver.step(&mut work, x, &y, h, &mut args).unwrap();
assert_eq!(work.stats.n_iterations, 2);
assert_eq!(work.stats.n_function, (n + 1) * 2 * ndim);
assert_eq!(work.stats.n_jacobian, (n + 1));
work.stats.n_accepted += 1; solver.accept(&mut work, &mut x, &mut y, h, &mut args).unwrap();
xx.push(x);
yy0_num.push(y[0]);
yy1_num.push(y[1]);
y_fn_x(&mut y_ana, x, &mut args);
err_y0.push(f64::abs(yy0_num.last().unwrap() - y_ana[0]));
err_y1.push(f64::abs(yy1_num.last().unwrap() - y_ana[1]));
}
array_approx_eq(&xx, &XX_MATH, 1e-15);
array_approx_eq(&yy0_num, &YY0_MATH, 1e-7);
array_approx_eq(&yy1_num, &YY1_MATH, 1e-6);
array_approx_eq(&err_y0, &ERR_Y0_MATH, 1e-7);
array_approx_eq(&err_y1, &ERR_Y1_MATH, 1e-6);
}
#[test]
fn euler_backward_captures_failed_iterations() {
let mut params = Params::new(Method::BwEuler);
let (system, x0, y0, mut args, _) = Samples::kreyszig_ex4_page920();
params.newton.n_iteration_max = 0;
let mut solver = EulerBackward::new(params, system);
let mut work = Workspace::new(Method::BwEuler);
assert_eq!(
solver.step(&mut work, x0, &y0, 0.1, &mut args).err(),
Some("Newton-Raphson method did not complete successfully")
);
}
#[test]
fn euler_backward_handles_errors() {
struct Args {
count_f: usize,
}
let mut system = System::new(1, |f, _, _, args: &mut Args| {
f[0] = 1.0;
args.count_f += 1;
if args.count_f == 1 {
Err("f: stop (count = 1)")
} else if args.count_f == 4 {
Err("f: stop (count = 4; num-jacobian)")
} else {
Ok(())
}
});
system
.set_jacobian(None, Sym::No, |jj, alpha, _x, _y, _args: &mut Args| {
jj.reset();
jj.put(0, 0, alpha * (0.0)).unwrap();
Err("jj: stop")
})
.unwrap();
let params = Params::new(Method::BwEuler);
let mut solver = EulerBackward::new(params, system);
let mut work = Workspace::new(Method::BwEuler);
let x = 0.0;
let y = Vector::from(&[0.0]);
let h = 0.1;
let mut args = Args { count_f: 0 };
assert_eq!(
solver.step(&mut work, x, &y, h, &mut args).err(),
Some("f: stop (count = 1)")
);
assert_eq!(solver.step(&mut work, x, &y, h, &mut args).err(), Some("jj: stop"));
solver.params.newton.use_numerical_jacobian = true;
assert_eq!(
solver.step(&mut work, x, &y, h, &mut args).err(),
Some("f: stop (count = 4; num-jacobian)")
);
let mut y_out = Vector::new(1);
let x_out = 0.1;
solver.reject(&mut work, h);
solver.dense_output(&mut y_out, x_out, x, &y, h);
solver.update_params(params);
}
}