use crate::attitude::RotationMatrix;
use crate::constants::{AngleFormat, DEG2RAD, OMEGA_EARTH, RAD2DEG};
use crate::coordinates::state_eci_to_koe;
use crate::frames::{polar_motion, state_ecef_to_eci, state_gcrf_to_eme2000, state_itrf_to_gcrf};
use crate::orbits::tle::{
TleFormat, calculate_tle_line_checksum, create_tle_lines, epoch_from_tle,
norad_id_numeric_to_alpha5, parse_norad_id, validate_tle_lines,
};
use crate::propagators::TrajectoryMode;
use crate::propagators::traits::{SOrbitStateProvider, SStatePropagator, SStateProvider};
use crate::time::{Epoch, TimeSystem};
use crate::trajectories::DOrbitTrajectory;
use crate::trajectories::traits::{OrbitFrame, OrbitRepresentation, Trajectory};
use crate::utils::{BraheError, Identifiable};
use nalgebra::{DVector, Vector3, Vector6};
use sgp4::chrono::{Datelike, NaiveDateTime, Timelike};
use crate::events::{DDetectedEvent, DEventDetector, EventAction, EventQuery, dscan_for_event};
fn tle_gmst82(epoch: Epoch, angle_format: AngleFormat) -> f64 {
let jd_ut1 = epoch.jd_as_time_system(TimeSystem::UT1);
let tut1 = (jd_ut1 - 2451545.0) / 36525.0;
let gmst_sec =
67310.54841 + (876600.0 * 3600.0 + 8640184.812866) * tut1 + 0.093104 * tut1 * tut1
- 6.2e-6 * tut1 * tut1 * tut1;
let theta = (gmst_sec * DEG2RAD / 240.0) % (2.0 * std::f64::consts::PI);
match angle_format {
AngleFormat::Radians => theta,
AngleFormat::Degrees => theta * RAD2DEG,
}
}
fn convert_state_from_spg4_frame(
epoch: Epoch,
tle_state: Vector6<f64>,
frame: OrbitFrame,
representation: OrbitRepresentation,
angle_format: Option<AngleFormat>,
) -> Vector6<f64> {
let gmst = tle_gmst82(epoch, AngleFormat::Radians);
#[allow(non_snake_case)]
let R = RotationMatrix::Rz(gmst, AngleFormat::Radians);
let omega_earth = Vector3::new(0.0, 0.0, OMEGA_EARTH);
let r_pef: Vector3<f64> = R * Vector3::<f64>::from(tle_state.fixed_rows::<3>(0));
let v_pef: Vector3<f64> =
R * Vector3::<f64>::from(tle_state.fixed_rows::<3>(3)) - omega_earth.cross(&r_pef);
#[allow(non_snake_case)]
let PM = polar_motion(epoch);
let r_ecef = PM * r_pef;
let v_ecef = PM * v_pef;
let ecef_state = Vector6::new(
r_ecef[0], r_ecef[1], r_ecef[2], v_ecef[0], v_ecef[1], v_ecef[2],
);
match representation {
OrbitRepresentation::Cartesian => match frame {
OrbitFrame::ECI => state_ecef_to_eci(epoch, ecef_state),
OrbitFrame::GCRF => state_ecef_to_eci(epoch, ecef_state),
OrbitFrame::EME2000 => {
let gcrf_state = state_ecef_to_eci(epoch, ecef_state);
state_gcrf_to_eme2000(gcrf_state)
}
OrbitFrame::ECEF => ecef_state,
OrbitFrame::ITRF => ecef_state,
},
OrbitRepresentation::Keplerian => {
if frame != OrbitFrame::ECI && frame != OrbitFrame::GCRF {
panic!("Keplerian elements must be in ECI or GCRF frame");
}
if let Some(format) = angle_format {
state_eci_to_koe(state_ecef_to_eci(epoch, ecef_state), format)
} else {
panic!("Angle format must be specified for Keplerian elements");
}
}
}
}
#[inline]
fn svec6_to_dvec(sv: &Vector6<f64>) -> DVector<f64> {
DVector::from_column_slice(sv.as_slice())
}
#[allow(non_camel_case_types)]
pub struct SGPPropagator {
pub line1: String,
pub line2: String,
pub satellite_name: Option<String>,
pub format: TleFormat,
pub norad_id_string: String,
pub norad_id: u32,
constants: sgp4::Constants,
pub epoch: Epoch,
initial_state: Vector6<f64>,
epoch_current: Epoch,
state_current: Vector6<f64>,
pub trajectory: DOrbitTrajectory,
trajectory_mode: TrajectoryMode,
pub step_size: f64,
pub frame: OrbitFrame,
pub representation: OrbitRepresentation,
pub angle_format: Option<AngleFormat>,
pub name: Option<String>,
pub id: Option<u64>,
pub uuid: Option<uuid::Uuid>,
event_detectors: Vec<Box<dyn DEventDetector>>,
event_log: Vec<DDetectedEvent>,
terminated: bool,
termination_error: Option<BraheError>,
}
impl Clone for SGPPropagator {
fn clone(&self) -> Self {
SGPPropagator {
line1: self.line1.clone(),
line2: self.line2.clone(),
satellite_name: self.satellite_name.clone(),
format: self.format,
norad_id_string: self.norad_id_string.clone(),
norad_id: self.norad_id,
constants: self.constants.clone(),
epoch: self.epoch,
initial_state: self.initial_state,
epoch_current: self.epoch_current,
state_current: self.state_current,
trajectory: self.trajectory.clone(),
trajectory_mode: self.trajectory_mode,
step_size: self.step_size,
frame: self.frame,
representation: self.representation,
angle_format: self.angle_format,
name: self.name.clone(),
id: self.id,
uuid: self.uuid,
event_detectors: Vec::new(),
event_log: Vec::new(),
terminated: false,
termination_error: None,
}
}
}
impl std::fmt::Debug for SGPPropagator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SGPPropagator")
.field("line1", &self.line1)
.field("line2", &self.line2)
.field("satellite_name", &self.satellite_name)
.field("format", &self.format)
.field("norad_id_string", &self.norad_id_string)
.field("norad_id", &self.norad_id)
.field("epoch", &self.epoch)
.field("initial_state", &self.initial_state)
.field("epoch_current", &self.epoch_current)
.field("state_current", &self.state_current)
.field("trajectory_mode", &self.trajectory_mode)
.field("step_size", &self.step_size)
.field("frame", &self.frame)
.field("representation", &self.representation)
.field("angle_format", &self.angle_format)
.field("name", &self.name)
.field("id", &self.id)
.field("uuid", &self.uuid)
.field(
"event_detectors",
&format!("[{} detectors]", self.event_detectors.len()),
)
.field("event_log", &self.event_log)
.field("terminated", &self.terminated)
.field("termination_error", &self.termination_error)
.finish()
}
}
impl SGPPropagator {
pub fn from_tle(line1: &str, line2: &str, step_size: f64) -> Result<Self, BraheError> {
Self::from_3le(None, line1, line2, step_size)
}
pub fn from_3le(
name: Option<&str>,
line1: &str,
line2: &str,
step_size: f64,
) -> Result<Self, BraheError> {
if !validate_tle_lines(line1, line2) {
return Err(BraheError::Error("Invalid TLE format".to_string()));
}
let norad_id_string = line1[2..7].trim().to_string();
let norad_id = parse_norad_id(&norad_id_string)?;
let format = if norad_id_string
.chars()
.next()
.unwrap_or('0')
.is_alphabetic()
{
TleFormat::Alpha5
} else {
TleFormat::Classic
};
let (sgp4_line1, sgp4_line2) = if format == TleFormat::Alpha5 {
let mut line1_chars: Vec<char> = line1.chars().collect();
let mut line2_chars: Vec<char> = line2.chars().collect();
for i in 2..7 {
if i < line1_chars.len() {
line1_chars[i] = '0';
}
if i < line2_chars.len() {
line2_chars[i] = '0';
}
}
let mut modified_line1: String = line1_chars.into_iter().collect();
let mut modified_line2: String = line2_chars.into_iter().collect();
if modified_line1.len() >= 69 {
let new_checksum1 = calculate_tle_line_checksum(&modified_line1);
modified_line1.replace_range(68..69, &new_checksum1.to_string());
}
if modified_line2.len() >= 69 {
let new_checksum2 = calculate_tle_line_checksum(&modified_line2);
modified_line2.replace_range(68..69, &new_checksum2.to_string());
}
(modified_line1, modified_line2)
} else {
(line1.to_string(), line2.to_string())
};
let elements = sgp4::Elements::from_tle(
Some(norad_id.to_string()),
sgp4_line1.as_bytes(),
sgp4_line2.as_bytes(),
)
.map_err(|e| BraheError::Error(format!("SGP4 parsing error: {:?}", e)))?;
let constants = sgp4::Constants::from_elements(&elements)
.map_err(|e| BraheError::Error(format!("SGP4 constants error: {:?}", e)))?;
let epoch = epoch_from_tle(line1)?;
let prediction = constants
.propagate(sgp4::MinutesSinceEpoch(0.0))
.map_err(|e| BraheError::Error(format!("SGP4 propagation error: {:?}", e)))?;
let tle_state = Vector6::new(
prediction.position[0] * 1000.0,
prediction.position[1] * 1000.0,
prediction.position[2] * 1000.0,
prediction.velocity[0] * 1000.0,
prediction.velocity[1] * 1000.0,
prediction.velocity[2] * 1000.0,
);
let initial_state = convert_state_from_spg4_frame(
epoch,
tle_state,
OrbitFrame::ECI,
OrbitRepresentation::Cartesian,
None, );
let mut trajectory =
DOrbitTrajectory::new(6, OrbitFrame::ECI, OrbitRepresentation::Cartesian, None);
if let Some(n) = name {
trajectory = trajectory.with_name(n);
}
trajectory = trajectory.with_id(norad_id as u64);
trajectory.add(epoch, svec6_to_dvec(&initial_state));
let mut result = Ok(SGPPropagator {
line1: line1.to_string(),
line2: line2.to_string(),
satellite_name: name.map(|s| s.to_string()),
format,
norad_id_string,
norad_id,
constants,
epoch,
initial_state,
epoch_current: epoch,
state_current: initial_state,
trajectory,
trajectory_mode: TrajectoryMode::OutputStepsOnly,
step_size,
frame: OrbitFrame::ECI,
representation: OrbitRepresentation::Cartesian,
angle_format: None, name: name.map(|s| s.to_string()),
id: Some(norad_id as u64),
uuid: None,
event_detectors: Vec::new(),
event_log: Vec::new(),
terminated: false,
termination_error: None,
});
if let Ok(ref mut prop) = result {
let uuid = uuid::Uuid::now_v7();
prop.uuid = Some(uuid);
prop.trajectory.uuid = Some(uuid);
}
result
}
#[allow(clippy::too_many_arguments)]
pub fn from_omm_elements(
epoch: &str,
mean_motion: f64,
eccentricity: f64,
inclination: f64,
raan: f64,
arg_of_pericenter: f64,
mean_anomaly: f64,
norad_id: u64,
step_size: f64,
object_name: Option<&str>,
object_id: Option<&str>,
classification: Option<char>,
bstar: Option<f64>,
mean_motion_dot: Option<f64>,
mean_motion_ddot: Option<f64>,
ephemeris_type: Option<u8>,
element_set_no: Option<u64>,
rev_at_epoch: Option<u64>,
) -> Result<Self, BraheError> {
let datetime = NaiveDateTime::parse_from_str(epoch, "%Y-%m-%dT%H:%M:%S%.f")
.or_else(|_| NaiveDateTime::parse_from_str(epoch, "%Y-%m-%dT%H:%M:%S"))
.map_err(|e| BraheError::Error(format!("Invalid epoch format '{}': {}", epoch, e)))?;
let classification_char = classification.unwrap_or('U');
let sgp4_classification = match classification_char {
'U' | ' ' => sgp4::Classification::Unclassified,
'C' => sgp4::Classification::Classified,
'S' => sgp4::Classification::Secret,
c => {
return Err(BraheError::Error(format!(
"Invalid classification character: '{}'. Must be 'U', 'C', or 'S'",
c
)));
}
};
let bstar_val = bstar.unwrap_or(0.0);
let mean_motion_dot_val = mean_motion_dot.unwrap_or(0.0);
let mean_motion_ddot_val = mean_motion_ddot.unwrap_or(0.0);
let ephemeris_type_val = ephemeris_type.unwrap_or(0);
let element_set_no_val = element_set_no.unwrap_or(999);
let rev_at_epoch_val = rev_at_epoch.unwrap_or(0);
let elements = sgp4::Elements {
object_name: object_name.map(|s| s.to_string()),
international_designator: object_id.map(|s| s.to_string()),
norad_id,
classification: sgp4_classification,
datetime,
mean_motion_dot: mean_motion_dot_val,
mean_motion_ddot: mean_motion_ddot_val,
drag_term: bstar_val,
element_set_number: element_set_no_val,
inclination,
right_ascension: raan,
eccentricity,
argument_of_perigee: arg_of_pericenter,
mean_anomaly,
mean_motion,
revolution_number: rev_at_epoch_val,
ephemeris_type: ephemeris_type_val,
};
let constants = sgp4::Constants::from_elements(&elements)
.map_err(|e| BraheError::Error(format!("SGP4 constants error: {:?}", e)))?;
let brahe_epoch = Epoch::from_datetime(
datetime.year() as u32,
datetime.month() as u8,
datetime.day() as u8,
datetime.hour() as u8,
datetime.minute() as u8,
datetime.second() as f64 + datetime.nanosecond() as f64 / 1e9,
0.0, TimeSystem::UTC,
);
let intl_designator = object_id.map(|id| {
if id.len() >= 5 && id.chars().nth(4) == Some('-') {
let year_2digit = &id[2..4]; let rest = &id[5..]; format!("{}{}", year_2digit, rest)
} else {
id.to_string()
}
});
let intl_designator_ref = intl_designator.as_deref().unwrap_or("");
let norad_id_str = norad_id_numeric_to_alpha5(norad_id as u32)?;
let (line1, line2) = create_tle_lines(
&brahe_epoch,
&norad_id_str,
classification_char,
intl_designator_ref,
mean_motion,
eccentricity,
inclination,
raan,
arg_of_pericenter,
mean_anomaly,
mean_motion_dot_val / 2.0,
mean_motion_ddot_val / 6.0,
bstar_val,
ephemeris_type_val,
element_set_no_val as u16,
rev_at_epoch_val as u32,
)?;
let format = if norad_id >= 100000 {
TleFormat::Alpha5
} else {
TleFormat::Classic
};
let prediction = constants
.propagate(sgp4::MinutesSinceEpoch(0.0))
.map_err(|e| BraheError::Error(format!("SGP4 propagation error: {:?}", e)))?;
let tle_state = Vector6::new(
prediction.position[0] * 1000.0,
prediction.position[1] * 1000.0,
prediction.position[2] * 1000.0,
prediction.velocity[0] * 1000.0,
prediction.velocity[1] * 1000.0,
prediction.velocity[2] * 1000.0,
);
let initial_state = convert_state_from_spg4_frame(
brahe_epoch,
tle_state,
OrbitFrame::ECI,
OrbitRepresentation::Cartesian,
None,
);
let mut trajectory =
DOrbitTrajectory::new(6, OrbitFrame::ECI, OrbitRepresentation::Cartesian, None);
if let Some(n) = object_name {
trajectory = trajectory.with_name(n);
}
trajectory = trajectory.with_id(norad_id);
trajectory.add(brahe_epoch, svec6_to_dvec(&initial_state));
let mut result = Ok(SGPPropagator {
line1,
line2,
satellite_name: object_name.map(|s| s.to_string()),
format,
norad_id_string: norad_id_str,
norad_id: norad_id as u32,
constants,
epoch: brahe_epoch,
initial_state,
epoch_current: brahe_epoch,
state_current: initial_state,
trajectory,
trajectory_mode: TrajectoryMode::OutputStepsOnly,
step_size,
frame: OrbitFrame::ECI,
representation: OrbitRepresentation::Cartesian,
angle_format: None,
name: object_name.map(|s| s.to_string()),
id: Some(norad_id),
uuid: None,
event_detectors: Vec::new(),
event_log: Vec::new(),
terminated: false,
termination_error: None,
});
if let Ok(ref mut prop) = result {
let uuid = uuid::Uuid::now_v7();
prop.uuid = Some(uuid);
prop.trajectory.uuid = Some(uuid);
}
result
}
pub fn from_gp_record(
record: &crate::types::GPRecord,
step_size: f64,
) -> Result<Self, BraheError> {
let epoch = record.epoch.as_deref().ok_or_else(|| {
BraheError::Error("GPRecord missing required field: epoch".to_string())
})?;
let mean_motion = record.mean_motion.ok_or_else(|| {
BraheError::Error("GPRecord missing required field: mean_motion".to_string())
})?;
let eccentricity = record.eccentricity.ok_or_else(|| {
BraheError::Error("GPRecord missing required field: eccentricity".to_string())
})?;
let inclination = record.inclination.ok_or_else(|| {
BraheError::Error("GPRecord missing required field: inclination".to_string())
})?;
let raan = record.ra_of_asc_node.ok_or_else(|| {
BraheError::Error("GPRecord missing required field: ra_of_asc_node".to_string())
})?;
let arg_of_pericenter = record.arg_of_pericenter.ok_or_else(|| {
BraheError::Error("GPRecord missing required field: arg_of_pericenter".to_string())
})?;
let mean_anomaly = record.mean_anomaly.ok_or_else(|| {
BraheError::Error("GPRecord missing required field: mean_anomaly".to_string())
})?;
let norad_cat_id = record.norad_cat_id.ok_or_else(|| {
BraheError::Error("GPRecord missing required field: norad_cat_id".to_string())
})?;
let classification = record
.classification_type
.as_deref()
.and_then(|s| s.chars().next());
Self::from_omm_elements(
epoch,
mean_motion,
eccentricity,
inclination,
raan,
arg_of_pericenter,
mean_anomaly,
norad_cat_id as u64,
step_size,
record.object_name.as_deref(),
record.object_id.as_deref(),
classification,
record.bstar,
record.mean_motion_dot,
record.mean_motion_ddot,
record.ephemeris_type,
record.element_set_no.map(|v| v as u64),
record.rev_at_epoch.map(|v| v as u64),
)
}
pub fn set_trajectory_mode(&mut self, mode: TrajectoryMode) {
self.trajectory_mode = mode;
}
pub fn trajectory_mode(&self) -> TrajectoryMode {
self.trajectory_mode
}
fn should_store_state(&self) -> bool {
!matches!(self.trajectory_mode, TrajectoryMode::Disabled)
}
pub fn with_output_format(
mut self,
frame: OrbitFrame,
representation: OrbitRepresentation,
angle_format: Option<AngleFormat>,
) -> Self {
if representation == OrbitRepresentation::Keplerian && angle_format.is_none() {
panic!("Angle format must be specified for Keplerian elements");
}
if representation == OrbitRepresentation::Keplerian && frame != OrbitFrame::ECI {
panic!("Keplerian elements must be in ECI frame");
}
if representation == OrbitRepresentation::Cartesian && angle_format.is_some() {
panic!("Angle format should be None for Cartesian representation");
}
self.frame = frame;
self.representation = representation;
self.angle_format = angle_format;
let name = self.trajectory.get_name().map(|s| s.to_string());
let uuid = self.trajectory.get_uuid();
let id = self.trajectory.get_id();
self.trajectory = DOrbitTrajectory::new(6, frame, representation, angle_format)
.with_identity(name.as_deref(), uuid, id);
let prediction = self
.constants
.propagate(sgp4::MinutesSinceEpoch(0.0))
.expect("SGP4 propagation failed");
let tle_state = Vector6::new(
prediction.position[0] * 1000.0,
prediction.position[1] * 1000.0,
prediction.position[2] * 1000.0,
prediction.velocity[0] * 1000.0,
prediction.velocity[1] * 1000.0,
prediction.velocity[2] * 1000.0,
);
let initial_state = convert_state_from_spg4_frame(
self.epoch,
tle_state,
frame,
representation,
angle_format,
);
self.initial_state = initial_state;
self.epoch_current = self.epoch;
self.state_current = initial_state;
self.trajectory
.add(self.epoch, svec6_to_dvec(&initial_state));
self
}
fn propagate_internal(&self, target_epoch: Epoch) -> Result<Vector6<f64>, BraheError> {
let dt = (target_epoch - self.epoch) / 60.0;
let prediction = self
.constants
.propagate(sgp4::MinutesSinceEpoch(dt))
.map_err(|e| {
BraheError::PropagatorError(format!(
"SGP4 propagation failed at {}: {}",
target_epoch, e
))
})?;
Ok(Vector6::new(
prediction.position[0] * 1000.0,
prediction.position[1] * 1000.0,
prediction.position[2] * 1000.0,
prediction.velocity[0] * 1000.0,
prediction.velocity[1] * 1000.0,
prediction.velocity[2] * 1000.0,
))
}
pub fn state_pef(&self, epoch: Epoch) -> Result<Vector6<f64>, BraheError> {
let tle_state = self.propagate_internal(epoch)?;
let gmst = tle_gmst82(epoch, AngleFormat::Radians);
#[allow(non_snake_case)]
let R = RotationMatrix::Rz(gmst, AngleFormat::Radians);
let omega_earth = Vector3::new(0.0, 0.0, OMEGA_EARTH);
let r_pef: Vector3<f64> = R * Vector3::<f64>::from(tle_state.fixed_rows::<3>(0));
let v_pef: Vector3<f64> =
R * Vector3::<f64>::from(tle_state.fixed_rows::<3>(3)) - omega_earth.cross(&r_pef);
Ok(Vector6::new(
r_pef[0], r_pef[1], r_pef[2], v_pef[0], v_pef[1], v_pef[2],
))
}
pub fn get_elements(&self, angle_format: AngleFormat) -> Result<Vector6<f64>, BraheError> {
use crate::orbits::keplerian_elements_from_tle;
let (_epoch, mut elements) = keplerian_elements_from_tle(&self.line1, &self.line2)?;
if angle_format == AngleFormat::Radians {
elements[2] *= DEG2RAD; elements[3] *= DEG2RAD; elements[4] *= DEG2RAD; elements[5] *= DEG2RAD; }
Ok(elements)
}
pub fn elements(&self) -> Vector6<f64> {
self.get_elements(AngleFormat::Degrees)
.expect("Failed to extract elements from TLE")
}
pub fn semi_major_axis(&self) -> f64 {
self.elements()[0]
}
pub fn eccentricity(&self) -> f64 {
self.elements()[1]
}
pub fn inclination(&self) -> f64 {
self.elements()[2]
}
pub fn right_ascension(&self) -> f64 {
self.elements()[3]
}
pub fn arg_perigee(&self) -> f64 {
self.elements()[4]
}
pub fn mean_anomaly(&self) -> f64 {
self.elements()[5]
}
pub fn ephemeris_age(&self) -> f64 {
Epoch::now() - self.epoch
}
fn state_eci_cartesian(&self, epoch: Epoch) -> Result<Vector6<f64>, BraheError> {
let tle_state = self.propagate_internal(epoch)?;
Ok(convert_state_from_spg4_frame(
epoch,
tle_state,
OrbitFrame::ECI,
OrbitRepresentation::Cartesian,
None,
))
}
fn scan_all_events(
&self,
epoch_prev: Epoch,
epoch_curr: Epoch,
state_prev: &DVector<f64>,
state_curr: &DVector<f64>,
) -> Vec<DDetectedEvent> {
let mut events = Vec::new();
let state_fn = |epoch: Epoch| -> DVector<f64> {
match self.state_eci_cartesian(epoch) {
Ok(state) => svec6_to_dvec(&state),
Err(_) => state_curr.clone(),
}
};
let params: Option<&DVector<f64>> = None;
for (idx, detector) in self.event_detectors.iter().enumerate() {
if detector.is_processed() {
continue;
}
if let Some(event) = dscan_for_event(
detector.as_ref(),
idx,
&state_fn,
epoch_prev,
epoch_curr,
state_prev,
state_curr,
params,
) {
events.push(event);
}
}
events.sort_by(|a, b| a.window_open.partial_cmp(&b.window_open).unwrap());
events
}
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 event_log(&self) -> &[DDetectedEvent] {
&self.event_log
}
pub fn events_by_name(&self, name: &str) -> Vec<&DDetectedEvent> {
self.event_log
.iter()
.filter(|e| e.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.event_log
.iter()
.filter(|e| e.window_open >= start && e.window_open <= end)
.collect()
}
pub fn query_events(&self) -> EventQuery<'_, std::slice::Iter<'_, DDetectedEvent>> {
EventQuery::new(self.event_log.iter())
}
pub fn events_by_detector_index(&self, index: usize) -> Vec<&DDetectedEvent> {
self.event_log
.iter()
.filter(|e| e.detector_index == index)
.collect()
}
pub fn events_by_detector_index_in_range(
&self,
index: usize,
start: Epoch,
end: Epoch,
) -> Vec<&DDetectedEvent> {
self.event_log
.iter()
.filter(|e| e.detector_index == index && e.window_open >= start && e.window_open <= end)
.collect()
}
pub fn events_by_name_in_range(
&self,
name: &str,
start: Epoch,
end: Epoch,
) -> Vec<&DDetectedEvent> {
self.event_log
.iter()
.filter(|e| e.name.contains(name) && e.window_open >= start && e.window_open <= end)
.collect()
}
pub fn clear_events(&mut self) {
self.event_log.clear();
}
pub fn reset_termination(&mut self) {
self.terminated = false;
self.termination_error = None;
}
pub fn is_terminated(&self) -> bool {
self.terminated
}
pub fn termination_error(&self) -> Option<&BraheError> {
self.termination_error.as_ref()
}
pub fn set_termination_error(&mut self, error: Option<BraheError>) {
self.termination_error = error;
}
pub fn take_termination_error(&mut self) -> Option<BraheError> {
self.termination_error.take()
}
}
impl SStatePropagator for SGPPropagator {
fn step_by(&mut self, step_size: f64) {
if self.terminated {
return;
}
let current_epoch = self.current_epoch();
let target_epoch = current_epoch + step_size;
let state_prev_eci = match self.state_eci_cartesian(current_epoch) {
Ok(state) => svec6_to_dvec(&state),
Err(e) => {
self.terminated = true;
self.termination_error = Some(e);
return;
}
};
let state_curr_eci = match self.state_eci_cartesian(target_epoch) {
Ok(state) => svec6_to_dvec(&state),
Err(e) => {
self.terminated = true;
self.termination_error = Some(e);
return;
}
};
if !self.event_detectors.is_empty() {
let detected_events = self.scan_all_events(
current_epoch,
target_epoch,
&state_prev_eci,
&state_curr_eci,
);
for event in detected_events {
let action = if let Some(detector) = self.event_detectors.get(event.detector_index)
{
if let Some(callback) = detector.callback() {
let (_, _, callback_action) =
callback(event.window_open, &event.entry_state, None);
callback_action
} else {
detector.action()
}
} else {
event.action
};
if let Some(detector) = self.event_detectors.get(event.detector_index) {
detector.mark_processed();
}
let mut logged_event = event.clone();
logged_event.action = action;
self.event_log.push(logged_event);
let event_tle_state = match self.propagate_internal(event.window_open) {
Ok(state) => state,
Err(e) => {
self.terminated = true;
self.termination_error = Some(e);
return;
}
};
let event_state_output = convert_state_from_spg4_frame(
event.window_open,
event_tle_state,
self.frame,
self.representation,
self.angle_format,
);
self.epoch_current = event.window_open;
self.state_current = event_state_output;
if self.should_store_state() {
self.trajectory
.add(event.window_open, svec6_to_dvec(&event_state_output));
}
if action == EventAction::Stop {
self.terminated = true;
return; }
}
}
let tle_state = match self.propagate_internal(target_epoch) {
Ok(state) => state,
Err(e) => {
self.terminated = true;
self.termination_error = Some(e);
return;
}
};
let new_state = convert_state_from_spg4_frame(
target_epoch,
tle_state,
self.frame,
self.representation,
self.angle_format,
);
self.epoch_current = target_epoch;
self.state_current = new_state;
if self.should_store_state() {
self.trajectory.add(target_epoch, svec6_to_dvec(&new_state));
}
}
fn initial_epoch(&self) -> Epoch {
self.epoch
}
fn initial_state(&self) -> Vector6<f64> {
self.initial_state
}
fn current_epoch(&self) -> Epoch {
self.epoch_current
}
fn current_state(&self) -> Vector6<f64> {
self.state_current
}
fn step_size(&self) -> f64 {
self.step_size
}
fn set_step_size(&mut self, step_size: f64) {
self.step_size = step_size;
}
fn reset(&mut self) {
self.epoch_current = self.epoch;
self.state_current = self.initial_state;
self.trajectory.clear();
self.trajectory
.add(self.epoch, svec6_to_dvec(&self.initial_state));
self.event_log.clear();
self.terminated = false;
self.termination_error = None;
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)
}
fn propagate_to(&mut self, target_epoch: Epoch) {
let step = self.step_size();
if step >= 0.0 {
if target_epoch <= self.current_epoch() {
return;
}
while !self.terminated && self.current_epoch() < target_epoch {
let remaining_time = target_epoch - self.current_epoch();
let step_size = remaining_time.min(step);
if step_size <= 1e-9 {
break;
}
self.step_by(step_size);
}
} else {
if target_epoch >= self.current_epoch() {
return;
}
while !self.terminated && self.current_epoch() > target_epoch {
let remaining_time = self.current_epoch() - target_epoch;
let step_size = -(remaining_time.min(step.abs()));
if step_size.abs() <= 1e-9 {
break;
}
self.step_by(step_size);
}
}
}
}
impl SStateProvider for SGPPropagator {
fn state(&self, epoch: Epoch) -> Result<Vector6<f64>, BraheError> {
self.propagate_internal(epoch)
}
}
impl SOrbitStateProvider for SGPPropagator {
fn state_eci(&self, epoch: Epoch) -> Result<Vector6<f64>, BraheError> {
let state_ecef = self.state_ecef(epoch)?;
Ok(state_ecef_to_eci(epoch, state_ecef))
}
fn state_ecef(&self, epoch: Epoch) -> Result<Vector6<f64>, BraheError> {
self.state_itrf(epoch)
}
fn state_itrf(&self, epoch: Epoch) -> Result<Vector6<f64>, BraheError> {
let state_pef = self.state_pef(epoch)?;
#[allow(non_snake_case)]
let PM = polar_motion(epoch);
let r_itrf = PM * Vector3::<f64>::from(state_pef.fixed_rows::<3>(0));
let v_itrf = PM * Vector3::<f64>::from(state_pef.fixed_rows::<3>(3));
Ok(Vector6::new(
r_itrf[0], r_itrf[1], r_itrf[2], v_itrf[0], v_itrf[1], v_itrf[2],
))
}
fn state_gcrf(&self, epoch: Epoch) -> Result<Vector6<f64>, BraheError> {
let state_itrf = self.state_itrf(epoch)?;
Ok(state_itrf_to_gcrf(epoch, state_itrf))
}
fn state_eme2000(&self, epoch: Epoch) -> Result<Vector6<f64>, BraheError> {
let gcrf_state = self.state_gcrf(epoch)?;
Ok(state_gcrf_to_eme2000(gcrf_state))
}
fn state_koe_osc(
&self,
epoch: Epoch,
angle_format: AngleFormat,
) -> Result<Vector6<f64>, BraheError> {
let state_eci = self.state_eci(epoch)?;
Ok(state_eci_to_koe(state_eci, angle_format))
}
}
impl crate::utils::DStateProvider for SGPPropagator {
fn state(&self, epoch: Epoch) -> Result<nalgebra::DVector<f64>, BraheError> {
let state_vec6 = <Self as SStateProvider>::state(self, epoch)?;
Ok(nalgebra::DVector::from_column_slice(state_vec6.as_slice()))
}
fn state_dim(&self) -> usize {
6
}
fn states(&self, epochs: &[Epoch]) -> Result<Vec<nalgebra::DVector<f64>>, BraheError> {
epochs
.iter()
.map(|&epoch| <Self as crate::utils::DStateProvider>::state(self, epoch))
.collect()
}
}
impl crate::utils::DOrbitStateProvider for SGPPropagator {
fn state_eci(&self, epoch: Epoch) -> Result<Vector6<f64>, BraheError> {
<Self as SOrbitStateProvider>::state_eci(self, epoch)
}
fn state_ecef(&self, epoch: Epoch) -> Result<Vector6<f64>, BraheError> {
<Self as SOrbitStateProvider>::state_ecef(self, epoch)
}
fn state_itrf(&self, epoch: Epoch) -> Result<Vector6<f64>, BraheError> {
<Self as SOrbitStateProvider>::state_itrf(self, epoch)
}
fn state_gcrf(&self, epoch: Epoch) -> Result<Vector6<f64>, BraheError> {
<Self as SOrbitStateProvider>::state_gcrf(self, epoch)
}
fn state_eme2000(&self, epoch: Epoch) -> Result<Vector6<f64>, BraheError> {
<Self as SOrbitStateProvider>::state_eme2000(self, epoch)
}
fn state_koe_osc(
&self,
epoch: Epoch,
angle_format: AngleFormat,
) -> Result<Vector6<f64>, BraheError> {
<Self as SOrbitStateProvider>::state_koe_osc(self, epoch, angle_format)
}
}
impl Identifiable for SGPPropagator {
fn with_name(mut self, name: &str) -> Self {
self.name = Some(name.to_string());
self.trajectory = self.trajectory.with_name(name);
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.trajectory = self.trajectory.with_id(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.trajectory = self.trajectory.with_identity(name, uuid, 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;
self.trajectory.set_identity(name, uuid, id);
}
fn set_id(&mut self, id: Option<u64>) {
self.id = id;
self.trajectory.set_id(id);
}
fn set_name(&mut self, name: Option<&str>) {
self.name = name.map(|s| s.to_string());
self.trajectory.set_name(name);
}
fn generate_uuid(&mut self) {
self.uuid = Some(uuid::Uuid::now_v7());
self.trajectory.generate_uuid();
}
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)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use crate::RADIANS;
use crate::utils::testing::{setup_global_test_eop, setup_global_test_eop_original_brahe};
use approx::assert_abs_diff_eq;
const ISS_LINE1: &str = "1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927";
const ISS_LINE2: &str = "2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537";
#[test]
fn test_sgppropagator_from_tle() {
setup_global_test_eop();
let propagator = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0);
assert!(propagator.is_ok());
let prop = propagator.unwrap();
assert_eq!(prop.step_size, 60.0);
assert_eq!(prop.line1, ISS_LINE1);
assert_eq!(prop.line2, ISS_LINE2);
}
#[test]
fn test_sgppropagator_from_3le() {
setup_global_test_eop();
let name = "ISS (ZARYA)";
let propagator = SGPPropagator::from_3le(Some(name), ISS_LINE1, ISS_LINE2, 60.0);
assert!(propagator.is_ok());
let prop = propagator.unwrap();
assert_eq!(prop.satellite_name, Some(name.to_string()));
assert_eq!(prop.get_name(), Some(name));
assert_eq!(prop.get_id(), Some(25544));
}
#[test]
fn test_sgppropagator_from_omm_elements() {
setup_global_test_eop();
let propagator = SGPPropagator::from_omm_elements(
"2025-11-29T20:01:44.058144",
15.49193835, 0.0003723, 51.6312, 206.3646, 184.1118, 175.9840, 25544, 60.0, Some("ISS (ZARYA)"),
Some("1998-067A"),
Some('U'),
Some(0.15237e-3),
Some(0.801e-4),
Some(0.0),
Some(0),
Some(999),
Some(54085),
);
assert!(
propagator.is_ok(),
"Failed to create propagator: {:?}",
propagator.as_ref().err()
);
let prop = propagator.unwrap();
assert_eq!(prop.norad_id, 25544);
assert_eq!(prop.step_size, 60.0);
assert_eq!(prop.satellite_name, Some("ISS (ZARYA)".to_string()));
assert_eq!(prop.epoch.year(), 2025);
assert_eq!(prop.epoch.month(), 11);
assert_eq!(prop.epoch.day(), 29);
assert_abs_diff_eq!(prop.eccentricity(), 0.0003723, epsilon = 1e-7);
assert_abs_diff_eq!(prop.inclination(), 51.6312, epsilon = 1e-4);
assert_abs_diff_eq!(prop.right_ascension(), 206.3646, epsilon = 1e-4);
assert_abs_diff_eq!(prop.arg_perigee(), 184.1118, epsilon = 1e-4);
assert_abs_diff_eq!(prop.mean_anomaly(), 175.9840, epsilon = 1e-4);
}
#[test]
fn test_sgppropagator_from_omm_elements_minimal() {
setup_global_test_eop();
let propagator = SGPPropagator::from_omm_elements(
"2025-11-29T20:01:44.058144",
15.49193835,
0.0003723,
51.6312,
206.3646,
184.1118,
175.9840,
25544,
60.0,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
assert!(propagator.is_ok());
let prop = propagator.unwrap();
assert_eq!(prop.norad_id, 25544);
assert_eq!(prop.satellite_name, None);
}
#[test]
fn test_sgppropagator_from_omm_elements_propagation() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_omm_elements(
"2025-11-29T20:01:44.058144",
15.49193835,
0.0003723,
51.6312,
206.3646,
184.1118,
175.9840,
25544,
60.0,
None,
None,
None,
None,
None,
None,
None,
None,
None,
)
.unwrap();
let initial_state = prop.current_state();
assert!(initial_state.iter().all(|x| x.is_finite()));
prop.step();
let new_state = prop.current_state();
assert!(new_state.iter().all(|x| x.is_finite()));
assert_ne!(new_state, initial_state);
}
#[test]
fn test_sgppropagator_from_omm_elements_invalid_epoch() {
setup_global_test_eop();
let propagator = SGPPropagator::from_omm_elements(
"not-a-valid-date",
15.49193835,
0.0003723,
51.6312,
206.3646,
184.1118,
175.9840,
25544,
60.0,
None,
None,
None,
None,
None,
None,
None,
None,
None,
);
assert!(propagator.is_err());
let err = propagator.unwrap_err();
assert!(err.to_string().contains("Invalid epoch format"));
}
#[test]
fn test_sgppropagator_from_tle_sets_id_without_name() {
setup_global_test_eop();
let propagator = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0);
assert!(propagator.is_ok());
let prop = propagator.unwrap();
assert_eq!(prop.get_name(), None);
assert_eq!(prop.get_id(), Some(25544));
assert_eq!(prop.trajectory.get_id(), Some(25544));
assert_eq!(prop.trajectory.get_name(), None);
}
#[test]
fn test_sgppropagator_orbitpropagator_step() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let initial_epoch = prop.current_epoch();
prop.step();
let new_epoch = prop.current_epoch();
assert_abs_diff_eq!(new_epoch - initial_epoch, 60.0, epsilon = 0.1);
assert_eq!(prop.trajectory.len(), 2);
let new_state = prop.current_state();
assert_ne!(new_state, prop.initial_state);
}
#[test]
fn test_sgppropagator_orbitpropagator_step_by() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let initial_epoch = prop.current_epoch();
prop.step_by(120.0);
let new_epoch = prop.current_epoch();
assert_abs_diff_eq!(new_epoch - initial_epoch, 120.0, epsilon = 0.1);
assert_eq!(prop.trajectory.len(), 2);
let new_state = prop.current_state();
assert_ne!(new_state, prop.initial_state);
}
#[test]
fn test_sgppropagator_orbitpropagator_propagate_steps() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let initial_epoch = prop.current_epoch();
prop.propagate_steps(5);
let new_epoch = prop.current_epoch();
assert_abs_diff_eq!(new_epoch - initial_epoch, 300.0, epsilon = 0.1);
assert_eq!(prop.trajectory.len(), 6);
let new_state = prop.current_state();
assert_ne!(new_state, prop.initial_state);
}
#[test]
fn test_sgppropagator_orbitpropagator_step_past() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let initial_epoch = prop.initial_epoch();
let target_epoch = initial_epoch + 250.0;
prop.step_past(target_epoch);
let current_epoch = prop.current_epoch();
assert!(current_epoch > target_epoch);
assert_eq!(prop.trajectory.len(), 6);
assert_abs_diff_eq!(current_epoch - initial_epoch, 300.0, epsilon = 0.1);
let new_state = prop.current_state();
assert_ne!(new_state, prop.initial_state);
}
#[test]
fn test_sgppropagator_orbitpropagator_propagate_to() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let initial_epoch = prop.initial_epoch();
let target_epoch = initial_epoch + 90.0;
prop.propagate_to(target_epoch);
let current_epoch = prop.current_epoch();
assert_eq!(current_epoch, target_epoch);
assert_eq!(prop.trajectory.len(), 3);
let new_state = prop.current_state();
assert_ne!(new_state, prop.initial_state);
}
#[test]
fn test_sgppropagator_orbitpropagator_current_state() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let state = prop.current_state();
assert!(state.norm() > 0.0);
}
#[test]
fn test_sgppropagator_orbitpropagator_current_epoch() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.current_epoch();
assert_eq!(epoch, prop.initial_epoch());
}
#[test]
fn test_sgppropagator_orbitpropagator_initial_state() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let state = prop.initial_state();
assert!(state.norm() > 0.0);
}
#[test]
fn test_sgppropagator_orbitpropagator_initial_epoch() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
assert!(epoch.jd() > 2454700.0 && epoch.jd() < 2454800.0);
}
#[test]
fn test_sgppropagator_orbitpropagator_step_size() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
assert_eq!(prop.step_size(), 60.0);
}
#[test]
fn test_sgppropagator_orbitpropagator_set_step_size() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
prop.set_step_size(120.0);
assert_eq!(prop.step_size(), 120.0);
}
#[test]
fn test_sgppropagator_orbitpropagator_reset() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
prop.propagate_steps(5);
assert_eq!(prop.trajectory.len(), 6);
prop.reset();
assert_eq!(prop.trajectory.len(), 1);
assert_eq!(prop.current_epoch(), prop.initial_epoch());
}
#[test]
fn test_sgppropagator_statepropagator_set_eviction_policy_max_size() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
prop.set_eviction_policy_max_size(5).unwrap();
prop.propagate_steps(10);
assert_eq!(prop.trajectory.len(), 5);
}
#[test]
fn test_sgppropagator_orbitpropagator_set_eviction_policy_max_age() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let result = prop.set_eviction_policy_max_age(120.0);
assert!(result.is_ok());
prop.propagate_steps(10);
assert!(prop.trajectory.len() <= 4);
assert!(prop.trajectory.len() > 0);
}
#[test]
fn test_sgppropagator_get_elements_radians() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let elements = prop.get_elements(RADIANS).unwrap();
assert_abs_diff_eq!(elements[0], 6730960.676936833, epsilon = 1.0); assert_abs_diff_eq!(elements[1], 0.0006703, epsilon = 1e-10); assert_abs_diff_eq!(elements[2], 0.9013159509979036, epsilon = 1e-10); assert_abs_diff_eq!(elements[3], 4.319038890874972, epsilon = 1e-10); assert_abs_diff_eq!(elements[4], 2.278282992383318, epsilon = 1e-10); assert_abs_diff_eq!(elements[5], 5.672822723806145, epsilon = 1e-10); }
#[test]
fn test_sgppropagator_get_elements_degrees() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let elements = prop.get_elements(AngleFormat::Degrees).unwrap();
assert_abs_diff_eq!(elements[0], 6730960.676936833, epsilon = 1.0); assert_abs_diff_eq!(elements[1], 0.0006703, epsilon = 1e-10); assert_abs_diff_eq!(elements[2], 51.6416, epsilon = 1e-10); assert_abs_diff_eq!(elements[3], 247.4627, epsilon = 1e-10); assert_abs_diff_eq!(elements[4], 130.5360, epsilon = 1e-10); assert_abs_diff_eq!(elements[5], 325.0288, epsilon = 1e-10); }
#[test]
fn test_sgppropagator_elements() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let elements = prop.elements();
assert_abs_diff_eq!(elements[0], 6730960.676936833, epsilon = 1.0); assert_abs_diff_eq!(elements[1], 0.0006703, epsilon = 1e-10); assert_abs_diff_eq!(elements[2], 51.6416, epsilon = 1e-10); assert_abs_diff_eq!(elements[3], 247.4627, epsilon = 1e-10); assert_abs_diff_eq!(elements[4], 130.5360, epsilon = 1e-10); assert_abs_diff_eq!(elements[5], 325.0288, epsilon = 1e-10); }
#[test]
fn test_sgppropagator_semi_major_axis() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let sma = prop.semi_major_axis();
assert_abs_diff_eq!(sma, 6730960.676936833, epsilon = 1.0);
}
#[test]
fn test_sgppropagator_eccentricity() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let ecc = prop.eccentricity();
assert_abs_diff_eq!(ecc, 0.0006703, epsilon = 1e-10);
}
#[test]
fn test_sgppropagator_inclination() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let inc = prop.inclination();
assert_abs_diff_eq!(inc, 51.6416, epsilon = 1e-10);
}
#[test]
fn test_sgppropagator_right_ascension() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let raan = prop.right_ascension();
assert_abs_diff_eq!(raan, 247.4627, epsilon = 1e-10);
}
#[test]
fn test_sgppropagator_arg_perigee() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let argp = prop.arg_perigee();
assert_abs_diff_eq!(argp, 130.5360, epsilon = 1e-10);
}
#[test]
fn test_sgppropagator_mean_anomaly() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let ma = prop.mean_anomaly();
assert_abs_diff_eq!(ma, 325.0288, epsilon = 1e-10);
}
#[test]
fn test_sgppropagator_ephemeris_age() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let age = prop.ephemeris_age();
assert!(age > 0.0);
assert!(age > 15.0 * 365.25 * 86400.0);
}
#[test]
fn test_sgppropagator_identifiable_with_name() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0)
.unwrap()
.with_name("My Satellite");
assert_eq!(prop.get_name(), Some("My Satellite"));
assert_eq!(prop.get_id(), Some(25544));
assert!(prop.get_uuid().is_some()); }
#[test]
fn test_sgppropagator_identifiable_with_id() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0)
.unwrap()
.with_id(12345);
assert_eq!(prop.get_id(), Some(12345));
assert_eq!(prop.get_name(), None);
assert!(prop.get_uuid().is_some()); }
#[test]
fn test_sgppropagator_identifiable_with_uuid() {
setup_global_test_eop();
let test_uuid = uuid::Uuid::now_v7();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0)
.unwrap()
.with_uuid(test_uuid);
assert_eq!(prop.get_uuid(), Some(test_uuid));
assert_eq!(prop.get_name(), None);
assert_eq!(prop.get_id(), Some(25544));
}
#[test]
fn test_sgppropagator_identifiable_with_new_uuid() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0)
.unwrap()
.with_new_uuid();
assert!(prop.get_uuid().is_some());
assert_eq!(prop.get_name(), None);
assert_eq!(prop.get_id(), Some(25544));
}
#[test]
fn test_sgppropagator_identifiable_with_identity() {
setup_global_test_eop();
let test_uuid = uuid::Uuid::now_v7();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0)
.unwrap()
.with_identity(Some("Satellite A"), Some(test_uuid), Some(999));
assert_eq!(prop.get_name(), Some("Satellite A"));
assert_eq!(prop.get_id(), Some(999));
assert_eq!(prop.get_uuid(), Some(test_uuid));
}
#[test]
fn test_sgppropagator_identifiable_set_name() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
prop.set_name(Some("Test Name"));
assert_eq!(prop.get_name(), Some("Test Name"));
prop.set_name(None);
assert_eq!(prop.get_name(), None);
}
#[test]
fn test_sgppropagator_identifiable_set_id() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
prop.set_id(Some(42));
assert_eq!(prop.get_id(), Some(42));
prop.set_id(None);
assert_eq!(prop.get_id(), None);
}
#[test]
fn test_sgppropagator_identifiable_generate_uuid() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let initial_uuid = prop.get_uuid();
assert!(initial_uuid.is_some());
prop.generate_uuid();
let uuid1 = prop.get_uuid();
assert!(uuid1.is_some());
prop.generate_uuid();
let uuid2 = prop.get_uuid();
assert!(uuid2.is_some());
assert_ne!(uuid1, uuid2);
}
#[test]
fn test_sgppropagator_identifiable_set_identity() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let test_uuid = uuid::Uuid::now_v7();
prop.set_identity(Some("Updated Name"), Some(test_uuid), Some(777));
assert_eq!(prop.get_name(), Some("Updated Name"));
assert_eq!(prop.get_id(), Some(777));
assert_eq!(prop.get_uuid(), Some(test_uuid));
prop.set_identity(None, None, None);
assert_eq!(prop.get_name(), None);
assert_eq!(prop.get_id(), None);
assert_eq!(prop.get_uuid(), None);
}
#[test]
fn test_sgppropagator_identifiable_chaining() {
setup_global_test_eop();
let test_uuid = uuid::Uuid::now_v7();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0)
.unwrap()
.with_name("Chained Satellite")
.with_id(123)
.with_uuid(test_uuid);
assert_eq!(prop.get_name(), Some("Chained Satellite"));
assert_eq!(prop.get_id(), Some(123));
assert_eq!(prop.get_uuid(), Some(test_uuid));
}
#[test]
fn test_sgppropagator_analyticpropagator_state_koe_osc() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let elements = prop.state_koe_osc(epoch, RADIANS).unwrap();
assert!(elements.iter().all(|&x| x.is_finite()));
assert!(elements[0] > 0.0);
assert!(elements[1] >= 0.0);
assert_abs_diff_eq!(elements[2], 51.6_f64.to_radians(), epsilon = 0.1);
}
#[test]
fn test_sgppropagator_analyticpropagator_states() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let initial_epoch = prop.initial_epoch();
let epochs = vec![initial_epoch, initial_epoch + 0.01, initial_epoch + 0.02];
let states = prop.states(&epochs).unwrap();
assert_eq!(states.len(), 3);
}
#[test]
fn test_sgppropagator_analyticpropagator_states_eci() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let initial_epoch = prop.initial_epoch();
let epochs = vec![initial_epoch, initial_epoch + 0.01];
let states = prop.states_eci(&epochs).unwrap();
assert_eq!(states.len(), 2);
for state in &states {
assert!(state.norm() > 0.0);
}
}
#[test]
fn test_sgppropagator_analyticpropagator_states_ecef() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let initial_epoch = prop.initial_epoch();
let epochs = vec![initial_epoch, initial_epoch + 0.01];
let states = prop.states_ecef(&epochs).unwrap();
assert_eq!(states.len(), 2);
for state in &states {
assert!(state.norm() > 0.0);
}
}
#[test]
fn test_sgppropagator_analyticpropagator_states_koe() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let initial_epoch = prop.initial_epoch();
let epochs = vec![initial_epoch, initial_epoch + 0.01];
let elements = prop.states_koe_osc(&epochs, RADIANS).unwrap();
assert_eq!(elements.len(), 2);
for elem in &elements {
assert!(elem[0] > 0.0); assert!(elem[1] >= 0.0); }
}
#[test]
fn test_sgppropagator_state_teme() {
setup_global_test_eop_original_brahe();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let state = prop.state(epoch).unwrap();
assert_eq!(state.len(), 6);
assert_abs_diff_eq!(state[0], 4083909.8260273533, epsilon = 1e-8);
assert_abs_diff_eq!(state[1], -993636.8325621719, epsilon = 1e-8);
assert_abs_diff_eq!(state[2], 5243614.536966579, epsilon = 1e-8);
assert_abs_diff_eq!(state[3], 2512.831950943635, epsilon = 1e-8);
assert_abs_diff_eq!(state[4], 7259.8698423432315, epsilon = 1e-8);
assert_abs_diff_eq!(state[5], -583.775727402632, epsilon = 1e-8);
}
#[test]
fn test_tle_gmst82() {
setup_global_test_eop_original_brahe();
let epoch = epoch_from_tle(ISS_LINE1).unwrap();
let gmst = tle_gmst82(epoch, AngleFormat::Radians);
assert_abs_diff_eq!(gmst, 3.2494565064865406, epsilon = 1e-6);
}
#[test]
fn test_sgppropagator_state_pef() {
setup_global_test_eop_original_brahe();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let state = prop.state_pef(epoch).unwrap();
assert_eq!(state.len(), 6);
assert_abs_diff_eq!(state[0], -3953205.7105210484, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[1], 1427514.704810681, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[2], 5243614.536966579, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[3], -3175.692140186211, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[4], -6658.887120918979, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[5], -583.775727402632, epsilon = 1.5e-1);
}
#[test]
#[ignore] fn test_sgppropagator_state_ecef_values() {
setup_global_test_eop_original_brahe();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let state = prop.state_ecef(epoch).unwrap();
assert_eq!(state.len(), 6);
assert_abs_diff_eq!(state[0], -3953198.5496517573, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[1], 1427508.1713723878, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[2], 5243621.714247745, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[3], -3414.313706718372, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[4], -7222.549343535009, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[5], -583.7798954042405, epsilon = 1.5e-1);
}
#[test]
#[ignore] fn test_sgppropagator_state_itrf_values() {
setup_global_test_eop_original_brahe();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let state = prop.state_itrf(epoch).unwrap();
assert_eq!(state.len(), 6);
assert_abs_diff_eq!(state[0], -3953198.5496517573, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[1], 1427508.1713723878, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[2], 5243621.714247745, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[3], -3414.313706718372, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[4], -7222.549343535009, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[5], -583.7798954042405, epsilon = 1.5e-1);
}
#[test]
#[ignore] fn test_sgppropagator_state_eci_values() {
setup_global_test_eop_original_brahe();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let state = prop.state_eci(epoch).unwrap();
assert_eq!(state.len(), 6);
assert_abs_diff_eq!(state[0], 4086521.040536244, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[1], -1001422.0787863219, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[2], 5240097.960898061, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[3], 2704.171077071122, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[4], 7840.6666110244705, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[5], -586.3906587951877, epsilon = 1.5e-1);
}
#[test]
#[ignore] fn test_sgppropagator_state_gcrf_values() {
setup_global_test_eop_original_brahe();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let state = prop.state_gcrf(epoch).unwrap();
assert_eq!(state.len(), 6);
assert_abs_diff_eq!(state[0], 4086521.040536244, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[1], -1001422.0787863219, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[2], 5240097.960898061, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[3], 2704.171077071122, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[4], 7840.6666110244705, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[5], -586.3906587951877, epsilon = 1.5e-1);
}
#[test]
#[ignore] fn test_sgppropagator_state_eme2000_values() {
setup_global_test_eop_original_brahe();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let state = prop.state_eme2000(epoch).unwrap();
assert_eq!(state.len(), 6);
assert_abs_diff_eq!(state[0], 4086547.890843119, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[1], -1001422.5866752749, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[2], 5240072.135733086, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[3], 2704.1707451936, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[4], 7840.6666131931, epsilon = 1.5e-1);
assert_abs_diff_eq!(state[5], -586.3938863063, epsilon = 1.5e-1);
}
#[test]
fn test_sgppropagator_with_output_format_eci_cartesian() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0)
.unwrap()
.with_output_format(OrbitFrame::ECI, OrbitRepresentation::Cartesian, None);
assert_eq!(prop.frame, OrbitFrame::ECI);
assert_eq!(prop.representation, OrbitRepresentation::Cartesian);
assert_eq!(prop.angle_format, None);
assert_eq!(prop.trajectory.len(), 1); }
#[test]
fn test_sgppropagator_with_output_format_ecef_cartesian() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0)
.unwrap()
.with_output_format(OrbitFrame::ECEF, OrbitRepresentation::Cartesian, None);
assert_eq!(prop.frame, OrbitFrame::ECEF);
assert_eq!(prop.representation, OrbitRepresentation::Cartesian);
assert_eq!(prop.angle_format, None);
let state = prop.current_state();
assert!(state.norm() > 0.0);
}
#[test]
fn test_sgppropagator_with_output_format_gcrf_cartesian() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0)
.unwrap()
.with_output_format(OrbitFrame::GCRF, OrbitRepresentation::Cartesian, None);
assert_eq!(prop.frame, OrbitFrame::GCRF);
assert_eq!(prop.representation, OrbitRepresentation::Cartesian);
assert_eq!(prop.angle_format, None);
let state = prop.current_state();
assert!(state.norm() > 0.0);
}
#[test]
fn test_sgppropagator_with_output_format_eme2000_cartesian() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0)
.unwrap()
.with_output_format(OrbitFrame::EME2000, OrbitRepresentation::Cartesian, None);
assert_eq!(prop.frame, OrbitFrame::EME2000);
assert_eq!(prop.representation, OrbitRepresentation::Cartesian);
assert_eq!(prop.angle_format, None);
let state = prop.current_state();
assert!(state.norm() > 0.0);
}
#[test]
fn test_sgppropagator_with_output_format_itrf_cartesian() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0)
.unwrap()
.with_output_format(OrbitFrame::ITRF, OrbitRepresentation::Cartesian, None);
assert_eq!(prop.frame, OrbitFrame::ITRF);
assert_eq!(prop.representation, OrbitRepresentation::Cartesian);
assert_eq!(prop.angle_format, None);
let state = prop.current_state();
assert!(state.norm() > 0.0);
}
#[test]
fn test_sgppropagator_with_output_format_eci_keplerian_degrees() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0)
.unwrap()
.with_output_format(
OrbitFrame::ECI,
OrbitRepresentation::Keplerian,
Some(AngleFormat::Degrees),
);
assert_eq!(prop.frame, OrbitFrame::ECI);
assert_eq!(prop.representation, OrbitRepresentation::Keplerian);
assert_eq!(prop.angle_format, Some(AngleFormat::Degrees));
let state = prop.current_state();
assert!(state[0] > 0.0); assert!(state[1] >= 0.0 && state[1] < 1.0); }
#[test]
fn test_sgppropagator_with_output_format_eci_keplerian_radians() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0)
.unwrap()
.with_output_format(
OrbitFrame::ECI,
OrbitRepresentation::Keplerian,
Some(AngleFormat::Radians),
);
assert_eq!(prop.frame, OrbitFrame::ECI);
assert_eq!(prop.representation, OrbitRepresentation::Keplerian);
assert_eq!(prop.angle_format, Some(AngleFormat::Radians));
let state = prop.current_state();
assert!(state[0] > 0.0); assert!(state[1] >= 0.0 && state[1] < 1.0); assert!(state[2] < std::f64::consts::PI);
}
#[test]
fn test_sgppropagator_with_output_format_resets_trajectory() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
prop.propagate_steps(5);
assert_eq!(prop.trajectory.len(), 6);
let prop = prop.with_output_format(OrbitFrame::ECEF, OrbitRepresentation::Cartesian, None);
assert_eq!(prop.trajectory.len(), 1); }
#[test]
fn test_sgppropagator_with_output_format_propagate_in_new_format() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0)
.unwrap()
.with_output_format(OrbitFrame::ECEF, OrbitRepresentation::Cartesian, None);
prop.propagate_steps(3);
assert_eq!(prop.trajectory.len(), 4);
let state = prop.current_state();
assert!(state.norm() > 0.0);
}
#[test]
#[should_panic(expected = "Angle format must be specified for Keplerian elements")]
fn test_sgppropagator_with_output_format_keplerian_without_angle_format() {
setup_global_test_eop();
let _prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0)
.unwrap()
.with_output_format(OrbitFrame::ECI, OrbitRepresentation::Keplerian, None);
}
#[test]
#[should_panic(expected = "Keplerian elements must be in ECI frame")]
fn test_sgppropagator_with_output_format_keplerian_non_eci_frame() {
setup_global_test_eop();
let _prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0)
.unwrap()
.with_output_format(
OrbitFrame::ECEF,
OrbitRepresentation::Keplerian,
Some(AngleFormat::Degrees),
);
}
#[test]
#[should_panic(expected = "Angle format should be None for Cartesian representation")]
fn test_sgppropagator_with_output_format_cartesian_with_angle_format() {
setup_global_test_eop();
let _prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0)
.unwrap()
.with_output_format(
OrbitFrame::ECI,
OrbitRepresentation::Cartesian,
Some(AngleFormat::Degrees),
);
}
#[test]
fn test_sgppropagator_state_gcrf() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let state = prop.state_gcrf(epoch).unwrap();
assert_eq!(state.len(), 6);
assert!(state.norm() > 0.0);
let r = (state[0].powi(2) + state[1].powi(2) + state[2].powi(2)).sqrt();
assert!(r > 6_000_000.0 && r < 7_000_000.0);
let v = (state[3].powi(2) + state[4].powi(2) + state[5].powi(2)).sqrt();
assert!(v > 7_000.0 && v < 8_000.0);
}
#[test]
fn test_sgppropagator_state_gcrf_at_different_epochs() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let initial_epoch = prop.initial_epoch();
let state1 = prop.state_gcrf(initial_epoch).unwrap();
let state2 = prop.state_gcrf(initial_epoch + 60.0).unwrap();
assert_ne!(state1, state2);
let r1 = (state1[0].powi(2) + state1[1].powi(2) + state1[2].powi(2)).sqrt();
let r2 = (state2[0].powi(2) + state2[1].powi(2) + state2[2].powi(2)).sqrt();
assert_abs_diff_eq!(r1, r2, epsilon = 10_000.0); }
#[test]
fn test_sgppropagator_state_eme2000() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let state = prop.state_eme2000(epoch).unwrap();
assert_eq!(state.len(), 6);
assert!(state.norm() > 0.0);
let r = (state[0].powi(2) + state[1].powi(2) + state[2].powi(2)).sqrt();
assert!(r > 6_000_000.0 && r < 7_000_000.0);
let v = (state[3].powi(2) + state[4].powi(2) + state[5].powi(2)).sqrt();
assert!(v > 7_000.0 && v < 8_000.0);
}
#[test]
fn test_sgppropagator_state_eme2000_at_different_epochs() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let initial_epoch = prop.initial_epoch();
let state1 = prop.state_eme2000(initial_epoch).unwrap();
let state2 = prop.state_eme2000(initial_epoch + 60.0).unwrap();
assert_ne!(state1, state2);
let r1 = (state1[0].powi(2) + state1[1].powi(2) + state1[2].powi(2)).sqrt();
let r2 = (state2[0].powi(2) + state2[1].powi(2) + state2[2].powi(2)).sqrt();
assert_abs_diff_eq!(r1, r2, epsilon = 10_000.0); }
#[test]
fn test_sgppropagator_state_gcrf_vs_eme2000() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let gcrf_state = prop.state_gcrf(epoch).unwrap();
let eme2000_state = prop.state_eme2000(epoch).unwrap();
let dr = ((gcrf_state[0] - eme2000_state[0]).powi(2)
+ (gcrf_state[1] - eme2000_state[1]).powi(2)
+ (gcrf_state[2] - eme2000_state[2]).powi(2))
.sqrt();
assert!(dr < 100.0, "Position difference {} m > 100 m", dr);
let dv = ((gcrf_state[3] - eme2000_state[3]).powi(2)
+ (gcrf_state[4] - eme2000_state[4]).powi(2)
+ (gcrf_state[5] - eme2000_state[5]).powi(2))
.sqrt();
assert!(dv < 0.1, "Velocity difference {} m/s > 0.1 m/s", dv);
}
#[test]
fn test_sgppropagator_state_gcrf_consistency_with_eci() {
setup_global_test_eop();
let prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let gcrf_state = prop.state_gcrf(epoch).unwrap();
let eci_state = prop.state_eci(epoch).unwrap();
assert_abs_diff_eq!(gcrf_state[0], eci_state[0], epsilon = 1.0);
assert_abs_diff_eq!(gcrf_state[1], eci_state[1], epsilon = 1.0);
assert_abs_diff_eq!(gcrf_state[2], eci_state[2], epsilon = 1.0);
assert_abs_diff_eq!(gcrf_state[3], eci_state[3], epsilon = 1e-6);
assert_abs_diff_eq!(gcrf_state[4], eci_state[4], epsilon = 1e-6);
assert_abs_diff_eq!(gcrf_state[5], eci_state[5], epsilon = 1e-6);
}
use crate::events::{DAscendingNodeEvent, DTimeEvent};
#[test]
fn test_sgppropagator_add_event_detector() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let detector = DTimeEvent::new(epoch + 300.0, "5 minute mark");
prop.add_event_detector(Box::new(detector));
assert!(prop.event_log().is_empty());
assert!(!prop.is_terminated());
}
#[test]
fn test_sgppropagator_time_event_detection() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let target_time = epoch + 150.0;
let detector = DTimeEvent::new(target_time, "Time Target");
prop.add_event_detector(Box::new(detector));
prop.propagate_to(epoch + 300.0);
let events = prop.event_log();
assert_eq!(events.len(), 1);
assert_eq!(events[0].name, "Time Target");
assert_abs_diff_eq!(events[0].window_open.jd(), target_time.jd(), epsilon = 1e-8);
}
#[test]
fn test_sgppropagator_multiple_events() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let detector1 = DTimeEvent::new(epoch + 100.0, "Event 1");
let detector2 = DTimeEvent::new(epoch + 200.0, "Event 2");
let detector3 = DTimeEvent::new(epoch + 300.0, "Event 3");
prop.add_event_detector(Box::new(detector1));
prop.add_event_detector(Box::new(detector2));
prop.add_event_detector(Box::new(detector3));
prop.propagate_to(epoch + 400.0);
let events = prop.event_log();
assert_eq!(events.len(), 3);
}
#[test]
fn test_sgppropagator_events_by_name() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let detector1 = DTimeEvent::new(epoch + 100.0, "Alpha Event");
let detector2 = DTimeEvent::new(epoch + 200.0, "Beta Event");
let detector3 = DTimeEvent::new(epoch + 300.0, "Alpha Prime");
prop.add_event_detector(Box::new(detector1));
prop.add_event_detector(Box::new(detector2));
prop.add_event_detector(Box::new(detector3));
prop.propagate_to(epoch + 400.0);
let alpha_events = prop.events_by_name("Alpha");
assert_eq!(alpha_events.len(), 2);
let beta_events = prop.events_by_name("Beta");
assert_eq!(beta_events.len(), 1);
}
#[test]
fn test_sgppropagator_latest_event() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
assert!(prop.latest_event().is_none());
let detector1 = DTimeEvent::new(epoch + 100.0, "First");
let detector2 = DTimeEvent::new(epoch + 200.0, "Second");
prop.add_event_detector(Box::new(detector1));
prop.add_event_detector(Box::new(detector2));
prop.propagate_to(epoch + 250.0);
let latest = prop.latest_event().unwrap();
assert_eq!(latest.name, "Second");
}
#[test]
fn test_sgppropagator_events_in_range() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let detector1 = DTimeEvent::new(epoch + 100.0, "Event 1");
let detector2 = DTimeEvent::new(epoch + 200.0, "Event 2");
let detector3 = DTimeEvent::new(epoch + 300.0, "Event 3");
prop.add_event_detector(Box::new(detector1));
prop.add_event_detector(Box::new(detector2));
prop.add_event_detector(Box::new(detector3));
prop.propagate_to(epoch + 400.0);
let range_events = prop.events_in_range(epoch + 150.0, epoch + 250.0);
assert_eq!(range_events.len(), 1);
assert_eq!(range_events[0].name, "Event 2");
}
#[test]
fn test_sgppropagator_events_by_detector_index() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let detector1 = DTimeEvent::new(epoch + 100.0, "Detector 0 Event");
let detector2 = DTimeEvent::new(epoch + 200.0, "Detector 1 Event");
prop.add_event_detector(Box::new(detector1));
prop.add_event_detector(Box::new(detector2));
prop.propagate_to(epoch + 300.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_0[0].name, "Detector 0 Event");
assert_eq!(events_1.len(), 1);
assert_eq!(events_1[0].name, "Detector 1 Event");
}
#[test]
fn test_sgppropagator_terminal_event() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let detector = DTimeEvent::new(epoch + 150.0, "Stop Here").set_terminal();
prop.add_event_detector(Box::new(detector));
prop.propagate_to(epoch + 300.0);
assert!(prop.is_terminated());
let current = prop.current_epoch();
assert!(
current < epoch + 200.0,
"Propagation should have stopped before 200s"
);
}
#[test]
fn test_sgppropagator_reset_clears_events() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let detector = DTimeEvent::new(epoch + 100.0, "Test Event");
prop.add_event_detector(Box::new(detector));
prop.propagate_to(epoch + 200.0);
assert_eq!(prop.event_log().len(), 1);
prop.reset();
assert!(prop.event_log().is_empty());
assert!(!prop.is_terminated());
}
#[test]
fn test_sgppropagator_clear_events() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let detector = DTimeEvent::new(epoch + 100.0, "Test Event");
prop.add_event_detector(Box::new(detector));
prop.propagate_to(epoch + 200.0);
assert_eq!(prop.event_log().len(), 1);
prop.clear_events();
assert!(prop.event_log().is_empty());
assert!(prop.trajectory.len() > 1);
}
#[test]
fn test_sgppropagator_reset_termination() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let detector = DTimeEvent::new(epoch + 100.0, "Terminal").set_terminal();
prop.add_event_detector(Box::new(detector));
prop.propagate_to(epoch + 200.0);
assert!(prop.is_terminated());
prop.reset_termination();
assert!(!prop.is_terminated());
prop.propagate_to(epoch + 300.0);
assert!(prop.current_epoch() > epoch + 250.0);
}
#[test]
fn test_sgppropagator_ascending_node_detection() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let detector = DAscendingNodeEvent::new("Ascending Node");
prop.add_event_detector(Box::new(detector));
prop.propagate_to(epoch + 5600.0);
let events = prop.event_log();
assert!(!events.is_empty(), "Should detect ascending node crossings");
}
#[test]
fn test_sgppropagator_clone_does_not_copy_events() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let detector = DTimeEvent::new(epoch + 100.0, "Test Event");
prop.add_event_detector(Box::new(detector));
prop.propagate_to(epoch + 200.0);
assert_eq!(prop.event_log().len(), 1);
let cloned = prop.clone();
assert!(cloned.event_log().is_empty());
assert!(!cloned.is_terminated());
}
#[test]
fn test_sgppropagator_query_events() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let detector1 = DTimeEvent::new(epoch + 100.0, "First");
let detector2 = DTimeEvent::new(epoch + 200.0, "Second");
let detector3 = DTimeEvent::new(epoch + 300.0, "Third");
prop.add_event_detector(Box::new(detector1));
prop.add_event_detector(Box::new(detector2));
prop.add_event_detector(Box::new(detector3));
prop.propagate_to(epoch + 400.0);
let filtered: Vec<_> = prop
.query_events()
.after(epoch + 150.0)
.before(epoch + 350.0)
.collect();
assert_eq!(filtered.len(), 2);
}
#[test]
fn test_sgppropagator_take_event_detectors() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let detector1 = DTimeEvent::new(epoch + 100.0, "Event1");
let detector2 = DTimeEvent::new(epoch + 200.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(epoch + 300.0);
assert!(prop.event_log().is_empty());
}
#[test]
fn test_sgppropagator_set_event_detectors() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let detectors: Vec<Box<dyn crate::events::DEventDetector>> = vec![
Box::new(DTimeEvent::new(epoch + 100.0, "Event1")),
Box::new(DTimeEvent::new(epoch + 200.0, "Event2")),
];
prop.set_event_detectors(detectors);
prop.propagate_to(epoch + 300.0);
assert_eq!(prop.event_log().len(), 2);
}
#[test]
fn test_sgppropagator_take_event_log() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let detector = DTimeEvent::new(epoch + 100.0, "TestEvent");
prop.add_event_detector(Box::new(detector));
prop.propagate_to(epoch + 200.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_sgppropagator_set_terminated_is_terminated() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
assert!(!prop.is_terminated());
prop.set_terminated(true);
assert!(prop.is_terminated());
prop.set_terminated(false);
assert!(!prop.is_terminated());
}
#[test]
fn test_sgppropagator_event_detector_roundtrip() {
setup_global_test_eop();
let mut prop = SGPPropagator::from_tle(ISS_LINE1, ISS_LINE2, 60.0).unwrap();
let epoch = prop.initial_epoch();
let detector = DTimeEvent::new(epoch + 150.0, "RoundtripEvent");
prop.add_event_detector(Box::new(detector));
let taken = prop.take_event_detectors();
assert_eq!(taken.len(), 1);
prop.propagate_to(epoch + 100.0);
assert!(prop.event_log().is_empty());
prop.set_event_detectors(taken);
prop.propagate_to(epoch + 200.0);
assert_eq!(prop.event_log().len(), 1);
assert!(prop.event_log()[0].name.contains("RoundtripEvent"));
}
fn iss_gp_record_full() -> crate::types::GPRecord {
let json = r#"{
"OBJECT_NAME": "ISS (ZARYA)",
"OBJECT_ID": "1998-067A",
"EPOCH": "2024-01-15T12:00:00.000000",
"MEAN_MOTION": 15.50000000,
"ECCENTRICITY": 0.00010000,
"INCLINATION": 51.6400,
"RA_OF_ASC_NODE": 200.0000,
"ARG_OF_PERICENTER": 100.0000,
"MEAN_ANOMALY": 260.0000,
"EPHEMERIS_TYPE": 0,
"CLASSIFICATION_TYPE": "U",
"NORAD_CAT_ID": 25544,
"ELEMENT_SET_NO": 999,
"REV_AT_EPOCH": 45000,
"BSTAR": 0.00034100,
"MEAN_MOTION_DOT": 0.00001000,
"MEAN_MOTION_DDOT": 0.00000000
}"#;
serde_json::from_str(json).unwrap()
}
fn iss_gp_record_minimal() -> crate::types::GPRecord {
let json = r#"{
"EPOCH": "2024-01-15T12:00:00.000000",
"MEAN_MOTION": 15.50000000,
"ECCENTRICITY": 0.00010000,
"INCLINATION": 51.6400,
"RA_OF_ASC_NODE": 200.0000,
"ARG_OF_PERICENTER": 100.0000,
"MEAN_ANOMALY": 260.0000,
"NORAD_CAT_ID": 25544
}"#;
serde_json::from_str(json).unwrap()
}
#[test]
fn test_from_gp_record_full() {
setup_global_test_eop();
let record = iss_gp_record_full();
let prop = SGPPropagator::from_gp_record(&record, 60.0);
assert!(prop.is_ok());
let prop = prop.unwrap();
assert_eq!(prop.step_size, 60.0);
assert_eq!(prop.norad_id, 25544);
assert!(prop.satellite_name.as_deref() == Some("ISS (ZARYA)"));
let state = prop.initial_state();
let pos_mag = (state[0] * state[0] + state[1] * state[1] + state[2] * state[2]).sqrt();
assert!(
pos_mag > 6000e3 && pos_mag < 7000e3,
"Position magnitude: {}",
pos_mag
);
}
#[test]
fn test_from_gp_record_minimal() {
setup_global_test_eop();
let record = iss_gp_record_minimal();
let prop = SGPPropagator::from_gp_record(&record, 120.0);
assert!(prop.is_ok());
let prop = prop.unwrap();
assert_eq!(prop.step_size, 120.0);
assert_eq!(prop.norad_id, 25544);
}
#[test]
fn test_from_gp_record_missing_epoch() {
let json = r#"{
"MEAN_MOTION": 15.5,
"ECCENTRICITY": 0.0001,
"INCLINATION": 51.64,
"RA_OF_ASC_NODE": 200.0,
"ARG_OF_PERICENTER": 100.0,
"MEAN_ANOMALY": 260.0,
"NORAD_CAT_ID": 25544
}"#;
let record: crate::types::GPRecord = serde_json::from_str(json).unwrap();
let result = SGPPropagator::from_gp_record(&record, 60.0);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("epoch"));
}
#[test]
fn test_from_gp_record_missing_mean_motion() {
let json = r#"{
"EPOCH": "2024-01-15T12:00:00.000000",
"ECCENTRICITY": 0.0001,
"INCLINATION": 51.64,
"RA_OF_ASC_NODE": 200.0,
"ARG_OF_PERICENTER": 100.0,
"MEAN_ANOMALY": 260.0,
"NORAD_CAT_ID": 25544
}"#;
let record: crate::types::GPRecord = serde_json::from_str(json).unwrap();
let result = SGPPropagator::from_gp_record(&record, 60.0);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("mean_motion"));
}
#[test]
fn test_from_gp_record_missing_norad_cat_id() {
let json = r#"{
"EPOCH": "2024-01-15T12:00:00.000000",
"MEAN_MOTION": 15.5,
"ECCENTRICITY": 0.0001,
"INCLINATION": 51.64,
"RA_OF_ASC_NODE": 200.0,
"ARG_OF_PERICENTER": 100.0,
"MEAN_ANOMALY": 260.0
}"#;
let record: crate::types::GPRecord = serde_json::from_str(json).unwrap();
let result = SGPPropagator::from_gp_record(&record, 60.0);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("norad_cat_id"));
}
#[test]
fn test_sgppropagator_trajectory_mode_default() {
setup_global_test_eop();
let line1 = "1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927";
let line2 = "2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537";
let prop = SGPPropagator::from_tle(line1, line2, 60.0).unwrap();
assert_eq!(prop.trajectory_mode(), TrajectoryMode::OutputStepsOnly);
}
#[test]
fn test_sgppropagator_trajectory_mode_disabled() {
setup_global_test_eop();
let line1 = "1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927";
let line2 = "2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537";
let mut prop = SGPPropagator::from_tle(line1, line2, 60.0).unwrap();
prop.set_trajectory_mode(TrajectoryMode::Disabled);
let initial_epoch = prop.initial_epoch();
let initial_state = prop.initial_state();
prop.step_by(60.0);
prop.step_by(60.0);
prop.step_by(60.0);
assert_eq!(prop.trajectory.len(), 1);
let current_epoch = prop.current_epoch();
assert!(current_epoch > initial_epoch);
assert_abs_diff_eq!((current_epoch - initial_epoch), 180.0, epsilon = 1e-6);
let current_state = prop.current_state();
assert_ne!(current_state, initial_state);
}
#[test]
fn test_sgppropagator_trajectory_mode_output_steps_only() {
setup_global_test_eop();
let line1 = "1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927";
let line2 = "2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537";
let mut prop = SGPPropagator::from_tle(line1, line2, 60.0).unwrap();
prop.step_by(60.0);
prop.step_by(60.0);
assert_eq!(prop.trajectory.len(), 3);
}
#[test]
fn test_sgppropagator_set_trajectory_mode_runtime() {
setup_global_test_eop();
let line1 = "1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927";
let line2 = "2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537";
let mut prop = SGPPropagator::from_tle(line1, line2, 60.0).unwrap();
prop.step_by(60.0);
assert_eq!(prop.trajectory.len(), 2);
prop.set_trajectory_mode(TrajectoryMode::Disabled);
assert_eq!(prop.trajectory_mode(), TrajectoryMode::Disabled);
prop.step_by(60.0);
prop.step_by(60.0);
assert_eq!(prop.trajectory.len(), 2);
let current_epoch = prop.current_epoch();
assert_abs_diff_eq!(
(current_epoch - prop.initial_epoch()),
180.0,
epsilon = 1e-6
);
}
#[test]
fn test_sgppropagator_trajectory_mode_reset() {
setup_global_test_eop();
let line1 = "1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927";
let line2 = "2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537";
let mut prop = SGPPropagator::from_tle(line1, line2, 60.0).unwrap();
prop.set_trajectory_mode(TrajectoryMode::Disabled);
prop.step_by(60.0);
prop.step_by(60.0);
prop.reset();
assert_eq!(prop.current_epoch(), prop.initial_epoch());
assert_eq!(prop.current_state(), prop.initial_state());
assert_eq!(prop.trajectory_mode(), TrajectoryMode::Disabled);
}
fn make_decaying_propagator(step_size: f64) -> SGPPropagator {
SGPPropagator::from_omm_elements(
"2026-04-06T00:15:48.265056",
16.25673795, 0.00191239, 42.9658, 302.8224, 303.9951, 55.9119, 59231, step_size,
Some("STARLINK-31304"),
Some("2024-049A"),
Some('U'),
Some(0.0038650148), Some(0.13132014), Some(9.155391e-06), Some(0),
Some(999),
Some(0),
)
.unwrap()
}
#[test]
fn test_sgppropagator_propagation_error_terminates() {
setup_global_test_eop();
let mut prop = make_decaying_propagator(3600.0);
let initial_epoch = prop.initial_epoch();
let target_epoch = initial_epoch + 2.0 * 86400.0;
prop.propagate_to(target_epoch);
assert!(prop.is_terminated());
assert!(prop.termination_error().is_some());
assert!(
prop.termination_error()
.unwrap()
.to_string()
.contains("SGP4 propagation failed")
);
let state = prop.current_state();
for i in 0..6 {
assert!(state[i].is_finite(), "State component {} is not finite", i);
}
assert!(prop.current_epoch() < target_epoch);
}
#[test]
fn test_sgppropagator_state_returns_error_on_diverged_epoch() {
setup_global_test_eop();
let prop = make_decaying_propagator(3600.0);
let initial_epoch = prop.initial_epoch();
let far_epoch = initial_epoch + 2.0 * 86400.0;
let result = prop.state(far_epoch);
assert!(result.is_err());
}
#[test]
fn test_sgppropagator_reset_clears_termination_error() {
setup_global_test_eop();
let mut prop = make_decaying_propagator(3600.0);
let initial_epoch = prop.initial_epoch();
let target_epoch = initial_epoch + 2.0 * 86400.0;
prop.propagate_to(target_epoch);
assert!(prop.is_terminated());
assert!(prop.termination_error().is_some());
prop.reset();
assert!(!prop.is_terminated());
assert!(prop.termination_error().is_none());
}
#[test]
fn test_sgppropagator_reset_termination_clears_error() {
setup_global_test_eop();
let mut prop = make_decaying_propagator(3600.0);
let initial_epoch = prop.initial_epoch();
let target_epoch = initial_epoch + 2.0 * 86400.0;
prop.propagate_to(target_epoch);
assert!(prop.is_terminated());
assert!(prop.termination_error().is_some());
prop.reset_termination();
assert!(!prop.is_terminated());
assert!(prop.termination_error().is_none());
}
}