use crate::astro::dynamics::{DynamicsContext, OrbitState};
use principia::{dop853_propagate, dopri5_propagate, rk4_propagate};
use qtty::{IntegratorTolerances, Second};
use super::error::DynamicsError;
use crate::pod::force::SiderustAccelerationModel;
pub trait Integrator {
fn propagate<FM: SiderustAccelerationModel>(
&self,
force: &FM,
state: OrbitState,
dt: Second,
ctx: &DynamicsContext,
) -> Result<OrbitState, DynamicsError>;
}
#[derive(Debug, Clone, Copy)]
pub struct Rk4Integrator {
pub step: Second,
}
impl Integrator for Rk4Integrator {
fn propagate<FM: SiderustAccelerationModel>(
&self,
force: &FM,
state: OrbitState,
dt: Second,
ctx: &DynamicsContext,
) -> Result<OrbitState, DynamicsError> {
let dt_s = dt.value();
if dt_s == 0.0 {
return Ok(state);
}
let step = self.step.value().abs();
if step <= 0.0 || step.is_nan() {
return Err(DynamicsError::Upstream(
crate::astro::dynamics::errors::DynamicsError::InvalidStepRequest {
reason: "Rk4Integrator.step must be strictly positive",
},
));
}
let n_steps = (dt_s.abs() / step).ceil().max(1.0) as usize;
let h = Second::new(dt_s / n_steps as f64);
Ok(rk4_propagate(force, state, h, dt, ctx)?)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Dopri5Integrator {
pub tolerances: IntegratorTolerances,
}
impl Integrator for Dopri5Integrator {
fn propagate<FM: SiderustAccelerationModel>(
&self,
force: &FM,
state: OrbitState,
dt: Second,
ctx: &DynamicsContext,
) -> Result<OrbitState, DynamicsError> {
Ok(dopri5_propagate(force, state, dt, self.tolerances, ctx)?)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Dop853Integrator {
pub tolerances: IntegratorTolerances,
}
impl Integrator for Dop853Integrator {
fn propagate<FM: SiderustAccelerationModel>(
&self,
force: &FM,
state: OrbitState,
dt: Second,
ctx: &DynamicsContext,
) -> Result<OrbitState, DynamicsError> {
Ok(dop853_propagate(force, state, dt, self.tolerances, ctx)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::astro::dynamics::forces::TwoBody;
use crate::astro::dynamics::{Position, Velocity};
use crate::coordinates::frames::GCRS;
use crate::time::JulianDate;
fn s0() -> OrbitState {
OrbitState::new(
JulianDate::new(2_451_545.0).to_j2000s(),
Position::<GCRS>::new(7_000.0, 0.0, 0.0),
Velocity::<GCRS>::new(0.0, 7.5450, 0.0),
)
}
#[test]
fn rk4_zero_dt_is_identity() {
let integ = Rk4Integrator {
step: Second::new(10.0),
};
let s = integ
.propagate(
&TwoBody::new(crate::astro::dynamics::GM_EARTH),
s0(),
Second::new(0.0),
&DynamicsContext::empty(),
)
.unwrap();
let dt_total = s.epoch - s0().epoch;
assert!(dt_total.value().abs() < 1e-15);
}
#[test]
fn rk4_invalid_step_rejected() {
let integ = Rk4Integrator {
step: Second::new(0.0),
};
let r = integ.propagate(
&TwoBody::new(crate::astro::dynamics::GM_EARTH),
s0(),
Second::new(60.0),
&DynamicsContext::empty(),
);
assert!(matches!(
r,
Err(DynamicsError::Upstream(
crate::astro::dynamics::errors::DynamicsError::InvalidStepRequest { .. }
))
));
}
#[test]
fn dop853_and_dopri5_agree_on_short_arc() {
let tol = IntegratorTolerances::uniform(1e-12, 1e-9, 1e-12);
let dt = Second::new(120.0);
let ctx = DynamicsContext::empty();
let a = Dop853Integrator { tolerances: tol }
.propagate(
&TwoBody::new(crate::astro::dynamics::GM_EARTH),
s0(),
dt,
&ctx,
)
.unwrap();
let b = Dopri5Integrator { tolerances: tol }
.propagate(
&TwoBody::new(crate::astro::dynamics::GM_EARTH),
s0(),
dt,
&ctx,
)
.unwrap();
let dx = (a.position.x().value() - b.position.x().value()).abs();
let dy = (a.position.y().value() - b.position.y().value()).abs();
let dz = (a.position.z().value() - b.position.z().value()).abs();
assert!(dx + dy + dz < 1e-3, "dx+dy+dz = {}", dx + dy + dz);
}
}