use std::sync::Arc;
use nalgebra::{DMatrix, DVector};
use crate::integrators::traits::DIntegrator;
use crate::math::interpolation::{
CovarianceInterpolationConfig, CovarianceInterpolationMethod, InterpolationConfig,
InterpolationMethod,
};
use crate::math::jacobian::DNumericalJacobian;
use crate::math::jacobian::DifferenceMethod;
use crate::math::sensitivity::DNumericalSensitivity;
use crate::time::Epoch;
use crate::trajectories::DTrajectory;
use crate::trajectories::traits::{
InterpolatableTrajectory, STMStorage, SensitivityStorage, Trajectory,
};
use crate::utils::errors::BraheError;
use crate::utils::identifiable::Identifiable;
use crate::utils::state_providers::{DCovarianceProvider, DStateProvider};
use super::TrajectoryMode;
use super::traits::DStatePropagator;
use crate::events::{DDetectedEvent, DEventDetector, EventAction, EventType, dscan_for_event};
use crate::integrators::traits::{DControlInput, DStateDynamics};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
enum PropagationMode {
StateOnly,
WithSTM,
WithSensitivity,
WithSTMAndSensitivity,
}
#[derive(Debug)]
enum EventProcessingResult {
NoEvents,
Restart { epoch: Epoch, state: DVector<f64> },
Terminal { epoch: Epoch, state: DVector<f64> },
}
type SharedDynamics =
Arc<dyn Fn(f64, &DVector<f64>, Option<&DVector<f64>>) -> DVector<f64> + Send + Sync>;
pub struct DNumericalPropagator {
epoch_initial: Epoch,
epoch_current: Epoch,
t_rel: f64,
integrator: Box<dyn DIntegrator>,
dt: f64,
dt_next: f64,
x_initial: DVector<f64>,
x_curr: DVector<f64>,
params: DVector<f64>,
state_dim: usize,
propagation_mode: PropagationMode,
stm: Option<DMatrix<f64>>,
sensitivity: Option<DMatrix<f64>>,
store_stm_history: bool,
store_sensitivity_history: bool,
initial_covariance: Option<DMatrix<f64>>,
current_covariance: Option<DMatrix<f64>>,
trajectory: DTrajectory,
trajectory_mode: TrajectoryMode,
interpolation_method: InterpolationMethod,
covariance_interpolation_method: CovarianceInterpolationMethod,
event_detectors: Vec<Box<dyn DEventDetector>>,
event_log: Vec<DDetectedEvent>,
terminated: bool,
pub name: Option<String>,
pub id: Option<u64>,
pub uuid: Option<uuid::Uuid>,
}
impl DNumericalPropagator {
#[allow(clippy::too_many_arguments)]
pub fn new(
epoch: Epoch,
state: DVector<f64>,
dynamics_fn: DStateDynamics,
propagation_config: super::NumericalPropagationConfig,
params: Option<DVector<f64>>,
control_input: DControlInput,
initial_covariance: Option<DMatrix<f64>>,
) -> Result<Self, BraheError> {
let params = params.unwrap_or_else(|| DVector::zeros(0));
let state_eci = state;
let state_dim = state_eci.len();
let enable_stm = propagation_config.variational.enable_stm || initial_covariance.is_some();
let enable_sensitivity = propagation_config.variational.enable_sensitivity;
if enable_sensitivity && params.is_empty() {
return Err(BraheError::PropagatorError(
"Sensitivity propagation requires params to be provided".to_string(),
));
}
if propagation_config.interpolation_method.requires_6d() && state_dim != 6 {
return Err(BraheError::PropagatorError(format!(
"{:?} interpolation requires 6D states with position/velocity structure \
[x, y, z, vx, vy, vz], but state dimension is {}. \
Use InterpolationMethod::Linear or InterpolationMethod::Lagrange {{ degree: N }} \
for generic N-dimensional systems.",
propagation_config.interpolation_method, state_dim
)));
}
let dynamics_fn = Arc::from(dynamics_fn);
let dynamics = Self::wrap_for_integrator(Arc::clone(&dynamics_fn));
let initial_dt = propagation_config.integrator.initial_step.unwrap_or(60.0);
let jacobian_provider = if enable_stm || enable_sensitivity {
Some(Self::build_jacobian_provider(
Arc::clone(&dynamics_fn),
propagation_config.variational.jacobian_method,
))
} else {
None
};
let sensitivity_provider = if enable_sensitivity {
Some(Self::build_sensitivity_provider(
Arc::clone(&dynamics_fn),
propagation_config.variational.sensitivity_method,
))
} else {
None
};
let integrator = crate::integrators::create_dintegrator(
propagation_config.method,
state_dim,
dynamics,
jacobian_provider,
sensitivity_provider,
control_input,
propagation_config.integrator,
);
let mut trajectory = DTrajectory::new(state_dim);
trajectory.set_interpolation_method(propagation_config.interpolation_method);
if propagation_config.variational.store_stm_history {
trajectory.enable_stm_storage();
}
if propagation_config.variational.store_sensitivity_history && !params.is_empty() {
trajectory.enable_sensitivity_storage(params.len());
}
let initial_stm = if propagation_config.variational.store_stm_history {
Some(DMatrix::identity(state_dim, state_dim))
} else {
None
};
let initial_sensitivity =
if propagation_config.variational.store_sensitivity_history && !params.is_empty() {
Some(DMatrix::zeros(state_dim, params.len()))
} else {
None
};
trajectory.add_full(
epoch,
state_eci.clone(),
initial_covariance.clone(),
initial_stm,
initial_sensitivity,
);
let propagation_mode = match (enable_stm, enable_sensitivity) {
(false, false) => PropagationMode::StateOnly,
(true, false) => PropagationMode::WithSTM,
(false, true) => PropagationMode::WithSensitivity,
(true, true) => PropagationMode::WithSTMAndSensitivity,
};
let stm = if enable_stm {
Some(DMatrix::identity(state_dim, state_dim))
} else {
None
};
let sensitivity = if enable_sensitivity {
Some(DMatrix::zeros(state_dim, params.len()))
} else {
None
};
let current_covariance = initial_covariance.clone();
let mut result = Ok(Self {
epoch_initial: epoch,
epoch_current: epoch,
t_rel: 0.0,
integrator,
dt: initial_dt,
dt_next: initial_dt,
x_initial: state_eci.clone(),
x_curr: state_eci,
params,
state_dim,
propagation_mode,
stm,
sensitivity,
store_stm_history: propagation_config.variational.store_stm_history,
store_sensitivity_history: propagation_config.variational.store_sensitivity_history,
initial_covariance,
current_covariance,
trajectory,
trajectory_mode: TrajectoryMode::AllSteps,
interpolation_method: propagation_config.interpolation_method,
covariance_interpolation_method: CovarianceInterpolationMethod::TwoWasserstein,
event_detectors: Vec::new(),
event_log: Vec::new(),
terminated: false,
name: None,
id: None,
uuid: None,
});
if let Ok(ref mut prop) = result {
prop.uuid = Some(uuid::Uuid::now_v7());
}
result
}
fn process_initial_events(&mut self) {
if self.event_detectors.is_empty() || self.terminated {
return;
}
let epoch = self.current_epoch();
let state = self.x_curr.clone();
let params_ref = if self.params.is_empty() {
None
} else {
Some(&self.params)
};
let mut events_to_process = Vec::new();
for (idx, detector) in self.event_detectors.iter().enumerate() {
if detector.is_processed() {
continue;
}
let value = detector.evaluate(epoch, &state, params_ref);
let target = detector.target_value();
if (value - target).abs() <= detector.value_tolerance() {
let event = DDetectedEvent {
window_open: epoch,
window_close: epoch,
entry_state: state.clone(),
exit_state: state.clone(),
value,
name: detector.name().to_string(),
action: detector.action(),
event_type: EventType::Instantaneous,
detector_index: idx,
};
events_to_process.push((idx, event));
}
}
if !events_to_process.is_empty() {
for &(idx, _) in &events_to_process {
self.event_detectors[idx].mark_processed();
}
match self.process_events_smart(events_to_process) {
EventProcessingResult::NoEvents => {}
EventProcessingResult::Restart { epoch, state } => {
self.t_rel = epoch - self.epoch_initial;
self.epoch_current = epoch;
self.x_curr = state;
self.integrator.reset_cache();
}
EventProcessingResult::Terminal {
epoch: term_epoch,
state: term_state,
} => {
self.epoch_current = term_epoch;
self.x_curr = term_state;
}
}
}
}
fn scan_all_events(
&self,
epoch_prev: Epoch,
epoch_new: Epoch,
x_prev: &DVector<f64>,
x_new: &DVector<f64>,
) -> Vec<(usize, DDetectedEvent)> {
let mut events = Vec::new();
let state_fn = |epoch: Epoch| -> DVector<f64> {
let t_rel = epoch - self.epoch_initial;
let t_prev = epoch_prev - self.epoch_initial;
let t_new = epoch_new - self.epoch_initial;
let alpha = (t_rel - t_prev) / (t_new - t_prev);
x_prev + (x_new - x_prev) * alpha
};
let params_opt = if !self.params.is_empty() {
Some(&self.params)
} else {
None
};
for (idx, detector) in self.event_detectors.iter().enumerate() {
if let Some(event) = dscan_for_event(
detector.as_ref(),
idx,
&state_fn,
epoch_prev,
epoch_new,
x_prev,
x_new,
params_opt,
) {
events.push((idx, event));
}
}
events
}
fn refine_event_states(
&self,
events: &mut [(usize, DDetectedEvent)],
epoch_prev: Epoch,
x_prev: &DVector<f64>,
) {
if events.is_empty() {
return;
}
let t_prev_rel = epoch_prev - self.epoch_initial;
let params_ref = if self.params.is_empty() {
None
} else {
Some(&self.params)
};
self.integrator.reset_cache();
for (_, event) in events.iter_mut() {
let dt_to_event = event.window_open - epoch_prev;
if dt_to_event.abs() <= 1e-12 {
event.entry_state = x_prev.clone();
event.exit_state = x_prev.clone();
continue;
}
let max_sub_dt = 10.0;
let n_steps = ((dt_to_event.abs() / max_sub_dt).ceil() as usize).max(1);
let sub_dt = dt_to_event / n_steps as f64;
let mut t = t_prev_rel;
let mut x = x_prev.clone();
for _ in 0..n_steps {
let result = self.integrator.step(t, x, params_ref, Some(sub_dt));
x = result.state;
t += result.dt_used;
}
event.entry_state = x.clone();
event.exit_state = x;
}
self.integrator.reset_cache();
}
fn process_events_smart(
&mut self,
detected_events: Vec<(usize, DDetectedEvent)>,
) -> EventProcessingResult {
if detected_events.is_empty() {
return EventProcessingResult::NoEvents;
}
let mut sorted_events = detected_events;
sorted_events.sort_by(|(_, a), (_, b)| a.window_open.partial_cmp(&b.window_open).unwrap());
let first_callback_idx = sorted_events
.iter()
.position(|(det_idx, _)| self.event_detectors[*det_idx].callback().is_some());
match first_callback_idx {
None => {
let mut terminal_event = None;
for (det_idx, event) in sorted_events {
self.event_log.push(event.clone());
self.event_detectors[det_idx].mark_processed();
if !matches!(self.trajectory_mode, TrajectoryMode::Disabled) {
self.trajectory
.add(event.window_open, event.entry_state.clone());
}
let detector = &self.event_detectors[det_idx];
if detector.action() == EventAction::Stop {
terminal_event = Some(event);
break; }
}
if let Some(term_event) = terminal_event {
self.terminated = true;
return EventProcessingResult::Terminal {
epoch: term_event.window_open,
state: term_event.entry_state.clone(),
};
}
EventProcessingResult::NoEvents }
Some(callback_idx) => {
for (det_idx, event) in sorted_events.iter().take(callback_idx) {
self.event_log.push(event.clone());
self.event_detectors[*det_idx].mark_processed();
if !matches!(self.trajectory_mode, TrajectoryMode::Disabled) {
self.trajectory
.add(event.window_open, event.entry_state.clone());
}
}
let (det_idx, callback_event) = &sorted_events[callback_idx];
let detector = &self.event_detectors[*det_idx];
self.event_log.push(callback_event.clone());
if !matches!(self.trajectory_mode, TrajectoryMode::Disabled) {
self.trajectory.add(
callback_event.window_open,
callback_event.entry_state.clone(),
);
}
if let Some(callback) = detector.callback() {
let (new_state, new_params, action) = callback(
callback_event.window_open,
&callback_event.entry_state,
Some(&self.params),
);
let y_after = if let Some(y_new) = new_state {
if !matches!(self.trajectory_mode, TrajectoryMode::Disabled) {
self.trajectory
.add(callback_event.window_open, y_new.clone());
}
y_new
} else {
callback_event.entry_state.clone()
};
if let Some(p_new) = new_params {
self.params = p_new;
}
detector.mark_processed();
if action == EventAction::Stop {
self.terminated = true;
return EventProcessingResult::Terminal {
epoch: callback_event.window_open,
state: y_after,
};
}
return EventProcessingResult::Restart {
epoch: callback_event.window_open,
state: y_after,
};
}
unreachable!("Callback event must have callback");
}
}
}
fn wrap_for_integrator(shared: SharedDynamics) -> DStateDynamics {
Box::new(
move |t: f64, state: &DVector<f64>, params: Option<&DVector<f64>>| -> DVector<f64> {
shared(t, state, params)
},
)
}
fn build_jacobian_provider(
shared_dynamics: SharedDynamics,
method: DifferenceMethod,
) -> Box<dyn crate::math::jacobian::DJacobianProvider> {
let dynamics_for_jacobian = Box::new(
move |t: f64, state: &DVector<f64>, params: Option<&DVector<f64>>| -> DVector<f64> {
shared_dynamics(t, state, params)
},
);
match method {
DifferenceMethod::Forward => {
Box::new(DNumericalJacobian::forward(dynamics_for_jacobian))
}
DifferenceMethod::Central => {
Box::new(DNumericalJacobian::central(dynamics_for_jacobian))
}
DifferenceMethod::Backward => {
Box::new(DNumericalJacobian::backward(dynamics_for_jacobian))
}
}
}
fn build_sensitivity_provider(
shared_dynamics: SharedDynamics,
method: DifferenceMethod,
) -> Box<dyn crate::math::sensitivity::DSensitivityProvider> {
let dynamics_with_params = Box::new(
move |t: f64, state: &DVector<f64>, params: &DVector<f64>| -> DVector<f64> {
shared_dynamics(t, state, Some(params))
},
);
match method {
DifferenceMethod::Forward => {
Box::new(DNumericalSensitivity::forward(dynamics_with_params))
}
DifferenceMethod::Central => {
Box::new(DNumericalSensitivity::central(dynamics_with_params))
}
DifferenceMethod::Backward => {
Box::new(DNumericalSensitivity::backward(dynamics_with_params))
}
}
}
pub fn set_trajectory_mode(&mut self, mode: TrajectoryMode) {
self.trajectory_mode = mode;
}
pub fn trajectory_mode(&self) -> TrajectoryMode {
self.trajectory_mode
}
pub fn set_output_step(&mut self, _step: f64) {
}
pub fn disable_output_step(&mut self) {
}
pub fn current_state_ref(&self) -> &DVector<f64> {
&self.x_curr
}
pub fn current_params(&self) -> &DVector<f64> {
&self.params
}
pub fn trajectory(&self) -> &DTrajectory {
&self.trajectory
}
pub fn stm(&self) -> Option<&DMatrix<f64>> {
self.stm.as_ref()
}
pub fn sensitivity(&self) -> Option<&DMatrix<f64>> {
self.sensitivity.as_ref()
}
pub fn stm_at_idx(&self, index: usize) -> Option<&DMatrix<f64>> {
self.trajectory.stm_at_idx(index)
}
pub fn sensitivity_at_idx(&self, index: usize) -> Option<&DMatrix<f64>> {
self.trajectory.sensitivity_at_idx(index)
}
pub fn stm_at(&self, epoch: Epoch) -> Option<DMatrix<f64>> {
self.trajectory.stm_at(epoch)
}
pub fn sensitivity_at(&self, epoch: Epoch) -> Option<DMatrix<f64>> {
self.trajectory.sensitivity_at(epoch)
}
pub fn reinitialize(
&mut self,
epoch: Epoch,
state: DVector<f64>,
covariance: Option<DMatrix<f64>>,
) {
self.t_rel = epoch - self.epoch_initial;
self.epoch_current = epoch;
self.x_curr = state;
match self.propagation_mode {
PropagationMode::StateOnly => {
}
PropagationMode::WithSTM => {
self.stm = Some(DMatrix::identity(self.state_dim, self.state_dim));
}
PropagationMode::WithSensitivity => {
let param_dim = self.params.len();
self.sensitivity = Some(DMatrix::zeros(self.state_dim, param_dim));
}
PropagationMode::WithSTMAndSensitivity => {
self.stm = Some(DMatrix::identity(self.state_dim, self.state_dim));
let param_dim = self.params.len();
self.sensitivity = Some(DMatrix::zeros(self.state_dim, param_dim));
}
}
self.integrator.reset_cache();
self.initial_covariance = covariance.clone();
self.current_covariance = covariance;
self.terminated = false;
}
pub fn current_covariance(&self) -> Option<&DMatrix<f64>> {
self.current_covariance.as_ref()
}
pub fn has_stm(&self) -> bool {
matches!(
self.propagation_mode,
PropagationMode::WithSTM | PropagationMode::WithSTMAndSensitivity
)
}
pub fn terminated(&self) -> bool {
self.terminated
}
pub fn add_event_detector(&mut self, detector: Box<dyn DEventDetector>) {
self.event_detectors.push(detector);
}
pub fn take_event_detectors(&mut self) -> Vec<Box<dyn DEventDetector>> {
std::mem::take(&mut self.event_detectors)
}
pub fn set_event_detectors(&mut self, detectors: Vec<Box<dyn DEventDetector>>) {
self.event_detectors = detectors;
}
pub fn take_event_log(&mut self) -> Vec<DDetectedEvent> {
std::mem::take(&mut self.event_log)
}
pub fn set_event_log(&mut self, log: Vec<DDetectedEvent>) {
self.event_log = log;
}
pub fn set_terminated(&mut self, terminated: bool) {
self.terminated = terminated;
}
pub fn is_terminated(&self) -> bool {
self.terminated
}
pub fn event_log(&self) -> &[DDetectedEvent] {
&self.event_log
}
pub fn events_by_name(&self, name: &str) -> Vec<&DDetectedEvent> {
self.query_events().by_name_contains(name).collect()
}
pub fn latest_event(&self) -> Option<&DDetectedEvent> {
self.event_log.last()
}
pub fn events_in_range(&self, start: Epoch, end: Epoch) -> Vec<&DDetectedEvent> {
self.query_events().in_time_range(start, end).collect()
}
pub fn query_events(
&self,
) -> crate::events::EventQuery<'_, std::slice::Iter<'_, DDetectedEvent>> {
crate::events::EventQuery::new(self.event_log.iter())
}
pub fn events_by_detector_index(&self, index: usize) -> Vec<&DDetectedEvent> {
self.query_events().by_detector_index(index).collect()
}
pub fn events_by_detector_index_in_range(
&self,
index: usize,
start: Epoch,
end: Epoch,
) -> Vec<&DDetectedEvent> {
self.query_events()
.by_detector_index(index)
.in_time_range(start, end)
.collect()
}
pub fn events_by_name_in_range(
&self,
name: &str,
start: Epoch,
end: Epoch,
) -> Vec<&DDetectedEvent> {
self.query_events()
.by_name_contains(name)
.in_time_range(start, end)
.collect()
}
pub fn clear_events(&mut self) {
self.event_log.clear();
}
pub fn reset_termination(&mut self) {
self.terminated = false;
}
fn step_once(&mut self) {
let epoch_prev = self.current_epoch();
let x_prev = self.x_curr.clone();
let dt_requested = self.dt_next;
let params_ref = if self.params.is_empty() {
None
} else {
Some(&self.params)
};
match self.propagation_mode {
PropagationMode::StateOnly => {
let result = self.integrator.step(
self.t_rel,
self.x_curr.clone(),
params_ref,
Some(dt_requested),
);
self.x_curr = result.state;
self.dt = result.dt_used;
self.dt_next = result.dt_next;
}
PropagationMode::WithSTM => {
let phi = self.stm.take().unwrap();
let result = self.integrator.step_with_varmat(
self.t_rel,
self.x_curr.clone(),
params_ref,
phi,
Some(dt_requested),
);
self.x_curr = result.state;
self.stm = result.phi;
self.dt = result.dt_used;
self.dt_next = result.dt_next;
if let Some(ref p0) = self.initial_covariance
&& let Some(ref phi_result) = self.stm
{
let p_new = phi_result * p0 * phi_result.transpose();
self.current_covariance = Some(p_new);
}
}
PropagationMode::WithSensitivity => {
let sens = self.sensitivity.take().unwrap();
let result = self.integrator.step_with_sensmat(
self.t_rel,
self.x_curr.clone(),
sens,
&self.params,
Some(dt_requested),
);
self.x_curr = result.state;
self.sensitivity = result.sens;
self.dt = result.dt_used;
self.dt_next = result.dt_next;
}
PropagationMode::WithSTMAndSensitivity => {
let phi = self.stm.take().unwrap();
let sens = self.sensitivity.take().unwrap();
let result = self.integrator.step_with_varmat_sensmat(
self.t_rel,
self.x_curr.clone(),
phi,
sens,
&self.params,
Some(dt_requested),
);
self.x_curr = result.state;
self.stm = result.phi.clone();
self.sensitivity = result.sens;
self.dt = result.dt_used;
self.dt_next = result.dt_next;
if let Some(ref p0) = self.initial_covariance
&& let Some(ref phi_result) = self.stm
{
let p_new = phi_result * p0 * phi_result.transpose();
self.current_covariance = Some(p_new);
}
}
}
self.t_rel += self.dt;
self.epoch_current = self.epoch_initial + self.t_rel;
let epoch_new = self.epoch_current;
let mut detected_events =
self.scan_all_events(epoch_prev, epoch_new, &x_prev, &self.x_curr);
self.refine_event_states(&mut detected_events, epoch_prev, &x_prev);
match self.process_events_smart(detected_events) {
EventProcessingResult::NoEvents => {
if self.should_store_state() {
let cov = self.current_covariance.clone();
let stm_to_store = if self.store_stm_history {
self.stm.clone()
} else {
None
};
let sens_to_store = if self.store_sensitivity_history {
self.sensitivity.clone()
} else {
None
};
if cov.is_some() || stm_to_store.is_some() || sens_to_store.is_some() {
self.trajectory.add_full(
epoch_new,
self.x_curr.clone(),
cov,
stm_to_store,
sens_to_store,
);
} else {
self.trajectory.add(epoch_new, self.x_curr.clone());
}
}
}
EventProcessingResult::Restart { epoch, state } => {
self.t_rel = epoch - self.epoch_initial;
self.epoch_current = epoch;
self.x_curr = state;
self.integrator.reset_cache();
}
EventProcessingResult::Terminal {
epoch: term_epoch,
state: term_state,
} => {
self.epoch_current = term_epoch;
self.x_curr = term_state.clone();
if self.should_store_state() {
let cov = self.current_covariance.clone();
let stm_to_store = if self.store_stm_history {
self.stm.clone()
} else {
None
};
let sens_to_store = if self.store_sensitivity_history {
self.sensitivity.clone()
} else {
None
};
if cov.is_some() || stm_to_store.is_some() || sens_to_store.is_some() {
self.trajectory.add_full(
term_epoch,
term_state,
cov,
stm_to_store,
sens_to_store,
);
} else {
self.trajectory.add(term_epoch, term_state);
}
}
}
}
}
fn should_store_state(&self) -> bool {
match self.trajectory_mode {
TrajectoryMode::OutputStepsOnly => true,
TrajectoryMode::AllSteps => true,
TrajectoryMode::Disabled => false,
}
}
}
impl super::traits::DStatePropagator for DNumericalPropagator {
fn step_by(&mut self, step_size: f64) {
let target_t = self.t_rel + step_size;
self.process_initial_events();
let is_forward = step_size >= 0.0;
while !self.terminated {
if is_forward {
if self.t_rel >= target_t {
break;
}
} else if self.t_rel <= target_t {
break;
}
let remaining = target_t - self.t_rel;
if remaining.abs() <= 1e-12 {
break;
}
let dt_max = if is_forward {
remaining.min(self.dt_next.abs())
} else {
-remaining.abs().min(self.dt_next.abs())
};
let saved_dt_next = self.dt_next;
self.dt_next = dt_max;
self.step_once();
if self.dt_next.abs() > saved_dt_next.abs() {
self.dt_next = saved_dt_next;
}
}
}
fn propagate_to(&mut self, target_epoch: Epoch) {
let target_rel = target_epoch - self.epoch_initial;
self.process_initial_events();
let is_forward = target_rel >= self.t_rel;
while !self.terminated {
if is_forward {
if self.t_rel >= target_rel {
break;
}
} else if self.t_rel <= target_rel {
break;
}
let remaining = target_rel - self.t_rel;
if remaining.abs() <= 1e-12 {
break;
}
let dt_max = if is_forward {
remaining.min(self.dt_next.abs())
} else {
-remaining.abs().min(self.dt_next.abs())
};
let saved_dt_next = self.dt_next;
self.dt_next = dt_max;
self.step_once();
if self.dt_next.abs() > saved_dt_next.abs() {
self.dt_next = saved_dt_next;
}
}
}
fn current_epoch(&self) -> Epoch {
self.epoch_current
}
fn current_state(&self) -> DVector<f64> {
self.x_curr.clone()
}
fn initial_epoch(&self) -> Epoch {
self.epoch_initial
}
fn initial_state(&self) -> DVector<f64> {
self.x_initial.clone()
}
fn state_dim(&self) -> usize {
self.state_dim
}
fn step_size(&self) -> f64 {
self.dt
}
fn set_step_size(&mut self, step_size: f64) {
self.dt = step_size;
self.dt_next = step_size;
}
fn reset(&mut self) {
self.t_rel = 0.0;
self.epoch_current = self.epoch_initial;
self.x_curr = self.x_initial.clone();
let initial_dt = self.dt; self.dt_next = initial_dt;
match self.propagation_mode {
PropagationMode::StateOnly => {
self.stm = None;
self.sensitivity = None;
}
PropagationMode::WithSTM => {
self.stm = Some(DMatrix::identity(self.state_dim, self.state_dim));
self.sensitivity = None;
}
PropagationMode::WithSensitivity => {
self.stm = None;
let param_dim = self.params.len();
self.sensitivity = Some(DMatrix::zeros(self.state_dim, param_dim));
}
PropagationMode::WithSTMAndSensitivity => {
self.stm = Some(DMatrix::identity(self.state_dim, self.state_dim));
let param_dim = self.params.len();
self.sensitivity = Some(DMatrix::zeros(self.state_dim, param_dim));
}
}
self.trajectory = DTrajectory::new(self.state_dim);
self.event_log.clear();
self.terminated = false;
for detector in &self.event_detectors {
detector.reset_processed();
}
}
fn set_eviction_policy_max_size(&mut self, max_size: usize) -> Result<(), BraheError> {
self.trajectory.set_eviction_policy_max_size(max_size)
}
fn set_eviction_policy_max_age(&mut self, max_age: f64) -> Result<(), BraheError> {
self.trajectory.set_eviction_policy_max_age(max_age)
}
}
impl InterpolationConfig for DNumericalPropagator {
fn with_interpolation_method(mut self, method: InterpolationMethod) -> Self {
self.interpolation_method = method;
self.trajectory.set_interpolation_method(method);
self
}
fn set_interpolation_method(&mut self, method: InterpolationMethod) {
self.interpolation_method = method;
self.trajectory.set_interpolation_method(method);
}
fn get_interpolation_method(&self) -> InterpolationMethod {
self.interpolation_method
}
}
impl CovarianceInterpolationConfig for DNumericalPropagator {
fn with_covariance_interpolation_method(
mut self,
method: CovarianceInterpolationMethod,
) -> Self {
self.covariance_interpolation_method = method;
self.trajectory.set_covariance_interpolation_method(method);
self
}
fn set_covariance_interpolation_method(&mut self, method: CovarianceInterpolationMethod) {
self.covariance_interpolation_method = method;
self.trajectory.set_covariance_interpolation_method(method);
}
fn get_covariance_interpolation_method(&self) -> CovarianceInterpolationMethod {
self.covariance_interpolation_method
}
}
impl DStateProvider for DNumericalPropagator {
fn state(&self, epoch: Epoch) -> Result<DVector<f64>, BraheError> {
if let Ok(state) = self.trajectory.interpolate(&epoch) {
return Ok(state);
}
if (self.current_epoch() - epoch).abs() < 1e-9 {
return Ok(self.x_curr.clone());
}
let start = self.trajectory.start_epoch().unwrap_or(self.epoch_initial);
let end = self.current_epoch();
Err(BraheError::OutOfBoundsError(format!(
"Cannot get state at epoch {}: outside propagator time range [{}, {}]. \
Call step_by() or propagate_to() to advance the propagator first.",
epoch, start, end
)))
}
fn state_dim(&self) -> usize {
self.state_dim
}
}
impl DCovarianceProvider for DNumericalPropagator {
fn covariance(&self, epoch: Epoch) -> Result<DMatrix<f64>, BraheError> {
if self.current_covariance.is_none() {
return Err(BraheError::InitializationError(
"Covariance not available: covariance tracking was not enabled for this propagator"
.to_string(),
));
}
if let Some(start) = self.trajectory.start_epoch()
&& epoch < start
{
return Err(BraheError::OutOfBoundsError(format!(
"Cannot get covariance at epoch {}: before trajectory start {}. \
Call step_by() or propagate_to() to advance the propagator first.",
epoch, start
)));
}
if let Some(end) = self.trajectory.end_epoch()
&& epoch > end
{
return Err(BraheError::OutOfBoundsError(format!(
"Cannot get covariance at epoch {}: after trajectory end {}. \
Call step_by() or propagate_to() to advance the propagator first.",
epoch, end
)));
}
self.trajectory.covariance_at(epoch).ok_or_else(|| {
BraheError::OutOfBoundsError(format!(
"Cannot get covariance at epoch {}: no covariance data available at this epoch",
epoch
))
})
}
fn covariance_dim(&self) -> usize {
self.state_dim
}
}
impl Identifiable for DNumericalPropagator {
fn with_name(mut self, name: &str) -> Self {
self.name = Some(name.to_string());
self
}
fn with_uuid(mut self, uuid: uuid::Uuid) -> Self {
self.uuid = Some(uuid);
self
}
fn with_new_uuid(mut self) -> Self {
self.uuid = Some(uuid::Uuid::now_v7());
self
}
fn with_id(mut self, id: u64) -> Self {
self.id = Some(id);
self
}
fn with_identity(
mut self,
name: Option<&str>,
uuid: Option<uuid::Uuid>,
id: Option<u64>,
) -> Self {
self.name = name.map(|s| s.to_string());
self.uuid = uuid;
self.id = id;
self
}
fn set_identity(&mut self, name: Option<&str>, uuid: Option<uuid::Uuid>, id: Option<u64>) {
self.name = name.map(|s| s.to_string());
self.uuid = uuid;
self.id = id;
}
fn set_id(&mut self, id: Option<u64>) {
self.id = id;
}
fn set_name(&mut self, name: Option<&str>) {
self.name = name.map(|s| s.to_string());
}
fn generate_uuid(&mut self) {
self.uuid = Some(uuid::Uuid::now_v7());
}
fn get_id(&self) -> Option<u64> {
self.id
}
fn get_name(&self) -> Option<&str> {
self.name.as_deref()
}
fn get_uuid(&self) -> Option<uuid::Uuid> {
self.uuid
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::events::{DEventCallback, DTimeEvent, DValueEvent, EventDirection};
use crate::propagators::NumericalPropagationConfig;
use crate::propagators::traits::DStatePropagator as DStatePropagatorTrait;
use crate::time::TimeSystem;
use crate::utils::state_providers::DStateProvider;
use approx::assert_abs_diff_eq;
use std::f64::consts::PI;
fn sho_dynamics(omega: f64) -> DStateDynamics {
Box::new(
move |_t: f64, x: &DVector<f64>, _p: Option<&DVector<f64>>| {
let pos = x[0];
let vel = x[1];
DVector::from_vec(vec![vel, -omega * omega * pos])
},
)
}
fn damped_sho_dynamics() -> DStateDynamics {
Box::new(|_t: f64, x: &DVector<f64>, p: Option<&DVector<f64>>| {
let params = p.expect("Damped SHO requires parameters [omega, zeta]");
let omega = params[0];
let zeta = params[1];
let pos = x[0];
let vel = x[1];
DVector::from_vec(vec![vel, -omega * omega * pos - 2.0 * zeta * omega * vel])
})
}
#[allow(dead_code)]
fn sho_analytical_solution(x0: f64, v0: f64, omega: f64, t: f64) -> (f64, f64) {
let x = x0 * (omega * t).cos() + (v0 / omega) * (omega * t).sin();
let v = -x0 * omega * (omega * t).sin() + v0 * (omega * t).cos();
(x, v)
}
#[allow(dead_code)]
fn sho_energy(x: f64, v: f64, omega: f64) -> f64 {
0.5 * (v * v + omega * omega * x * x)
}
fn create_test_sho_propagator() -> DNumericalPropagator {
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]); let omega = 1.0;
let dynamics = sho_dynamics(omega);
let mut config = NumericalPropagationConfig::default();
config.integrator.max_step = Some(0.5);
DNumericalPropagator::new(epoch, state, dynamics, config, None, None, None).unwrap()
}
fn create_test_damped_sho_with_sensitivity() -> DNumericalPropagator {
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let params = DVector::from_vec(vec![1.0, 0.1]); let dynamics = damped_sho_dynamics();
let mut config = NumericalPropagationConfig::default();
config.variational.enable_sensitivity = true;
DNumericalPropagator::new(epoch, state, dynamics, config, Some(params), None, None).unwrap()
}
fn create_test_sho_with_stm() -> DNumericalPropagator {
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let omega = 1.0;
let dynamics = sho_dynamics(omega);
let mut config = NumericalPropagationConfig::default();
config.variational.enable_stm = true;
config.variational.store_stm_history = true;
DNumericalPropagator::new(epoch, state, dynamics, config, None, None, None).unwrap()
}
#[test]
fn test_dnumericalpropagator_construction_default() {
let prop = create_test_sho_propagator();
assert_eq!(DStatePropagatorTrait::state_dim(&prop), 2);
assert_eq!(prop.current_state(), DVector::from_vec(vec![1.0, 0.0]));
assert!(prop.stm().is_none()); assert!(prop.sensitivity().is_none()); }
#[test]
fn test_dnumericalpropagator_construction_with_stm() {
let prop = create_test_sho_with_stm();
assert_eq!(DStatePropagatorTrait::state_dim(&prop), 2);
let stm = prop.stm().expect("STM should be enabled");
assert_eq!(stm.nrows(), 2);
assert_eq!(stm.ncols(), 2);
assert_abs_diff_eq!(stm[(0, 0)], 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(stm[(1, 1)], 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(stm[(0, 1)], 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(stm[(1, 0)], 0.0, epsilon = 1e-12);
}
#[test]
fn test_dnumericalpropagator_construction_with_sensitivity() {
let prop = create_test_damped_sho_with_sensitivity();
assert_eq!(DStatePropagatorTrait::state_dim(&prop), 2);
let sens = prop.sensitivity().expect("Sensitivity should be enabled");
assert_eq!(sens.nrows(), 2); assert_eq!(sens.ncols(), 2);
for i in 0..2 {
for j in 0..2 {
assert_abs_diff_eq!(sens[(i, j)], 0.0, epsilon = 1e-12);
}
}
}
#[test]
fn test_dstatepropagator_step_by_forward() {
let mut prop = create_test_sho_propagator();
let initial_epoch = prop.current_epoch();
let period = 2.0 * PI;
prop.step_by(period);
let state = prop.current_state();
assert_abs_diff_eq!(state[0], 1.0, epsilon = 1e-3);
assert_abs_diff_eq!(state[1], 0.0, epsilon = 2e-3);
assert_abs_diff_eq!(
(prop.current_epoch() - initial_epoch).abs(),
period,
epsilon = 0.1
);
}
#[test]
fn test_dstatepropagator_propagate_to_forward() {
let mut prop = create_test_sho_propagator();
let initial_epoch = prop.current_epoch();
let target_epoch = initial_epoch + PI;
prop.propagate_to(target_epoch);
let state = prop.current_state();
assert_abs_diff_eq!(state[0], -1.0, epsilon = 1e-3);
assert_abs_diff_eq!(state[1], 0.0, epsilon = 2e-3);
assert_abs_diff_eq!(
(prop.current_epoch() - target_epoch).abs(),
0.0,
epsilon = 0.1
);
}
#[test]
fn test_dstatepropagator_step_by_backward() {
let mut prop = create_test_sho_propagator();
prop.step_by(PI);
let _mid_state = prop.current_state();
prop.step_by(-PI);
let state = prop.current_state();
assert_abs_diff_eq!(state[0], 1.0, epsilon = 1e-3);
assert_abs_diff_eq!(state[1], 0.0, epsilon = 2e-3);
}
#[test]
fn test_dstatepropagator_propagate_to_backward() {
let mut prop = create_test_sho_propagator();
let initial_epoch = prop.current_epoch();
prop.propagate_to(initial_epoch + PI);
prop.propagate_to(initial_epoch);
let state = prop.current_state();
assert_abs_diff_eq!(state[0], 1.0, epsilon = 1e-3);
assert_abs_diff_eq!(state[1], 0.0, epsilon = 2e-3);
}
#[test]
fn test_dstatepropagator_propagate_steps() {
let mut prop = create_test_sho_propagator();
prop.propagate_steps(10);
assert!(prop.trajectory().len() >= 10);
let state = prop.current_state();
assert!((state[0] - 1.0).abs() > 1e-6 || (state[1] - 0.0).abs() > 1e-6);
}
#[test]
fn test_dstatepropagator_reset() {
let mut prop = create_test_sho_propagator();
let initial_epoch = prop.initial_epoch();
let initial_state = prop.initial_state();
prop.propagate_to(initial_epoch + 10.0);
assert!((prop.current_state()[0] - initial_state[0]).abs() > 1e-6);
prop.reset();
assert_eq!(prop.current_epoch(), initial_epoch);
assert_eq!(prop.current_state(), initial_state);
assert_eq!(prop.trajectory().len(), 0); }
#[test]
fn test_dstatepropagator_getters() {
let mut prop = create_test_sho_propagator();
let initial_epoch = prop.initial_epoch();
assert_eq!(DStatePropagatorTrait::state_dim(&prop), 2);
assert_eq!(prop.current_epoch(), initial_epoch);
assert_eq!(prop.initial_epoch(), initial_epoch);
assert_eq!(prop.initial_state(), DVector::from_vec(vec![1.0, 0.0]));
prop.step_by(1.0);
assert!(prop.current_epoch() > initial_epoch);
assert_ne!(prop.current_state(), prop.initial_state());
}
#[test]
fn test_dstateprovider_state_at_current() {
let mut prop = create_test_sho_propagator();
prop.propagate_to(prop.initial_epoch() + 1.0);
let current_epoch = prop.current_epoch();
let state = prop.state(current_epoch).unwrap();
assert_abs_diff_eq!(state[0], prop.current_state()[0], epsilon = 1e-12);
assert_abs_diff_eq!(state[1], prop.current_state()[1], epsilon = 1e-12);
}
#[test]
fn test_dstateprovider_state_interpolation() {
let mut prop = create_test_sho_propagator();
let initial_epoch = prop.initial_epoch();
prop.propagate_to(initial_epoch + 10.0);
let mid_epoch = initial_epoch + 5.0;
let state = prop.state(mid_epoch).unwrap();
assert_eq!(state.len(), 2);
assert!(state[0].abs() <= 1.5); }
#[test]
fn test_dstateprovider_state_out_of_bounds() {
let prop = create_test_sho_propagator();
let initial_epoch = prop.initial_epoch();
let result = prop.state(initial_epoch - 10.0);
assert!(result.is_err());
}
#[test]
fn test_dstateprovider_state_dim() {
let prop = create_test_sho_propagator();
assert_eq!(DStateProvider::state_dim(&prop), 2);
}
#[test]
fn test_dstateprovider_state_dimension_preservation() {
let mut prop = create_test_sho_propagator();
prop.propagate_to(prop.initial_epoch() + 5.0);
let mid_epoch = prop.initial_epoch() + 2.5;
let state = prop.state(mid_epoch).unwrap();
assert_eq!(state.len(), 2); }
#[test]
fn test_dcovarianceprovider_no_covariance() {
let prop = create_test_sho_propagator();
let result = prop.covariance(prop.current_epoch());
assert!(result.is_err());
}
#[test]
fn test_dcovarianceprovider_with_initial_covariance() {
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let dynamics = sho_dynamics(1.0);
let initial_cov = DMatrix::from_diagonal(&DVector::from_vec(vec![0.01, 0.01]));
let mut config = NumericalPropagationConfig::default();
config.variational.enable_stm = true;
let mut prop = DNumericalPropagator::new(
epoch,
state,
dynamics,
config,
None,
None,
Some(initial_cov.clone()),
)
.unwrap();
prop.propagate_to(epoch + 5.0);
let cov = prop.covariance(prop.current_epoch()).unwrap();
assert_eq!(cov.nrows(), 2);
assert_eq!(cov.ncols(), 2);
}
#[test]
fn test_dcovarianceprovider_positive_definiteness() {
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let dynamics = sho_dynamics(1.0);
let initial_cov = DMatrix::from_diagonal(&DVector::from_vec(vec![0.01, 0.01]));
let mut config = NumericalPropagationConfig::default();
config.variational.enable_stm = true;
let mut prop = DNumericalPropagator::new(
epoch,
state,
dynamics,
config,
None,
None,
Some(initial_cov),
)
.unwrap();
prop.propagate_to(epoch + 5.0);
let cov = prop.covariance(prop.current_epoch()).unwrap();
assert!(cov[(0, 0)] > 0.0);
assert!(cov[(1, 1)] > 0.0);
assert_abs_diff_eq!(cov[(0, 1)], cov[(1, 0)], epsilon = 1e-12);
}
#[test]
fn test_dcovarianceprovider_interpolation() {
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let dynamics = sho_dynamics(1.0);
let initial_cov = DMatrix::from_diagonal(&DVector::from_vec(vec![0.01, 0.01]));
let mut config = NumericalPropagationConfig::default();
config.variational.enable_stm = true;
let mut prop = DNumericalPropagator::new(
epoch,
state,
dynamics,
config,
None,
None,
Some(initial_cov),
)
.unwrap();
prop.propagate_to(epoch + 10.0);
let mid_epoch = epoch + 5.0;
let cov = prop.covariance(mid_epoch).unwrap();
assert_eq!(cov.nrows(), 2);
assert_eq!(cov.ncols(), 2);
}
#[test]
fn test_dcovarianceprovider_out_of_bounds() {
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let dynamics = sho_dynamics(1.0);
let initial_cov = DMatrix::from_diagonal(&DVector::from_vec(vec![0.01, 0.01]));
let mut config = NumericalPropagationConfig::default();
config.variational.enable_stm = true;
let mut prop = DNumericalPropagator::new(
epoch,
state,
dynamics,
config,
None,
None,
Some(initial_cov),
)
.unwrap();
prop.propagate_to(epoch + 10.0);
let result = prop.covariance(epoch + 20.0);
assert!(result.is_err());
}
#[test]
fn test_stm_identity_initialization() {
let prop = create_test_sho_with_stm();
let stm = prop.stm().expect("STM should be enabled");
for i in 0..2 {
for j in 0..2 {
let expected = if i == j { 1.0 } else { 0.0 };
assert_abs_diff_eq!(stm[(i, j)], expected, epsilon = 1e-12);
}
}
}
#[test]
fn test_stm_propagation() {
let mut prop = create_test_sho_with_stm();
prop.propagate_to(prop.initial_epoch() + 5.0);
let stm = prop.stm().expect("STM should be available");
assert!((stm[(0, 0)] - 1.0).abs() > 1e-6 || (stm[(1, 1)] - 1.0).abs() > 1e-6);
assert_eq!(stm.nrows(), 2);
assert_eq!(stm.ncols(), 2);
}
#[test]
fn test_stm_storage_in_trajectory() {
let mut prop = create_test_sho_with_stm();
prop.propagate_to(prop.initial_epoch() + 10.0);
assert!(prop.trajectory().len() > 0);
let stm_at_0 = prop.stm_at_idx(0);
assert!(stm_at_0.is_some());
let last_idx = prop.trajectory().len() - 1;
let stm_at_last = prop.stm_at_idx(last_idx);
assert!(stm_at_last.is_some());
}
#[test]
fn test_stm_interpolation() {
let mut prop = create_test_sho_with_stm();
let initial_epoch = prop.initial_epoch();
prop.propagate_to(initial_epoch + 10.0);
let mid_epoch = initial_epoch + 5.0;
let stm = prop.stm_at(mid_epoch);
assert!(stm.is_some());
let stm_matrix = stm.unwrap();
assert_eq!(stm_matrix.nrows(), 2);
assert_eq!(stm_matrix.ncols(), 2);
}
#[test]
fn test_stm_reset() {
let mut prop = create_test_sho_with_stm();
prop.propagate_to(prop.initial_epoch() + 5.0);
let stm_after_prop = prop.stm().unwrap().clone();
assert!((stm_after_prop[(0, 0)] - 1.0).abs() > 1e-6);
prop.reset();
let stm = prop.stm().expect("STM should still be enabled");
for i in 0..2 {
for j in 0..2 {
let expected = if i == j { 1.0 } else { 0.0 };
assert_abs_diff_eq!(stm[(i, j)], expected, epsilon = 1e-12);
}
}
}
#[test]
fn test_stm_energy_conservation_check() {
let mut prop = create_test_sho_with_stm();
let period = 2.0 * PI;
prop.propagate_to(prop.initial_epoch() + period);
let state = prop.current_state();
assert_abs_diff_eq!(state[0], 1.0, epsilon = 1e-3);
assert_abs_diff_eq!(state[1], 0.0, epsilon = 2e-3);
let stm = prop.stm().unwrap();
let det = stm[(0, 0)] * stm[(1, 1)] - stm[(0, 1)] * stm[(1, 0)];
assert_abs_diff_eq!(det, 1.0, epsilon = 1e-3);
}
#[test]
fn test_sensitivity_zero_initialization() {
let prop = create_test_damped_sho_with_sensitivity();
let sens = prop.sensitivity().expect("Sensitivity should be enabled");
for i in 0..2 {
for j in 0..2 {
assert_abs_diff_eq!(sens[(i, j)], 0.0, epsilon = 1e-12);
}
}
}
#[test]
fn test_sensitivity_propagation() {
let mut prop = create_test_damped_sho_with_sensitivity();
prop.propagate_to(prop.initial_epoch() + 5.0);
let sens = prop.sensitivity().expect("Sensitivity should be available");
let mut has_nonzero = false;
for i in 0..2 {
for j in 0..2 {
if sens[(i, j)].abs() > 1e-6 {
has_nonzero = true;
break;
}
}
}
assert!(has_nonzero, "Sensitivity should have non-zero elements");
assert_eq!(sens.nrows(), 2); assert_eq!(sens.ncols(), 2); }
#[test]
fn test_sensitivity_parameter_dependence() {
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let mut config = NumericalPropagationConfig::default();
config.variational.enable_sensitivity = true;
let params1 = DVector::from_vec(vec![1.0, 0.1]); let params2 = DVector::from_vec(vec![1.0, 0.5]);
let mut prop1 = DNumericalPropagator::new(
epoch,
state.clone(),
damped_sho_dynamics(),
config.clone(),
Some(params1),
None,
None,
)
.unwrap();
let mut prop2 = DNumericalPropagator::new(
epoch,
state,
damped_sho_dynamics(),
config,
Some(params2),
None,
None,
)
.unwrap();
prop1.propagate_to(epoch + 5.0);
prop2.propagate_to(epoch + 5.0);
let state1 = prop1.current_state();
let state2 = prop2.current_state();
assert!((state1[0] - state2[0]).abs() > 1e-6 || (state1[1] - state2[1]).abs() > 1e-6);
}
#[test]
fn test_sensitivity_storage_in_trajectory() {
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let params = DVector::from_vec(vec![1.0, 0.1]);
let dynamics = damped_sho_dynamics();
let mut config = NumericalPropagationConfig::default();
config.variational.enable_sensitivity = true;
config.variational.store_sensitivity_history = true;
let mut prop =
DNumericalPropagator::new(epoch, state, dynamics, config, Some(params), None, None)
.unwrap();
prop.propagate_to(epoch + 10.0);
assert!(prop.trajectory().len() > 0);
let sens_at_0 = prop.sensitivity_at_idx(0);
assert!(sens_at_0.is_some());
}
#[test]
fn test_sensitivity_interpolation() {
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let params = DVector::from_vec(vec![1.0, 0.1]);
let dynamics = damped_sho_dynamics();
let mut config = NumericalPropagationConfig::default();
config.variational.enable_sensitivity = true;
config.variational.store_sensitivity_history = true;
let mut prop =
DNumericalPropagator::new(epoch, state, dynamics, config, Some(params), None, None)
.unwrap();
prop.propagate_to(epoch + 10.0);
let mid_epoch = epoch + 5.0;
let sens = prop.sensitivity_at(mid_epoch);
assert!(sens.is_some());
let sens_matrix = sens.unwrap();
assert_eq!(sens_matrix.nrows(), 2);
assert_eq!(sens_matrix.ncols(), 2);
}
#[test]
fn test_sensitivity_reset() {
let mut prop = create_test_damped_sho_with_sensitivity();
prop.propagate_to(prop.initial_epoch() + 5.0);
let sens_after_prop = prop.sensitivity().unwrap().clone();
let mut has_nonzero = false;
for i in 0..2 {
for j in 0..2 {
if sens_after_prop[(i, j)].abs() > 1e-6 {
has_nonzero = true;
break;
}
}
}
assert!(has_nonzero);
prop.reset();
let sens = prop
.sensitivity()
.expect("Sensitivity should still be enabled");
for i in 0..2 {
for j in 0..2 {
assert_abs_diff_eq!(sens[(i, j)], 0.0, epsilon = 1e-12);
}
}
}
#[test]
fn test_interpolationconfig_builder() {
use crate::math::interpolation::InterpolationMethod;
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let dynamics = sho_dynamics(1.0);
let prop = DNumericalPropagator::new(
epoch,
state,
dynamics,
NumericalPropagationConfig::default(),
None,
None,
None,
)
.unwrap()
.with_interpolation_method(InterpolationMethod::Linear);
assert_eq!(prop.get_interpolation_method(), InterpolationMethod::Linear);
}
#[test]
fn test_interpolationconfig_setter_getter() {
use crate::math::interpolation::InterpolationMethod;
let mut prop = create_test_sho_propagator();
assert_eq!(prop.get_interpolation_method(), InterpolationMethod::Linear);
prop.set_interpolation_method(InterpolationMethod::HermiteCubic);
assert_eq!(
prop.get_interpolation_method(),
InterpolationMethod::HermiteCubic
);
}
#[test]
fn test_interpolationconfig_persistence() {
use crate::math::interpolation::InterpolationMethod;
let mut prop = create_test_sho_propagator();
prop.set_interpolation_method(InterpolationMethod::Linear);
prop.propagate_to(prop.initial_epoch() + 5.0);
assert_eq!(prop.get_interpolation_method(), InterpolationMethod::Linear);
}
#[test]
fn test_hermite_interpolation_requires_6d_states() {
use crate::math::interpolation::InterpolationMethod;
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let config_cubic = NumericalPropagationConfig::default()
.with_interpolation_method(InterpolationMethod::HermiteCubic);
let result = DNumericalPropagator::new(
epoch,
state.clone(),
sho_dynamics(1.0),
config_cubic,
None,
None,
None,
);
assert!(result.is_err());
let err = result.err().unwrap();
let err_msg = err.to_string();
assert!(err_msg.contains("HermiteCubic"));
assert!(err_msg.contains("6D states"));
let config_quintic = NumericalPropagationConfig::default()
.with_interpolation_method(InterpolationMethod::HermiteQuintic);
let result = DNumericalPropagator::new(
epoch,
state,
sho_dynamics(1.0),
config_quintic,
None,
None,
None,
);
assert!(result.is_err());
let err = result.err().unwrap();
let err_msg = err.to_string();
assert!(err_msg.contains("HermiteQuintic"));
assert!(err_msg.contains("6D states"));
}
#[test]
fn test_covarianceinterpolationconfig_builder() {
use crate::math::interpolation::CovarianceInterpolationMethod;
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let dynamics = sho_dynamics(1.0);
let prop = DNumericalPropagator::new(
epoch,
state,
dynamics,
NumericalPropagationConfig::default(),
None,
None,
None,
)
.unwrap()
.with_covariance_interpolation_method(CovarianceInterpolationMethod::MatrixSquareRoot);
assert_eq!(
prop.get_covariance_interpolation_method(),
CovarianceInterpolationMethod::MatrixSquareRoot
);
}
#[test]
fn test_covarianceinterpolationconfig_setter_getter() {
use crate::math::interpolation::CovarianceInterpolationMethod;
let mut prop = create_test_sho_propagator();
assert_eq!(
prop.get_covariance_interpolation_method(),
CovarianceInterpolationMethod::TwoWasserstein
);
prop.set_covariance_interpolation_method(CovarianceInterpolationMethod::MatrixSquareRoot);
assert_eq!(
prop.get_covariance_interpolation_method(),
CovarianceInterpolationMethod::MatrixSquareRoot
);
}
#[test]
fn test_covarianceinterpolationconfig_persistence() {
use crate::math::interpolation::CovarianceInterpolationMethod;
let mut prop = create_test_sho_propagator();
prop.set_covariance_interpolation_method(CovarianceInterpolationMethod::MatrixSquareRoot);
prop.propagate_to(prop.initial_epoch() + 5.0);
assert_eq!(
prop.get_covariance_interpolation_method(),
CovarianceInterpolationMethod::MatrixSquareRoot
);
}
#[test]
fn test_identifiable_with_name() {
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let dynamics = sho_dynamics(1.0);
let prop = DNumericalPropagator::new(
epoch,
state,
dynamics,
NumericalPropagationConfig::default(),
None,
None,
None,
)
.unwrap()
.with_name("TestProp");
assert_eq!(prop.get_name(), Some("TestProp"));
}
#[test]
fn test_identifiable_set_name() {
let mut prop = create_test_sho_propagator();
assert_eq!(prop.get_name(), None);
prop.set_name(Some("MyPropagator"));
assert_eq!(prop.get_name(), Some("MyPropagator"));
prop.set_name(None);
assert_eq!(prop.get_name(), None);
}
#[test]
fn test_identifiable_with_id() {
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let dynamics = sho_dynamics(1.0);
let prop = DNumericalPropagator::new(
epoch,
state,
dynamics,
NumericalPropagationConfig::default(),
None,
None,
None,
)
.unwrap()
.with_id(12345);
assert_eq!(prop.get_id(), Some(12345));
}
#[test]
fn test_identifiable_set_id() {
let mut prop = create_test_sho_propagator();
assert_eq!(prop.get_id(), None);
prop.set_id(Some(999));
assert_eq!(prop.get_id(), Some(999));
prop.set_id(None);
assert_eq!(prop.get_id(), None);
}
#[test]
fn test_identifiable_with_uuid() {
use uuid::Uuid;
let test_uuid = Uuid::new_v4();
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let dynamics = sho_dynamics(1.0);
let prop = DNumericalPropagator::new(
epoch,
state,
dynamics,
NumericalPropagationConfig::default(),
None,
None,
None,
)
.unwrap()
.with_uuid(test_uuid);
assert_eq!(prop.get_uuid(), Some(test_uuid));
}
#[test]
fn test_identifiable_with_new_uuid() {
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let dynamics = sho_dynamics(1.0);
let prop = DNumericalPropagator::new(
epoch,
state,
dynamics,
NumericalPropagationConfig::default(),
None,
None,
None,
)
.unwrap()
.with_new_uuid();
assert!(prop.get_uuid().is_some());
}
#[test]
fn test_identifiable_with_identity() {
use uuid::Uuid;
let test_uuid = Uuid::new_v4();
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let dynamics = sho_dynamics(1.0);
let prop = DNumericalPropagator::new(
epoch,
state,
dynamics,
NumericalPropagationConfig::default(),
None,
None,
None,
)
.unwrap()
.with_identity(Some("TestProp"), Some(test_uuid), Some(123));
assert_eq!(prop.get_name(), Some("TestProp"));
assert_eq!(prop.get_uuid(), Some(test_uuid));
assert_eq!(prop.get_id(), Some(123));
}
#[test]
fn test_identifiable_persistence_through_propagation() {
let mut prop = create_test_sho_propagator();
prop.set_name(Some("TestProp"));
prop.set_id(Some(42));
prop.propagate_to(prop.initial_epoch() + 5.0);
assert_eq!(prop.get_name(), Some("TestProp"));
assert_eq!(prop.get_id(), Some(42));
}
#[test]
fn test_event_time_event() {
let mut prop = create_test_sho_propagator();
let initial_epoch = prop.initial_epoch();
let event = DTimeEvent::new(initial_epoch + 5.0, "TimeEvent".to_string());
prop.add_event_detector(Box::new(event));
prop.propagate_to(initial_epoch + 10.0);
let events = prop.event_log();
assert!(!events.is_empty());
let detected = events.iter().any(|e| e.name.contains("TimeEvent"));
assert!(detected);
}
#[test]
fn test_event_value_event() {
let mut prop = create_test_sho_propagator();
let initial_epoch = prop.initial_epoch();
let value_fn =
|_epoch: Epoch, state: &DVector<f64>, _params: Option<&DVector<f64>>| state[0];
let event = DValueEvent::new(
"PositionCrossing",
value_fn,
0.0, EventDirection::Decreasing, );
prop.add_event_detector(Box::new(event));
prop.propagate_to(initial_epoch + 10.0);
let events = prop.event_log();
assert!(!events.is_empty(), "No events detected in event log");
let detected = events.iter().any(|e| e.name.contains("PositionCrossing"));
assert!(detected, "PositionCrossing event not found in event log");
}
#[test]
fn test_event_callback_state_modification() {
use crate::events::EventAction;
let mut prop = create_test_sho_propagator();
let initial_epoch = prop.initial_epoch();
let callback: DEventCallback = Box::new(|_epoch, state, _params| {
let mut new_state = state.clone();
new_state[1] += 0.1; (Some(new_state), None, EventAction::Continue)
});
let value_fn =
|_epoch: Epoch, state: &DVector<f64>, _params: Option<&DVector<f64>>| state[0];
let event = DValueEvent::new("ImpulseAtZero", value_fn, 0.0, EventDirection::Decreasing)
.with_callback(callback);
prop.add_event_detector(Box::new(event));
prop.propagate_to(initial_epoch + 3.0);
let events = prop.event_log();
assert!(!events.is_empty(), "No events detected in event log");
let detected = events.iter().any(|e| e.name.contains("ImpulseAtZero"));
assert!(detected, "ImpulseAtZero event not found in event log");
}
#[test]
fn test_event_terminal() {
use crate::events::EventAction;
let mut prop = create_test_sho_propagator();
let initial_epoch = prop.initial_epoch();
let callback: DEventCallback =
Box::new(|_epoch, _state, _params| (None, None, EventAction::Stop));
let event =
DTimeEvent::new(initial_epoch + 5.0, "Terminal".to_string()).with_callback(callback);
prop.add_event_detector(Box::new(event));
prop.propagate_to(initial_epoch + 10.0);
assert!(prop.terminated());
let time_diff: f64 = prop.current_epoch() - (initial_epoch + 5.0);
assert!(time_diff.abs() < 1.0);
}
#[test]
fn test_event_query_by_detector_index() {
let mut prop = create_test_sho_propagator();
let initial_epoch = prop.initial_epoch();
prop.add_event_detector(Box::new(DTimeEvent::new(
initial_epoch + 2.0,
"Event1".to_string(),
)));
prop.add_event_detector(Box::new(DTimeEvent::new(
initial_epoch + 4.0,
"Event2".to_string(),
)));
prop.propagate_to(initial_epoch + 5.0);
let events_0 = prop.events_by_detector_index(0);
let events_1 = prop.events_by_detector_index(1);
assert!(!events_0.is_empty());
assert!(!events_1.is_empty());
}
#[test]
fn test_event_query_in_range() {
let mut prop = create_test_sho_propagator();
let initial_epoch = prop.initial_epoch();
prop.add_event_detector(Box::new(DTimeEvent::new(
initial_epoch + 2.0,
"Early".to_string(),
)));
prop.add_event_detector(Box::new(DTimeEvent::new(
initial_epoch + 8.0,
"Late".to_string(),
)));
prop.propagate_to(initial_epoch + 10.0);
let events_early = prop.events_in_range(initial_epoch, initial_epoch + 5.0);
let events_late = prop.events_in_range(initial_epoch + 5.0, initial_epoch + 10.0);
let has_early = events_early.iter().any(|e| e.name.contains("Early"));
assert!(has_early);
let has_late = events_late.iter().any(|e| e.name.contains("Late"));
assert!(has_late);
}
#[test]
fn test_event_clear_and_reset_termination() {
use crate::events::EventAction;
let mut prop = create_test_sho_propagator();
let initial_epoch = prop.initial_epoch();
let callback: DEventCallback =
Box::new(|_epoch, _state, _params| (None, None, EventAction::Stop));
let event =
DTimeEvent::new(initial_epoch + 5.0, "Terminal".to_string()).with_callback(callback);
prop.add_event_detector(Box::new(event));
prop.propagate_to(initial_epoch + 10.0);
assert!(prop.terminated());
prop.clear_events();
prop.reset_termination();
assert!(!prop.terminated());
assert_eq!(prop.event_log().len(), 0);
prop.propagate_to(initial_epoch + 15.0);
}
#[test]
fn test_dnumericalpropagator_events_combined_filters() {
let mut prop = create_test_sho_propagator();
let initial_epoch = prop.initial_epoch();
let event1 = DTimeEvent::new(initial_epoch + 2.0, "Early".to_string());
let event2 = DTimeEvent::new(initial_epoch + 4.0, "Middle".to_string());
let event3 = DTimeEvent::new(initial_epoch + 6.0, "Late".to_string());
let event4 = DTimeEvent::new(initial_epoch + 8.0, "VeryLate".to_string());
prop.add_event_detector(Box::new(event1));
prop.add_event_detector(Box::new(event2));
prop.add_event_detector(Box::new(event3));
prop.add_event_detector(Box::new(event4));
prop.propagate_to(initial_epoch + 10.0);
let events_0 = prop.events_by_detector_index(0);
let events_1 = prop.events_by_detector_index(1);
assert_eq!(events_0.len(), 1);
assert_eq!(events_1.len(), 1);
let early_events = prop.events_in_range(initial_epoch, initial_epoch + 3.0);
let middle_events = prop.events_in_range(initial_epoch + 3.0, initial_epoch + 5.0);
let late_events = prop.events_in_range(initial_epoch + 5.0, initial_epoch + 10.0);
assert_eq!(early_events.len(), 1);
assert!(early_events[0].name.contains("Early"));
assert_eq!(middle_events.len(), 1);
assert!(middle_events[0].name.contains("Middle"));
assert_eq!(late_events.len(), 2);
assert!(late_events.iter().any(|e| e.name.contains("Late")));
assert!(late_events.iter().any(|e| e.name.contains("VeryLate")));
let early_by_name = prop.events_by_name("Early");
assert_eq!(early_by_name.len(), 1);
let middle_by_name = prop.events_by_name("Middle");
assert_eq!(middle_by_name.len(), 1);
let middle_event = &middle_by_name[0];
let time_diff: f64 = middle_event.window_open - (initial_epoch + 4.0);
assert!(time_diff.abs() < 0.1);
}
#[test]
fn test_trajectory_allsteps_mode() {
let mut prop = create_test_sho_propagator();
prop.set_trajectory_mode(TrajectoryMode::AllSteps);
prop.propagate_to(prop.initial_epoch() + 5.0);
assert!(prop.trajectory().len() > 1);
}
#[test]
fn test_trajectory_disabled_mode() {
let mut prop = create_test_sho_propagator();
prop.reset();
prop.set_trajectory_mode(TrajectoryMode::Disabled);
prop.propagate_to(prop.initial_epoch() + 5.0);
assert_eq!(prop.trajectory().len(), 0);
}
#[test]
fn test_trajectory_eviction_max_size() {
let mut prop = create_test_sho_propagator();
prop.set_eviction_policy_max_size(10).unwrap();
prop.propagate_to(prop.initial_epoch() + 50.0);
assert!(prop.trajectory().len() <= 10);
}
#[test]
fn test_trajectory_stm_sensitivity_storage() {
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let params = DVector::from_vec(vec![1.0, 0.1]);
let dynamics = damped_sho_dynamics();
let mut config = NumericalPropagationConfig::default();
config.variational.enable_stm = true;
config.variational.store_stm_history = true;
config.variational.enable_sensitivity = true;
config.variational.store_sensitivity_history = true;
let mut prop =
DNumericalPropagator::new(epoch, state, dynamics, config, Some(params), None, None)
.unwrap();
prop.propagate_to(epoch + 5.0);
assert!(prop.stm_at_idx(0).is_some());
assert!(prop.sensitivity_at_idx(0).is_some());
}
#[test]
fn test_corner_case_zero_parameters() {
let mut prop = create_test_sho_propagator();
assert_eq!(prop.current_params().len(), 0);
prop.propagate_to(prop.initial_epoch() + 5.0);
let state = prop.current_state();
assert_eq!(state.len(), 2);
}
#[test]
fn test_corner_case_single_parameter() {
let single_param_dynamics: DStateDynamics = Box::new(|_t, x, p| {
let k = p.map(|params| params[0]).unwrap_or(1.0);
DVector::from_vec(vec![x[1], -k * k * x[0]])
});
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let params = DVector::from_vec(vec![1.5]);
let mut config = NumericalPropagationConfig::default();
config.variational.enable_sensitivity = true;
let mut prop = DNumericalPropagator::new(
epoch,
state,
single_param_dynamics,
config,
Some(params),
None,
None,
)
.unwrap();
prop.propagate_to(epoch + 5.0);
let sens = prop.sensitivity().unwrap();
assert_eq!(sens.nrows(), 2);
assert_eq!(sens.ncols(), 1);
}
#[test]
fn test_corner_case_sensitivity_without_params() {
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let dynamics = sho_dynamics(1.0);
let mut config = NumericalPropagationConfig::default();
config.variational.enable_sensitivity = true;
let result = DNumericalPropagator::new(epoch, state, dynamics, config, None, None, None);
assert!(result.is_err());
}
#[test]
fn test_corner_case_very_small_timestep() {
let mut prop = create_test_sho_propagator();
let initial_epoch = prop.initial_epoch();
prop.propagate_to(initial_epoch + 1e-6);
assert!(prop.current_epoch() > initial_epoch);
}
#[test]
fn test_dnumericalpropagator_take_event_detectors() {
let mut prop = create_test_sho_propagator();
let initial_epoch = prop.initial_epoch();
let detector1 = DTimeEvent::new(initial_epoch + 2.0, "Event1");
let detector2 = DTimeEvent::new(initial_epoch + 4.0, "Event2");
prop.add_event_detector(Box::new(detector1));
prop.add_event_detector(Box::new(detector2));
let taken = prop.take_event_detectors();
assert_eq!(taken.len(), 2);
prop.propagate_to(initial_epoch + 5.0);
assert!(prop.event_log().is_empty());
}
#[test]
fn test_dnumericalpropagator_set_event_detectors() {
let mut prop = create_test_sho_propagator();
let initial_epoch = prop.initial_epoch();
let detectors: Vec<Box<dyn crate::events::DEventDetector>> = vec![
Box::new(DTimeEvent::new(initial_epoch + 2.0, "Event1")),
Box::new(DTimeEvent::new(initial_epoch + 4.0, "Event2")),
];
prop.set_event_detectors(detectors);
prop.propagate_to(initial_epoch + 5.0);
assert_eq!(prop.event_log().len(), 2);
}
#[test]
fn test_dnumericalpropagator_take_event_log() {
let mut prop = create_test_sho_propagator();
let initial_epoch = prop.initial_epoch();
let detector = DTimeEvent::new(initial_epoch + 2.0, "TestEvent");
prop.add_event_detector(Box::new(detector));
prop.propagate_to(initial_epoch + 3.0);
assert_eq!(prop.event_log().len(), 1);
let taken_log = prop.take_event_log();
assert_eq!(taken_log.len(), 1);
assert_eq!(taken_log[0].name, "TestEvent");
assert!(prop.event_log().is_empty());
}
#[test]
fn test_dnumericalpropagator_set_terminated_is_terminated() {
let mut prop = create_test_sho_propagator();
assert!(!prop.is_terminated());
prop.set_terminated(true);
assert!(prop.is_terminated());
prop.set_terminated(false);
assert!(!prop.is_terminated());
}
#[test]
fn test_dnumericalpropagator_event_detector_roundtrip() {
let mut prop = create_test_sho_propagator();
let initial_epoch = prop.initial_epoch();
let detector = DTimeEvent::new(initial_epoch + 3.0, "RoundtripEvent");
prop.add_event_detector(Box::new(detector));
let taken = prop.take_event_detectors();
assert_eq!(taken.len(), 1);
prop.propagate_to(initial_epoch + 2.0);
assert!(prop.event_log().is_empty());
prop.set_event_detectors(taken);
prop.propagate_to(initial_epoch + 4.0);
assert_eq!(prop.event_log().len(), 1);
assert!(prop.event_log()[0].name.contains("RoundtripEvent"));
}
#[test]
fn test_dnumericalpropagator_reinitialize_epoch_and_state() {
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let dynamics = sho_dynamics(1.0);
let config = NumericalPropagationConfig::default();
let mut prop =
DNumericalPropagator::new(epoch, state.clone(), dynamics, config, None, None, None)
.unwrap();
prop.propagate_to(epoch + 1.0);
let state_at_1 = prop.current_state();
assert_ne!(state_at_1[0], 1.0, "State should have changed");
let new_state = DVector::from_vec(vec![2.0, 0.0]);
let new_epoch = epoch + 1.0;
prop.reinitialize(new_epoch, new_state.clone(), None);
assert_eq!(prop.current_epoch(), new_epoch);
assert_eq!(prop.current_state(), new_state);
prop.propagate_to(new_epoch + 1.0);
let state_at_2 = prop.current_state();
assert_ne!(
state_at_2, new_state,
"State should change after propagation from reinitialize"
);
}
#[test]
fn test_dnumericalpropagator_reinitialize_stm_reset() {
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let dynamics = sho_dynamics(1.0);
let mut config = NumericalPropagationConfig::default();
config.variational.enable_stm = true;
let mut prop =
DNumericalPropagator::new(epoch, state.clone(), dynamics, config, None, None, None)
.unwrap();
let stm = prop.stm().unwrap().clone();
assert_eq!(stm, DMatrix::identity(2, 2));
prop.propagate_to(epoch + 1.0);
let stm_after = prop.stm().unwrap().clone();
assert_ne!(stm_after, DMatrix::identity(2, 2));
let current_state = prop.current_state();
prop.reinitialize(epoch + 1.0, current_state, None);
let stm_reinit = prop.stm().unwrap().clone();
assert_eq!(stm_reinit, DMatrix::identity(2, 2));
}
#[test]
fn test_dnumericalpropagator_reinitialize_preserves_dynamics() {
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let state = DVector::from_vec(vec![1.0, 0.0]);
let dynamics = sho_dynamics(1.0);
let config = NumericalPropagationConfig::default();
let mut prop =
DNumericalPropagator::new(epoch, state, dynamics, config, None, None, None).unwrap();
let half_pi = PI / 2.0;
prop.propagate_to(epoch + half_pi);
let state_half_pi = prop.current_state();
assert_abs_diff_eq!(state_half_pi[0], 0.0, epsilon = 1e-3);
assert_abs_diff_eq!(state_half_pi[1], -1.0, epsilon = 1e-3);
prop.reinitialize(epoch + half_pi, state_half_pi.clone(), None);
prop.propagate_to(epoch + PI);
let state_pi = prop.current_state();
assert_abs_diff_eq!(state_pi[0], -1.0, epsilon = 1e-3);
assert_abs_diff_eq!(state_pi[1], 0.0, epsilon = 1e-3);
}
}