use std::path::Path;
use crate::ccsds::common::{
CCSDSCovariance, CCSDSFormat, CCSDSRefFrame, CCSDSSpacecraftParameters, CCSDSTimeSystem,
CCSDSUserDefined, ODMHeader,
};
use crate::time::Epoch;
use crate::utils::errors::BraheError;
#[derive(Debug, Clone)]
pub struct OPM {
pub header: ODMHeader,
pub metadata: OPMMetadata,
pub state_vector: OPMStateVector,
pub keplerian_elements: Option<OPMKeplerianElements>,
pub spacecraft_parameters: Option<CCSDSSpacecraftParameters>,
pub covariance: Option<CCSDSCovariance>,
pub maneuvers: Vec<OPMManeuver>,
pub user_defined: Option<CCSDSUserDefined>,
}
#[derive(Debug, Clone)]
pub struct OPMMetadata {
pub object_name: String,
pub object_id: String,
pub center_name: String,
pub ref_frame: CCSDSRefFrame,
pub ref_frame_epoch: Option<Epoch>,
pub time_system: CCSDSTimeSystem,
pub comments: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct OPMStateVector {
pub epoch: Epoch,
pub position: [f64; 3],
pub velocity: [f64; 3],
pub comments: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct OPMKeplerianElements {
pub semi_major_axis: f64,
pub eccentricity: f64,
pub inclination: f64,
pub ra_of_asc_node: f64,
pub arg_of_pericenter: f64,
pub true_anomaly: Option<f64>,
pub mean_anomaly: Option<f64>,
pub gm: Option<f64>,
pub comments: Vec<String>,
}
impl OPMMetadata {
pub fn new(
object_name: String,
object_id: String,
center_name: String,
ref_frame: CCSDSRefFrame,
time_system: CCSDSTimeSystem,
) -> Self {
Self {
object_name,
object_id,
center_name,
ref_frame,
ref_frame_epoch: None,
time_system,
comments: Vec::new(),
}
}
}
impl OPMStateVector {
pub fn new(epoch: Epoch, position: [f64; 3], velocity: [f64; 3]) -> Self {
Self {
epoch,
position,
velocity,
comments: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct OPMManeuver {
pub epoch_ignition: Epoch,
pub duration: f64,
pub delta_mass: Option<f64>,
pub ref_frame: CCSDSRefFrame,
pub dv: [f64; 3],
pub comments: Vec<String>,
}
impl OPMManeuver {
pub fn new(
epoch_ignition: Epoch,
duration: f64,
ref_frame: CCSDSRefFrame,
dv: [f64; 3],
) -> Self {
Self {
epoch_ignition,
duration,
delta_mass: None,
ref_frame,
dv,
comments: Vec::new(),
}
}
pub fn with_delta_mass(mut self, delta_mass: f64) -> Self {
self.delta_mass = Some(delta_mass);
self
}
}
impl OPM {
pub fn new(originator: String, metadata: OPMMetadata, state_vector: OPMStateVector) -> Self {
Self {
header: ODMHeader {
format_version: 3.0,
classification: None,
creation_date: Epoch::now(),
originator,
message_id: None,
comments: Vec::new(),
},
metadata,
state_vector,
keplerian_elements: None,
spacecraft_parameters: None,
covariance: None,
maneuvers: Vec::new(),
user_defined: None,
}
}
pub fn push_maneuver(&mut self, maneuver: OPMManeuver) {
self.maneuvers.push(maneuver);
}
#[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_opm(content),
CCSDSFormat::XML => crate::ccsds::xml::parse_opm_xml(content),
CCSDSFormat::JSON => crate::ccsds::json::parse_opm_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 OPM file: {}", e)))?;
Self::from_str(&content)
}
pub fn to_string(&self, format: CCSDSFormat) -> Result<String, BraheError> {
match format {
CCSDSFormat::KVN => crate::ccsds::kvn::write_opm(self),
CCSDSFormat::XML => crate::ccsds::xml::write_opm_xml(self),
CCSDSFormat::JSON => crate::ccsds::json::write_opm_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_opm_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 OPM file: {}", e)))
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use crate::ccsds::common::CCSDSJsonKeyCase;
use crate::time::TimeSystem;
#[test]
fn test_opm_builder() {
let metadata = OPMMetadata::new(
"SAT1".to_string(),
"2024-001A".to_string(),
"EARTH".to_string(),
CCSDSRefFrame::GCRF,
CCSDSTimeSystem::UTC,
);
let sv = OPMStateVector::new(Epoch::now(), [7000e3, 0.0, 0.0], [0.0, 7500.0, 0.0]);
let mut opm = OPM::new("TEST_ORG".to_string(), metadata, sv);
assert_eq!(opm.header.originator, "TEST_ORG");
assert_eq!(opm.maneuvers.len(), 0);
let m = OPMManeuver::new(Epoch::now(), 120.0, CCSDSRefFrame::RTN, [10.0, 0.0, 0.0])
.with_delta_mass(-15.0);
opm.push_maneuver(m);
assert_eq!(opm.maneuvers.len(), 1);
assert_eq!(opm.maneuvers[0].delta_mass, Some(-15.0));
}
#[test]
fn test_opm_json_round_trip_via_dispatch() {
let opm = OPM::from_file("test_assets/ccsds/opm/OPMExample1.txt").unwrap();
let json_str = opm.to_string(CCSDSFormat::JSON).unwrap();
assert!(json_str.contains("object_name") || json_str.contains("OBJECT_NAME"));
let opm2 = OPM::from_str(&json_str).unwrap();
assert_eq!(opm2.metadata.object_name, opm.metadata.object_name);
assert_eq!(opm2.metadata.object_id, opm.metadata.object_id);
assert!((opm2.state_vector.position[0] - opm.state_vector.position[0]).abs() < 1.0);
assert!((opm2.state_vector.velocity[0] - opm.state_vector.velocity[0]).abs() < 0.001);
}
#[test]
fn test_opm_from_file_nonexistent() {
let result = OPM::from_file("nonexistent_file.txt");
assert!(result.is_err());
}
#[test]
fn test_opm_metadata_new() {
let meta = OPMMetadata::new(
"ISS".to_string(),
"1998-067A".to_string(),
"EARTH".to_string(),
CCSDSRefFrame::ITRF2000,
CCSDSTimeSystem::UTC,
);
assert_eq!(meta.object_name, "ISS");
assert_eq!(meta.object_id, "1998-067A");
assert_eq!(meta.center_name, "EARTH");
assert!(matches!(meta.ref_frame, CCSDSRefFrame::ITRF2000));
assert!(matches!(meta.time_system, CCSDSTimeSystem::UTC));
assert!(meta.ref_frame_epoch.is_none());
assert!(meta.comments.is_empty());
}
#[test]
fn test_opm_state_vector_new() {
let epoch = Epoch::from_datetime(2024, 3, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let pos = [6503.514e3, 1239.647e3, -717.490e3];
let vel = [-873.160, 8740.420, -4191.076];
let sv = OPMStateVector::new(epoch, pos, vel);
assert!((sv.position[0] - 6503.514e3).abs() < 1e-6);
assert!((sv.position[1] - 1239.647e3).abs() < 1e-6);
assert!((sv.position[2] - (-717.490e3)).abs() < 1e-6);
assert!((sv.velocity[0] - (-873.160)).abs() < 1e-6);
assert!((sv.velocity[1] - 8740.420).abs() < 1e-6);
assert!((sv.velocity[2] - (-4191.076)).abs() < 1e-6);
assert!(sv.comments.is_empty());
}
#[test]
fn test_opm_new() {
let meta = OPMMetadata::new(
"SAT1".to_string(),
"2024-001A".to_string(),
"EARTH".to_string(),
CCSDSRefFrame::GCRF,
CCSDSTimeSystem::UTC,
);
let epoch = Epoch::from_datetime(2024, 3, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let sv = OPMStateVector::new(epoch, [7000e3, 0.0, 0.0], [0.0, 7500.0, 0.0]);
let opm = OPM::new("TEST_ORG".to_string(), meta, sv);
assert_eq!(opm.header.originator, "TEST_ORG");
assert!((opm.header.format_version - 3.0).abs() < 1e-15);
assert!(opm.header.classification.is_none());
assert!(opm.header.message_id.is_none());
assert_eq!(opm.metadata.object_name, "SAT1");
assert_eq!(opm.metadata.object_id, "2024-001A");
assert!(opm.keplerian_elements.is_none());
assert!(opm.spacecraft_parameters.is_none());
assert!(opm.covariance.is_none());
assert!(opm.maneuvers.is_empty());
assert!(opm.user_defined.is_none());
}
#[test]
fn test_opm_kvn_parse_example1() {
let opm = OPM::from_file("test_assets/ccsds/opm/OPMExample1.txt").unwrap();
assert_eq!(opm.metadata.object_name, "GODZILLA 5");
assert_eq!(opm.metadata.object_id, "1998-999A");
assert_eq!(opm.metadata.center_name, "EARTH");
assert!(matches!(opm.metadata.ref_frame, CCSDSRefFrame::ITRF2000));
assert!(matches!(opm.metadata.time_system, CCSDSTimeSystem::UTC));
assert!((opm.state_vector.position[0] - 6503.514e3).abs() < 1.0);
assert!((opm.state_vector.position[1] - 1239.647e3).abs() < 1.0);
assert!((opm.state_vector.position[2] - (-717.490e3)).abs() < 1.0);
assert!((opm.state_vector.velocity[0] - (-873.160)).abs() < 0.01);
assert!((opm.state_vector.velocity[1] - 8740.420).abs() < 0.01);
assert!((opm.state_vector.velocity[2] - (-4191.076)).abs() < 0.01);
}
#[test]
fn test_opm_with_keplerian_elements() {
let opm = OPM::from_file("test_assets/ccsds/opm/OPMExample5.txt").unwrap();
assert!(
opm.keplerian_elements.is_some(),
"OPMExample5 should have Keplerian elements"
);
let ke = opm.keplerian_elements.as_ref().unwrap();
assert!((ke.eccentricity - 0.020842611).abs() < 1e-9);
assert!((ke.inclination - 0.117746).abs() < 1e-6);
assert!((ke.ra_of_asc_node - 17.604721).abs() < 1e-6);
assert!((ke.arg_of_pericenter - 218.242943).abs() < 1e-6);
assert!(ke.true_anomaly.is_some());
assert!((ke.true_anomaly.unwrap() - 41.922339).abs() < 1e-6);
}
#[test]
fn test_opm_with_maneuvers() {
let opm = OPM::from_file("test_assets/ccsds/opm/OPMExample5.txt").unwrap();
assert!(
opm.maneuvers.len() >= 2,
"OPMExample5 should have maneuvers"
);
assert!(opm.maneuvers[0].delta_mass.is_some());
assert!((opm.maneuvers[0].delta_mass.unwrap() - (-18.418)).abs() < 0.001);
assert!((opm.maneuvers[0].duration - 132.60).abs() < 0.01);
}
#[test]
fn test_opm_kvn_round_trip() {
let opm = OPM::from_file("test_assets/ccsds/opm/OPMExample1.txt").unwrap();
let kvn_str = opm.to_string(CCSDSFormat::KVN).unwrap();
let opm2 = OPM::from_str(&kvn_str).unwrap();
assert_eq!(opm2.metadata.object_name, opm.metadata.object_name);
assert_eq!(opm2.metadata.object_id, opm.metadata.object_id);
assert!((opm2.state_vector.position[0] - opm.state_vector.position[0]).abs() < 1.0);
assert!((opm2.state_vector.velocity[0] - opm.state_vector.velocity[0]).abs() < 0.001);
assert!(opm2.spacecraft_parameters.is_some());
let sp2 = opm2.spacecraft_parameters.as_ref().unwrap();
assert!((sp2.mass.unwrap() - 3000.0).abs() < 0.01);
}
#[test]
fn test_opm_kvn_round_trip_with_keplerian_and_maneuvers() {
let opm = OPM::from_file("test_assets/ccsds/opm/OPMExample5.txt").unwrap();
let kvn_str = opm.to_string(CCSDSFormat::KVN).unwrap();
let opm2 = OPM::from_str(&kvn_str).unwrap();
assert!(opm2.keplerian_elements.is_some());
let ke1 = opm.keplerian_elements.as_ref().unwrap();
let ke2 = opm2.keplerian_elements.as_ref().unwrap();
assert!((ke2.eccentricity - ke1.eccentricity).abs() < 1e-9);
assert!((ke2.semi_major_axis - ke1.semi_major_axis).abs() < 1.0);
assert_eq!(opm2.maneuvers.len(), opm.maneuvers.len());
assert!((opm2.maneuvers[0].duration - opm.maneuvers[0].duration).abs() < 0.01);
assert!((opm2.maneuvers[0].dv[0] - opm.maneuvers[0].dv[0]).abs() < 0.01);
}
#[test]
fn test_opm_xml_round_trip() {
let opm = OPM::from_file("test_assets/ccsds/opm/OPMExample3.xml").unwrap();
let xml_str = opm.to_string(CCSDSFormat::XML).unwrap();
let opm2 = OPM::from_str(&xml_str).unwrap();
assert_eq!(opm2.metadata.object_name, opm.metadata.object_name);
assert_eq!(opm2.metadata.object_id, opm.metadata.object_id);
assert!((opm2.state_vector.position[0] - opm.state_vector.position[0]).abs() < 1.0);
assert!((opm2.state_vector.velocity[0] - opm.state_vector.velocity[0]).abs() < 0.001);
assert!(opm2.covariance.is_some());
let cov1 = opm.covariance.as_ref().unwrap();
let cov2 = opm2.covariance.as_ref().unwrap();
assert!((cov2.matrix[(0, 0)] - cov1.matrix[(0, 0)]).abs() < 1.0);
}
#[test]
fn test_opm_xml_parse_example3() {
let opm = OPM::from_file("test_assets/ccsds/opm/OPMExample3.xml").unwrap();
assert_eq!(opm.metadata.object_name, "OSPREY 5");
assert_eq!(opm.metadata.object_id, "1998-999A");
assert_eq!(opm.metadata.center_name, "EARTH");
assert!(matches!(opm.metadata.ref_frame, CCSDSRefFrame::TOD));
assert!(opm.metadata.ref_frame_epoch.is_some());
assert!((opm.state_vector.position[0] - 6503514.0).abs() < 1.0);
assert!((opm.state_vector.velocity[0] - (-873.16)).abs() < 0.01);
assert!(opm.spacecraft_parameters.is_some());
let sp = opm.spacecraft_parameters.as_ref().unwrap();
assert!((sp.mass.unwrap() - 3000.0).abs() < 0.01);
assert!(opm.covariance.is_some());
let cov = opm.covariance.as_ref().unwrap();
assert_eq!(cov.cov_ref_frame.as_ref().unwrap(), &CCSDSRefFrame::ITRF97);
assert!((cov.matrix[(0, 0)] - 316000.0).abs() < 1.0);
}
#[test]
fn test_opm_to_file_kvn() {
let opm = OPM::from_file("test_assets/ccsds/opm/OPMExample1.txt").unwrap();
let dir = std::env::temp_dir();
let path = dir.join("brahe_test_opm.txt");
opm.to_file(&path, CCSDSFormat::KVN).unwrap();
let opm2 = OPM::from_file(&path).unwrap();
assert_eq!(opm2.metadata.object_name, opm.metadata.object_name);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_opm_maneuver_new() {
let epoch = Epoch::from_datetime(2024, 6, 1, 9, 0, 0.0, 0.0, TimeSystem::UTC);
let m = OPMManeuver::new(epoch, 132.6, CCSDSRefFrame::EME2000, [10.0, -5.0, 2.0]);
assert!((m.duration - 132.6).abs() < 1e-15);
assert!(matches!(m.ref_frame, CCSDSRefFrame::EME2000));
assert!((m.dv[0] - 10.0).abs() < 1e-15);
assert!((m.dv[1] - (-5.0)).abs() < 1e-15);
assert!((m.dv[2] - 2.0).abs() < 1e-15);
assert!(m.delta_mass.is_none());
assert!(m.comments.is_empty());
}
#[test]
fn test_opm_maneuver_with_delta_mass() {
let epoch = Epoch::from_datetime(2024, 6, 1, 9, 0, 0.0, 0.0, TimeSystem::UTC);
let m = OPMManeuver::new(epoch, 100.0, CCSDSRefFrame::RTN, [1.0, 0.0, 0.0])
.with_delta_mass(-10.5);
assert_eq!(m.delta_mass, Some(-10.5));
}
#[test]
fn test_opm_push_maneuver() {
let meta = OPMMetadata::new(
"SAT".to_string(),
"2024-001A".to_string(),
"EARTH".to_string(),
CCSDSRefFrame::GCRF,
CCSDSTimeSystem::UTC,
);
let sv = OPMStateVector::new(Epoch::now(), [7000e3, 0.0, 0.0], [0.0, 7500.0, 0.0]);
let mut opm = OPM::new("ORG".to_string(), meta, sv);
assert_eq!(opm.maneuvers.len(), 0);
let epoch = Epoch::from_datetime(2024, 6, 1, 9, 0, 0.0, 0.0, TimeSystem::UTC);
opm.push_maneuver(OPMManeuver::new(
epoch,
60.0,
CCSDSRefFrame::RTN,
[1.0, 0.0, 0.0],
));
opm.push_maneuver(OPMManeuver::new(
epoch,
30.0,
CCSDSRefFrame::RTN,
[0.0, 1.0, 0.0],
));
assert_eq!(opm.maneuvers.len(), 2);
assert!((opm.maneuvers[0].duration - 60.0).abs() < 1e-15);
assert!((opm.maneuvers[1].duration - 30.0).abs() < 1e-15);
}
#[test]
fn test_opm_kvn_parse_spacecraft_params() {
let opm = OPM::from_file("test_assets/ccsds/opm/OPMExample1.txt").unwrap();
assert!(opm.spacecraft_parameters.is_some());
let sp = opm.spacecraft_parameters.as_ref().unwrap();
assert!(sp.mass.is_some());
assert!((sp.mass.unwrap() - 3000.0).abs() < 0.01);
}
#[test]
fn test_opm_to_json_string_upper_key_case() {
let opm = OPM::from_file("test_assets/ccsds/opm/OPMExample1.txt").unwrap();
let json_str = opm.to_json_string(CCSDSJsonKeyCase::Upper).unwrap();
assert!(json_str.contains("OBJECT_NAME"));
assert!(json_str.contains("OBJECT_ID"));
}
fn assert_opm_fields_match(opm1: &OPM, opm2: &OPM) {
assert_eq!(opm1.header.format_version, opm2.header.format_version);
assert_eq!(opm1.header.originator, opm2.header.originator);
assert_eq!(opm1.header.classification, opm2.header.classification);
assert_eq!(opm1.header.message_id, opm2.header.message_id);
assert_eq!(opm1.metadata.object_name, opm2.metadata.object_name);
assert_eq!(opm1.metadata.object_id, opm2.metadata.object_id);
assert_eq!(opm1.metadata.center_name, opm2.metadata.center_name);
assert_eq!(opm1.metadata.ref_frame, opm2.metadata.ref_frame);
assert_eq!(opm1.metadata.time_system, opm2.metadata.time_system);
for i in 0..3 {
assert!(
(opm1.state_vector.position[i] - opm2.state_vector.position[i]).abs() < 1.0,
"position[{}] mismatch: {} vs {}",
i,
opm1.state_vector.position[i],
opm2.state_vector.position[i]
);
assert!(
(opm1.state_vector.velocity[i] - opm2.state_vector.velocity[i]).abs() < 0.001,
"velocity[{}] mismatch: {} vs {}",
i,
opm1.state_vector.velocity[i],
opm2.state_vector.velocity[i]
);
}
assert_eq!(
opm1.keplerian_elements.is_some(),
opm2.keplerian_elements.is_some()
);
if let (Some(ke1), Some(ke2)) = (&opm1.keplerian_elements, &opm2.keplerian_elements) {
assert!((ke1.semi_major_axis - ke2.semi_major_axis).abs() < 1.0);
assert!((ke1.eccentricity - ke2.eccentricity).abs() < 1e-9);
assert!((ke1.inclination - ke2.inclination).abs() < 1e-6);
assert!((ke1.ra_of_asc_node - ke2.ra_of_asc_node).abs() < 1e-6);
assert!((ke1.arg_of_pericenter - ke2.arg_of_pericenter).abs() < 1e-6);
assert_eq!(ke1.true_anomaly.is_some(), ke2.true_anomaly.is_some());
if let (Some(ta1), Some(ta2)) = (ke1.true_anomaly, ke2.true_anomaly) {
assert!((ta1 - ta2).abs() < 1e-6);
}
assert_eq!(ke1.mean_anomaly.is_some(), ke2.mean_anomaly.is_some());
assert_eq!(ke1.gm.is_some(), ke2.gm.is_some());
if let (Some(gm1), Some(gm2)) = (ke1.gm, ke2.gm) {
assert!((gm1 - gm2).abs() < 1e3);
}
}
assert_eq!(
opm1.spacecraft_parameters.is_some(),
opm2.spacecraft_parameters.is_some()
);
if let (Some(sp1), Some(sp2)) = (&opm1.spacecraft_parameters, &opm2.spacecraft_parameters) {
assert_eq!(sp1.mass.is_some(), sp2.mass.is_some());
if let (Some(m1), Some(m2)) = (sp1.mass, sp2.mass) {
assert!((m1 - m2).abs() < 0.01);
}
assert_eq!(sp1.solar_rad_area.is_some(), sp2.solar_rad_area.is_some());
if let (Some(a1), Some(a2)) = (sp1.solar_rad_area, sp2.solar_rad_area) {
assert!((a1 - a2).abs() < 0.01);
}
assert_eq!(sp1.solar_rad_coeff.is_some(), sp2.solar_rad_coeff.is_some());
if let (Some(c1), Some(c2)) = (sp1.solar_rad_coeff, sp2.solar_rad_coeff) {
assert!((c1 - c2).abs() < 0.01);
}
assert_eq!(sp1.drag_area.is_some(), sp2.drag_area.is_some());
if let (Some(a1), Some(a2)) = (sp1.drag_area, sp2.drag_area) {
assert!((a1 - a2).abs() < 0.01);
}
assert_eq!(sp1.drag_coeff.is_some(), sp2.drag_coeff.is_some());
if let (Some(c1), Some(c2)) = (sp1.drag_coeff, sp2.drag_coeff) {
assert!((c1 - c2).abs() < 0.01);
}
}
assert_eq!(opm1.covariance.is_some(), opm2.covariance.is_some());
if let (Some(cov1), Some(cov2)) = (&opm1.covariance, &opm2.covariance) {
assert_eq!(cov1.cov_ref_frame, cov2.cov_ref_frame);
for i in 0..6 {
for j in 0..6 {
let rel = if cov1.matrix[(i, j)].abs() > 1e-20 {
((cov1.matrix[(i, j)] - cov2.matrix[(i, j)]) / cov1.matrix[(i, j)]).abs()
} else {
(cov1.matrix[(i, j)] - cov2.matrix[(i, j)]).abs()
};
assert!(
rel < 1e-4,
"cov({},{}) mismatch: {} vs {}",
i,
j,
cov1.matrix[(i, j)],
cov2.matrix[(i, j)]
);
}
}
}
assert_eq!(opm1.maneuvers.len(), opm2.maneuvers.len());
for (m1, m2) in opm1.maneuvers.iter().zip(opm2.maneuvers.iter()) {
assert!((m1.duration - m2.duration).abs() < 0.01);
assert_eq!(m1.ref_frame, m2.ref_frame);
for i in 0..3 {
assert!((m1.dv[i] - m2.dv[i]).abs() < 0.01);
}
assert_eq!(m1.delta_mass.is_some(), m2.delta_mass.is_some());
if let (Some(dm1), Some(dm2)) = (m1.delta_mass, m2.delta_mass) {
assert!((dm1 - dm2).abs() < 0.01);
}
}
assert_eq!(opm1.user_defined.is_some(), opm2.user_defined.is_some());
if let (Some(ud1), Some(ud2)) = (&opm1.user_defined, &opm2.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_opm_kvn_full_round_trip() {
let opm1 = OPM::from_file("test_assets/ccsds/opm/OPMExample4.txt").unwrap();
let kvn = opm1.to_string(CCSDSFormat::KVN).unwrap();
let opm2 = OPM::from_str(&kvn).unwrap();
assert_opm_fields_match(&opm1, &opm2);
}
#[test]
fn test_opm_xml_full_round_trip() {
let opm1 = OPM::from_file("test_assets/ccsds/opm/OPMExample4.txt").unwrap();
let xml = opm1.to_string(CCSDSFormat::XML).unwrap();
let opm2 = OPM::from_str(&xml).unwrap();
assert_opm_fields_match(&opm1, &opm2);
}
#[test]
fn test_opm_json_full_round_trip() {
let opm1 = OPM::from_file("test_assets/ccsds/opm/OPMExample4.txt").unwrap();
let json = opm1.to_string(CCSDSFormat::JSON).unwrap();
let opm2 = OPM::from_str(&json).unwrap();
assert_opm_fields_match(&opm1, &opm2);
}
#[test]
fn test_opm_kvn_full_round_trip_with_maneuvers() {
let opm1 = OPM::from_file("test_assets/ccsds/opm/OPMExample5.txt").unwrap();
let kvn = opm1.to_string(CCSDSFormat::KVN).unwrap();
let opm2 = OPM::from_str(&kvn).unwrap();
assert_opm_fields_match(&opm1, &opm2);
}
}