use crate::astro::dynamics::{DynamicsContext, OrbitState, StateTransitionMatrix};
use crate::coordinates::frames::GCRS;
use qtty::Second;
use crate::pod::force::SiderustAccelerationModel;
use crate::pod::propagation::Integrator;
use affn::matrix6::FrameMatrix6;
use super::pod_error::PodDynamicsError;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ParamColumn(pub [f64; 6]);
impl ParamColumn {
pub fn max_relative_difference(&self, other: &ParamColumn) -> f64 {
let scale = other.0.iter().map(|x| x.abs()).fold(0.0_f64, f64::max);
let scale = if scale > 0.0 { scale } else { 1.0 };
self.0
.iter()
.zip(other.0.iter())
.map(|(a, b)| (a - b).abs() / scale)
.fold(0.0_f64, f64::max)
}
}
#[derive(Debug, Clone, Copy)]
pub struct ParamStmReport {
pub coarse: ParamColumn,
pub refined: ParamColumn,
pub max_rel_diff: f64,
}
pub fn param_partials_central_diff<I, FM, B>(
integrator: &I,
state: OrbitState,
dt: Second,
ctx: &DynamicsContext,
p_nominal: f64,
epsilon: f64,
mut build_force: B,
) -> Result<ParamColumn, PodDynamicsError>
where
I: Integrator,
FM: SiderustAccelerationModel,
B: FnMut(f64) -> FM,
{
if !epsilon.is_finite() || epsilon <= 0.0 {
return Err(PodDynamicsError::InvalidParamStep(
"epsilon must be strictly positive and finite",
));
}
let force_plus = build_force(p_nominal + epsilon);
let force_minus = build_force(p_nominal - epsilon);
let y_plus = integrator.propagate(&force_plus, state, dt, ctx)?;
let y_minus = integrator.propagate(&force_minus, state, dt, ctx)?;
let inv_2eps = 1.0 / (2.0 * epsilon);
let col = [
(y_plus.position.x().value() - y_minus.position.x().value()) * inv_2eps,
(y_plus.position.y().value() - y_minus.position.y().value()) * inv_2eps,
(y_plus.position.z().value() - y_minus.position.z().value()) * inv_2eps,
(y_plus.velocity.x().value() - y_minus.velocity.x().value()) * inv_2eps,
(y_plus.velocity.y().value() - y_minus.velocity.y().value()) * inv_2eps,
(y_plus.velocity.z().value() - y_minus.velocity.z().value()) * inv_2eps,
];
Ok(ParamColumn(col))
}
#[derive(Debug, Clone)]
pub struct PropagatedArc {
pub steps: Vec<(OrbitState, StateTransitionMatrix<GCRS>)>,
}
impl PropagatedArc {
pub fn len(&self) -> usize {
self.steps.len()
}
pub fn is_empty(&self) -> bool {
self.steps.is_empty()
}
pub fn final_state(&self) -> Option<&OrbitState> {
self.steps.last().map(|(s, _)| s)
}
pub fn final_stm(&self) -> Option<&StateTransitionMatrix<GCRS>> {
self.steps.last().map(|(_, phi)| phi)
}
}
pub struct VariationalPropagator {
pub step: Second,
}
impl VariationalPropagator {
pub fn propagate<FM>(
&self,
force: &FM,
initial: OrbitState,
n_steps: usize,
ctx: &DynamicsContext,
) -> Result<PropagatedArc, PodDynamicsError>
where
FM: SiderustAccelerationModel,
{
let mut state = initial;
let mut phi_total: StateTransitionMatrix<GCRS> = FrameMatrix6::identity();
let mut steps = Vec::with_capacity(n_steps);
for _ in 0..n_steps {
let (new_state, phi_step) = principia::propagate_stm(force, state, self.step, ctx)
.map_err(|e: principia::PrincipiaError| PodDynamicsError::Dynamics(e.into()))?;
phi_total = phi_step * phi_total;
steps.push((new_state, phi_total));
state = new_state;
}
Ok(PropagatedArc { steps })
}
}
#[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::pod::propagation::Rk4Integrator;
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 rejects_bad_epsilon() {
let r = param_partials_central_diff(
&Rk4Integrator {
step: Second::new(10.0),
},
s0(),
Second::new(60.0),
&DynamicsContext::empty(),
1.0,
0.0,
|_| TwoBody::new(crate::astro::dynamics::GM_EARTH),
);
assert!(matches!(r, Err(PodDynamicsError::InvalidParamStep(_))));
}
#[test]
fn invariant_force_yields_zero_column() {
let col = param_partials_central_diff(
&Rk4Integrator {
step: Second::new(10.0),
},
s0(),
Second::new(60.0),
&DynamicsContext::empty(),
1.0,
1e-6,
|_| TwoBody::new(crate::astro::dynamics::GM_EARTH),
)
.unwrap();
assert!(col.0.iter().all(|v| v.abs() < 1e-12));
}
#[test]
fn variational_propagator_accumulates_steps() {
let s = s0();
let prop = VariationalPropagator {
step: Second::new(30.0),
};
let arc = prop
.propagate(
&TwoBody::new(crate::astro::dynamics::GM_EARTH),
s,
4,
&DynamicsContext::empty(),
)
.unwrap();
assert_eq!(arc.len(), 4);
assert!(!arc.is_empty());
assert!(arc.final_state().is_some());
assert!(arc.final_stm().is_some());
}
#[test]
fn variational_propagator_stm_close_to_identity_for_short_arc() {
let s = s0();
let prop = VariationalPropagator {
step: Second::new(1.0),
};
let arc = prop
.propagate(
&TwoBody::new(crate::astro::dynamics::GM_EARTH),
s,
1,
&DynamicsContext::empty(),
)
.unwrap();
let phi = arc.steps[0].1.as_array();
for (i, row) in phi.iter().enumerate() {
assert!(
(row[i] - 1.0).abs() < 1e-2,
"diagonal entry [{i}][{i}] = {} should be ≈ 1",
row[i]
);
}
}
#[test]
fn prop_arc_empty_when_zero_steps() {
let s = s0();
let prop = VariationalPropagator {
step: Second::new(60.0),
};
let arc = prop
.propagate(
&TwoBody::new(crate::astro::dynamics::GM_EARTH),
s,
0,
&DynamicsContext::empty(),
)
.unwrap();
assert!(arc.is_empty());
assert!(arc.final_state().is_none());
assert!(arc.final_stm().is_none());
}
}