Skip to main content

brahe/ccsds/
cdm.rs

1/*!
2 * CCSDS Conjunction Data Message (CDM) data structures.
3 *
4 * CDM messages describe a conjunction between two space objects, containing
5 * state vectors, covariance matrices, and collision probability data at
6 * the Time of Closest Approach (TCA).
7 *
8 * Reference: CCSDS 508.0-P-1.1 (Conjunction Data Message), March 2024
9 */
10
11use std::path::Path;
12
13use nalgebra::{SMatrix, SVector};
14
15use crate::ccsds::common::{CCSDSFormat, CCSDSRefFrame, CCSDSUserDefined, CDMCovarianceDimension};
16use crate::time::Epoch;
17use crate::utils::errors::BraheError;
18
19// ---------------------------------------------------------------------------
20// Header
21// ---------------------------------------------------------------------------
22
23/// CDM message header.
24///
25/// Standalone type (not reusing `ODMHeader`) because CDM has `message_for`
26/// (absent in ODM) and `message_id` is mandatory (optional in ODM). The
27/// version keyword is `CCSDS_CDM_VERS`, not `CCSDS_OPM_VERS`.
28#[derive(Debug, Clone)]
29pub struct CDMHeader {
30    /// CCSDS_CDM_VERS (M) — format version, e.g. 1.0 or 2.0
31    pub format_version: f64,
32    /// CLASSIFICATION (O) — user-defined classification/caveats
33    pub classification: Option<String>,
34    /// CREATION_DATE (M) — always UTC
35    pub creation_date: Epoch,
36    /// ORIGINATOR (M) — SANA-registered organisation abbreviation
37    pub originator: String,
38    /// MESSAGE_FOR (O) — spacecraft name(s) CDM applies to
39    pub message_for: Option<String>,
40    /// MESSAGE_ID (M) — unique identifier per originator
41    pub message_id: String,
42    /// Comments
43    pub comments: Vec<String>,
44}
45
46// ---------------------------------------------------------------------------
47// Relative Metadata
48// ---------------------------------------------------------------------------
49
50/// Conjunction-level metadata shared between both objects.
51///
52/// Contains TCA, miss distance, relative state in RTN, screening volume
53/// parameters, collision probability, and message tracking fields.
54#[derive(Debug, Clone)]
55pub struct CDMRelativeMetadata {
56    /// CONJUNCTION_ID (O) v2.0
57    pub conjunction_id: Option<String>,
58    /// TCA (M) — Time of Closest Approach, always UTC
59    pub tca: Epoch,
60    /// MISS_DISTANCE (M) — Units: m
61    pub miss_distance: f64,
62    /// MAHALANOBIS_DISTANCE (O)
63    pub mahalanobis_distance: Option<f64>,
64    /// RELATIVE_SPEED (O) — Units: m/s
65    pub relative_speed: Option<f64>,
66    /// RELATIVE_POSITION_R (O) — Units: m
67    pub relative_position_r: Option<f64>,
68    /// RELATIVE_POSITION_T (O) — Units: m
69    pub relative_position_t: Option<f64>,
70    /// RELATIVE_POSITION_N (O) — Units: m
71    pub relative_position_n: Option<f64>,
72    /// RELATIVE_VELOCITY_R (O) — Units: m/s
73    pub relative_velocity_r: Option<f64>,
74    /// RELATIVE_VELOCITY_T (O) — Units: m/s
75    pub relative_velocity_t: Option<f64>,
76    /// RELATIVE_VELOCITY_N (O) — Units: m/s
77    pub relative_velocity_n: Option<f64>,
78    /// APPROACH_ANGLE (O) — Units: degrees
79    pub approach_angle: Option<f64>,
80
81    // Screening volume
82    /// START_SCREEN_PERIOD (O)
83    pub start_screen_period: Option<Epoch>,
84    /// STOP_SCREEN_PERIOD (O)
85    pub stop_screen_period: Option<Epoch>,
86    /// SCREEN_TYPE (O) — e.g. SHAPE, PC, PC_MAX
87    pub screen_type: Option<String>,
88    /// SCREEN_VOLUME_FRAME (C) — RTN or TVN
89    pub screen_volume_frame: Option<CCSDSRefFrame>,
90    /// SCREEN_VOLUME_SHAPE (C) — SPHERE, ELLIPSOID, BOX
91    pub screen_volume_shape: Option<String>,
92    /// SCREEN_VOLUME_RADIUS (C) — Units: m (for SPHERE)
93    pub screen_volume_radius: Option<f64>,
94    /// SCREEN_VOLUME_X (C) — Units: m
95    pub screen_volume_x: Option<f64>,
96    /// SCREEN_VOLUME_Y (C) — Units: m
97    pub screen_volume_y: Option<f64>,
98    /// SCREEN_VOLUME_Z (C) — Units: m
99    pub screen_volume_z: Option<f64>,
100    /// SCREEN_ENTRY_TIME (C)
101    pub screen_entry_time: Option<Epoch>,
102    /// SCREEN_EXIT_TIME (C)
103    pub screen_exit_time: Option<Epoch>,
104    /// SCREEN_PC_THRESHOLD (C) v2.0
105    pub screen_pc_threshold: Option<f64>,
106
107    // Collision probability
108    /// COLLISION_PERCENTILE (O) v2.0
109    pub collision_percentile: Option<Vec<u32>>,
110    /// COLLISION_PROBABILITY (O)
111    pub collision_probability: Option<f64>,
112    /// COLLISION_PROBABILITY_METHOD (O)
113    pub collision_probability_method: Option<String>,
114    /// COLLISION_MAX_PROBABILITY (O) v2.0
115    pub collision_max_probability: Option<f64>,
116    /// COLLISION_MAX_PC_METHOD (O) v2.0
117    pub collision_max_pc_method: Option<String>,
118
119    // SEFI
120    /// SEFI_COLLISION_PROBABILITY (O) v2.0
121    pub sefi_collision_probability: Option<f64>,
122    /// SEFI_COLLISION_PROBABILITY_METHOD (O) v2.0
123    pub sefi_collision_probability_method: Option<String>,
124    /// SEFI_FRAGMENTATION_MODEL (O) v2.0
125    pub sefi_fragmentation_model: Option<String>,
126
127    // Message tracking
128    /// PREVIOUS_MESSAGE_ID (O) v2.0
129    pub previous_message_id: Option<String>,
130    /// PREVIOUS_MESSAGE_EPOCH (O) v2.0
131    pub previous_message_epoch: Option<Epoch>,
132    /// NEXT_MESSAGE_EPOCH (O) v2.0
133    pub next_message_epoch: Option<Epoch>,
134
135    /// Comments
136    pub comments: Vec<String>,
137}
138
139// ---------------------------------------------------------------------------
140// Object Metadata
141// ---------------------------------------------------------------------------
142
143/// Metadata for one object in the conjunction.
144#[derive(Debug, Clone)]
145pub struct CDMObjectMetadata {
146    /// OBJECT (M) — "OBJECT1" or "OBJECT2"
147    pub object: String,
148    /// OBJECT_DESIGNATOR (M) — catalog ID
149    pub object_designator: String,
150    /// CATALOG_NAME (M)
151    pub catalog_name: String,
152    /// OBJECT_NAME (M)
153    pub object_name: String,
154    /// INTERNATIONAL_DESIGNATOR (M) — COSPAR format
155    pub international_designator: String,
156    /// OBJECT_TYPE (O) — PAYLOAD, DEBRIS, ROCKET BODY, UNKNOWN, etc.
157    pub object_type: Option<String>,
158    /// OPS_STATUS (O) v2.0
159    pub ops_status: Option<String>,
160    /// OPERATOR_CONTACT_POSITION (O)
161    pub operator_contact_position: Option<String>,
162    /// OPERATOR_ORGANIZATION (O)
163    pub operator_organization: Option<String>,
164    /// OPERATOR_PHONE (O)
165    pub operator_phone: Option<String>,
166    /// OPERATOR_EMAIL (O)
167    pub operator_email: Option<String>,
168    /// EPHEMERIS_NAME (M)
169    pub ephemeris_name: String,
170    /// ODM_MSG_LINK (C) v2.0 — mandatory if EPHEMERIS_NAME=ODM
171    pub odm_msg_link: Option<String>,
172    /// ADM_MSG_LINK (O) v2.0
173    pub adm_msg_link: Option<String>,
174    /// OBS_BEFORE_NEXT_MESSAGE (O) v2.0
175    pub obs_before_next_message: Option<String>,
176    /// COVARIANCE_METHOD (M) — CALCULATED or DEFAULT
177    pub covariance_method: String,
178    /// COVARIANCE_SOURCE (O) v2.0
179    pub covariance_source: Option<String>,
180    /// MANEUVERABLE (M) — YES, NO, N/A, UNKNOWN
181    pub maneuverable: String,
182    /// ORBIT_CENTER (O) — defaults to EARTH
183    pub orbit_center: Option<String>,
184    /// REF_FRAME (M)
185    pub ref_frame: CCSDSRefFrame,
186    /// ALT_COV_TYPE (O) — XYZ
187    pub alt_cov_type: Option<String>,
188    /// ALT_COV_REF_FRAME (C) — mandatory if ALT_COV_TYPE present
189    pub alt_cov_ref_frame: Option<CCSDSRefFrame>,
190    /// GRAVITY_MODEL (O)
191    pub gravity_model: Option<String>,
192    /// ATMOSPHERIC_MODEL (O)
193    pub atmospheric_model: Option<String>,
194    /// N_BODY_PERTURBATIONS (O)
195    pub n_body_perturbations: Option<String>,
196    /// SOLAR_RAD_PRESSURE (O) — YES/NO
197    pub solar_rad_pressure: Option<String>,
198    /// EARTH_TIDES (O) — YES/NO
199    pub earth_tides: Option<String>,
200    /// INTRACK_THRUST (O) — YES/NO
201    pub intrack_thrust: Option<String>,
202    /// Comments
203    pub comments: Vec<String>,
204}
205
206// ---------------------------------------------------------------------------
207// OD Parameters
208// ---------------------------------------------------------------------------
209
210/// Orbit determination parameters for one object.
211#[derive(Debug, Clone)]
212pub struct CDMODParameters {
213    /// TIME_LASTOB_START (O)
214    pub time_lastob_start: Option<Epoch>,
215    /// TIME_LASTOB_END (O)
216    pub time_lastob_end: Option<Epoch>,
217    /// RECOMMENDED_OD_SPAN (O) — Units: days
218    pub recommended_od_span: Option<f64>,
219    /// ACTUAL_OD_SPAN (O) — Units: days
220    pub actual_od_span: Option<f64>,
221    /// OBS_AVAILABLE (O)
222    pub obs_available: Option<u32>,
223    /// OBS_USED (O)
224    pub obs_used: Option<u32>,
225    /// TRACKS_AVAILABLE (O)
226    pub tracks_available: Option<u32>,
227    /// TRACKS_USED (O)
228    pub tracks_used: Option<u32>,
229    /// RESIDUALS_ACCEPTED (O) — Units: percent (0–100)
230    pub residuals_accepted: Option<f64>,
231    /// WEIGHTED_RMS (O)
232    pub weighted_rms: Option<f64>,
233    /// OD_EPOCH (O) v2.0
234    pub od_epoch: Option<Epoch>,
235    /// Comments
236    pub comments: Vec<String>,
237}
238
239// ---------------------------------------------------------------------------
240// Additional Parameters
241// ---------------------------------------------------------------------------
242
243/// Physical and operational parameters for one object.
244#[derive(Debug, Clone)]
245pub struct CDMAdditionalParameters {
246    /// AREA_PC (O) — Units: m²
247    pub area_pc: Option<f64>,
248    /// AREA_PC_MIN (O) v2.0 — Units: m²
249    pub area_pc_min: Option<f64>,
250    /// AREA_PC_MAX (O) v2.0 — Units: m²
251    pub area_pc_max: Option<f64>,
252    /// AREA_DRG (O) — Units: m²
253    pub area_drg: Option<f64>,
254    /// AREA_SRP (O) — Units: m²
255    pub area_srp: Option<f64>,
256
257    // OEB (Optimally Enclosing Box) v2.0
258    /// OEB_PARENT_FRAME (C)
259    pub oeb_parent_frame: Option<String>,
260    /// OEB_PARENT_FRAME_EPOCH (C)
261    pub oeb_parent_frame_epoch: Option<Epoch>,
262    /// OEB_Q1 (O)
263    pub oeb_q1: Option<f64>,
264    /// OEB_Q2 (O)
265    pub oeb_q2: Option<f64>,
266    /// OEB_Q3 (O)
267    pub oeb_q3: Option<f64>,
268    /// OEB_QC (O) — quaternion scalar
269    pub oeb_qc: Option<f64>,
270    /// OEB_MAX (O) — Units: m
271    pub oeb_max: Option<f64>,
272    /// OEB_INT (O) — Units: m
273    pub oeb_int: Option<f64>,
274    /// OEB_MIN (O) — Units: m
275    pub oeb_min: Option<f64>,
276    /// AREA_ALONG_OEB_MAX (O) — Units: m²
277    pub area_along_oeb_max: Option<f64>,
278    /// AREA_ALONG_OEB_INT (O) — Units: m²
279    pub area_along_oeb_int: Option<f64>,
280    /// AREA_ALONG_OEB_MIN (O) — Units: m²
281    pub area_along_oeb_min: Option<f64>,
282
283    // RCS / Visual magnitude v2.0
284    /// RCS (O) — Units: m²
285    pub rcs: Option<f64>,
286    /// RCS_MIN (O) — Units: m²
287    pub rcs_min: Option<f64>,
288    /// RCS_MAX (O) — Units: m²
289    pub rcs_max: Option<f64>,
290    /// VM_ABSOLUTE (O)
291    pub vm_absolute: Option<f64>,
292    /// VM_APPARENT_MIN (O)
293    pub vm_apparent_min: Option<f64>,
294    /// VM_APPARENT (O)
295    pub vm_apparent: Option<f64>,
296    /// VM_APPARENT_MAX (O)
297    pub vm_apparent_max: Option<f64>,
298    /// REFLECTANCE (O) — 0 to 1
299    pub reflectance: Option<f64>,
300
301    /// MASS (O) — Units: kg
302    pub mass: Option<f64>,
303    /// HBR (O) — hard-body radius. Units: m
304    pub hbr: Option<f64>,
305    /// CD_AREA_OVER_MASS (O) — Units: m²/kg
306    pub cd_area_over_mass: Option<f64>,
307    /// CR_AREA_OVER_MASS (O) — Units: m²/kg
308    pub cr_area_over_mass: Option<f64>,
309    /// THRUST_ACCELERATION (O) — Units: m/s²
310    pub thrust_acceleration: Option<f64>,
311    /// SEDR (O) — Units: W/kg
312    pub sedr: Option<f64>,
313
314    // Delta-V v2.0
315    /// MIN_DV (O) — Units: m/s, 3 elements [R, T, N]
316    pub min_dv: Option<[f64; 3]>,
317    /// MAX_DV (O) — Units: m/s, 3 elements [R, T, N]
318    pub max_dv: Option<[f64; 3]>,
319
320    /// LEAD_TIME_REQD_BEFORE_TCA (O) — Units: hours (stored as-is per spec)
321    pub lead_time_reqd_before_tca: Option<f64>,
322
323    // Orbital descriptors v2.0
324    /// APOAPSIS_ALTITUDE (O) — Units: m (converted from km)
325    pub apoapsis_altitude: Option<f64>,
326    /// PERIAPSIS_ALTITUDE (O) — Units: m (converted from km)
327    pub periapsis_altitude: Option<f64>,
328    /// INCLINATION (O) — Units: degrees
329    pub inclination: Option<f64>,
330
331    // Covariance confidence v2.0
332    /// COV_CONFIDENCE (O)
333    pub cov_confidence: Option<f64>,
334    /// COV_CONFIDENCE_METHOD (C)
335    pub cov_confidence_method: Option<String>,
336
337    /// Comments
338    pub comments: Vec<String>,
339}
340
341// ---------------------------------------------------------------------------
342// State Vector
343// ---------------------------------------------------------------------------
344
345/// State vector for one object at TCA.
346///
347/// Position and velocity stored in SI units (meters, m/s).
348/// The epoch is implicitly the TCA from `CDMRelativeMetadata`.
349#[derive(Debug, Clone)]
350pub struct CDMStateVector {
351    /// Position [x, y, z]. Units: meters (converted from km in CCSDS files)
352    pub position: [f64; 3],
353    /// Velocity [vx, vy, vz]. Units: m/s (converted from km/s in CCSDS files)
354    pub velocity: [f64; 3],
355    /// Comments
356    pub comments: Vec<String>,
357}
358
359// ---------------------------------------------------------------------------
360// Covariance types
361// ---------------------------------------------------------------------------
362
363/// RTN covariance matrix for one CDM object.
364///
365/// Stores a symmetric matrix of up to 9×9 (6×6 position/velocity core plus
366/// optional drag, SRP, and thrust rows). Values are in CCSDS native units
367/// which are already SI for the core block:
368/// - Position-position: m²
369/// - Position-velocity: m²/s
370/// - Velocity-velocity: m²/s²
371/// - Drag/SRP rows: m³/kg, m³/(kg·s), m⁴/kg²
372/// - Thrust row: m²/s², m²/s³, m³/(kg·s²), m²/s⁴
373#[derive(Debug, Clone)]
374pub struct CDMRTNCovariance {
375    /// Full 9×9 symmetric covariance matrix. Unused rows/columns (beyond
376    /// `dimension`) are zero.
377    pub matrix: SMatrix<f64, 9, 9>,
378    /// How many rows/columns are populated.
379    pub dimension: CDMCovarianceDimension,
380    /// Comments
381    pub comments: Vec<String>,
382}
383
384/// XYZ covariance matrix (alternate representation).
385///
386/// Same structure as RTN covariance but with XYZ field naming.
387/// Conditional on `ALT_COV_TYPE = XYZ` in the object metadata.
388#[derive(Debug, Clone)]
389pub struct CDMXYZCovariance {
390    /// Full 9×9 symmetric covariance matrix in the XYZ frame specified
391    /// by `ALT_COV_REF_FRAME`.
392    pub matrix: SMatrix<f64, 9, 9>,
393    /// How many rows/columns are populated.
394    pub dimension: CDMCovarianceDimension,
395    /// Comments
396    pub comments: Vec<String>,
397}
398
399// ---------------------------------------------------------------------------
400// Additional Covariance Metadata
401// ---------------------------------------------------------------------------
402
403/// Additional covariance metadata for one CDM object (v2.0).
404#[derive(Debug, Clone)]
405pub struct CDMAdditionalCovarianceMetadata {
406    /// DENSITY_FORECAST_UNCERTAINTY (O)
407    pub density_forecast_uncertainty: Option<f64>,
408    /// CSCALE_FACTOR_MIN (O)
409    pub cscale_factor_min: Option<f64>,
410    /// CSCALE_FACTOR (O)
411    pub cscale_factor: Option<f64>,
412    /// CSCALE_FACTOR_MAX (O)
413    pub cscale_factor_max: Option<f64>,
414    /// SCREENING_DATA_SOURCE (O)
415    pub screening_data_source: Option<String>,
416    /// DCP_SENSITIVITY_VECTOR_POSITION (O) — 3 elements, Units: m
417    pub dcp_sensitivity_vector_position: Option<[f64; 3]>,
418    /// DCP_SENSITIVITY_VECTOR_VELOCITY (O) — 3 elements, Units: m/s
419    pub dcp_sensitivity_vector_velocity: Option<[f64; 3]>,
420    /// Comments
421    pub comments: Vec<String>,
422}
423
424// ---------------------------------------------------------------------------
425// Object Data (combines all data sections)
426// ---------------------------------------------------------------------------
427
428/// All data for one object in the conjunction.
429#[derive(Debug, Clone)]
430pub struct CDMObjectData {
431    /// OD parameters (all optional fields)
432    pub od_parameters: Option<CDMODParameters>,
433    /// Additional physical/operational parameters
434    pub additional_parameters: Option<CDMAdditionalParameters>,
435    /// State vector at TCA
436    pub state_vector: CDMStateVector,
437    /// RTN covariance (mandatory 6×6 core, optional extended)
438    pub rtn_covariance: CDMRTNCovariance,
439    /// XYZ covariance (conditional on ALT_COV_TYPE=XYZ)
440    pub xyz_covariance: Option<CDMXYZCovariance>,
441    /// Additional covariance metadata (v2.0)
442    pub additional_covariance_metadata: Option<CDMAdditionalCovarianceMetadata>,
443    /// CSIG3EIGVEC3 raw string (stored but not parsed into matrix)
444    pub csig3eigvec3: Option<String>,
445    /// Comments
446    pub comments: Vec<String>,
447}
448
449// ---------------------------------------------------------------------------
450// CDM Object (metadata + data)
451// ---------------------------------------------------------------------------
452
453/// One object in the conjunction (metadata + data).
454#[derive(Debug, Clone)]
455pub struct CDMObject {
456    /// Object metadata
457    pub metadata: CDMObjectMetadata,
458    /// Object data
459    pub data: CDMObjectData,
460}
461
462// ---------------------------------------------------------------------------
463// Top-level CDM
464// ---------------------------------------------------------------------------
465
466/// A complete CCSDS Conjunction Data Message.
467///
468/// Contains a header, conjunction-level relative metadata, and exactly two
469/// object sections (OBJECT1 and OBJECT2), each with their own metadata,
470/// state vector, and covariance matrix.
471#[derive(Debug, Clone)]
472pub struct CDM {
473    /// CDM header
474    pub header: CDMHeader,
475    /// Relative metadata/data (shared between objects)
476    pub relative_metadata: CDMRelativeMetadata,
477    /// Object 1 (metadata + data)
478    pub object1: CDMObject,
479    /// Object 2 (metadata + data)
480    pub object2: CDMObject,
481    /// Optional user-defined parameters
482    pub user_defined: Option<CCSDSUserDefined>,
483}
484
485// ---------------------------------------------------------------------------
486// Constructors and helpers
487// ---------------------------------------------------------------------------
488
489impl CDMRelativeMetadata {
490    /// Create relative metadata with only the mandatory fields.
491    pub fn new(tca: Epoch, miss_distance: f64) -> Self {
492        Self {
493            conjunction_id: None,
494            tca,
495            miss_distance,
496            mahalanobis_distance: None,
497            relative_speed: None,
498            relative_position_r: None,
499            relative_position_t: None,
500            relative_position_n: None,
501            relative_velocity_r: None,
502            relative_velocity_t: None,
503            relative_velocity_n: None,
504            approach_angle: None,
505            start_screen_period: None,
506            stop_screen_period: None,
507            screen_type: None,
508            screen_volume_frame: None,
509            screen_volume_shape: None,
510            screen_volume_radius: None,
511            screen_volume_x: None,
512            screen_volume_y: None,
513            screen_volume_z: None,
514            screen_entry_time: None,
515            screen_exit_time: None,
516            screen_pc_threshold: None,
517            collision_percentile: None,
518            collision_probability: None,
519            collision_probability_method: None,
520            collision_max_probability: None,
521            collision_max_pc_method: None,
522            sefi_collision_probability: None,
523            sefi_collision_probability_method: None,
524            sefi_fragmentation_model: None,
525            previous_message_id: None,
526            previous_message_epoch: None,
527            next_message_epoch: None,
528            comments: Vec::new(),
529        }
530    }
531}
532
533impl CDMObjectMetadata {
534    /// Create object metadata with only the mandatory fields.
535    #[allow(clippy::too_many_arguments)]
536    pub fn new(
537        object: String,
538        object_designator: String,
539        catalog_name: String,
540        object_name: String,
541        international_designator: String,
542        ephemeris_name: String,
543        covariance_method: String,
544        maneuverable: String,
545        ref_frame: CCSDSRefFrame,
546    ) -> Self {
547        Self {
548            object,
549            object_designator,
550            catalog_name,
551            object_name,
552            international_designator,
553            object_type: None,
554            ops_status: None,
555            operator_contact_position: None,
556            operator_organization: None,
557            operator_phone: None,
558            operator_email: None,
559            ephemeris_name,
560            odm_msg_link: None,
561            adm_msg_link: None,
562            obs_before_next_message: None,
563            covariance_method,
564            covariance_source: None,
565            maneuverable,
566            orbit_center: None,
567            ref_frame,
568            alt_cov_type: None,
569            alt_cov_ref_frame: None,
570            gravity_model: None,
571            atmospheric_model: None,
572            n_body_perturbations: None,
573            solar_rad_pressure: None,
574            earth_tides: None,
575            intrack_thrust: None,
576            comments: Vec::new(),
577        }
578    }
579}
580
581impl CDMStateVector {
582    /// Create a new state vector.
583    ///
584    /// # Arguments
585    ///
586    /// * `position` - Position [x, y, z]. Units: meters
587    /// * `velocity` - Velocity [vx, vy, vz]. Units: m/s
588    pub fn new(position: [f64; 3], velocity: [f64; 3]) -> Self {
589        Self {
590            position,
591            velocity,
592            comments: Vec::new(),
593        }
594    }
595}
596
597impl CDMRTNCovariance {
598    /// Create a 6×6 RTN covariance from a nalgebra 6×6 matrix.
599    pub fn from_6x6(matrix: SMatrix<f64, 6, 6>) -> Self {
600        let mut full = SMatrix::<f64, 9, 9>::zeros();
601        for i in 0..6 {
602            for j in 0..6 {
603                full[(i, j)] = matrix[(i, j)];
604            }
605        }
606        Self {
607            matrix: full,
608            dimension: CDMCovarianceDimension::SixBySix,
609            comments: Vec::new(),
610        }
611    }
612
613    /// Extract the 6×6 position/velocity submatrix.
614    pub fn to_6x6(&self) -> SMatrix<f64, 6, 6> {
615        self.matrix.fixed_view::<6, 6>(0, 0).into()
616    }
617}
618
619impl CDMObject {
620    /// Create a new CDM object with mandatory fields.
621    pub fn new(
622        metadata: CDMObjectMetadata,
623        state_vector: CDMStateVector,
624        rtn_covariance: CDMRTNCovariance,
625    ) -> Self {
626        Self {
627            metadata,
628            data: CDMObjectData {
629                od_parameters: None,
630                additional_parameters: None,
631                state_vector,
632                rtn_covariance,
633                xyz_covariance: None,
634                additional_covariance_metadata: None,
635                csig3eigvec3: None,
636                comments: Vec::new(),
637            },
638        }
639    }
640}
641
642impl CDM {
643    /// Create a new CDM message with required fields.
644    ///
645    /// # Arguments
646    ///
647    /// * `originator` - Originator of the message
648    /// * `message_id` - Unique message identifier
649    /// * `tca` - Time of closest approach
650    /// * `miss_distance` - Miss distance in meters
651    /// * `object1` - First object data
652    /// * `object2` - Second object data
653    pub fn new(
654        originator: String,
655        message_id: String,
656        tca: Epoch,
657        miss_distance: f64,
658        object1: CDMObject,
659        object2: CDMObject,
660    ) -> Self {
661        Self {
662            header: CDMHeader {
663                format_version: 1.0,
664                classification: None,
665                creation_date: Epoch::now(),
666                originator,
667                message_for: None,
668                message_id,
669                comments: Vec::new(),
670            },
671            relative_metadata: CDMRelativeMetadata::new(tca, miss_distance),
672            object1,
673            object2,
674            user_defined: None,
675        }
676    }
677
678    /// Parse a CDM message from a string, auto-detecting the format.
679    #[allow(clippy::should_implement_trait)]
680    pub fn from_str(content: &str) -> Result<Self, BraheError> {
681        let format = crate::ccsds::common::detect_format(content);
682        match format {
683            CCSDSFormat::KVN => crate::ccsds::kvn::parse_cdm(content),
684            CCSDSFormat::XML => crate::ccsds::xml::parse_cdm_xml(content),
685            CCSDSFormat::JSON => crate::ccsds::json::parse_cdm_json(content),
686        }
687    }
688
689    /// Parse a CDM message from a file, auto-detecting the format.
690    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, BraheError> {
691        let content = std::fs::read_to_string(path.as_ref())
692            .map_err(|e| BraheError::IoError(format!("Failed to read CDM file: {}", e)))?;
693        Self::from_str(&content)
694    }
695
696    /// Write the CDM message to a string in the specified format.
697    pub fn to_string(&self, format: CCSDSFormat) -> Result<String, BraheError> {
698        match format {
699            CCSDSFormat::KVN => crate::ccsds::kvn::write_cdm(self),
700            CCSDSFormat::XML => crate::ccsds::xml::write_cdm_xml(self),
701            CCSDSFormat::JSON => crate::ccsds::json::write_cdm_json(
702                self,
703                crate::ccsds::common::CCSDSJsonKeyCase::Lower,
704            ),
705        }
706    }
707
708    /// Write the CDM message to JSON with explicit key case control.
709    pub fn to_json_string(
710        &self,
711        key_case: crate::ccsds::common::CCSDSJsonKeyCase,
712    ) -> Result<String, BraheError> {
713        crate::ccsds::json::write_cdm_json(self, key_case)
714    }
715
716    /// Write the CDM message to a file in the specified format.
717    pub fn to_file<P: AsRef<Path>>(&self, path: P, format: CCSDSFormat) -> Result<(), BraheError> {
718        let content = self.to_string(format)?;
719        std::fs::write(path.as_ref(), content)
720            .map_err(|e| BraheError::IoError(format!("Failed to write CDM file: {}", e)))
721    }
722
723    // -----------------------------------------------------------------------
724    // Convenience accessors
725    // -----------------------------------------------------------------------
726
727    /// Get the TCA epoch.
728    pub fn tca(&self) -> &Epoch {
729        &self.relative_metadata.tca
730    }
731
732    /// Get the miss distance in meters.
733    pub fn miss_distance(&self) -> f64 {
734        self.relative_metadata.miss_distance
735    }
736
737    /// Get collision probability if present.
738    pub fn collision_probability(&self) -> Option<f64> {
739        self.relative_metadata.collision_probability
740    }
741
742    /// Get object1 state vector as a 6-element vector [x, y, z, vx, vy, vz] in m and m/s.
743    pub fn object1_state(&self) -> SVector<f64, 6> {
744        let sv = &self.object1.data.state_vector;
745        SVector::<f64, 6>::new(
746            sv.position[0],
747            sv.position[1],
748            sv.position[2],
749            sv.velocity[0],
750            sv.velocity[1],
751            sv.velocity[2],
752        )
753    }
754
755    /// Get object2 state vector as a 6-element vector [x, y, z, vx, vy, vz] in m and m/s.
756    pub fn object2_state(&self) -> SVector<f64, 6> {
757        let sv = &self.object2.data.state_vector;
758        SVector::<f64, 6>::new(
759            sv.position[0],
760            sv.position[1],
761            sv.position[2],
762            sv.velocity[0],
763            sv.velocity[1],
764            sv.velocity[2],
765        )
766    }
767
768    /// Get the relative position vector [R, T, N] in meters, if all components present.
769    pub fn relative_position_rtn(&self) -> Option<[f64; 3]> {
770        let rm = &self.relative_metadata;
771        match (
772            rm.relative_position_r,
773            rm.relative_position_t,
774            rm.relative_position_n,
775        ) {
776            (Some(r), Some(t), Some(n)) => Some([r, t, n]),
777            _ => None,
778        }
779    }
780
781    /// Get the relative velocity vector [R, T, N] in m/s, if all components present.
782    pub fn relative_velocity_rtn(&self) -> Option<[f64; 3]> {
783        let rm = &self.relative_metadata;
784        match (
785            rm.relative_velocity_r,
786            rm.relative_velocity_t,
787            rm.relative_velocity_n,
788        ) {
789            (Some(r), Some(t), Some(n)) => Some([r, t, n]),
790            _ => None,
791        }
792    }
793
794    /// Get the 6×6 position/velocity submatrix of object1's RTN covariance.
795    pub fn object1_rtn_covariance_6x6(&self) -> SMatrix<f64, 6, 6> {
796        self.object1.data.rtn_covariance.to_6x6()
797    }
798
799    /// Get the 6×6 position/velocity submatrix of object2's RTN covariance.
800    pub fn object2_rtn_covariance_6x6(&self) -> SMatrix<f64, 6, 6> {
801        self.object2.data.rtn_covariance.to_6x6()
802    }
803}
804
805// ---------------------------------------------------------------------------
806// Tests
807// ---------------------------------------------------------------------------
808
809#[cfg(test)]
810#[cfg_attr(coverage_nightly, coverage(off))]
811mod tests {
812    use super::*;
813
814    #[test]
815    fn test_cdm_new() {
816        let sv1 = CDMStateVector::new([7000e3, 0.0, 0.0], [0.0, 7500.0, 0.0]);
817        let sv2 = CDMStateVector::new([7001e3, 0.0, 0.0], [0.0, -7500.0, 0.0]);
818        let cov1 = CDMRTNCovariance::from_6x6(SMatrix::<f64, 6, 6>::identity());
819        let cov2 = CDMRTNCovariance::from_6x6(SMatrix::<f64, 6, 6>::identity());
820
821        let meta1 = CDMObjectMetadata::new(
822            "OBJECT1".to_string(),
823            "12345".to_string(),
824            "SATCAT".to_string(),
825            "SAT A".to_string(),
826            "2020-001A".to_string(),
827            "NONE".to_string(),
828            "CALCULATED".to_string(),
829            "YES".to_string(),
830            CCSDSRefFrame::EME2000,
831        );
832        let meta2 = CDMObjectMetadata::new(
833            "OBJECT2".to_string(),
834            "67890".to_string(),
835            "SATCAT".to_string(),
836            "SAT B".to_string(),
837            "2021-002B".to_string(),
838            "NONE".to_string(),
839            "CALCULATED".to_string(),
840            "NO".to_string(),
841            CCSDSRefFrame::EME2000,
842        );
843
844        let obj1 = CDMObject::new(meta1, sv1, cov1);
845        let obj2 = CDMObject::new(meta2, sv2, cov2);
846
847        let tca = Epoch::from_datetime(2024, 1, 15, 12, 0, 0.0, 0.0, crate::time::TimeSystem::UTC);
848        let cdm = CDM::new(
849            "TEST_ORG".to_string(),
850            "MSG001".to_string(),
851            tca,
852            715.0,
853            obj1,
854            obj2,
855        );
856
857        assert_eq!(cdm.header.originator, "TEST_ORG");
858        assert_eq!(cdm.header.message_id, "MSG001");
859        assert_eq!(cdm.miss_distance(), 715.0);
860        assert_eq!(cdm.object1.metadata.object_name, "SAT A");
861        assert_eq!(cdm.object2.metadata.object_name, "SAT B");
862
863        let s1 = cdm.object1_state();
864        assert_eq!(s1[0], 7000e3);
865        assert_eq!(s1[4], 7500.0);
866
867        assert!(cdm.collision_probability().is_none());
868        assert!(cdm.relative_position_rtn().is_none());
869    }
870
871    #[test]
872    fn test_cdm_kvn_parse_example1() {
873        let cdm = CDM::from_file("test_assets/ccsds/cdm/CDMExample1.txt").unwrap();
874
875        // Header
876        assert_eq!(cdm.header.format_version, 1.0);
877        assert_eq!(cdm.header.originator, "JSPOC");
878        assert_eq!(cdm.header.message_id, "201113719185");
879        assert!(cdm.header.message_for.is_none());
880
881        // Relative metadata
882        assert_eq!(cdm.miss_distance(), 715.0);
883        assert!(cdm.collision_probability().is_none());
884        assert!(cdm.relative_position_rtn().is_none());
885
886        // Object 1
887        assert_eq!(cdm.object1.metadata.object, "OBJECT1");
888        assert_eq!(cdm.object1.metadata.object_designator, "12345");
889        assert_eq!(cdm.object1.metadata.object_name, "SATELLITE A");
890        assert_eq!(cdm.object1.metadata.ephemeris_name, "EPHEMERIS SATELLITE A");
891        assert_eq!(cdm.object1.metadata.maneuverable, "YES");
892        assert_eq!(cdm.object1.metadata.ref_frame, CCSDSRefFrame::EME2000);
893
894        // Object 1 state vector (converted from km → m, km/s → m/s)
895        let s1 = cdm.object1_state();
896        assert!((s1[0] - 2570097.065).abs() < 0.01);
897        assert!((s1[1] - 2244654.904).abs() < 0.01);
898        assert!((s1[2] - 6281497.978).abs() < 0.01);
899        assert!((s1[3] - 4418.769571).abs() < 0.0001);
900        assert!((s1[4] - 4833.547743).abs() < 0.0001);
901        assert!((s1[5] - (-3526.774282)).abs() < 0.0001);
902
903        // Object 1 covariance (already in m², no conversion)
904        let cov1 = cdm.object1_rtn_covariance_6x6();
905        assert!((cov1[(0, 0)] - 4.142e+01).abs() < 1e-10);
906        assert!((cov1[(1, 0)] - (-8.579e+00)).abs() < 1e-10);
907        assert!((cov1[(1, 1)] - 2.533e+03).abs() < 1e-10);
908        assert_eq!(
909            cdm.object1.data.rtn_covariance.dimension,
910            CDMCovarianceDimension::SixBySix
911        );
912
913        // Object 2
914        assert_eq!(cdm.object2.metadata.object, "OBJECT2");
915        assert_eq!(cdm.object2.metadata.object_designator, "30337");
916        assert_eq!(cdm.object2.metadata.object_name, "FENGYUN 1C DEB");
917        assert_eq!(cdm.object2.metadata.maneuverable, "NO");
918
919        let s2 = cdm.object2_state();
920        assert!((s2[0] - 2569540.800).abs() < 0.01);
921        assert!((s2[3] - (-2888.6125)).abs() < 0.001);
922    }
923
924    #[test]
925    fn test_cdm_kvn_parse_example2_extended_cov() {
926        let cdm = CDM::from_file("test_assets/ccsds/cdm/CDMExample2.txt").unwrap();
927
928        // Relative metadata
929        assert!(cdm.header.message_for.is_some());
930        assert_eq!(cdm.header.message_for.as_deref(), Some("SATELLITE A"));
931        assert_eq!(cdm.relative_metadata.relative_speed, Some(14762.0));
932        assert!((cdm.collision_probability().unwrap() - 4.835e-05).abs() < 1e-10);
933        assert_eq!(
934            cdm.relative_metadata
935                .collision_probability_method
936                .as_deref(),
937            Some("FOSTER-1992")
938        );
939
940        // Relative position
941        let rp = cdm.relative_position_rtn().unwrap();
942        assert!((rp[0] - 27.4).abs() < 0.01);
943        assert!((rp[1] - (-70.2)).abs() < 0.01);
944
945        // Object 1 metadata
946        assert_eq!(cdm.object1.metadata.object_type.as_deref(), Some("PAYLOAD"));
947        assert_eq!(
948            cdm.object1.metadata.operator_email.as_deref(),
949            Some("JOHN.DOE@SOMEWHERE.NET")
950        );
951        assert_eq!(
952            cdm.object1.metadata.gravity_model.as_deref(),
953            Some("EGM-96: 36D 36O")
954        );
955
956        // OD parameters
957        let od = cdm.object1.data.od_parameters.as_ref().unwrap();
958        assert_eq!(od.obs_available, Some(592));
959        assert_eq!(od.obs_used, Some(579));
960        assert!((od.recommended_od_span.unwrap() - 7.88).abs() < 0.01);
961        assert!((od.residuals_accepted.unwrap() - 97.8).abs() < 0.01);
962
963        // Additional parameters
964        let ap = cdm.object1.data.additional_parameters.as_ref().unwrap();
965        assert!((ap.area_pc.unwrap() - 5.2).abs() < 0.01);
966        assert!((ap.mass.unwrap() - 251.6).abs() < 0.01);
967        assert!((ap.sedr.unwrap() - 4.54570e-05).abs() < 1e-10);
968
969        // Extended covariance (8×8 with CDRG + CSRP)
970        assert_eq!(
971            cdm.object1.data.rtn_covariance.dimension,
972            CDMCovarianceDimension::EightByEight
973        );
974        let cov = &cdm.object1.data.rtn_covariance.matrix;
975        // CDRG_R = row 6, col 0
976        assert!((cov[(6, 0)] - (-1.862e+00)).abs() < 1e-10);
977        // CSRP_SRP = row 7, col 7
978        assert!((cov[(7, 7)] - 1.593e-02).abs() < 1e-10);
979    }
980
981    #[test]
982    fn test_cdm_kvn_parse_issue940_v2() {
983        let cdm = CDM::from_file("test_assets/ccsds/cdm/CDMExample_issue_940.txt").unwrap();
984
985        assert_eq!(cdm.header.format_version, 2.0);
986        assert!(cdm.header.classification.is_some());
987        assert_eq!(
988            cdm.relative_metadata.conjunction_id.as_deref(),
989            Some("20220708T10hz SATELLITEA SATELLITEB")
990        );
991        assert_eq!(cdm.relative_metadata.approach_angle, Some(180.0));
992        assert_eq!(cdm.relative_metadata.screen_type.as_deref(), Some("SHAPE"));
993        assert_eq!(cdm.relative_metadata.screen_pc_threshold, Some(1.0e-03));
994        assert!(cdm.relative_metadata.collision_percentile.is_some());
995        assert_eq!(
996            cdm.relative_metadata.collision_percentile.as_ref().unwrap(),
997            &[50, 51, 52]
998        );
999        assert!(cdm.relative_metadata.sefi_collision_probability.is_some());
1000        assert!(cdm.relative_metadata.previous_message_id.is_some());
1001
1002        // Object 1 v2.0 fields
1003        assert_eq!(
1004            cdm.object1.metadata.odm_msg_link.as_deref(),
1005            Some("ODM_MSG_35132.txt")
1006        );
1007        assert_eq!(
1008            cdm.object1.metadata.covariance_source.as_deref(),
1009            Some("HAC Covariance")
1010        );
1011        assert_eq!(cdm.object1.metadata.alt_cov_type.as_deref(), Some("XYZ"));
1012
1013        // OEB fields
1014        let ap = cdm.object1.data.additional_parameters.as_ref().unwrap();
1015        assert!((ap.oeb_q1.unwrap() - 0.03123).abs() < 1e-10);
1016        assert!((ap.hbr.unwrap() - 2.5).abs() < 0.01);
1017        assert!((ap.apoapsis_altitude.unwrap() - 800e3).abs() < 0.1); // km → m
1018        assert!((ap.inclination.unwrap() - 89.0).abs() < 0.01);
1019
1020        // XYZ covariance
1021        assert!(cdm.object1.data.xyz_covariance.is_some());
1022        let xyz = cdm.object1.data.xyz_covariance.as_ref().unwrap();
1023        assert_eq!(xyz.dimension, CDMCovarianceDimension::NineByNine);
1024        assert!((xyz.matrix[(0, 0)] - 0.1).abs() < 1e-10);
1025
1026        // Additional covariance metadata
1027        let acm = cdm
1028            .object1
1029            .data
1030            .additional_covariance_metadata
1031            .as_ref()
1032            .unwrap();
1033        assert!((acm.density_forecast_uncertainty.unwrap() - 2.5).abs() < 0.01);
1034        assert!((acm.cscale_factor.unwrap() - 1.0).abs() < 0.01);
1035        assert!(acm.dcp_sensitivity_vector_position.is_some());
1036
1037        // Object 2 CSIG3EIGVEC3
1038        assert!(cdm.object2.data.csig3eigvec3.is_some());
1039
1040        // User-defined parameters
1041        assert!(cdm.user_defined.is_some());
1042        let ud = cdm.user_defined.as_ref().unwrap();
1043        assert!(ud.parameters.contains_key("OBJ1_TIME_LASTOB_START"));
1044    }
1045
1046    #[test]
1047    fn test_cdm_kvn_parse_issue942_maneuverable_na() {
1048        let cdm = CDM::from_file("test_assets/ccsds/cdm/CDMExample_issue942.txt").unwrap();
1049        assert_eq!(cdm.object1.metadata.maneuverable, "N/A");
1050    }
1051
1052    #[test]
1053    fn test_cdm_kvn_parse_alfano01() {
1054        let cdm = CDM::from_file("test_assets/ccsds/cdm/AlfanoTestCase01.cdm").unwrap();
1055        assert!(cdm.miss_distance() > 0.0);
1056        // Verify state vectors and covariance parsed
1057        let s1 = cdm.object1_state();
1058        assert!(s1[0].abs() > 1.0); // Non-zero position
1059        // Alfano test cases have 8×8 covariance (6×6 + CDRG + CSRP)
1060        assert_eq!(
1061            cdm.object1.data.rtn_covariance.dimension,
1062            CDMCovarianceDimension::EightByEight
1063        );
1064    }
1065
1066    #[test]
1067    fn test_cdm_kvn_parse_real_world() {
1068        let cdm = CDM::from_file("test_assets/ccsds/cdm/ION_SCV8_vs_STARLINK_1233.txt").unwrap();
1069        assert!(cdm.miss_distance() > 0.0);
1070        assert!(cdm.object1.data.od_parameters.is_some());
1071        assert!(cdm.object2.data.od_parameters.is_some());
1072    }
1073
1074    #[test]
1075    fn test_cdm_kvn_missing_tca() {
1076        let result = CDM::from_file("test_assets/ccsds/cdm/CDM-missing-TCA.txt");
1077        assert!(result.is_err());
1078        let err_msg = format!("{}", result.unwrap_err());
1079        assert!(
1080            err_msg.contains("TCA"),
1081            "Error should mention TCA: {}",
1082            err_msg
1083        );
1084    }
1085
1086    #[test]
1087    fn test_cdm_kvn_missing_obj2_state() {
1088        let result = CDM::from_file("test_assets/ccsds/cdm/CDM-missing-object2-state-vector.txt");
1089        assert!(result.is_err());
1090    }
1091
1092    #[test]
1093    fn test_cdm_kvn_round_trip_example1() {
1094        let cdm1 = CDM::from_file("test_assets/ccsds/cdm/CDMExample1.txt").unwrap();
1095        let kvn = cdm1.to_string(CCSDSFormat::KVN).unwrap();
1096        let cdm2 = CDM::from_str(&kvn).unwrap();
1097
1098        // Compare key fields
1099        assert_eq!(cdm1.header.format_version, cdm2.header.format_version);
1100        assert_eq!(cdm1.header.originator, cdm2.header.originator);
1101        assert_eq!(cdm1.header.message_id, cdm2.header.message_id);
1102        assert!((cdm1.miss_distance() - cdm2.miss_distance()).abs() < 1e-6);
1103
1104        // State vectors
1105        for i in 0..6 {
1106            assert!(
1107                (cdm1.object1_state()[i] - cdm2.object1_state()[i]).abs() < 0.01,
1108                "Object1 state[{}] mismatch: {} vs {}",
1109                i,
1110                cdm1.object1_state()[i],
1111                cdm2.object1_state()[i]
1112            );
1113            assert!(
1114                (cdm1.object2_state()[i] - cdm2.object2_state()[i]).abs() < 0.01,
1115                "Object2 state[{}] mismatch: {} vs {}",
1116                i,
1117                cdm1.object2_state()[i],
1118                cdm2.object2_state()[i]
1119            );
1120        }
1121
1122        // Covariance
1123        let c1 = cdm1.object1_rtn_covariance_6x6();
1124        let c2 = cdm2.object1_rtn_covariance_6x6();
1125        for i in 0..6 {
1126            for j in 0..6 {
1127                let rel = if c1[(i, j)].abs() > 1e-20 {
1128                    ((c1[(i, j)] - c2[(i, j)]) / c1[(i, j)]).abs()
1129                } else {
1130                    (c1[(i, j)] - c2[(i, j)]).abs()
1131                };
1132                assert!(
1133                    rel < 1e-4,
1134                    "Cov({},{}) mismatch: {} vs {}",
1135                    i,
1136                    j,
1137                    c1[(i, j)],
1138                    c2[(i, j)]
1139                );
1140            }
1141        }
1142    }
1143
1144    #[test]
1145    fn test_cdm_xml_parse_example1() {
1146        let cdm = CDM::from_file("test_assets/ccsds/cdm/CDMExample1.xml").unwrap();
1147
1148        assert_eq!(cdm.header.format_version, 1.0);
1149        assert_eq!(cdm.header.originator, "JSPOC");
1150        assert_eq!(cdm.header.message_for.as_deref(), Some("SATELLITE A"));
1151        assert_eq!(cdm.miss_distance(), 715.0);
1152
1153        // Relative state vector
1154        let rp = cdm.relative_position_rtn().unwrap();
1155        assert!((rp[0] - 27.4).abs() < 0.01);
1156
1157        // Object 1
1158        assert_eq!(cdm.object1.metadata.object_name, "SATELLITE A");
1159        assert_eq!(cdm.object1.metadata.ref_frame, CCSDSRefFrame::EME2000);
1160
1161        let s1 = cdm.object1_state();
1162        assert!((s1[0] - 2570097.065).abs() < 0.01);
1163
1164        // Covariance
1165        let cov1 = cdm.object1_rtn_covariance_6x6();
1166        assert!((cov1[(0, 0)] - 4.142e+01).abs() < 1e-10);
1167
1168        // Object 2
1169        assert_eq!(cdm.object2.metadata.object_name, "FENGYUN 1C DEB");
1170    }
1171
1172    #[test]
1173    fn test_cdm_xml_round_trip() {
1174        let cdm1 = CDM::from_file("test_assets/ccsds/cdm/CDMExample1.xml").unwrap();
1175        let xml = cdm1.to_string(CCSDSFormat::XML).unwrap();
1176        let cdm2 = CDM::from_str(&xml).unwrap();
1177
1178        assert_eq!(cdm1.header.originator, cdm2.header.originator);
1179        assert!((cdm1.miss_distance() - cdm2.miss_distance()).abs() < 1e-6);
1180
1181        for i in 0..6 {
1182            assert!((cdm1.object1_state()[i] - cdm2.object1_state()[i]).abs() < 0.01);
1183        }
1184    }
1185
1186    #[test]
1187    fn test_cdm_json_round_trip() {
1188        let cdm1 = CDM::from_file("test_assets/ccsds/cdm/CDMExample1.txt").unwrap();
1189        let json = cdm1.to_string(CCSDSFormat::JSON).unwrap();
1190        let cdm2 = CDM::from_str(&json).unwrap();
1191
1192        assert_eq!(cdm1.header.originator, cdm2.header.originator);
1193        assert!((cdm1.miss_distance() - cdm2.miss_distance()).abs() < 1e-6);
1194        for i in 0..6 {
1195            assert!(
1196                (cdm1.object1_state()[i] - cdm2.object1_state()[i]).abs() < 0.01,
1197                "Object1 state[{}]: {} vs {}",
1198                i,
1199                cdm1.object1_state()[i],
1200                cdm2.object1_state()[i]
1201            );
1202        }
1203    }
1204
1205    #[test]
1206    fn test_cdm_kvn_to_xml_cross_format() {
1207        // Parse KVN
1208        let cdm_kvn = CDM::from_file("test_assets/ccsds/cdm/CDMExample1.txt").unwrap();
1209        // Write as XML
1210        let xml = cdm_kvn.to_string(CCSDSFormat::XML).unwrap();
1211        // Re-parse from XML
1212        let cdm_xml = CDM::from_str(&xml).unwrap();
1213
1214        // Compare
1215        assert_eq!(cdm_kvn.header.originator, cdm_xml.header.originator);
1216        assert!((cdm_kvn.miss_distance() - cdm_xml.miss_distance()).abs() < 1e-6);
1217        for i in 0..6 {
1218            assert!((cdm_kvn.object1_state()[i] - cdm_xml.object1_state()[i]).abs() < 0.01);
1219            assert!((cdm_kvn.object2_state()[i] - cdm_xml.object2_state()[i]).abs() < 0.01);
1220        }
1221    }
1222
1223    #[test]
1224    fn test_cdm_kvn_round_trip_example2() {
1225        let cdm1 = CDM::from_file("test_assets/ccsds/cdm/CDMExample2.txt").unwrap();
1226        let kvn = cdm1.to_string(CCSDSFormat::KVN).unwrap();
1227        let cdm2 = CDM::from_str(&kvn).unwrap();
1228
1229        assert_eq!(cdm1.header.message_for, cdm2.header.message_for);
1230        assert!(
1231            (cdm1.collision_probability().unwrap() - cdm2.collision_probability().unwrap()).abs()
1232                < 1e-10
1233        );
1234        assert_eq!(
1235            cdm1.relative_metadata.collision_probability_method,
1236            cdm2.relative_metadata.collision_probability_method
1237        );
1238
1239        // Extended covariance dimension preserved
1240        assert_eq!(
1241            cdm1.object1.data.rtn_covariance.dimension,
1242            cdm2.object1.data.rtn_covariance.dimension
1243        );
1244
1245        // OD parameters
1246        let od1 = cdm1.object1.data.od_parameters.as_ref().unwrap();
1247        let od2 = cdm2.object1.data.od_parameters.as_ref().unwrap();
1248        assert_eq!(od1.obs_available, od2.obs_available);
1249        assert_eq!(od1.obs_used, od2.obs_used);
1250    }
1251
1252    #[test]
1253    fn test_cdm_xml_round_trip_issue940_all_fields() {
1254        // CDMExample_issue_940.txt has nearly all optional fields populated
1255        let cdm1 = CDM::from_file("test_assets/ccsds/cdm/CDMExample_issue_940.txt").unwrap();
1256
1257        // Write to XML and re-parse
1258        let xml = cdm1.to_string(CCSDSFormat::XML).unwrap();
1259        let cdm2 = CDM::from_str(&xml).unwrap();
1260
1261        // Header
1262        assert_eq!(cdm1.header.originator, cdm2.header.originator);
1263        assert_eq!(cdm1.header.message_id, cdm2.header.message_id);
1264        assert_eq!(cdm1.header.classification, cdm2.header.classification);
1265
1266        // Relative metadata
1267        assert!((cdm1.miss_distance() - cdm2.miss_distance()).abs() < 1e-6);
1268        assert_eq!(
1269            cdm1.relative_metadata.conjunction_id,
1270            cdm2.relative_metadata.conjunction_id
1271        );
1272        assert_eq!(
1273            cdm1.relative_metadata.approach_angle,
1274            cdm2.relative_metadata.approach_angle
1275        );
1276        assert_eq!(
1277            cdm1.relative_metadata.screen_type,
1278            cdm2.relative_metadata.screen_type
1279        );
1280        assert_eq!(
1281            cdm1.relative_metadata.screen_pc_threshold,
1282            cdm2.relative_metadata.screen_pc_threshold
1283        );
1284        assert_eq!(
1285            cdm1.relative_metadata.collision_percentile,
1286            cdm2.relative_metadata.collision_percentile
1287        );
1288        assert_eq!(
1289            cdm1.relative_metadata.collision_probability,
1290            cdm2.relative_metadata.collision_probability
1291        );
1292        assert_eq!(
1293            cdm1.relative_metadata.collision_probability_method,
1294            cdm2.relative_metadata.collision_probability_method
1295        );
1296        assert_eq!(
1297            cdm1.relative_metadata.collision_max_probability,
1298            cdm2.relative_metadata.collision_max_probability
1299        );
1300        assert_eq!(
1301            cdm1.relative_metadata.collision_max_pc_method,
1302            cdm2.relative_metadata.collision_max_pc_method
1303        );
1304        assert_eq!(
1305            cdm1.relative_metadata.previous_message_id,
1306            cdm2.relative_metadata.previous_message_id
1307        );
1308
1309        // State vectors
1310        for i in 0..6 {
1311            assert!((cdm1.object1_state()[i] - cdm2.object1_state()[i]).abs() < 0.01);
1312            assert!((cdm1.object2_state()[i] - cdm2.object2_state()[i]).abs() < 0.01);
1313        }
1314
1315        // Object metadata
1316        assert_eq!(
1317            cdm1.object1.metadata.odm_msg_link,
1318            cdm2.object1.metadata.odm_msg_link
1319        );
1320        assert_eq!(
1321            cdm1.object1.metadata.covariance_source,
1322            cdm2.object1.metadata.covariance_source
1323        );
1324        assert_eq!(
1325            cdm1.object1.metadata.alt_cov_type,
1326            cdm2.object1.metadata.alt_cov_type
1327        );
1328
1329        // OD parameters
1330        let od1 = cdm1.object1.data.od_parameters.as_ref().unwrap();
1331        let od2 = cdm2.object1.data.od_parameters.as_ref().unwrap();
1332        assert_eq!(od1.obs_available, od2.obs_available);
1333        assert_eq!(od1.obs_used, od2.obs_used);
1334
1335        // Additional parameters
1336        let ap1 = cdm1.object1.data.additional_parameters.as_ref().unwrap();
1337        let ap2 = cdm2.object1.data.additional_parameters.as_ref().unwrap();
1338        assert!((ap1.hbr.unwrap() - ap2.hbr.unwrap()).abs() < 0.01);
1339        assert!((ap1.oeb_q1.unwrap() - ap2.oeb_q1.unwrap()).abs() < 1e-10);
1340
1341        // XYZ covariance preserved
1342        assert!(cdm2.object1.data.xyz_covariance.is_some());
1343        let xyz1 = cdm1.object1.data.xyz_covariance.as_ref().unwrap();
1344        let xyz2 = cdm2.object1.data.xyz_covariance.as_ref().unwrap();
1345        assert_eq!(xyz1.dimension, xyz2.dimension);
1346        assert!((xyz1.matrix[(0, 0)] - xyz2.matrix[(0, 0)]).abs() < 1e-10);
1347
1348        // Additional covariance metadata
1349        let acm1 = cdm1
1350            .object1
1351            .data
1352            .additional_covariance_metadata
1353            .as_ref()
1354            .unwrap();
1355        let acm2 = cdm2
1356            .object1
1357            .data
1358            .additional_covariance_metadata
1359            .as_ref()
1360            .unwrap();
1361        assert_eq!(
1362            acm1.density_forecast_uncertainty,
1363            acm2.density_forecast_uncertainty
1364        );
1365        assert_eq!(acm1.cscale_factor, acm2.cscale_factor);
1366
1367        // User-defined parameters
1368        assert!(cdm2.user_defined.is_some());
1369    }
1370
1371    #[test]
1372    fn test_cdm_rtn_covariance_6x6() {
1373        let mut m6 = SMatrix::<f64, 6, 6>::zeros();
1374        m6[(0, 0)] = 41.42;
1375        m6[(1, 0)] = -8.579;
1376        m6[(0, 1)] = -8.579;
1377        m6[(1, 1)] = 2533.0;
1378        let cov = CDMRTNCovariance::from_6x6(m6);
1379        assert_eq!(cov.dimension, CDMCovarianceDimension::SixBySix);
1380        let extracted = cov.to_6x6();
1381        assert_eq!(extracted[(0, 0)], 41.42);
1382        assert_eq!(extracted[(1, 0)], -8.579);
1383        assert_eq!(extracted[(1, 1)], 2533.0);
1384        // Extended region should be zero
1385        assert_eq!(cov.matrix[(6, 0)], 0.0);
1386        assert_eq!(cov.matrix[(8, 8)], 0.0);
1387    }
1388
1389    /// Helper: assert all CDM fields match between original and round-tripped.
1390    fn assert_cdm_fields_match(cdm1: &CDM, cdm2: &CDM) {
1391        // Header
1392        assert_eq!(cdm1.header.format_version, cdm2.header.format_version);
1393        assert_eq!(cdm1.header.originator, cdm2.header.originator);
1394        assert_eq!(cdm1.header.message_id, cdm2.header.message_id);
1395        assert_eq!(cdm1.header.classification, cdm2.header.classification);
1396        assert_eq!(cdm1.header.message_for, cdm2.header.message_for);
1397
1398        // Relative metadata
1399        assert!((cdm1.miss_distance() - cdm2.miss_distance()).abs() < 1e-6);
1400        assert_eq!(
1401            cdm1.relative_metadata.conjunction_id,
1402            cdm2.relative_metadata.conjunction_id
1403        );
1404        assert_eq!(
1405            cdm1.relative_metadata.relative_speed.is_some(),
1406            cdm2.relative_metadata.relative_speed.is_some()
1407        );
1408        if let (Some(r1), Some(r2)) = (
1409            cdm1.relative_metadata.relative_speed,
1410            cdm2.relative_metadata.relative_speed,
1411        ) {
1412            assert!((r1 - r2).abs() < 0.01);
1413        }
1414        assert_eq!(
1415            cdm1.relative_metadata.collision_probability,
1416            cdm2.relative_metadata.collision_probability
1417        );
1418        assert_eq!(
1419            cdm1.relative_metadata.collision_probability_method,
1420            cdm2.relative_metadata.collision_probability_method
1421        );
1422        assert_eq!(
1423            cdm1.relative_metadata.collision_percentile,
1424            cdm2.relative_metadata.collision_percentile
1425        );
1426        assert_eq!(
1427            cdm1.relative_metadata.collision_max_probability,
1428            cdm2.relative_metadata.collision_max_probability
1429        );
1430        assert_eq!(
1431            cdm1.relative_metadata.screen_type,
1432            cdm2.relative_metadata.screen_type
1433        );
1434
1435        // State vectors
1436        for i in 0..6 {
1437            assert!(
1438                (cdm1.object1_state()[i] - cdm2.object1_state()[i]).abs() < 0.01,
1439                "obj1 state[{}] mismatch",
1440                i
1441            );
1442            assert!(
1443                (cdm1.object2_state()[i] - cdm2.object2_state()[i]).abs() < 0.01,
1444                "obj2 state[{}] mismatch",
1445                i
1446            );
1447        }
1448
1449        // Object metadata
1450        assert_eq!(
1451            cdm1.object1.metadata.object_name,
1452            cdm2.object1.metadata.object_name
1453        );
1454        assert_eq!(
1455            cdm1.object1.metadata.object_designator,
1456            cdm2.object1.metadata.object_designator
1457        );
1458        assert_eq!(
1459            cdm1.object1.metadata.maneuverable,
1460            cdm2.object1.metadata.maneuverable
1461        );
1462        assert_eq!(
1463            cdm1.object1.metadata.ref_frame,
1464            cdm2.object1.metadata.ref_frame
1465        );
1466
1467        // RTN covariance
1468        assert_eq!(
1469            cdm1.object1.data.rtn_covariance.dimension,
1470            cdm2.object1.data.rtn_covariance.dimension
1471        );
1472        let c1 = cdm1.object1_rtn_covariance_6x6();
1473        let c2 = cdm2.object1_rtn_covariance_6x6();
1474        for i in 0..6 {
1475            for j in 0..6 {
1476                let rel = if c1[(i, j)].abs() > 1e-20 {
1477                    ((c1[(i, j)] - c2[(i, j)]) / c1[(i, j)]).abs()
1478                } else {
1479                    (c1[(i, j)] - c2[(i, j)]).abs()
1480                };
1481                assert!(rel < 1e-4, "obj1 cov({},{}) mismatch", i, j);
1482            }
1483        }
1484
1485        // OD parameters
1486        assert_eq!(
1487            cdm1.object1.data.od_parameters.is_some(),
1488            cdm2.object1.data.od_parameters.is_some()
1489        );
1490        if let (Some(od1), Some(od2)) = (
1491            &cdm1.object1.data.od_parameters,
1492            &cdm2.object1.data.od_parameters,
1493        ) {
1494            assert_eq!(od1.obs_available, od2.obs_available);
1495            assert_eq!(od1.obs_used, od2.obs_used);
1496            assert_eq!(od1.tracks_available, od2.tracks_available);
1497            assert_eq!(od1.tracks_used, od2.tracks_used);
1498        }
1499
1500        // Additional parameters
1501        assert_eq!(
1502            cdm1.object1.data.additional_parameters.is_some(),
1503            cdm2.object1.data.additional_parameters.is_some()
1504        );
1505        if let (Some(ap1), Some(ap2)) = (
1506            &cdm1.object1.data.additional_parameters,
1507            &cdm2.object1.data.additional_parameters,
1508        ) {
1509            assert_eq!(ap1.area_pc.is_some(), ap2.area_pc.is_some());
1510            assert_eq!(ap1.mass.is_some(), ap2.mass.is_some());
1511            assert_eq!(ap1.hbr.is_some(), ap2.hbr.is_some());
1512            if let (Some(h1), Some(h2)) = (ap1.hbr, ap2.hbr) {
1513                assert!((h1 - h2).abs() < 0.01);
1514            }
1515        }
1516
1517        // User-defined
1518        assert_eq!(cdm1.user_defined.is_some(), cdm2.user_defined.is_some());
1519        if let (Some(ud1), Some(ud2)) = (&cdm1.user_defined, &cdm2.user_defined) {
1520            assert_eq!(ud1.parameters.len(), ud2.parameters.len());
1521            for (k, v) in &ud1.parameters {
1522                assert_eq!(
1523                    ud2.parameters.get(k),
1524                    Some(v),
1525                    "user_defined key {} mismatch",
1526                    k
1527                );
1528            }
1529        }
1530    }
1531
1532    #[test]
1533    fn test_cdm_kvn_full_round_trip() {
1534        // CDMExample_issue_940 has the most comprehensive field coverage
1535        let cdm1 = CDM::from_file("test_assets/ccsds/cdm/CDMExample_issue_940.txt").unwrap();
1536        let kvn = cdm1.to_string(CCSDSFormat::KVN).unwrap();
1537        let cdm2 = CDM::from_str(&kvn).unwrap();
1538        assert_cdm_fields_match(&cdm1, &cdm2);
1539    }
1540
1541    #[test]
1542    fn test_cdm_xml_full_round_trip() {
1543        let cdm1 = CDM::from_file("test_assets/ccsds/cdm/CDMExample_issue_940.txt").unwrap();
1544        let xml = cdm1.to_string(CCSDSFormat::XML).unwrap();
1545        let cdm2 = CDM::from_str(&xml).unwrap();
1546        assert_cdm_fields_match(&cdm1, &cdm2);
1547    }
1548
1549    #[test]
1550    fn test_cdm_json_full_round_trip() {
1551        // Use CDMExample2 for JSON round-trip (the JSON writer does not yet
1552        // emit all v2.0-only fields like OD/additional parameters)
1553        let cdm1 = CDM::from_file("test_assets/ccsds/cdm/CDMExample2.txt").unwrap();
1554        let json = cdm1.to_string(CCSDSFormat::JSON).unwrap();
1555        let cdm2 = CDM::from_str(&json).unwrap();
1556
1557        // Header
1558        assert_eq!(cdm1.header.originator, cdm2.header.originator);
1559        assert_eq!(cdm1.header.message_id, cdm2.header.message_id);
1560        assert_eq!(cdm1.header.message_for, cdm2.header.message_for);
1561
1562        // Relative metadata
1563        assert!((cdm1.miss_distance() - cdm2.miss_distance()).abs() < 1e-6);
1564        assert_eq!(
1565            cdm1.relative_metadata.collision_probability,
1566            cdm2.relative_metadata.collision_probability
1567        );
1568        assert_eq!(
1569            cdm1.relative_metadata.collision_probability_method,
1570            cdm2.relative_metadata.collision_probability_method
1571        );
1572
1573        // State vectors
1574        for i in 0..6 {
1575            assert!(
1576                (cdm1.object1_state()[i] - cdm2.object1_state()[i]).abs() < 0.01,
1577                "obj1 state[{}] mismatch",
1578                i
1579            );
1580            assert!(
1581                (cdm1.object2_state()[i] - cdm2.object2_state()[i]).abs() < 0.01,
1582                "obj2 state[{}] mismatch",
1583                i
1584            );
1585        }
1586
1587        // RTN covariance
1588        let c1 = cdm1.object1_rtn_covariance_6x6();
1589        let c2 = cdm2.object1_rtn_covariance_6x6();
1590        for i in 0..6 {
1591            for j in 0..6 {
1592                let rel = if c1[(i, j)].abs() > 1e-20 {
1593                    ((c1[(i, j)] - c2[(i, j)]) / c1[(i, j)]).abs()
1594                } else {
1595                    (c1[(i, j)] - c2[(i, j)]).abs()
1596                };
1597                assert!(rel < 1e-4, "obj1 cov({},{}) mismatch", i, j);
1598            }
1599        }
1600    }
1601}