use std::{cmp::min_by, ops::Mul};
use ndarray::{Array1, Array2, LinalgScalar, ScalarOperand, ShapeBuilder};
use ndarray_linalg::Scalar;
use crate::dynamics::Dynamics;
pub trait Integrator<T, D, U>
where
T: LinalgScalar,
D: Dynamics<T>,
U: Fn(T, &Array1<T>) -> Array1<T>,
{
fn step(t0: T, tf: T, x0: &Array1<T>, dynamics: &D, input: &U) -> Array1<T>;
fn simulate(times: &Vec<T>, x0: &Array1<T>, dynamics: &D, input: &U) -> Array2<T> {
let mut history = Array2::zeros((x0.len(), times.len()).f());
history.column_mut(0).assign(x0);
let mut x_curr = x0.clone();
for (i, &t0) in times.iter().enumerate() {
if i == times.len() - 1 {
break;
};
let tf = times[i + 1];
let x_next = Self::step(t0, tf, &x_curr, dynamics, input);
history.column_mut(i + 1).assign(&x_next);
x_curr = x_next;
}
history
}
}
pub struct EulerIntegration;
impl<T, D, U> Integrator<T, D, U> for EulerIntegration
where
T: LinalgScalar + ScalarOperand,
D: Dynamics<T>,
U: Fn(T, &Array1<T>) -> Array1<T>,
{
fn step(t0: T, tf: T, x0: &Array1<T>, dynamics: &D, input: &U) -> Array1<T> {
let delta_t = tf - t0;
let u = input(t0, x0);
x0 + dynamics.dynamics(t0, x0, &u) * delta_t
}
}
pub struct RK4;
impl<T, D, U> Integrator<T, D, U> for RK4
where
T: LinalgScalar + ScalarOperand + From<f64>,
D: Dynamics<T>,
U: Fn(T, &Array1<T>) -> Array1<T>,
{
fn step(t0: T, tf: T, x0: &Array1<T>, dynamics: &D, input: &U) -> Array1<T> {
let delta_t = tf - t0;
let half = T::from(0.5);
let t_half = delta_t * half;
let k1 = &dynamics.dynamics(t0, x0, &input(t0, x0));
let t_mid = t0 + t_half;
let x_mid_1 = x0 + &k1.mul(t_half);
let k2 = &dynamics.dynamics(t_mid, &x_mid_1, &input(t_mid, &x_mid_1));
let x_mid_2 = x0 + &k2.mul(t_half);
let k3 = &dynamics.dynamics(t_mid, &x_mid_2, &input(t_mid, &x_mid_2));
let x_end_pre = x0 + &k3.mul(delta_t);
let k4 = &dynamics.dynamics(tf, &x_end_pre, &input(tf, &x_end_pre));
x0 + (k1 + k2.mul(T::from(2.0)) + k3.mul(T::from(2.0)) + k4)
.mul(delta_t.mul(T::from(1. / 6.)))
}
}
#[derive(Clone, Copy, Debug)]
pub struct AdaptiveRK2 {
tol: f64,
}
impl AdaptiveRK2 {
pub fn new(tol: f64) -> Self {
AdaptiveRK2 { tol }
}
pub fn adaptive_step<T, D, U>(
&self,
t0: T,
tf: T,
x0: &Array1<T>,
dynamics: &D,
input: &U,
) -> Array1<T>
where
T: LinalgScalar + Scalar + ScalarOperand + PartialOrd,
D: Dynamics<T>,
U: Fn(T, &Array1<T>) -> Array1<T>,
{
let mut t_curr = t0;
let mut x_curr = x0.clone();
let mut step = T::from(0.5 * self.tol.powf(0.5)).unwrap();
if step > (tf - t0) {
step = tf - t0;
}
let mut i = 0;
let mut j = 0;
while t_curr < tf {
if t_curr + step == t_curr {
panic!("The step size dropped below machine precision.");
}
let k1 = &dynamics.dynamics(t_curr, &x_curr, &input(t_curr, &x_curr));
let x_tmp = &x_curr + k1 * step;
let k2 = &dynamics.dynamics(t_curr + step, &x_tmp, &input(t_curr + step, &x_tmp));
let err = (k2 - k1) * step * T::from(0.5).unwrap();
let err_inf_norm: T = T::from_real(
err.iter()
.map(|v| v.abs())
.max_by(|&a, &b| a.partial_cmp(&b).unwrap())
.unwrap(),
);
let x_inf_norm = T::from_real(
x_curr
.iter()
.map(|v| v.abs())
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap(),
);
let max_err = T::from(self.tol).unwrap() * (T::one() + x_inf_norm);
if err_inf_norm < max_err {
t_curr += step;
x_curr = &x_curr + (k1 + k2) * step * T::from(0.5).unwrap();
j += 1;
}
let mut multiplier = T::from(0.9).unwrap() * (max_err / err_inf_norm).powf(T::from(0.5).unwrap().re());
multiplier = min_by(multiplier, T::from(4.0).unwrap(), |a, b| {
a.partial_cmp(b).unwrap()
});
step = min_by(multiplier * step, tf - t_curr, |a, b| {
a.partial_cmp(b).unwrap()
});
i += 1;
}
println!("Adaptive step used {i} iterations with {j} steps");
x_curr.into_owned()
}
}
impl Default for AdaptiveRK2 {
fn default() -> Self {
AdaptiveRK2 { tol: 1e-8 }
}
}
impl<T, D, U> Integrator<T, D, U> for AdaptiveRK2
where
T: LinalgScalar + ScalarOperand + ndarray_linalg::Scalar + std::cmp::PartialOrd,
D: Dynamics<T>,
U: Fn(T, &Array1<T>) -> Array1<T>,
{
fn step(t0: T, tf: T, x0: &Array1<T>, dynamics: &D, input: &U) -> Array1<T> {
let integrator = AdaptiveRK2::default();
integrator.adaptive_step(t0, tf, x0, dynamics, input)
}
}
#[cfg(test)]
mod tests {
use ndarray::{array, s, Array};
use super::*;
use crate::dynamics::LtiDynamics;
#[test]
fn test_adaptive_step_harmonic_oscillator() {
let lti = LtiDynamics::new(array![[0., 1.], [-1., 0.]], array![[0.], [1.]]);
let control = |_t, _x: &_| array![0.];
let x_0 = array![2., 0.];
let adapt_int = AdaptiveRK2::default();
let start = std::time::Instant::now();
let step = adapt_int.adaptive_step(0., 1.0, &x_0, <i, &control);
let duration = start.elapsed().as_secs_f64();
println!("Step time: {duration:.5}");
assert!(step.abs_diff_eq(&array![1.080604608324292, -1.682941965891026], 1e-8));
}
#[test]
fn test_adaptive_step_stable() {
let lti = LtiDynamics::new(array![[-1., 1.], [0., -1.]], array![[0.], [1.]]);
let control = |_t, _x: &_| array![0.];
let x_0 = array![2., 0.];
let adapt_int = AdaptiveRK2::default();
let start = std::time::Instant::now();
let step = adapt_int.adaptive_step(0., 1.0, &x_0, <i, &control);
let duration = start.elapsed().as_secs_f64();
println!("Step time: {duration:.5}");
assert!(step.abs_diff_eq(&array![0.735758884760948, 0.], 1e-8));
}
#[test]
fn test_adaptive_step_unstable() {
let lti = LtiDynamics::new(array![[1., 1.], [0., 2.]], array![[0.], [1.]]);
let control = |_t, _x: &_| array![0.];
let x_0 = array![2., 0.];
let adapt_int = AdaptiveRK2::default();
let start = std::time::Instant::now();
let step = adapt_int.adaptive_step(0., 1.0, &x_0, <i, &control);
let duration = start.elapsed().as_secs_f64();
println!("Step time: {duration:.5}");
assert!(step.abs_diff_eq(&array![5.436563669594182, 0.], 1e-7));
}
#[test]
fn test_rk4_harmonic_oscillator() {
let lti = LtiDynamics::new(array![[0., 1.], [-1., 0.]], array![[0.], [1.]]);
let control = |_t, _x: &_| array![0.];
let x_0 = array![2., 0.];
let start = std::time::Instant::now();
let times = Array::linspace(0., 1., 100).into_iter().collect();
let states = RK4::simulate(×, &x_0, <i, &control);
let duration = start.elapsed().as_secs_f64();
println!("Step time: {duration:.5}");
assert!(states.slice(s![.., -1]).abs_diff_eq(&array![1.080604608324292, -1.682941965891026], 1e-8));
}
#[test]
fn test_rk4_stable() {
let lti = LtiDynamics::new(array![[-1., 1.], [0., -1.]], array![[0.], [1.]]);
let control = |_t, _x: &_| array![0.];
let x_0 = array![2., 0.];
let start = std::time::Instant::now();
let times = Array::linspace(0., 1., 100).into_iter().collect();
let states = RK4::simulate(×, &x_0, <i, &control);
let duration = start.elapsed().as_secs_f64();
println!("Step time: {duration:.5}");
assert!(states.slice(s![.., -1]).abs_diff_eq(&array![0.735758884760948, 0.], 1e-8));
}
#[test]
fn test_rk4_unstable() {
let lti = LtiDynamics::new(array![[1., 1.], [0., 2.]], array![[0.], [1.]]);
let control = |_t, _x: &_| array![0.];
let x_0 = array![2., 0.];
let start = std::time::Instant::now();
let times = Array::linspace(0., 1., 100).into_iter().collect();
let states = RK4::simulate(×, &x_0, <i, &control);
let duration = start.elapsed().as_secs_f64();
println!("Step time: {duration:.5}");
assert!(states.slice(s![.., -1]).abs_diff_eq(&array![5.436563669594182, 0.], 1e-7));
}
}