use std::path::Path;
use nalgebra::{SMatrix, SVector};
use crate::ccsds::common::{CCSDSFormat, CCSDSRefFrame, CCSDSUserDefined, CDMCovarianceDimension};
use crate::time::Epoch;
use crate::utils::errors::BraheError;
#[derive(Debug, Clone)]
pub struct CDMHeader {
pub format_version: f64,
pub classification: Option<String>,
pub creation_date: Epoch,
pub originator: String,
pub message_for: Option<String>,
pub message_id: String,
pub comments: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CDMRelativeMetadata {
pub conjunction_id: Option<String>,
pub tca: Epoch,
pub miss_distance: f64,
pub mahalanobis_distance: Option<f64>,
pub relative_speed: Option<f64>,
pub relative_position_r: Option<f64>,
pub relative_position_t: Option<f64>,
pub relative_position_n: Option<f64>,
pub relative_velocity_r: Option<f64>,
pub relative_velocity_t: Option<f64>,
pub relative_velocity_n: Option<f64>,
pub approach_angle: Option<f64>,
pub start_screen_period: Option<Epoch>,
pub stop_screen_period: Option<Epoch>,
pub screen_type: Option<String>,
pub screen_volume_frame: Option<CCSDSRefFrame>,
pub screen_volume_shape: Option<String>,
pub screen_volume_radius: Option<f64>,
pub screen_volume_x: Option<f64>,
pub screen_volume_y: Option<f64>,
pub screen_volume_z: Option<f64>,
pub screen_entry_time: Option<Epoch>,
pub screen_exit_time: Option<Epoch>,
pub screen_pc_threshold: Option<f64>,
pub collision_percentile: Option<Vec<u32>>,
pub collision_probability: Option<f64>,
pub collision_probability_method: Option<String>,
pub collision_max_probability: Option<f64>,
pub collision_max_pc_method: Option<String>,
pub sefi_collision_probability: Option<f64>,
pub sefi_collision_probability_method: Option<String>,
pub sefi_fragmentation_model: Option<String>,
pub previous_message_id: Option<String>,
pub previous_message_epoch: Option<Epoch>,
pub next_message_epoch: Option<Epoch>,
pub comments: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CDMObjectMetadata {
pub object: String,
pub object_designator: String,
pub catalog_name: String,
pub object_name: String,
pub international_designator: String,
pub object_type: Option<String>,
pub ops_status: Option<String>,
pub operator_contact_position: Option<String>,
pub operator_organization: Option<String>,
pub operator_phone: Option<String>,
pub operator_email: Option<String>,
pub ephemeris_name: String,
pub odm_msg_link: Option<String>,
pub adm_msg_link: Option<String>,
pub obs_before_next_message: Option<String>,
pub covariance_method: String,
pub covariance_source: Option<String>,
pub maneuverable: String,
pub orbit_center: Option<String>,
pub ref_frame: CCSDSRefFrame,
pub alt_cov_type: Option<String>,
pub alt_cov_ref_frame: Option<CCSDSRefFrame>,
pub gravity_model: Option<String>,
pub atmospheric_model: Option<String>,
pub n_body_perturbations: Option<String>,
pub solar_rad_pressure: Option<String>,
pub earth_tides: Option<String>,
pub intrack_thrust: Option<String>,
pub comments: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CDMODParameters {
pub time_lastob_start: Option<Epoch>,
pub time_lastob_end: Option<Epoch>,
pub recommended_od_span: Option<f64>,
pub actual_od_span: Option<f64>,
pub obs_available: Option<u32>,
pub obs_used: Option<u32>,
pub tracks_available: Option<u32>,
pub tracks_used: Option<u32>,
pub residuals_accepted: Option<f64>,
pub weighted_rms: Option<f64>,
pub od_epoch: Option<Epoch>,
pub comments: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CDMAdditionalParameters {
pub area_pc: Option<f64>,
pub area_pc_min: Option<f64>,
pub area_pc_max: Option<f64>,
pub area_drg: Option<f64>,
pub area_srp: Option<f64>,
pub oeb_parent_frame: Option<String>,
pub oeb_parent_frame_epoch: Option<Epoch>,
pub oeb_q1: Option<f64>,
pub oeb_q2: Option<f64>,
pub oeb_q3: Option<f64>,
pub oeb_qc: Option<f64>,
pub oeb_max: Option<f64>,
pub oeb_int: Option<f64>,
pub oeb_min: Option<f64>,
pub area_along_oeb_max: Option<f64>,
pub area_along_oeb_int: Option<f64>,
pub area_along_oeb_min: Option<f64>,
pub rcs: Option<f64>,
pub rcs_min: Option<f64>,
pub rcs_max: Option<f64>,
pub vm_absolute: Option<f64>,
pub vm_apparent_min: Option<f64>,
pub vm_apparent: Option<f64>,
pub vm_apparent_max: Option<f64>,
pub reflectance: Option<f64>,
pub mass: Option<f64>,
pub hbr: Option<f64>,
pub cd_area_over_mass: Option<f64>,
pub cr_area_over_mass: Option<f64>,
pub thrust_acceleration: Option<f64>,
pub sedr: Option<f64>,
pub min_dv: Option<[f64; 3]>,
pub max_dv: Option<[f64; 3]>,
pub lead_time_reqd_before_tca: Option<f64>,
pub apoapsis_altitude: Option<f64>,
pub periapsis_altitude: Option<f64>,
pub inclination: Option<f64>,
pub cov_confidence: Option<f64>,
pub cov_confidence_method: Option<String>,
pub comments: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CDMStateVector {
pub position: [f64; 3],
pub velocity: [f64; 3],
pub comments: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CDMRTNCovariance {
pub matrix: SMatrix<f64, 9, 9>,
pub dimension: CDMCovarianceDimension,
pub comments: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CDMXYZCovariance {
pub matrix: SMatrix<f64, 9, 9>,
pub dimension: CDMCovarianceDimension,
pub comments: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CDMAdditionalCovarianceMetadata {
pub density_forecast_uncertainty: Option<f64>,
pub cscale_factor_min: Option<f64>,
pub cscale_factor: Option<f64>,
pub cscale_factor_max: Option<f64>,
pub screening_data_source: Option<String>,
pub dcp_sensitivity_vector_position: Option<[f64; 3]>,
pub dcp_sensitivity_vector_velocity: Option<[f64; 3]>,
pub comments: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CDMObjectData {
pub od_parameters: Option<CDMODParameters>,
pub additional_parameters: Option<CDMAdditionalParameters>,
pub state_vector: CDMStateVector,
pub rtn_covariance: CDMRTNCovariance,
pub xyz_covariance: Option<CDMXYZCovariance>,
pub additional_covariance_metadata: Option<CDMAdditionalCovarianceMetadata>,
pub csig3eigvec3: Option<String>,
pub comments: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CDMObject {
pub metadata: CDMObjectMetadata,
pub data: CDMObjectData,
}
#[derive(Debug, Clone)]
pub struct CDM {
pub header: CDMHeader,
pub relative_metadata: CDMRelativeMetadata,
pub object1: CDMObject,
pub object2: CDMObject,
pub user_defined: Option<CCSDSUserDefined>,
}
impl CDMRelativeMetadata {
pub fn new(tca: Epoch, miss_distance: f64) -> Self {
Self {
conjunction_id: None,
tca,
miss_distance,
mahalanobis_distance: None,
relative_speed: None,
relative_position_r: None,
relative_position_t: None,
relative_position_n: None,
relative_velocity_r: None,
relative_velocity_t: None,
relative_velocity_n: None,
approach_angle: None,
start_screen_period: None,
stop_screen_period: None,
screen_type: None,
screen_volume_frame: None,
screen_volume_shape: None,
screen_volume_radius: None,
screen_volume_x: None,
screen_volume_y: None,
screen_volume_z: None,
screen_entry_time: None,
screen_exit_time: None,
screen_pc_threshold: None,
collision_percentile: None,
collision_probability: None,
collision_probability_method: None,
collision_max_probability: None,
collision_max_pc_method: None,
sefi_collision_probability: None,
sefi_collision_probability_method: None,
sefi_fragmentation_model: None,
previous_message_id: None,
previous_message_epoch: None,
next_message_epoch: None,
comments: Vec::new(),
}
}
}
impl CDMObjectMetadata {
#[allow(clippy::too_many_arguments)]
pub fn new(
object: String,
object_designator: String,
catalog_name: String,
object_name: String,
international_designator: String,
ephemeris_name: String,
covariance_method: String,
maneuverable: String,
ref_frame: CCSDSRefFrame,
) -> Self {
Self {
object,
object_designator,
catalog_name,
object_name,
international_designator,
object_type: None,
ops_status: None,
operator_contact_position: None,
operator_organization: None,
operator_phone: None,
operator_email: None,
ephemeris_name,
odm_msg_link: None,
adm_msg_link: None,
obs_before_next_message: None,
covariance_method,
covariance_source: None,
maneuverable,
orbit_center: None,
ref_frame,
alt_cov_type: None,
alt_cov_ref_frame: None,
gravity_model: None,
atmospheric_model: None,
n_body_perturbations: None,
solar_rad_pressure: None,
earth_tides: None,
intrack_thrust: None,
comments: Vec::new(),
}
}
}
impl CDMStateVector {
pub fn new(position: [f64; 3], velocity: [f64; 3]) -> Self {
Self {
position,
velocity,
comments: Vec::new(),
}
}
}
impl CDMRTNCovariance {
pub fn from_6x6(matrix: SMatrix<f64, 6, 6>) -> Self {
let mut full = SMatrix::<f64, 9, 9>::zeros();
for i in 0..6 {
for j in 0..6 {
full[(i, j)] = matrix[(i, j)];
}
}
Self {
matrix: full,
dimension: CDMCovarianceDimension::SixBySix,
comments: Vec::new(),
}
}
pub fn to_6x6(&self) -> SMatrix<f64, 6, 6> {
self.matrix.fixed_view::<6, 6>(0, 0).into()
}
}
impl CDMObject {
pub fn new(
metadata: CDMObjectMetadata,
state_vector: CDMStateVector,
rtn_covariance: CDMRTNCovariance,
) -> Self {
Self {
metadata,
data: CDMObjectData {
od_parameters: None,
additional_parameters: None,
state_vector,
rtn_covariance,
xyz_covariance: None,
additional_covariance_metadata: None,
csig3eigvec3: None,
comments: Vec::new(),
},
}
}
}
impl CDM {
pub fn new(
originator: String,
message_id: String,
tca: Epoch,
miss_distance: f64,
object1: CDMObject,
object2: CDMObject,
) -> Self {
Self {
header: CDMHeader {
format_version: 1.0,
classification: None,
creation_date: Epoch::now(),
originator,
message_for: None,
message_id,
comments: Vec::new(),
},
relative_metadata: CDMRelativeMetadata::new(tca, miss_distance),
object1,
object2,
user_defined: None,
}
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(content: &str) -> Result<Self, BraheError> {
let format = crate::ccsds::common::detect_format(content);
match format {
CCSDSFormat::KVN => crate::ccsds::kvn::parse_cdm(content),
CCSDSFormat::XML => crate::ccsds::xml::parse_cdm_xml(content),
CCSDSFormat::JSON => crate::ccsds::json::parse_cdm_json(content),
}
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, BraheError> {
let content = std::fs::read_to_string(path.as_ref())
.map_err(|e| BraheError::IoError(format!("Failed to read CDM file: {}", e)))?;
Self::from_str(&content)
}
pub fn to_string(&self, format: CCSDSFormat) -> Result<String, BraheError> {
match format {
CCSDSFormat::KVN => crate::ccsds::kvn::write_cdm(self),
CCSDSFormat::XML => crate::ccsds::xml::write_cdm_xml(self),
CCSDSFormat::JSON => crate::ccsds::json::write_cdm_json(
self,
crate::ccsds::common::CCSDSJsonKeyCase::Lower,
),
}
}
pub fn to_json_string(
&self,
key_case: crate::ccsds::common::CCSDSJsonKeyCase,
) -> Result<String, BraheError> {
crate::ccsds::json::write_cdm_json(self, key_case)
}
pub fn to_file<P: AsRef<Path>>(&self, path: P, format: CCSDSFormat) -> Result<(), BraheError> {
let content = self.to_string(format)?;
std::fs::write(path.as_ref(), content)
.map_err(|e| BraheError::IoError(format!("Failed to write CDM file: {}", e)))
}
pub fn tca(&self) -> &Epoch {
&self.relative_metadata.tca
}
pub fn miss_distance(&self) -> f64 {
self.relative_metadata.miss_distance
}
pub fn collision_probability(&self) -> Option<f64> {
self.relative_metadata.collision_probability
}
pub fn object1_state(&self) -> SVector<f64, 6> {
let sv = &self.object1.data.state_vector;
SVector::<f64, 6>::new(
sv.position[0],
sv.position[1],
sv.position[2],
sv.velocity[0],
sv.velocity[1],
sv.velocity[2],
)
}
pub fn object2_state(&self) -> SVector<f64, 6> {
let sv = &self.object2.data.state_vector;
SVector::<f64, 6>::new(
sv.position[0],
sv.position[1],
sv.position[2],
sv.velocity[0],
sv.velocity[1],
sv.velocity[2],
)
}
pub fn relative_position_rtn(&self) -> Option<[f64; 3]> {
let rm = &self.relative_metadata;
match (
rm.relative_position_r,
rm.relative_position_t,
rm.relative_position_n,
) {
(Some(r), Some(t), Some(n)) => Some([r, t, n]),
_ => None,
}
}
pub fn relative_velocity_rtn(&self) -> Option<[f64; 3]> {
let rm = &self.relative_metadata;
match (
rm.relative_velocity_r,
rm.relative_velocity_t,
rm.relative_velocity_n,
) {
(Some(r), Some(t), Some(n)) => Some([r, t, n]),
_ => None,
}
}
pub fn object1_rtn_covariance_6x6(&self) -> SMatrix<f64, 6, 6> {
self.object1.data.rtn_covariance.to_6x6()
}
pub fn object2_rtn_covariance_6x6(&self) -> SMatrix<f64, 6, 6> {
self.object2.data.rtn_covariance.to_6x6()
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
#[test]
fn test_cdm_new() {
let sv1 = CDMStateVector::new([7000e3, 0.0, 0.0], [0.0, 7500.0, 0.0]);
let sv2 = CDMStateVector::new([7001e3, 0.0, 0.0], [0.0, -7500.0, 0.0]);
let cov1 = CDMRTNCovariance::from_6x6(SMatrix::<f64, 6, 6>::identity());
let cov2 = CDMRTNCovariance::from_6x6(SMatrix::<f64, 6, 6>::identity());
let meta1 = CDMObjectMetadata::new(
"OBJECT1".to_string(),
"12345".to_string(),
"SATCAT".to_string(),
"SAT A".to_string(),
"2020-001A".to_string(),
"NONE".to_string(),
"CALCULATED".to_string(),
"YES".to_string(),
CCSDSRefFrame::EME2000,
);
let meta2 = CDMObjectMetadata::new(
"OBJECT2".to_string(),
"67890".to_string(),
"SATCAT".to_string(),
"SAT B".to_string(),
"2021-002B".to_string(),
"NONE".to_string(),
"CALCULATED".to_string(),
"NO".to_string(),
CCSDSRefFrame::EME2000,
);
let obj1 = CDMObject::new(meta1, sv1, cov1);
let obj2 = CDMObject::new(meta2, sv2, cov2);
let tca = Epoch::from_datetime(2024, 1, 15, 12, 0, 0.0, 0.0, crate::time::TimeSystem::UTC);
let cdm = CDM::new(
"TEST_ORG".to_string(),
"MSG001".to_string(),
tca,
715.0,
obj1,
obj2,
);
assert_eq!(cdm.header.originator, "TEST_ORG");
assert_eq!(cdm.header.message_id, "MSG001");
assert_eq!(cdm.miss_distance(), 715.0);
assert_eq!(cdm.object1.metadata.object_name, "SAT A");
assert_eq!(cdm.object2.metadata.object_name, "SAT B");
let s1 = cdm.object1_state();
assert_eq!(s1[0], 7000e3);
assert_eq!(s1[4], 7500.0);
assert!(cdm.collision_probability().is_none());
assert!(cdm.relative_position_rtn().is_none());
}
#[test]
fn test_cdm_kvn_parse_example1() {
let cdm = CDM::from_file("test_assets/ccsds/cdm/CDMExample1.txt").unwrap();
assert_eq!(cdm.header.format_version, 1.0);
assert_eq!(cdm.header.originator, "JSPOC");
assert_eq!(cdm.header.message_id, "201113719185");
assert!(cdm.header.message_for.is_none());
assert_eq!(cdm.miss_distance(), 715.0);
assert!(cdm.collision_probability().is_none());
assert!(cdm.relative_position_rtn().is_none());
assert_eq!(cdm.object1.metadata.object, "OBJECT1");
assert_eq!(cdm.object1.metadata.object_designator, "12345");
assert_eq!(cdm.object1.metadata.object_name, "SATELLITE A");
assert_eq!(cdm.object1.metadata.ephemeris_name, "EPHEMERIS SATELLITE A");
assert_eq!(cdm.object1.metadata.maneuverable, "YES");
assert_eq!(cdm.object1.metadata.ref_frame, CCSDSRefFrame::EME2000);
let s1 = cdm.object1_state();
assert!((s1[0] - 2570097.065).abs() < 0.01);
assert!((s1[1] - 2244654.904).abs() < 0.01);
assert!((s1[2] - 6281497.978).abs() < 0.01);
assert!((s1[3] - 4418.769571).abs() < 0.0001);
assert!((s1[4] - 4833.547743).abs() < 0.0001);
assert!((s1[5] - (-3526.774282)).abs() < 0.0001);
let cov1 = cdm.object1_rtn_covariance_6x6();
assert!((cov1[(0, 0)] - 4.142e+01).abs() < 1e-10);
assert!((cov1[(1, 0)] - (-8.579e+00)).abs() < 1e-10);
assert!((cov1[(1, 1)] - 2.533e+03).abs() < 1e-10);
assert_eq!(
cdm.object1.data.rtn_covariance.dimension,
CDMCovarianceDimension::SixBySix
);
assert_eq!(cdm.object2.metadata.object, "OBJECT2");
assert_eq!(cdm.object2.metadata.object_designator, "30337");
assert_eq!(cdm.object2.metadata.object_name, "FENGYUN 1C DEB");
assert_eq!(cdm.object2.metadata.maneuverable, "NO");
let s2 = cdm.object2_state();
assert!((s2[0] - 2569540.800).abs() < 0.01);
assert!((s2[3] - (-2888.6125)).abs() < 0.001);
}
#[test]
fn test_cdm_kvn_parse_example2_extended_cov() {
let cdm = CDM::from_file("test_assets/ccsds/cdm/CDMExample2.txt").unwrap();
assert!(cdm.header.message_for.is_some());
assert_eq!(cdm.header.message_for.as_deref(), Some("SATELLITE A"));
assert_eq!(cdm.relative_metadata.relative_speed, Some(14762.0));
assert!((cdm.collision_probability().unwrap() - 4.835e-05).abs() < 1e-10);
assert_eq!(
cdm.relative_metadata
.collision_probability_method
.as_deref(),
Some("FOSTER-1992")
);
let rp = cdm.relative_position_rtn().unwrap();
assert!((rp[0] - 27.4).abs() < 0.01);
assert!((rp[1] - (-70.2)).abs() < 0.01);
assert_eq!(cdm.object1.metadata.object_type.as_deref(), Some("PAYLOAD"));
assert_eq!(
cdm.object1.metadata.operator_email.as_deref(),
Some("JOHN.DOE@SOMEWHERE.NET")
);
assert_eq!(
cdm.object1.metadata.gravity_model.as_deref(),
Some("EGM-96: 36D 36O")
);
let od = cdm.object1.data.od_parameters.as_ref().unwrap();
assert_eq!(od.obs_available, Some(592));
assert_eq!(od.obs_used, Some(579));
assert!((od.recommended_od_span.unwrap() - 7.88).abs() < 0.01);
assert!((od.residuals_accepted.unwrap() - 97.8).abs() < 0.01);
let ap = cdm.object1.data.additional_parameters.as_ref().unwrap();
assert!((ap.area_pc.unwrap() - 5.2).abs() < 0.01);
assert!((ap.mass.unwrap() - 251.6).abs() < 0.01);
assert!((ap.sedr.unwrap() - 4.54570e-05).abs() < 1e-10);
assert_eq!(
cdm.object1.data.rtn_covariance.dimension,
CDMCovarianceDimension::EightByEight
);
let cov = &cdm.object1.data.rtn_covariance.matrix;
assert!((cov[(6, 0)] - (-1.862e+00)).abs() < 1e-10);
assert!((cov[(7, 7)] - 1.593e-02).abs() < 1e-10);
}
#[test]
fn test_cdm_kvn_parse_issue940_v2() {
let cdm = CDM::from_file("test_assets/ccsds/cdm/CDMExample_issue_940.txt").unwrap();
assert_eq!(cdm.header.format_version, 2.0);
assert!(cdm.header.classification.is_some());
assert_eq!(
cdm.relative_metadata.conjunction_id.as_deref(),
Some("20220708T10hz SATELLITEA SATELLITEB")
);
assert_eq!(cdm.relative_metadata.approach_angle, Some(180.0));
assert_eq!(cdm.relative_metadata.screen_type.as_deref(), Some("SHAPE"));
assert_eq!(cdm.relative_metadata.screen_pc_threshold, Some(1.0e-03));
assert!(cdm.relative_metadata.collision_percentile.is_some());
assert_eq!(
cdm.relative_metadata.collision_percentile.as_ref().unwrap(),
&[50, 51, 52]
);
assert!(cdm.relative_metadata.sefi_collision_probability.is_some());
assert!(cdm.relative_metadata.previous_message_id.is_some());
assert_eq!(
cdm.object1.metadata.odm_msg_link.as_deref(),
Some("ODM_MSG_35132.txt")
);
assert_eq!(
cdm.object1.metadata.covariance_source.as_deref(),
Some("HAC Covariance")
);
assert_eq!(cdm.object1.metadata.alt_cov_type.as_deref(), Some("XYZ"));
let ap = cdm.object1.data.additional_parameters.as_ref().unwrap();
assert!((ap.oeb_q1.unwrap() - 0.03123).abs() < 1e-10);
assert!((ap.hbr.unwrap() - 2.5).abs() < 0.01);
assert!((ap.apoapsis_altitude.unwrap() - 800e3).abs() < 0.1); assert!((ap.inclination.unwrap() - 89.0).abs() < 0.01);
assert!(cdm.object1.data.xyz_covariance.is_some());
let xyz = cdm.object1.data.xyz_covariance.as_ref().unwrap();
assert_eq!(xyz.dimension, CDMCovarianceDimension::NineByNine);
assert!((xyz.matrix[(0, 0)] - 0.1).abs() < 1e-10);
let acm = cdm
.object1
.data
.additional_covariance_metadata
.as_ref()
.unwrap();
assert!((acm.density_forecast_uncertainty.unwrap() - 2.5).abs() < 0.01);
assert!((acm.cscale_factor.unwrap() - 1.0).abs() < 0.01);
assert!(acm.dcp_sensitivity_vector_position.is_some());
assert!(cdm.object2.data.csig3eigvec3.is_some());
assert!(cdm.user_defined.is_some());
let ud = cdm.user_defined.as_ref().unwrap();
assert!(ud.parameters.contains_key("OBJ1_TIME_LASTOB_START"));
}
#[test]
fn test_cdm_kvn_parse_issue942_maneuverable_na() {
let cdm = CDM::from_file("test_assets/ccsds/cdm/CDMExample_issue942.txt").unwrap();
assert_eq!(cdm.object1.metadata.maneuverable, "N/A");
}
#[test]
fn test_cdm_kvn_parse_alfano01() {
let cdm = CDM::from_file("test_assets/ccsds/cdm/AlfanoTestCase01.cdm").unwrap();
assert!(cdm.miss_distance() > 0.0);
let s1 = cdm.object1_state();
assert!(s1[0].abs() > 1.0); assert_eq!(
cdm.object1.data.rtn_covariance.dimension,
CDMCovarianceDimension::EightByEight
);
}
#[test]
fn test_cdm_kvn_parse_real_world() {
let cdm = CDM::from_file("test_assets/ccsds/cdm/ION_SCV8_vs_STARLINK_1233.txt").unwrap();
assert!(cdm.miss_distance() > 0.0);
assert!(cdm.object1.data.od_parameters.is_some());
assert!(cdm.object2.data.od_parameters.is_some());
}
#[test]
fn test_cdm_kvn_missing_tca() {
let result = CDM::from_file("test_assets/ccsds/cdm/CDM-missing-TCA.txt");
assert!(result.is_err());
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("TCA"),
"Error should mention TCA: {}",
err_msg
);
}
#[test]
fn test_cdm_kvn_missing_obj2_state() {
let result = CDM::from_file("test_assets/ccsds/cdm/CDM-missing-object2-state-vector.txt");
assert!(result.is_err());
}
#[test]
fn test_cdm_kvn_round_trip_example1() {
let cdm1 = CDM::from_file("test_assets/ccsds/cdm/CDMExample1.txt").unwrap();
let kvn = cdm1.to_string(CCSDSFormat::KVN).unwrap();
let cdm2 = CDM::from_str(&kvn).unwrap();
assert_eq!(cdm1.header.format_version, cdm2.header.format_version);
assert_eq!(cdm1.header.originator, cdm2.header.originator);
assert_eq!(cdm1.header.message_id, cdm2.header.message_id);
assert!((cdm1.miss_distance() - cdm2.miss_distance()).abs() < 1e-6);
for i in 0..6 {
assert!(
(cdm1.object1_state()[i] - cdm2.object1_state()[i]).abs() < 0.01,
"Object1 state[{}] mismatch: {} vs {}",
i,
cdm1.object1_state()[i],
cdm2.object1_state()[i]
);
assert!(
(cdm1.object2_state()[i] - cdm2.object2_state()[i]).abs() < 0.01,
"Object2 state[{}] mismatch: {} vs {}",
i,
cdm1.object2_state()[i],
cdm2.object2_state()[i]
);
}
let c1 = cdm1.object1_rtn_covariance_6x6();
let c2 = cdm2.object1_rtn_covariance_6x6();
for i in 0..6 {
for j in 0..6 {
let rel = if c1[(i, j)].abs() > 1e-20 {
((c1[(i, j)] - c2[(i, j)]) / c1[(i, j)]).abs()
} else {
(c1[(i, j)] - c2[(i, j)]).abs()
};
assert!(
rel < 1e-4,
"Cov({},{}) mismatch: {} vs {}",
i,
j,
c1[(i, j)],
c2[(i, j)]
);
}
}
}
#[test]
fn test_cdm_xml_parse_example1() {
let cdm = CDM::from_file("test_assets/ccsds/cdm/CDMExample1.xml").unwrap();
assert_eq!(cdm.header.format_version, 1.0);
assert_eq!(cdm.header.originator, "JSPOC");
assert_eq!(cdm.header.message_for.as_deref(), Some("SATELLITE A"));
assert_eq!(cdm.miss_distance(), 715.0);
let rp = cdm.relative_position_rtn().unwrap();
assert!((rp[0] - 27.4).abs() < 0.01);
assert_eq!(cdm.object1.metadata.object_name, "SATELLITE A");
assert_eq!(cdm.object1.metadata.ref_frame, CCSDSRefFrame::EME2000);
let s1 = cdm.object1_state();
assert!((s1[0] - 2570097.065).abs() < 0.01);
let cov1 = cdm.object1_rtn_covariance_6x6();
assert!((cov1[(0, 0)] - 4.142e+01).abs() < 1e-10);
assert_eq!(cdm.object2.metadata.object_name, "FENGYUN 1C DEB");
}
#[test]
fn test_cdm_xml_round_trip() {
let cdm1 = CDM::from_file("test_assets/ccsds/cdm/CDMExample1.xml").unwrap();
let xml = cdm1.to_string(CCSDSFormat::XML).unwrap();
let cdm2 = CDM::from_str(&xml).unwrap();
assert_eq!(cdm1.header.originator, cdm2.header.originator);
assert!((cdm1.miss_distance() - cdm2.miss_distance()).abs() < 1e-6);
for i in 0..6 {
assert!((cdm1.object1_state()[i] - cdm2.object1_state()[i]).abs() < 0.01);
}
}
#[test]
fn test_cdm_json_round_trip() {
let cdm1 = CDM::from_file("test_assets/ccsds/cdm/CDMExample1.txt").unwrap();
let json = cdm1.to_string(CCSDSFormat::JSON).unwrap();
let cdm2 = CDM::from_str(&json).unwrap();
assert_eq!(cdm1.header.originator, cdm2.header.originator);
assert!((cdm1.miss_distance() - cdm2.miss_distance()).abs() < 1e-6);
for i in 0..6 {
assert!(
(cdm1.object1_state()[i] - cdm2.object1_state()[i]).abs() < 0.01,
"Object1 state[{}]: {} vs {}",
i,
cdm1.object1_state()[i],
cdm2.object1_state()[i]
);
}
}
#[test]
fn test_cdm_kvn_to_xml_cross_format() {
let cdm_kvn = CDM::from_file("test_assets/ccsds/cdm/CDMExample1.txt").unwrap();
let xml = cdm_kvn.to_string(CCSDSFormat::XML).unwrap();
let cdm_xml = CDM::from_str(&xml).unwrap();
assert_eq!(cdm_kvn.header.originator, cdm_xml.header.originator);
assert!((cdm_kvn.miss_distance() - cdm_xml.miss_distance()).abs() < 1e-6);
for i in 0..6 {
assert!((cdm_kvn.object1_state()[i] - cdm_xml.object1_state()[i]).abs() < 0.01);
assert!((cdm_kvn.object2_state()[i] - cdm_xml.object2_state()[i]).abs() < 0.01);
}
}
#[test]
fn test_cdm_kvn_round_trip_example2() {
let cdm1 = CDM::from_file("test_assets/ccsds/cdm/CDMExample2.txt").unwrap();
let kvn = cdm1.to_string(CCSDSFormat::KVN).unwrap();
let cdm2 = CDM::from_str(&kvn).unwrap();
assert_eq!(cdm1.header.message_for, cdm2.header.message_for);
assert!(
(cdm1.collision_probability().unwrap() - cdm2.collision_probability().unwrap()).abs()
< 1e-10
);
assert_eq!(
cdm1.relative_metadata.collision_probability_method,
cdm2.relative_metadata.collision_probability_method
);
assert_eq!(
cdm1.object1.data.rtn_covariance.dimension,
cdm2.object1.data.rtn_covariance.dimension
);
let od1 = cdm1.object1.data.od_parameters.as_ref().unwrap();
let od2 = cdm2.object1.data.od_parameters.as_ref().unwrap();
assert_eq!(od1.obs_available, od2.obs_available);
assert_eq!(od1.obs_used, od2.obs_used);
}
#[test]
fn test_cdm_xml_round_trip_issue940_all_fields() {
let cdm1 = CDM::from_file("test_assets/ccsds/cdm/CDMExample_issue_940.txt").unwrap();
let xml = cdm1.to_string(CCSDSFormat::XML).unwrap();
let cdm2 = CDM::from_str(&xml).unwrap();
assert_eq!(cdm1.header.originator, cdm2.header.originator);
assert_eq!(cdm1.header.message_id, cdm2.header.message_id);
assert_eq!(cdm1.header.classification, cdm2.header.classification);
assert!((cdm1.miss_distance() - cdm2.miss_distance()).abs() < 1e-6);
assert_eq!(
cdm1.relative_metadata.conjunction_id,
cdm2.relative_metadata.conjunction_id
);
assert_eq!(
cdm1.relative_metadata.approach_angle,
cdm2.relative_metadata.approach_angle
);
assert_eq!(
cdm1.relative_metadata.screen_type,
cdm2.relative_metadata.screen_type
);
assert_eq!(
cdm1.relative_metadata.screen_pc_threshold,
cdm2.relative_metadata.screen_pc_threshold
);
assert_eq!(
cdm1.relative_metadata.collision_percentile,
cdm2.relative_metadata.collision_percentile
);
assert_eq!(
cdm1.relative_metadata.collision_probability,
cdm2.relative_metadata.collision_probability
);
assert_eq!(
cdm1.relative_metadata.collision_probability_method,
cdm2.relative_metadata.collision_probability_method
);
assert_eq!(
cdm1.relative_metadata.collision_max_probability,
cdm2.relative_metadata.collision_max_probability
);
assert_eq!(
cdm1.relative_metadata.collision_max_pc_method,
cdm2.relative_metadata.collision_max_pc_method
);
assert_eq!(
cdm1.relative_metadata.previous_message_id,
cdm2.relative_metadata.previous_message_id
);
for i in 0..6 {
assert!((cdm1.object1_state()[i] - cdm2.object1_state()[i]).abs() < 0.01);
assert!((cdm1.object2_state()[i] - cdm2.object2_state()[i]).abs() < 0.01);
}
assert_eq!(
cdm1.object1.metadata.odm_msg_link,
cdm2.object1.metadata.odm_msg_link
);
assert_eq!(
cdm1.object1.metadata.covariance_source,
cdm2.object1.metadata.covariance_source
);
assert_eq!(
cdm1.object1.metadata.alt_cov_type,
cdm2.object1.metadata.alt_cov_type
);
let od1 = cdm1.object1.data.od_parameters.as_ref().unwrap();
let od2 = cdm2.object1.data.od_parameters.as_ref().unwrap();
assert_eq!(od1.obs_available, od2.obs_available);
assert_eq!(od1.obs_used, od2.obs_used);
let ap1 = cdm1.object1.data.additional_parameters.as_ref().unwrap();
let ap2 = cdm2.object1.data.additional_parameters.as_ref().unwrap();
assert!((ap1.hbr.unwrap() - ap2.hbr.unwrap()).abs() < 0.01);
assert!((ap1.oeb_q1.unwrap() - ap2.oeb_q1.unwrap()).abs() < 1e-10);
assert!(cdm2.object1.data.xyz_covariance.is_some());
let xyz1 = cdm1.object1.data.xyz_covariance.as_ref().unwrap();
let xyz2 = cdm2.object1.data.xyz_covariance.as_ref().unwrap();
assert_eq!(xyz1.dimension, xyz2.dimension);
assert!((xyz1.matrix[(0, 0)] - xyz2.matrix[(0, 0)]).abs() < 1e-10);
let acm1 = cdm1
.object1
.data
.additional_covariance_metadata
.as_ref()
.unwrap();
let acm2 = cdm2
.object1
.data
.additional_covariance_metadata
.as_ref()
.unwrap();
assert_eq!(
acm1.density_forecast_uncertainty,
acm2.density_forecast_uncertainty
);
assert_eq!(acm1.cscale_factor, acm2.cscale_factor);
assert!(cdm2.user_defined.is_some());
}
#[test]
fn test_cdm_rtn_covariance_6x6() {
let mut m6 = SMatrix::<f64, 6, 6>::zeros();
m6[(0, 0)] = 41.42;
m6[(1, 0)] = -8.579;
m6[(0, 1)] = -8.579;
m6[(1, 1)] = 2533.0;
let cov = CDMRTNCovariance::from_6x6(m6);
assert_eq!(cov.dimension, CDMCovarianceDimension::SixBySix);
let extracted = cov.to_6x6();
assert_eq!(extracted[(0, 0)], 41.42);
assert_eq!(extracted[(1, 0)], -8.579);
assert_eq!(extracted[(1, 1)], 2533.0);
assert_eq!(cov.matrix[(6, 0)], 0.0);
assert_eq!(cov.matrix[(8, 8)], 0.0);
}
fn assert_cdm_fields_match(cdm1: &CDM, cdm2: &CDM) {
assert_eq!(cdm1.header.format_version, cdm2.header.format_version);
assert_eq!(cdm1.header.originator, cdm2.header.originator);
assert_eq!(cdm1.header.message_id, cdm2.header.message_id);
assert_eq!(cdm1.header.classification, cdm2.header.classification);
assert_eq!(cdm1.header.message_for, cdm2.header.message_for);
assert!((cdm1.miss_distance() - cdm2.miss_distance()).abs() < 1e-6);
assert_eq!(
cdm1.relative_metadata.conjunction_id,
cdm2.relative_metadata.conjunction_id
);
assert_eq!(
cdm1.relative_metadata.relative_speed.is_some(),
cdm2.relative_metadata.relative_speed.is_some()
);
if let (Some(r1), Some(r2)) = (
cdm1.relative_metadata.relative_speed,
cdm2.relative_metadata.relative_speed,
) {
assert!((r1 - r2).abs() < 0.01);
}
assert_eq!(
cdm1.relative_metadata.collision_probability,
cdm2.relative_metadata.collision_probability
);
assert_eq!(
cdm1.relative_metadata.collision_probability_method,
cdm2.relative_metadata.collision_probability_method
);
assert_eq!(
cdm1.relative_metadata.collision_percentile,
cdm2.relative_metadata.collision_percentile
);
assert_eq!(
cdm1.relative_metadata.collision_max_probability,
cdm2.relative_metadata.collision_max_probability
);
assert_eq!(
cdm1.relative_metadata.screen_type,
cdm2.relative_metadata.screen_type
);
for i in 0..6 {
assert!(
(cdm1.object1_state()[i] - cdm2.object1_state()[i]).abs() < 0.01,
"obj1 state[{}] mismatch",
i
);
assert!(
(cdm1.object2_state()[i] - cdm2.object2_state()[i]).abs() < 0.01,
"obj2 state[{}] mismatch",
i
);
}
assert_eq!(
cdm1.object1.metadata.object_name,
cdm2.object1.metadata.object_name
);
assert_eq!(
cdm1.object1.metadata.object_designator,
cdm2.object1.metadata.object_designator
);
assert_eq!(
cdm1.object1.metadata.maneuverable,
cdm2.object1.metadata.maneuverable
);
assert_eq!(
cdm1.object1.metadata.ref_frame,
cdm2.object1.metadata.ref_frame
);
assert_eq!(
cdm1.object1.data.rtn_covariance.dimension,
cdm2.object1.data.rtn_covariance.dimension
);
let c1 = cdm1.object1_rtn_covariance_6x6();
let c2 = cdm2.object1_rtn_covariance_6x6();
for i in 0..6 {
for j in 0..6 {
let rel = if c1[(i, j)].abs() > 1e-20 {
((c1[(i, j)] - c2[(i, j)]) / c1[(i, j)]).abs()
} else {
(c1[(i, j)] - c2[(i, j)]).abs()
};
assert!(rel < 1e-4, "obj1 cov({},{}) mismatch", i, j);
}
}
assert_eq!(
cdm1.object1.data.od_parameters.is_some(),
cdm2.object1.data.od_parameters.is_some()
);
if let (Some(od1), Some(od2)) = (
&cdm1.object1.data.od_parameters,
&cdm2.object1.data.od_parameters,
) {
assert_eq!(od1.obs_available, od2.obs_available);
assert_eq!(od1.obs_used, od2.obs_used);
assert_eq!(od1.tracks_available, od2.tracks_available);
assert_eq!(od1.tracks_used, od2.tracks_used);
}
assert_eq!(
cdm1.object1.data.additional_parameters.is_some(),
cdm2.object1.data.additional_parameters.is_some()
);
if let (Some(ap1), Some(ap2)) = (
&cdm1.object1.data.additional_parameters,
&cdm2.object1.data.additional_parameters,
) {
assert_eq!(ap1.area_pc.is_some(), ap2.area_pc.is_some());
assert_eq!(ap1.mass.is_some(), ap2.mass.is_some());
assert_eq!(ap1.hbr.is_some(), ap2.hbr.is_some());
if let (Some(h1), Some(h2)) = (ap1.hbr, ap2.hbr) {
assert!((h1 - h2).abs() < 0.01);
}
}
assert_eq!(cdm1.user_defined.is_some(), cdm2.user_defined.is_some());
if let (Some(ud1), Some(ud2)) = (&cdm1.user_defined, &cdm2.user_defined) {
assert_eq!(ud1.parameters.len(), ud2.parameters.len());
for (k, v) in &ud1.parameters {
assert_eq!(
ud2.parameters.get(k),
Some(v),
"user_defined key {} mismatch",
k
);
}
}
}
#[test]
fn test_cdm_kvn_full_round_trip() {
let cdm1 = CDM::from_file("test_assets/ccsds/cdm/CDMExample_issue_940.txt").unwrap();
let kvn = cdm1.to_string(CCSDSFormat::KVN).unwrap();
let cdm2 = CDM::from_str(&kvn).unwrap();
assert_cdm_fields_match(&cdm1, &cdm2);
}
#[test]
fn test_cdm_xml_full_round_trip() {
let cdm1 = CDM::from_file("test_assets/ccsds/cdm/CDMExample_issue_940.txt").unwrap();
let xml = cdm1.to_string(CCSDSFormat::XML).unwrap();
let cdm2 = CDM::from_str(&xml).unwrap();
assert_cdm_fields_match(&cdm1, &cdm2);
}
#[test]
fn test_cdm_json_full_round_trip() {
let cdm1 = CDM::from_file("test_assets/ccsds/cdm/CDMExample2.txt").unwrap();
let json = cdm1.to_string(CCSDSFormat::JSON).unwrap();
let cdm2 = CDM::from_str(&json).unwrap();
assert_eq!(cdm1.header.originator, cdm2.header.originator);
assert_eq!(cdm1.header.message_id, cdm2.header.message_id);
assert_eq!(cdm1.header.message_for, cdm2.header.message_for);
assert!((cdm1.miss_distance() - cdm2.miss_distance()).abs() < 1e-6);
assert_eq!(
cdm1.relative_metadata.collision_probability,
cdm2.relative_metadata.collision_probability
);
assert_eq!(
cdm1.relative_metadata.collision_probability_method,
cdm2.relative_metadata.collision_probability_method
);
for i in 0..6 {
assert!(
(cdm1.object1_state()[i] - cdm2.object1_state()[i]).abs() < 0.01,
"obj1 state[{}] mismatch",
i
);
assert!(
(cdm1.object2_state()[i] - cdm2.object2_state()[i]).abs() < 0.01,
"obj2 state[{}] mismatch",
i
);
}
let c1 = cdm1.object1_rtn_covariance_6x6();
let c2 = cdm2.object1_rtn_covariance_6x6();
for i in 0..6 {
for j in 0..6 {
let rel = if c1[(i, j)].abs() > 1e-20 {
((c1[(i, j)] - c2[(i, j)]) / c1[(i, j)]).abs()
} else {
(c1[(i, j)] - c2[(i, j)]).abs()
};
assert!(rel < 1e-4, "obj1 cov({},{}) mismatch", i, j);
}
}
}
}