use nalgebra::{DMatrix, DVector, SMatrix, SVector};
use crate::integrators::butcher_tableau::{ButcherTableau, RK4_TABLEAU};
use crate::integrators::config::IntegratorConfig;
use crate::integrators::traits::{
DControlInput, DIntegrator, DIntegratorConstructor, DIntegratorStepResult, DSensitivity,
DStateDynamics, DVariationalMatrix, SControlInput, SIntegrator, SIntegratorConstructor,
SIntegratorStepResult, SSensitivity, SStateDynamics, SVariationalMatrix, get_step_size,
};
pub struct RK4SIntegrator<const S: usize, const P: usize> {
f: SStateDynamics<S, P>,
varmat: SVariationalMatrix<S, P>,
sensmat: SSensitivity<S, P>,
control: SControlInput<S, P>,
bt: ButcherTableau<4>,
config: IntegratorConfig,
}
impl<const S: usize, const P: usize> RK4SIntegrator<S, P> {
fn step_internal(
&self,
t: f64,
state: SVector<f64, S>,
phi: Option<SMatrix<f64, S, S>>,
sens: Option<SMatrix<f64, S, P>>,
params: Option<&SVector<f64, P>>,
dt: f64,
) -> SIntegratorStepResult<S, P> {
let compute_phi = phi.is_some();
let compute_sens = sens.is_some();
let mut k = SMatrix::<f64, S, 4>::zeros();
let mut k_phi = [SMatrix::<f64, S, S>::zeros(); 4];
let mut k_sens = [SMatrix::<f64, S, P>::zeros(); 4];
for i in 0..4 {
let mut ksum = SVector::<f64, S>::zeros();
let mut k_phi_sum = SMatrix::<f64, S, S>::zeros();
let mut k_sens_sum = SMatrix::<f64, S, P>::zeros();
for j in 0..i {
ksum += self.bt.a[(i, j)] * k.column(j);
if compute_phi {
k_phi_sum += self.bt.a[(i, j)] * k_phi[j];
}
if compute_sens {
k_sens_sum += self.bt.a[(i, j)] * k_sens[j];
}
}
let state_i = state + dt * ksum;
let t_i = t + self.bt.c[i] * dt;
let mut k_i = (self.f)(t_i, &state_i, params);
if let Some(ref ctrl) = self.control {
k_i += ctrl(t_i, &state_i, params);
}
k.set_column(i, &k_i);
if compute_phi || compute_sens {
let a_i = self
.varmat
.as_ref()
.expect("varmat required for step_with_varmat or step_with_sensmat")
.compute(t_i, &state_i, params);
if compute_phi {
k_phi[i] = a_i * (phi.unwrap() + dt * k_phi_sum);
}
if compute_sens {
let b_i = self
.sensmat
.as_ref()
.expect("sensmat required for step_with_sensmat")
.compute(t_i, &state_i, params.unwrap());
k_sens[i] = a_i * (sens.unwrap() + dt * k_sens_sum) + b_i;
}
}
}
let mut state_update = SVector::<f64, S>::zeros();
let mut phi_update = SMatrix::<f64, S, S>::zeros();
let mut sens_update = SMatrix::<f64, S, P>::zeros();
for i in 0..4 {
state_update += dt * self.bt.b[i] * k.column(i);
if compute_phi {
phi_update += dt * self.bt.b[i] * k_phi[i];
}
if compute_sens {
sens_update += dt * self.bt.b[i] * k_sens[i];
}
}
SIntegratorStepResult {
state: state + state_update,
phi: phi.map(|p| p + phi_update),
sens: sens.map(|s| s + sens_update),
dt_used: dt,
error_estimate: None,
dt_next: dt,
}
}
}
impl<const S: usize, const P: usize> SIntegrator<S, P> for RK4SIntegrator<S, P> {
fn config(&self) -> &IntegratorConfig {
&self.config
}
fn step(
&self,
t: f64,
state: SVector<f64, S>,
params: Option<&SVector<f64, P>>,
dt: Option<f64>,
) -> SIntegratorStepResult<S, P> {
let dt = get_step_size(dt, &self.config);
self.step_internal(t, state, None, None, params, dt)
}
fn step_with_varmat(
&self,
t: f64,
state: SVector<f64, S>,
params: Option<&SVector<f64, P>>,
phi: SMatrix<f64, S, S>,
dt: Option<f64>,
) -> SIntegratorStepResult<S, P> {
let dt = get_step_size(dt, &self.config);
self.step_internal(t, state, Some(phi), None, params, dt)
}
fn step_with_sensmat(
&self,
t: f64,
state: SVector<f64, S>,
sens: SMatrix<f64, S, P>,
params: &SVector<f64, P>,
dt: Option<f64>,
) -> SIntegratorStepResult<S, P> {
let dt = get_step_size(dt, &self.config);
self.step_internal(t, state, None, Some(sens), Some(params), dt)
}
fn step_with_varmat_sensmat(
&self,
t: f64,
state: SVector<f64, S>,
phi: SMatrix<f64, S, S>,
sens: SMatrix<f64, S, P>,
params: &SVector<f64, P>,
dt: Option<f64>,
) -> SIntegratorStepResult<S, P> {
let dt = get_step_size(dt, &self.config);
self.step_internal(t, state, Some(phi), Some(sens), Some(params), dt)
}
}
impl<const S: usize, const P: usize> SIntegratorConstructor<S, P> for RK4SIntegrator<S, P> {
fn new(
f: SStateDynamics<S, P>,
varmat: SVariationalMatrix<S, P>,
sensmat: SSensitivity<S, P>,
control: SControlInput<S, P>,
) -> Self {
Self::with_config(f, varmat, sensmat, control, IntegratorConfig::default())
}
fn with_config(
f: SStateDynamics<S, P>,
varmat: SVariationalMatrix<S, P>,
sensmat: SSensitivity<S, P>,
control: SControlInput<S, P>,
config: IntegratorConfig,
) -> Self {
Self {
f,
varmat,
sensmat,
control,
bt: RK4_TABLEAU,
config,
}
}
}
pub struct RK4DIntegrator {
dimension: usize,
f: DStateDynamics,
varmat: DVariationalMatrix,
sensmat: DSensitivity,
control: DControlInput,
bt: ButcherTableau<4>,
config: IntegratorConfig,
}
impl RK4DIntegrator {
pub fn new(
dimension: usize,
f: DStateDynamics,
varmat: DVariationalMatrix,
sensmat: DSensitivity,
control: DControlInput,
) -> Self {
Self::with_config(
dimension,
f,
varmat,
sensmat,
control,
IntegratorConfig::default(),
)
}
pub fn with_config(
dimension: usize,
f: DStateDynamics,
varmat: DVariationalMatrix,
sensmat: DSensitivity,
control: DControlInput,
config: IntegratorConfig,
) -> Self {
Self {
dimension,
f,
varmat,
sensmat,
control,
bt: RK4_TABLEAU,
config,
}
}
}
impl DIntegrator for RK4DIntegrator {
fn dimension(&self) -> usize {
self.dimension
}
fn config(&self) -> &IntegratorConfig {
&self.config
}
fn step(
&self,
t: f64,
state: DVector<f64>,
params: Option<&DVector<f64>>,
dt: Option<f64>,
) -> DIntegratorStepResult {
let dt = get_step_size(dt, &self.config);
self.step_internal(t, state, None, None, params, dt)
}
fn step_with_varmat(
&self,
t: f64,
state: DVector<f64>,
params: Option<&DVector<f64>>,
phi: DMatrix<f64>,
dt: Option<f64>,
) -> DIntegratorStepResult {
let dt = get_step_size(dt, &self.config);
self.step_internal(t, state, Some(phi), None, params, dt)
}
fn step_with_sensmat(
&self,
t: f64,
state: DVector<f64>,
sens: DMatrix<f64>,
params: &DVector<f64>,
dt: Option<f64>,
) -> DIntegratorStepResult {
let dt = get_step_size(dt, &self.config);
self.step_internal(t, state, None, Some(sens), Some(params), dt)
}
fn step_with_varmat_sensmat(
&self,
t: f64,
state: DVector<f64>,
phi: DMatrix<f64>,
sens: DMatrix<f64>,
params: &DVector<f64>,
dt: Option<f64>,
) -> DIntegratorStepResult {
let dt = get_step_size(dt, &self.config);
self.step_internal(t, state, Some(phi), Some(sens), Some(params), dt)
}
}
impl DIntegratorConstructor for RK4DIntegrator {
fn with_config(
dimension: usize,
f: DStateDynamics,
varmat: DVariationalMatrix,
sensmat: DSensitivity,
control: DControlInput,
config: IntegratorConfig,
) -> Self {
Self {
dimension,
f,
varmat,
sensmat,
control,
bt: RK4_TABLEAU,
config,
}
}
}
impl RK4DIntegrator {
fn step_internal(
&self,
t: f64,
state: DVector<f64>,
phi: Option<DMatrix<f64>>,
sens: Option<DMatrix<f64>>,
params: Option<&DVector<f64>>,
dt: f64,
) -> DIntegratorStepResult {
assert_eq!(
state.len(),
self.dimension,
"State dimension {} doesn't match integrator dimension {}",
state.len(),
self.dimension
);
if let Some(ref p) = phi {
assert_eq!(
p.nrows(),
self.dimension,
"STM rows {} doesn't match integrator dimension {}",
p.nrows(),
self.dimension
);
assert_eq!(
p.ncols(),
self.dimension,
"STM cols {} doesn't match integrator dimension {}",
p.ncols(),
self.dimension
);
}
if let Some(ref s) = sens {
assert_eq!(
s.nrows(),
self.dimension,
"Sensitivity rows {} doesn't match integrator dimension {}",
s.nrows(),
self.dimension
);
}
let compute_phi = phi.is_some();
let compute_sens = sens.is_some();
let num_params = sens.as_ref().map(|s| s.ncols()).unwrap_or(0);
let mut k = DMatrix::<f64>::zeros(self.dimension, 4);
let mut k_phi = if compute_phi {
vec![DMatrix::<f64>::zeros(self.dimension, self.dimension); 4]
} else {
vec![]
};
let mut k_sens = if compute_sens {
vec![DMatrix::<f64>::zeros(self.dimension, num_params); 4]
} else {
vec![]
};
for i in 0..4 {
let mut ksum = DVector::<f64>::zeros(self.dimension);
let mut k_phi_sum = if compute_phi {
DMatrix::<f64>::zeros(self.dimension, self.dimension)
} else {
DMatrix::<f64>::zeros(0, 0)
};
let mut k_sens_sum = if compute_sens {
DMatrix::<f64>::zeros(self.dimension, num_params)
} else {
DMatrix::<f64>::zeros(0, 0)
};
for j in 0..i {
ksum += self.bt.a[(i, j)] * k.column(j);
if compute_phi {
k_phi_sum += self.bt.a[(i, j)] * &k_phi[j];
}
if compute_sens {
k_sens_sum += self.bt.a[(i, j)] * &k_sens[j];
}
}
let state_i = &state + dt * &ksum;
let t_i = t + self.bt.c[i] * dt;
let mut k_i = (self.f)(t_i, &state_i, params);
if let Some(ref ctrl) = self.control {
k_i += ctrl(t_i, &state_i, params);
}
k.set_column(i, &k_i);
if compute_phi || compute_sens {
let a_i = self
.varmat
.as_ref()
.expect("varmat required for step_with_varmat or step_with_sensmat")
.compute(t_i, &state_i, params);
if compute_phi {
k_phi[i] = &a_i * (phi.as_ref().unwrap() + dt * k_phi_sum);
}
if compute_sens {
let b_i = self
.sensmat
.as_ref()
.expect("sensmat required for step_with_sensmat")
.compute(t_i, &state_i, params.unwrap());
k_sens[i] = &a_i * (sens.as_ref().unwrap() + dt * k_sens_sum) + b_i;
}
}
}
let mut state_update = DVector::<f64>::zeros(self.dimension);
let mut phi_update = if compute_phi {
DMatrix::<f64>::zeros(self.dimension, self.dimension)
} else {
DMatrix::<f64>::zeros(0, 0)
};
let mut sens_update = if compute_sens {
DMatrix::<f64>::zeros(self.dimension, num_params)
} else {
DMatrix::<f64>::zeros(0, 0)
};
for i in 0..4 {
state_update += dt * self.bt.b[i] * k.column(i);
if compute_phi {
phi_update += dt * self.bt.b[i] * &k_phi[i];
}
if compute_sens {
sens_update += dt * self.bt.b[i] * &k_sens[i];
}
}
DIntegratorStepResult {
state: state + state_update,
phi: phi.map(|p| p + phi_update),
sens: sens.map(|s| s + sens_update),
dt_used: dt,
error_estimate: None,
dt_next: dt,
}
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use approx::assert_abs_diff_eq;
use nalgebra::{DMatrix, DVector, SMatrix, SVector};
use crate::constants::{DEGREES, RADIANS};
use crate::integrators::rk4::{RK4DIntegrator, RK4SIntegrator};
use crate::integrators::traits::{DIntegrator, SIntegrator, SIntegratorConstructor};
use crate::math::jacobian::{DNumericalJacobian, SNumericalJacobian};
use crate::time::{Epoch, TimeSystem};
use crate::{GM_EARTH, R_EARTH, orbital_period, state_koe_to_eci};
fn point_earth(
_: f64,
x: &SVector<f64, 6>,
_params: Option<&SVector<f64, 0>>,
) -> SVector<f64, 6> {
let r = x.fixed_rows::<3>(0);
let v = x.fixed_rows::<3>(3);
let a = -GM_EARTH / r.norm().powi(3);
let r_dot = v;
let v_dot = a * r;
let mut x_dot = SVector::<f64, 6>::zeros();
x_dot.fixed_rows_mut::<3>(0).copy_from(&r_dot);
x_dot.fixed_rows_mut::<3>(3).copy_from(&v_dot);
x_dot
}
#[test]
fn test_rk4s_integrator_cubic() {
let f =
|t: f64, _: &SVector<f64, 1>, _params: Option<&SVector<f64, 0>>| -> SVector<f64, 1> {
let mut state_new = SVector::<f64, 1>::zeros();
state_new[0] = 3.0 * t * t;
state_new
};
let rk4: RK4SIntegrator<1, 0> = RK4SIntegrator::new(Box::new(f), None, None, None);
let mut state = SVector::<f64, 1>::new(0.0);
let dt = 1.0;
for i in 0..10 {
state = rk4.step(i as f64, state, None, Some(dt)).state;
}
assert_abs_diff_eq!(state[0], 1000.0, epsilon = 1.0e-12);
}
#[test]
fn test_rk4s_integrator_parabola() {
let f =
|t: f64, _: &SVector<f64, 1>, _params: Option<&SVector<f64, 0>>| -> SVector<f64, 1> {
let mut state_new = SVector::<f64, 1>::zeros();
state_new[0] = 2.0 * t;
state_new
};
let rk4: RK4SIntegrator<1, 0> = RK4SIntegrator::new(Box::new(f), None, None, None);
let mut t = 0.0;
let mut state = SVector::<f64, 1>::new(0.0);
let dt = 0.01;
for _ in 0..100 {
state = rk4.step(t, state, None, Some(dt)).state;
t += dt;
}
assert_abs_diff_eq!(state[0], 1.0, epsilon = 1.0e-12);
}
#[test]
fn test_rk4s_integrator_orbit() {
let rk4: RK4SIntegrator<6, 0> =
RK4SIntegrator::new(Box::new(point_earth), None, None, None);
let oe0 = SVector::<f64, 6>::new(R_EARTH + 500e3, 0.01, 90.0, 0.0, 0.0, 0.0);
let state0 = state_koe_to_eci(oe0, RADIANS);
let mut state = state0;
let epc0 = Epoch::from_date(2024, 1, 1, TimeSystem::TAI);
let epcf = epc0 + orbital_period(oe0[0]);
let mut dt;
let mut epc = epc0;
while epc < epcf {
dt = (epcf - epc).min(1.0);
state = rk4.step(epc - epc0, state, None, Some(dt)).state;
epc += dt;
}
assert_abs_diff_eq!(state.norm(), state0.norm(), epsilon = 1.0e-7);
assert_abs_diff_eq!(state[0], state0[0], epsilon = 1.0e-5);
assert_abs_diff_eq!(state[1], state0[1], epsilon = 1.0e-5);
assert_abs_diff_eq!(state[2], state0[2], epsilon = 1.0e-5);
assert_abs_diff_eq!(state[3], state0[3], epsilon = 1.0e-5);
assert_abs_diff_eq!(state[4], state0[4], epsilon = 1.0e-5);
assert_abs_diff_eq!(state[5], state0[5], epsilon = 1.0e-5);
}
#[test]
fn test_rk4s_integrator_varmat() {
let jacobian = SNumericalJacobian::new(Box::new(point_earth)).with_fixed_offset(1.0);
let rk4: RK4SIntegrator<6, 0> =
RK4SIntegrator::new(Box::new(point_earth), Some(Box::new(jacobian)), None, None);
let oe0 = SVector::<f64, 6>::new(R_EARTH + 500e3, 0.01, 90.0, 0.0, 0.0, 0.0);
let state0 = state_koe_to_eci(oe0, RADIANS);
let phi0 = SMatrix::<f64, 6, 6>::identity();
let result = rk4.step_with_varmat(0.0, state0, None, phi0, Some(0.0));
let phi1 = result.phi;
for i in 0..6 {
for j in 0..6 {
if i == j {
assert_abs_diff_eq!(phi1.unwrap()[(i, j)], 1.0, epsilon = 1.0e-12);
} else {
assert_abs_diff_eq!(phi1.unwrap()[(i, j)], 0.0, epsilon = 1.0e-12);
}
}
}
let result = rk4.step_with_varmat(0.0, state0, None, phi0, Some(1.0));
let phi2 = result.phi;
for i in 0..6 {
for j in 0..6 {
if i == j {
assert_ne!(phi2.unwrap()[(i, i)], 1.0);
assert_ne!(phi2.unwrap()[(i, i)], 0.0);
assert_abs_diff_eq!(phi2.unwrap()[(i, i)], 1.0, epsilon = 1.0e-5);
} else {
assert_ne!(phi2.unwrap()[(i, j)], 0.0);
}
}
}
let pert = SVector::<f64, 6>::new(1.0, 0.0, 0.0, 0.0, 0.0, 0.0);
let jacobian2 = SNumericalJacobian::central(Box::new(point_earth)).with_fixed_offset(1.0);
let rk4: RK4SIntegrator<6, 0> =
RK4SIntegrator::new(Box::new(point_earth), Some(Box::new(jacobian2)), None, None);
let result = rk4.step_with_varmat(0.0, state0 + pert, None, phi0, Some(1.0));
let state_pert = result.state;
let state_stm = rk4.step(0.0, state0, None, Some(1.0)).state + phi2.unwrap() * pert;
assert_abs_diff_eq!(state_pert[0], state_stm[0], epsilon = 1.0e-9);
assert_abs_diff_eq!(state_pert[1], state_stm[1], epsilon = 1.0e-9);
assert_abs_diff_eq!(state_pert[2], state_stm[2], epsilon = 1.0e-9);
assert_abs_diff_eq!(state_pert[3], state_stm[3], epsilon = 1.0e-9);
assert_abs_diff_eq!(state_pert[4], state_stm[4], epsilon = 1.0e-9);
assert_abs_diff_eq!(state_pert[5], state_stm[5], epsilon = 1.0e-9);
}
fn point_earth_dynamic(
_: f64,
x: &DVector<f64>,
_params: Option<&DVector<f64>>,
) -> DVector<f64> {
assert_eq!(x.len(), 6, "State must be 6D for orbital mechanics");
let r = x.rows(0, 3);
let v = x.rows(3, 3);
let r_norm = r.norm();
let a = -GM_EARTH / r_norm.powi(3);
let mut x_dot = DVector::<f64>::zeros(6);
x_dot.rows_mut(0, 3).copy_from(&v);
x_dot.rows_mut(3, 3).copy_from(&(a * r));
x_dot
}
#[test]
fn test_rk4d_integrator_cubic() {
let f = |t: f64, _: &DVector<f64>, _: Option<&DVector<f64>>| -> DVector<f64> {
DVector::from_vec(vec![3.0 * t * t])
};
let rk4 = RK4DIntegrator::new(1, Box::new(f), None, None, None);
let mut state = DVector::from_vec(vec![0.0]);
let dt = 1.0;
for i in 0..10 {
state = rk4.step(i as f64, state, None, Some(dt)).state;
}
assert_abs_diff_eq!(state[0], 1000.0, epsilon = 1.0e-12);
}
#[test]
fn test_rk4d_integrator_parabola() {
let f = |t: f64, _: &DVector<f64>, _: Option<&DVector<f64>>| -> DVector<f64> {
DVector::from_vec(vec![2.0 * t])
};
let rk4 = RK4DIntegrator::new(1, Box::new(f), None, None, None);
let mut t = 0.0;
let mut state = DVector::from_vec(vec![0.0]);
let dt = 0.01;
for _ in 0..100 {
state = rk4.step(t, state, None, Some(dt)).state;
t += dt;
}
assert_abs_diff_eq!(state[0], 1.0, epsilon = 1.0e-12);
}
#[test]
fn test_rk4d_integrator_orbit() {
let rk4 = RK4DIntegrator::new(6, Box::new(point_earth_dynamic), None, None, None);
let oe0 = SVector::<f64, 6>::new(R_EARTH + 500e3, 0.01, 90.0, 0.0, 0.0, 0.0);
let state0_static = state_koe_to_eci(oe0, RADIANS);
let state0 = DVector::from_vec(state0_static.as_slice().to_vec());
let mut state = state0.clone();
let epc0 = Epoch::from_date(2024, 1, 1, TimeSystem::TAI);
let epcf = epc0 + orbital_period(oe0[0]);
let mut dt;
let mut epc = epc0;
while epc < epcf {
dt = (epcf - epc).min(1.0);
state = rk4.step(epc - epc0, state, None, Some(dt)).state;
epc += dt;
}
assert_abs_diff_eq!(state.norm(), state0.norm(), epsilon = 1.0e-7);
assert_abs_diff_eq!(state[0], state0[0], epsilon = 1.0e-5);
assert_abs_diff_eq!(state[1], state0[1], epsilon = 1.0e-5);
assert_abs_diff_eq!(state[2], state0[2], epsilon = 1.0e-5);
assert_abs_diff_eq!(state[3], state0[3], epsilon = 1.0e-5);
assert_abs_diff_eq!(state[4], state0[4], epsilon = 1.0e-5);
assert_abs_diff_eq!(state[5], state0[5], epsilon = 1.0e-5);
}
#[test]
fn test_rk4d_integrator_varmat() {
let point_earth_for_jacobian = |t: f64,
x: &DVector<f64>,
_params: Option<&DVector<f64>>|
-> DVector<f64> { point_earth_dynamic(t, x, None) };
let jacobian =
DNumericalJacobian::new(Box::new(point_earth_for_jacobian)).with_fixed_offset(1.0);
let rk4 = RK4DIntegrator::new(
6,
Box::new(point_earth_dynamic),
Some(Box::new(jacobian)),
None,
None,
);
let oe0 = SVector::<f64, 6>::new(R_EARTH + 500e3, 0.01, 90.0, 0.0, 0.0, 0.0);
let state0_static = state_koe_to_eci(oe0, RADIANS);
let state0 = DVector::from_vec(state0_static.as_slice().to_vec());
let phi0 = DMatrix::<f64>::identity(6, 6);
let result = rk4.step_with_varmat(0.0, state0.clone(), None, phi0.clone(), Some(0.0));
let phi1 = result.phi.unwrap();
for i in 0..6 {
for j in 0..6 {
if i == j {
assert_abs_diff_eq!(phi1[(i, j)], 1.0, epsilon = 1.0e-12);
} else {
assert_abs_diff_eq!(phi1[(i, j)], 0.0, epsilon = 1.0e-12);
}
}
}
let result = rk4.step_with_varmat(0.0, state0.clone(), None, phi0.clone(), Some(1.0));
let phi2 = result.phi.unwrap();
for i in 0..6 {
for j in 0..6 {
if i == j {
assert_ne!(phi2[(i, i)], 1.0);
assert_ne!(phi2[(i, i)], 0.0);
assert_abs_diff_eq!(phi2[(i, i)], 1.0, epsilon = 1.0e-5);
} else {
assert_ne!(phi2[(i, j)], 0.0);
}
}
}
let pert = DVector::from_vec(vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
let point_earth_for_jacobian2 =
|t: f64, x: &DVector<f64>, _params: Option<&DVector<f64>>| -> DVector<f64> {
point_earth_dynamic(t, x, None)
};
let jacobian2 =
DNumericalJacobian::central(Box::new(point_earth_for_jacobian2)).with_fixed_offset(1.0);
let rk4 = RK4DIntegrator::new(
6,
Box::new(point_earth_dynamic),
Some(Box::new(jacobian2)),
None,
None,
);
let result = rk4.step_with_varmat(0.0, &state0 + &pert, None, phi0, Some(1.0));
let state_pert = result.state;
let state_stm = rk4.step(0.0, state0.clone(), None, Some(1.0)).state + &phi2 * &pert;
assert_abs_diff_eq!(state_pert[0], state_stm[0], epsilon = 1.0e-9);
assert_abs_diff_eq!(state_pert[1], state_stm[1], epsilon = 1.0e-9);
assert_abs_diff_eq!(state_pert[2], state_stm[2], epsilon = 1.0e-9);
assert_abs_diff_eq!(state_pert[3], state_stm[3], epsilon = 1.0e-9);
assert_abs_diff_eq!(state_pert[4], state_stm[4], epsilon = 1.0e-9);
assert_abs_diff_eq!(state_pert[5], state_stm[5], epsilon = 1.0e-9);
}
#[test]
fn test_rk4_s_vs_d_consistency() {
let f_static = |_t: f64,
x: &SVector<f64, 3>,
_params: Option<&SVector<f64, 0>>|
-> SVector<f64, 3> { SVector::<f64, 3>::new(-x[0], -x[1], -x[2]) };
let f_dynamic = |_t: f64, x: &DVector<f64>, _: Option<&DVector<f64>>| -> DVector<f64> {
DVector::from_vec(vec![-x[0], -x[1], -x[2]])
};
let rk4_s: RK4SIntegrator<3, 0> = RK4SIntegrator::new(Box::new(f_static), None, None, None);
let rk4_d = RK4DIntegrator::new(3, Box::new(f_dynamic), None, None, None);
let state_s = SVector::<f64, 3>::new(1.0, 2.0, 3.0);
let state_d = DVector::from_vec(vec![1.0, 2.0, 3.0]);
let dt = 0.1;
let result_s = rk4_s.step(0.0, state_s, None, Some(dt));
let result_d = rk4_d.step(0.0, state_d, None, Some(dt));
assert_abs_diff_eq!(result_s.state[0], result_d.state[0], epsilon = 1.0e-15);
assert_abs_diff_eq!(result_s.state[1], result_d.state[1], epsilon = 1.0e-15);
assert_abs_diff_eq!(result_s.state[2], result_d.state[2], epsilon = 1.0e-15);
}
#[test]
fn test_rk4s_backward_integration() {
let rk4: RK4SIntegrator<6, 0> =
RK4SIntegrator::new(Box::new(point_earth), None, None, None);
let oe0 = SVector::<f64, 6>::new(R_EARTH + 500e3, 0.01, 90.0, 0.0, 0.0, 0.0);
let state0 = state_koe_to_eci(oe0, DEGREES);
let dt_forward = 1.0;
let mut state_fwd = state0;
for _ in 0..100 {
state_fwd = rk4.step(0.0, state_fwd, None, Some(dt_forward)).state;
}
let dt_back = -1.0; let mut state_back = state_fwd;
for _ in 0..100 {
state_back = rk4.step(0.0, state_back, None, Some(dt_back)).state;
}
for i in 0..6 {
assert_abs_diff_eq!(state_back[i], state0[i], epsilon = 1.0e-9);
}
}
#[test]
fn test_rk4d_backward_integration() {
let rk4 = RK4DIntegrator::new(6, Box::new(point_earth_dynamic), None, None, None);
let oe0 = SVector::<f64, 6>::new(R_EARTH + 500e3, 0.01, 90.0, 0.0, 0.0, 0.0);
let state0_static = state_koe_to_eci(oe0, DEGREES);
let state0 = DVector::from_vec(state0_static.as_slice().to_vec());
let dt_forward = 1.0;
let mut state_fwd = state0.clone();
for _ in 0..100 {
state_fwd = rk4.step(0.0, state_fwd, None, Some(dt_forward)).state;
}
let dt_back = -1.0; let mut state_back = state_fwd;
for _ in 0..100 {
state_back = rk4.step(0.0, state_back, None, Some(dt_back)).state;
}
for i in 0..6 {
assert_abs_diff_eq!(state_back[i], state0[i], epsilon = 1.0e-9);
}
}
#[test]
fn test_rk4s_integrator_with_control_input() {
let f = |_t: f64, _x: &SVector<f64, 1>, _p: Option<&SVector<f64, 0>>| -> SVector<f64, 1> {
SVector::<f64, 1>::zeros()
};
let control = |_t: f64,
_x: &SVector<f64, 1>,
_p: Option<&SVector<f64, 0>>|
-> SVector<f64, 1> { SVector::<f64, 1>::new(1.0) };
let rk4_no_ctrl: RK4SIntegrator<1, 0> = RK4SIntegrator::new(Box::new(f), None, None, None);
let state0 = SVector::<f64, 1>::new(0.0);
let state_no_ctrl = rk4_no_ctrl.step(0.0, state0, None, Some(1.0));
assert_abs_diff_eq!(state_no_ctrl.state[0], 0.0, epsilon = 1.0e-12);
let rk4_ctrl: RK4SIntegrator<1, 0> =
RK4SIntegrator::new(Box::new(f), None, None, Some(Box::new(control)));
let state_ctrl = rk4_ctrl.step(0.0, state0, None, Some(1.0));
assert_abs_diff_eq!(state_ctrl.state[0], 1.0, epsilon = 1.0e-12);
}
#[test]
fn test_rk4d_integrator_with_control_input() {
let f = |_t: f64, x: &DVector<f64>, _: Option<&DVector<f64>>| -> DVector<f64> {
DVector::zeros(x.len())
};
let control = |_t: f64, _x: &DVector<f64>, _p: Option<&DVector<f64>>| -> DVector<f64> {
DVector::from_vec(vec![1.0, 2.0])
};
let rk4_no_ctrl = RK4DIntegrator::new(2, Box::new(f), None, None, None);
let state0 = DVector::from_vec(vec![0.0, 0.0]);
let state_no_ctrl = rk4_no_ctrl.step(0.0, state0.clone(), None, Some(1.0));
assert_abs_diff_eq!(state_no_ctrl.state[0], 0.0, epsilon = 1.0e-12);
assert_abs_diff_eq!(state_no_ctrl.state[1], 0.0, epsilon = 1.0e-12);
let rk4_ctrl = RK4DIntegrator::new(2, Box::new(f), None, None, Some(Box::new(control)));
let state_ctrl = rk4_ctrl.step(0.0, state0, None, Some(1.0));
assert_abs_diff_eq!(state_ctrl.state[0], 1.0, epsilon = 1.0e-12);
assert_abs_diff_eq!(state_ctrl.state[1], 2.0, epsilon = 1.0e-12);
}
#[test]
fn test_rk4s_integrator_control_with_dynamics() {
let f = |_t: f64,
x: &SVector<f64, 1>,
_params: Option<&SVector<f64, 0>>|
-> SVector<f64, 1> { -x };
let control = |_t: f64,
_x: &SVector<f64, 1>,
_p: Option<&SVector<f64, 0>>|
-> SVector<f64, 1> { SVector::<f64, 1>::new(1.0) };
let rk4: RK4SIntegrator<1, 0> =
RK4SIntegrator::new(Box::new(f), None, None, Some(Box::new(control)));
let mut state = SVector::<f64, 1>::new(0.0);
let dt = 0.1;
for _ in 0..100 {
state = rk4.step(0.0, state, None, Some(dt)).state;
}
assert_abs_diff_eq!(state[0], 1.0, epsilon = 1.0e-3);
}
#[test]
fn test_rk4s_integrator_state_dependent_control() {
let f = |_t: f64, _x: &SVector<f64, 1>, _p: Option<&SVector<f64, 0>>| -> SVector<f64, 1> {
SVector::<f64, 1>::zeros()
};
let control =
|_t: f64, x: &SVector<f64, 1>, _p: Option<&SVector<f64, 0>>| -> SVector<f64, 1> { -x };
let rk4: RK4SIntegrator<1, 0> =
RK4SIntegrator::new(Box::new(f), None, None, Some(Box::new(control)));
let mut state = SVector::<f64, 1>::new(1.0);
let dt = 0.1;
for _ in 0..50 {
state = rk4.step(0.0, state, None, Some(dt)).state;
}
assert!(state[0] < 0.1);
}
#[test]
fn test_rk4s_integrator_sensmat() {
use crate::math::jacobian::SJacobianProvider;
use crate::math::sensitivity::SSensitivityProvider;
struct DecayJacobian;
impl SJacobianProvider<1, 1> for DecayJacobian {
fn compute(
&self,
_t: f64,
_state: &SVector<f64, 1>,
_params: Option<&SVector<f64, 1>>,
) -> SMatrix<f64, 1, 1> {
SMatrix::<f64, 1, 1>::new(-1.0)
}
}
struct DecaySensitivity;
impl SSensitivityProvider<1, 1> for DecaySensitivity {
fn compute(
&self,
_t: f64,
state: &SVector<f64, 1>,
_params: &SVector<f64, 1>,
) -> SMatrix<f64, 1, 1> {
SMatrix::<f64, 1, 1>::new(-state[0])
}
}
let f = |_t: f64,
x: &SVector<f64, 1>,
_params: Option<&SVector<f64, 1>>|
-> SVector<f64, 1> { -x };
let rk4: RK4SIntegrator<1, 1> = RK4SIntegrator::new(
Box::new(f),
Some(Box::new(DecayJacobian)),
Some(Box::new(DecaySensitivity)),
None,
);
let x0 = 1.0;
let state0 = SVector::<f64, 1>::new(x0);
let sens0 = SMatrix::<f64, 1, 1>::zeros(); let params = SVector::<f64, 1>::new(1.0); let dt = 0.01;
let mut state = state0;
let mut sens = sens0;
let mut t = 0.0;
for _ in 0..100 {
let result = rk4.step_with_sensmat(t, state, sens, ¶ms, Some(dt));
let new_state = result.state;
let new_sens = result.sens;
state = new_state;
sens = new_sens.unwrap();
t += dt;
}
let k = params[0];
let x_analytical = x0 * (-k * t).exp();
let sens_analytical = -x0 * t * (-k * t).exp();
assert_abs_diff_eq!(state[0], x_analytical, epsilon = 1e-6);
assert_abs_diff_eq!(sens[(0, 0)], sens_analytical, epsilon = 1e-4);
}
#[test]
fn test_rk4d_integrator_sensmat() {
use crate::math::jacobian::DJacobianProvider;
use crate::math::sensitivity::DSensitivityProvider;
struct DecayJacobian;
impl DJacobianProvider for DecayJacobian {
fn compute(
&self,
_t: f64,
_state: &DVector<f64>,
_params: Option<&DVector<f64>>,
) -> DMatrix<f64> {
DMatrix::from_vec(1, 1, vec![-1.0])
}
}
struct DecaySensitivity;
impl DSensitivityProvider for DecaySensitivity {
fn compute(
&self,
_t: f64,
state: &DVector<f64>,
_params: &DVector<f64>,
) -> DMatrix<f64> {
DMatrix::from_vec(1, 1, vec![-state[0]])
}
}
let f = |_t: f64, x: &DVector<f64>, _: Option<&DVector<f64>>| -> DVector<f64> { -x };
let rk4 = RK4DIntegrator::new(
1,
Box::new(f),
Some(Box::new(DecayJacobian)),
Some(Box::new(DecaySensitivity)),
None,
);
let x0 = 1.0;
let state0 = DVector::from_vec(vec![x0]);
let sens0 = DMatrix::zeros(1, 1);
let params = DVector::from_vec(vec![1.0]); let dt = 0.01;
let mut state = state0;
let mut sens = sens0;
let mut t = 0.0;
for _ in 0..100 {
let result = rk4.step_with_sensmat(t, state, sens, ¶ms, Some(dt));
let new_state = result.state;
let new_sens = result.sens;
state = new_state;
sens = new_sens.unwrap();
t += dt;
}
let k = params[0];
let x_analytical = x0 * (-k * t).exp();
let sens_analytical = -x0 * t * (-k * t).exp();
assert_abs_diff_eq!(state[0], x_analytical, epsilon = 1e-6);
assert_abs_diff_eq!(sens[(0, 0)], sens_analytical, epsilon = 1e-4);
}
#[test]
fn test_rk4s_integrator_varmat_sensmat() {
use crate::math::jacobian::SJacobianProvider;
use crate::math::sensitivity::SSensitivityProvider;
struct DecayJacobian;
impl SJacobianProvider<1, 1> for DecayJacobian {
fn compute(
&self,
_t: f64,
_state: &SVector<f64, 1>,
_params: Option<&SVector<f64, 1>>,
) -> SMatrix<f64, 1, 1> {
SMatrix::<f64, 1, 1>::new(-1.0)
}
}
struct DecaySensitivity;
impl SSensitivityProvider<1, 1> for DecaySensitivity {
fn compute(
&self,
_t: f64,
state: &SVector<f64, 1>,
_params: &SVector<f64, 1>,
) -> SMatrix<f64, 1, 1> {
SMatrix::<f64, 1, 1>::new(-state[0])
}
}
let f = |_t: f64,
x: &SVector<f64, 1>,
_params: Option<&SVector<f64, 1>>|
-> SVector<f64, 1> { -x };
let rk4: RK4SIntegrator<1, 1> = RK4SIntegrator::new(
Box::new(f),
Some(Box::new(DecayJacobian)),
Some(Box::new(DecaySensitivity)),
None,
);
let x0 = 1.0;
let state0 = SVector::<f64, 1>::new(x0);
let phi0 = SMatrix::<f64, 1, 1>::identity();
let sens0 = SMatrix::<f64, 1, 1>::zeros();
let params = SVector::<f64, 1>::new(1.0);
let dt = 0.01;
let mut state = state0;
let mut phi = phi0;
let mut sens = sens0;
let mut t = 0.0;
for _ in 0..100 {
let result = rk4.step_with_varmat_sensmat(t, state, phi, sens, ¶ms, Some(dt));
let new_state = result.state;
let new_phi = result.phi;
let new_sens = result.sens;
state = new_state;
phi = new_phi.unwrap();
sens = new_sens.unwrap();
t += dt;
}
let k = params[0];
let x_analytical = x0 * (-k * t).exp();
let phi_analytical = (-k * t).exp();
let sens_analytical = -x0 * t * (-k * t).exp();
assert_abs_diff_eq!(state[0], x_analytical, epsilon = 1e-6);
assert_abs_diff_eq!(phi[(0, 0)], phi_analytical, epsilon = 1e-4);
assert_abs_diff_eq!(sens[(0, 0)], sens_analytical, epsilon = 1e-4);
let delta = 1e-6;
let state0_pert = SVector::<f64, 1>::new(x0 + delta);
let mut state_pert = state0_pert;
for _ in 0..100 {
state_pert = rk4.step(0.0, state_pert, None, Some(dt)).state;
}
let state_pert_predicted = state[0] + phi[(0, 0)] * delta;
let relative_error = (state_pert[0] - state_pert_predicted).abs() / delta;
assert!(
relative_error < 1e-3,
"STM prediction error: {}",
relative_error
);
}
#[test]
fn test_rk4d_integrator_varmat_sensmat() {
use crate::math::jacobian::DJacobianProvider;
use crate::math::sensitivity::DSensitivityProvider;
struct DecayJacobian;
impl DJacobianProvider for DecayJacobian {
fn compute(
&self,
_t: f64,
_state: &DVector<f64>,
_params: Option<&DVector<f64>>,
) -> DMatrix<f64> {
DMatrix::from_vec(1, 1, vec![-1.0])
}
}
struct DecaySensitivity;
impl DSensitivityProvider for DecaySensitivity {
fn compute(
&self,
_t: f64,
state: &DVector<f64>,
_params: &DVector<f64>,
) -> DMatrix<f64> {
DMatrix::from_vec(1, 1, vec![-state[0]])
}
}
let f = |_t: f64, x: &DVector<f64>, _: Option<&DVector<f64>>| -> DVector<f64> { -x };
let rk4 = RK4DIntegrator::new(
1,
Box::new(f),
Some(Box::new(DecayJacobian)),
Some(Box::new(DecaySensitivity)),
None,
);
let x0 = 1.0;
let state0 = DVector::from_vec(vec![x0]);
let phi0 = DMatrix::identity(1, 1);
let sens0 = DMatrix::zeros(1, 1);
let params = DVector::from_vec(vec![1.0]);
let dt = 0.01;
let mut state = state0.clone();
let mut phi = phi0;
let mut sens = sens0;
let mut t = 0.0;
for _ in 0..100 {
let result = rk4.step_with_varmat_sensmat(t, state, phi, sens, ¶ms, Some(dt));
let new_state = result.state;
let new_phi = result.phi;
let new_sens = result.sens;
state = new_state;
phi = new_phi.unwrap();
sens = new_sens.unwrap();
t += dt;
}
let k = params[0];
let x_analytical = x0 * (-k * t).exp();
let phi_analytical = (-k * t).exp();
let sens_analytical = -x0 * t * (-k * t).exp();
assert_abs_diff_eq!(state[0], x_analytical, epsilon = 1e-6);
assert_abs_diff_eq!(phi[(0, 0)], phi_analytical, epsilon = 1e-4);
assert_abs_diff_eq!(sens[(0, 0)], sens_analytical, epsilon = 1e-4);
let delta = 1e-6;
let state0_pert = DVector::from_vec(vec![x0 + delta]);
let mut state_pert = state0_pert;
for _ in 0..100 {
state_pert = rk4.step(0.0, state_pert, None, Some(dt)).state;
}
let state_pert_predicted = state[0] + phi[(0, 0)] * delta;
let relative_error = (state_pert[0] - state_pert_predicted).abs() / delta;
assert!(
relative_error < 1e-3,
"STM prediction error: {}",
relative_error
);
}
#[test]
fn test_rk4s_new_uses_default_config() {
fn dynamics(
_t: f64,
state: &SVector<f64, 1>,
_params: Option<&SVector<f64, 0>>,
) -> SVector<f64, 1> {
*state
}
let integrator: RK4SIntegrator<1, 0> =
RK4SIntegrator::new(Box::new(dynamics), None, None, None);
let config = integrator.config();
let default_config = crate::integrators::config::IntegratorConfig::default();
assert_eq!(config.abs_tol, default_config.abs_tol);
assert_eq!(config.rel_tol, default_config.rel_tol);
assert_eq!(config.max_step_attempts, default_config.max_step_attempts);
assert_eq!(config.min_step, default_config.min_step);
assert_eq!(config.max_step, default_config.max_step);
}
#[test]
fn test_rk4s_with_config_stores_config() {
fn dynamics(
_t: f64,
state: &SVector<f64, 1>,
_params: Option<&SVector<f64, 0>>,
) -> SVector<f64, 1> {
*state
}
let custom_config = crate::integrators::config::IntegratorConfig {
abs_tol: 1e-10,
rel_tol: 1e-8,
initial_step: None,
max_step_attempts: 20,
min_step: Some(1e-15),
max_step: Some(100.0),
step_safety_factor: Some(0.9),
max_step_scale_factor: Some(5.0),
min_step_scale_factor: Some(0.1),
fixed_step_size: None,
};
let integrator: RK4SIntegrator<1, 0> = RK4SIntegrator::with_config(
Box::new(dynamics),
None,
None,
None,
custom_config.clone(),
);
let config = integrator.config();
assert_eq!(config.abs_tol, 1e-10);
assert_eq!(config.rel_tol, 1e-8);
assert_eq!(config.max_step_attempts, 20);
assert_eq!(config.min_step, Some(1e-15));
assert_eq!(config.max_step, Some(100.0));
assert_eq!(config.max_step_scale_factor, Some(5.0));
assert_eq!(config.min_step_scale_factor, Some(0.1));
}
#[test]
fn test_rk4s_config_returns_reference() {
fn dynamics(
_t: f64,
state: &SVector<f64, 1>,
_params: Option<&SVector<f64, 0>>,
) -> SVector<f64, 1> {
*state
}
let integrator: RK4SIntegrator<1, 0> =
RK4SIntegrator::new(Box::new(dynamics), None, None, None);
let config1 = integrator.config();
let config2 = integrator.config();
assert_eq!(config1.abs_tol, config2.abs_tol);
assert_eq!(config1.rel_tol, config2.rel_tol);
assert_eq!(config1.max_step_attempts, config2.max_step_attempts);
}
#[test]
fn test_rk4d_new_uses_default_config() {
fn dynamics(_t: f64, state: &DVector<f64>, _params: Option<&DVector<f64>>) -> DVector<f64> {
state.clone()
}
let integrator = RK4DIntegrator::new(1, Box::new(dynamics), None, None, None);
let config = integrator.config();
let default_config = crate::integrators::config::IntegratorConfig::default();
assert_eq!(config.abs_tol, default_config.abs_tol);
assert_eq!(config.rel_tol, default_config.rel_tol);
assert_eq!(config.max_step_attempts, default_config.max_step_attempts);
assert_eq!(config.min_step, default_config.min_step);
assert_eq!(config.max_step, default_config.max_step);
}
#[test]
fn test_rk4d_with_config_stores_config() {
fn dynamics(_t: f64, state: &DVector<f64>, _params: Option<&DVector<f64>>) -> DVector<f64> {
state.clone()
}
let custom_config = crate::integrators::config::IntegratorConfig {
abs_tol: 1e-10,
rel_tol: 1e-8,
initial_step: None,
max_step_attempts: 20,
min_step: Some(1e-15),
max_step: Some(100.0),
step_safety_factor: Some(0.9),
max_step_scale_factor: Some(5.0),
min_step_scale_factor: Some(0.1),
fixed_step_size: None,
};
let integrator = RK4DIntegrator::with_config(
1,
Box::new(dynamics),
None,
None,
None,
custom_config.clone(),
);
let config = integrator.config();
assert_eq!(config.abs_tol, 1e-10);
assert_eq!(config.rel_tol, 1e-8);
assert_eq!(config.max_step_attempts, 20);
assert_eq!(config.min_step, Some(1e-15));
assert_eq!(config.max_step, Some(100.0));
assert_eq!(config.max_step_scale_factor, Some(5.0));
assert_eq!(config.min_step_scale_factor, Some(0.1));
}
#[test]
fn test_rk4d_config_returns_reference() {
fn dynamics(_t: f64, state: &DVector<f64>, _params: Option<&DVector<f64>>) -> DVector<f64> {
state.clone()
}
let integrator = RK4DIntegrator::new(1, Box::new(dynamics), None, None, None);
let config1 = integrator.config();
let config2 = integrator.config();
assert_eq!(config1.abs_tol, config2.abs_tol);
assert_eq!(config1.rel_tol, config2.rel_tol);
assert_eq!(config1.max_step_attempts, config2.max_step_attempts);
}
#[test]
fn test_rk4d_dimension_method() {
fn dynamics(_t: f64, state: &DVector<f64>, _params: Option<&DVector<f64>>) -> DVector<f64> {
state.clone()
}
let integrator = RK4DIntegrator::new(6, Box::new(dynamics), None, None, None);
assert_eq!(integrator.dimension(), 6);
let integrator2 = RK4DIntegrator::new(12, Box::new(dynamics), None, None, None);
assert_eq!(integrator2.dimension(), 12);
}
#[test]
fn test_rk4s_params_affect_step_output() {
let f =
|_t: f64, x: &SVector<f64, 1>, params: Option<&SVector<f64, 1>>| -> SVector<f64, 1> {
let k = params.map(|p| p[0]).unwrap_or(1.0);
SVector::<f64, 1>::new(-k * x[0])
};
let rk4: RK4SIntegrator<1, 1> = RK4SIntegrator::new(Box::new(f), None, None, None);
let x0 = SVector::<f64, 1>::new(1.0);
let dt = 0.1;
let t = 0.0;
let params_slow = SVector::<f64, 1>::new(1.0);
let result_slow = rk4.step(t, x0, Some(¶ms_slow), Some(dt));
let params_fast = SVector::<f64, 1>::new(5.0);
let result_fast = rk4.step(t, x0, Some(¶ms_fast), Some(dt));
assert!(
(result_slow.state[0] - result_fast.state[0]).abs() > 0.1,
"Different params should produce different states: slow={}, fast={}",
result_slow.state[0],
result_fast.state[0]
);
let x_slow_analytical = 1.0_f64 * (-dt).exp();
let x_fast_analytical = 1.0_f64 * (-5.0 * dt).exp();
assert_abs_diff_eq!(result_slow.state[0], x_slow_analytical, epsilon = 1e-3);
assert_abs_diff_eq!(result_fast.state[0], x_fast_analytical, epsilon = 1e-3);
}
#[test]
fn test_rk4d_params_affect_step_output() {
let f = |_t: f64, x: &DVector<f64>, params: Option<&DVector<f64>>| -> DVector<f64> {
let k = params.map(|p| p[0]).unwrap_or(1.0);
DVector::from_element(1, -k * x[0])
};
let rk4 = RK4DIntegrator::new(1, Box::new(f), None, None, None);
let x0 = DVector::from_element(1, 1.0);
let dt = 0.1;
let t = 0.0;
let params_slow = DVector::from_element(1, 1.0);
let result_slow = rk4.step(t, x0.clone(), Some(¶ms_slow), Some(dt));
let params_fast = DVector::from_element(1, 5.0);
let result_fast = rk4.step(t, x0, Some(¶ms_fast), Some(dt));
assert!(
(result_slow.state[0] - result_fast.state[0]).abs() > 0.1,
"Different params should produce different states: slow={}, fast={}",
result_slow.state[0],
result_fast.state[0]
);
let x_slow_analytical = 1.0_f64 * (-dt).exp();
let x_fast_analytical = 1.0_f64 * (-5.0 * dt).exp();
assert_abs_diff_eq!(result_slow.state[0], x_slow_analytical, epsilon = 1e-3);
assert_abs_diff_eq!(result_fast.state[0], x_fast_analytical, epsilon = 1e-3);
}
#[test]
fn test_rk4s_params_multi_step_propagation() {
let f =
|_t: f64, x: &SVector<f64, 1>, params: Option<&SVector<f64, 1>>| -> SVector<f64, 1> {
let k = params.map(|p| p[0]).unwrap_or(1.0);
SVector::<f64, 1>::new(-k * x[0])
};
let rk4: RK4SIntegrator<1, 1> = RK4SIntegrator::new(Box::new(f), None, None, None);
let x0 = SVector::<f64, 1>::new(1.0);
let dt = 0.01;
let n_steps = 100;
let params_slow = SVector::<f64, 1>::new(0.5);
let mut state_slow = x0;
let mut t = 0.0;
for _ in 0..n_steps {
let result = rk4.step(t, state_slow, Some(¶ms_slow), Some(dt));
state_slow = result.state;
t += dt;
}
let params_fast = SVector::<f64, 1>::new(2.0);
let mut state_fast = x0;
let mut t = 0.0;
for _ in 0..n_steps {
let result = rk4.step(t, state_fast, Some(¶ms_fast), Some(dt));
state_fast = result.state;
t += dt;
}
let t_final: f64 = 1.0;
let x_slow_analytical = 1.0_f64 * (-0.5 * t_final).exp(); let x_fast_analytical = 1.0_f64 * (-2.0 * t_final).exp();
assert_abs_diff_eq!(state_slow[0], x_slow_analytical, epsilon = 1e-6);
assert_abs_diff_eq!(state_fast[0], x_fast_analytical, epsilon = 1e-6);
assert!(
(state_slow[0] - state_fast[0]).abs() > 0.4,
"Multi-step propagation with different params should differ: slow={}, fast={}",
state_slow[0],
state_fast[0]
);
}
#[test]
fn test_rk4s_params_with_varmat() {
use crate::math::jacobian::SJacobianProvider;
struct ParamDependentJacobian;
impl SJacobianProvider<1, 1> for ParamDependentJacobian {
fn compute(
&self,
_t: f64,
_state: &SVector<f64, 1>,
params: Option<&SVector<f64, 1>>,
) -> SMatrix<f64, 1, 1> {
let k = params.map(|p| p[0]).unwrap_or(1.0);
SMatrix::<f64, 1, 1>::new(-k)
}
}
let f =
|_t: f64, x: &SVector<f64, 1>, params: Option<&SVector<f64, 1>>| -> SVector<f64, 1> {
let k = params.map(|p| p[0]).unwrap_or(1.0);
SVector::<f64, 1>::new(-k * x[0])
};
let rk4: RK4SIntegrator<1, 1> = RK4SIntegrator::new(
Box::new(f),
Some(Box::new(ParamDependentJacobian)),
None,
None,
);
let x0 = SVector::<f64, 1>::new(1.0);
let phi0 = SMatrix::<f64, 1, 1>::identity();
let dt = 0.1;
let t = 0.0;
let params_slow = SVector::<f64, 1>::new(1.0);
let result_slow = rk4.step_with_varmat(t, x0, Some(¶ms_slow), phi0, Some(dt));
let params_fast = SVector::<f64, 1>::new(5.0);
let result_fast = rk4.step_with_varmat(t, x0, Some(¶ms_fast), phi0, Some(dt));
assert!(
(result_slow.state[0] - result_fast.state[0]).abs() > 0.1,
"Different params should produce different states in step_with_varmat"
);
let phi_slow = result_slow.phi.unwrap();
let phi_fast = result_fast.phi.unwrap();
assert!(
(phi_slow[(0, 0)] - phi_fast[(0, 0)]).abs() > 0.1,
"Different params should produce different STMs: slow={}, fast={}",
phi_slow[(0, 0)],
phi_fast[(0, 0)]
);
}
#[test]
fn test_rk4d_params_with_varmat() {
use crate::math::jacobian::DJacobianProvider;
struct ParamDependentJacobian;
impl DJacobianProvider for ParamDependentJacobian {
fn compute(
&self,
_t: f64,
_state: &DVector<f64>,
params: Option<&DVector<f64>>,
) -> DMatrix<f64> {
let k = params.map(|p| p[0]).unwrap_or(1.0);
DMatrix::from_element(1, 1, -k)
}
}
let f = |_t: f64, x: &DVector<f64>, params: Option<&DVector<f64>>| -> DVector<f64> {
let k = params.map(|p| p[0]).unwrap_or(1.0);
DVector::from_element(1, -k * x[0])
};
let rk4 = RK4DIntegrator::new(
1,
Box::new(f),
Some(Box::new(ParamDependentJacobian)),
None,
None,
);
let x0 = DVector::from_element(1, 1.0);
let phi0 = DMatrix::identity(1, 1);
let dt = 0.1;
let t = 0.0;
let params_slow = DVector::from_element(1, 1.0);
let result_slow =
rk4.step_with_varmat(t, x0.clone(), Some(¶ms_slow), phi0.clone(), Some(dt));
let params_fast = DVector::from_element(1, 5.0);
let result_fast = rk4.step_with_varmat(t, x0, Some(¶ms_fast), phi0, Some(dt));
assert!(
(result_slow.state[0] - result_fast.state[0]).abs() > 0.1,
"Different params should produce different states in step_with_varmat"
);
let phi_slow = result_slow.phi.unwrap();
let phi_fast = result_fast.phi.unwrap();
assert!(
(phi_slow[(0, 0)] - phi_fast[(0, 0)]).abs() > 0.1,
"Different params should produce different STMs"
);
}
}