use std::collections::HashMap;
use std::fmt;
use nalgebra::SMatrix;
use serde::{Deserialize, Serialize};
use crate::time::Epoch;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CCSDSJsonKeyCase {
Lower,
Upper,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CCSDSFormat {
KVN,
XML,
JSON,
}
impl fmt::Display for CCSDSFormat {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
CCSDSFormat::KVN => write!(f, "KVN"),
CCSDSFormat::XML => write!(f, "XML"),
CCSDSFormat::JSON => write!(f, "JSON"),
}
}
}
pub(crate) fn detect_format(content: &str) -> CCSDSFormat {
let trimmed = content.trim_start();
if trimmed.starts_with("<?xml") || trimmed.starts_with('<') {
CCSDSFormat::XML
} else if trimmed.starts_with('{') || trimmed.starts_with('[') {
CCSDSFormat::JSON
} else {
CCSDSFormat::KVN
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum CCSDSTimeSystem {
UTC,
TAI,
GPS,
TT,
UT1,
TDB,
TCB,
TDR,
TCG,
BDT,
GST,
GMST,
MET,
MRT,
SCLK,
}
impl fmt::Display for CCSDSTimeSystem {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
CCSDSTimeSystem::UTC => write!(f, "UTC"),
CCSDSTimeSystem::TAI => write!(f, "TAI"),
CCSDSTimeSystem::GPS => write!(f, "GPS"),
CCSDSTimeSystem::TT => write!(f, "TT"),
CCSDSTimeSystem::UT1 => write!(f, "UT1"),
CCSDSTimeSystem::TDB => write!(f, "TDB"),
CCSDSTimeSystem::TCB => write!(f, "TCB"),
CCSDSTimeSystem::TDR => write!(f, "TDR"),
CCSDSTimeSystem::TCG => write!(f, "TCG"),
CCSDSTimeSystem::BDT => write!(f, "BDT"),
CCSDSTimeSystem::GST => write!(f, "GST"),
CCSDSTimeSystem::GMST => write!(f, "GMST"),
CCSDSTimeSystem::MET => write!(f, "MET"),
CCSDSTimeSystem::MRT => write!(f, "MRT"),
CCSDSTimeSystem::SCLK => write!(f, "SCLK"),
}
}
}
impl CCSDSTimeSystem {
pub fn parse(s: &str) -> Result<Self, crate::utils::errors::BraheError> {
match s.trim() {
"UTC" => Ok(CCSDSTimeSystem::UTC),
"TAI" => Ok(CCSDSTimeSystem::TAI),
"GPS" => Ok(CCSDSTimeSystem::GPS),
"TT" => Ok(CCSDSTimeSystem::TT),
"UT1" => Ok(CCSDSTimeSystem::UT1),
"TDB" => Ok(CCSDSTimeSystem::TDB),
"TCB" => Ok(CCSDSTimeSystem::TCB),
"TDR" => Ok(CCSDSTimeSystem::TDR),
"TCG" => Ok(CCSDSTimeSystem::TCG),
"BDT" => Ok(CCSDSTimeSystem::BDT),
"GST" => Ok(CCSDSTimeSystem::GST),
"GMST" => Ok(CCSDSTimeSystem::GMST),
"MET" => Ok(CCSDSTimeSystem::MET),
"MRT" => Ok(CCSDSTimeSystem::MRT),
"SCLK" => Ok(CCSDSTimeSystem::SCLK),
_ => Err(crate::ccsds::error::ccsds_parse_error(
"common",
&format!("unknown time system '{}'", s),
)),
}
}
pub fn to_time_system(&self) -> Option<crate::time::TimeSystem> {
match self {
CCSDSTimeSystem::UTC => Some(crate::time::TimeSystem::UTC),
CCSDSTimeSystem::TAI => Some(crate::time::TimeSystem::TAI),
CCSDSTimeSystem::GPS => Some(crate::time::TimeSystem::GPS),
CCSDSTimeSystem::TT => Some(crate::time::TimeSystem::TT),
CCSDSTimeSystem::UT1 => Some(crate::time::TimeSystem::UT1),
CCSDSTimeSystem::TDB => Some(crate::time::TimeSystem::TDB),
CCSDSTimeSystem::TCG => Some(crate::time::TimeSystem::TCG),
CCSDSTimeSystem::TCB => Some(crate::time::TimeSystem::TCB),
CCSDSTimeSystem::BDT => Some(crate::time::TimeSystem::BDT),
CCSDSTimeSystem::GST => Some(crate::time::TimeSystem::GST),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum CCSDSRefFrame {
EME2000,
GCRF,
ITRF2000,
ITRF93,
ITRF97,
ITRF2005,
ITRF2008,
ITRF2014,
TEME,
TOD,
J2000,
TDR,
RTN,
TNW,
RSW,
Other(String),
}
impl fmt::Display for CCSDSRefFrame {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
CCSDSRefFrame::EME2000 => write!(f, "EME2000"),
CCSDSRefFrame::GCRF => write!(f, "GCRF"),
CCSDSRefFrame::ITRF2000 => write!(f, "ITRF2000"),
CCSDSRefFrame::ITRF93 => write!(f, "ITRF93"),
CCSDSRefFrame::ITRF97 => write!(f, "ITRF97"),
CCSDSRefFrame::ITRF2005 => write!(f, "ITRF2005"),
CCSDSRefFrame::ITRF2008 => write!(f, "ITRF2008"),
CCSDSRefFrame::ITRF2014 => write!(f, "ITRF2014"),
CCSDSRefFrame::TEME => write!(f, "TEME"),
CCSDSRefFrame::TOD => write!(f, "TOD"),
CCSDSRefFrame::J2000 => write!(f, "J2000"),
CCSDSRefFrame::TDR => write!(f, "TDR"),
CCSDSRefFrame::RTN => write!(f, "RTN"),
CCSDSRefFrame::TNW => write!(f, "TNW"),
CCSDSRefFrame::RSW => write!(f, "RSW"),
CCSDSRefFrame::Other(s) => write!(f, "{}", s),
}
}
}
impl CCSDSRefFrame {
pub fn parse(s: &str) -> Self {
match s.trim() {
"EME2000" => CCSDSRefFrame::EME2000,
"GCRF" => CCSDSRefFrame::GCRF,
"ITRF2000" | "ITRF-2000" => CCSDSRefFrame::ITRF2000,
"ITRF93" | "ITRF-93" => CCSDSRefFrame::ITRF93,
"ITRF97" | "ITRF-97" | "ITRF1997" => CCSDSRefFrame::ITRF97,
"ITRF2005" | "ITRF-2005" => CCSDSRefFrame::ITRF2005,
"ITRF2008" | "ITRF-2008" => CCSDSRefFrame::ITRF2008,
"ITRF2014" | "ITRF-2014" => CCSDSRefFrame::ITRF2014,
"TEME" => CCSDSRefFrame::TEME,
"TOD" => CCSDSRefFrame::TOD,
"J2000" => CCSDSRefFrame::J2000,
"TDR" => CCSDSRefFrame::TDR,
"RTN" => CCSDSRefFrame::RTN,
"TNW" => CCSDSRefFrame::TNW,
"RSW" => CCSDSRefFrame::RSW,
other => CCSDSRefFrame::Other(other.to_string()),
}
}
}
#[derive(Debug, Clone)]
pub struct ODMHeader {
pub format_version: f64,
pub classification: Option<String>,
pub creation_date: Epoch,
pub originator: String,
pub message_id: Option<String>,
pub comments: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CCSDSCovariance {
pub epoch: Option<Epoch>,
pub cov_ref_frame: Option<CCSDSRefFrame>,
pub matrix: SMatrix<f64, 6, 6>,
pub comments: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CCSDSSpacecraftParameters {
pub mass: Option<f64>,
pub solar_rad_area: Option<f64>,
pub solar_rad_coeff: Option<f64>,
pub drag_area: Option<f64>,
pub drag_coeff: Option<f64>,
pub comments: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CCSDSUserDefined {
pub parameters: HashMap<String, String>,
}
pub fn parse_ccsds_datetime(
s: &str,
time_system: &CCSDSTimeSystem,
) -> Result<Epoch, crate::utils::errors::BraheError> {
let s = s.trim();
let ts = time_system.to_time_system().ok_or_else(|| {
crate::ccsds::error::ccsds_parse_error(
"common",
&format!(
"time system '{}' is not supported for epoch conversion. Supported: UTC, TAI, GPS, TT, UT1, TDB, TCG, TCB, BDT, GST",
time_system
),
)
})?;
if let Some(t_pos) = s.find('T') {
let date_part = &s[..t_pos];
let time_part = &s[t_pos + 1..];
let parts: Vec<&str> = date_part.split('-').collect();
if parts.len() == 2 && parts[1].len() == 3 {
let year: u32 = parts[0].parse().map_err(|_| {
crate::ccsds::error::ccsds_parse_error(
"datetime",
&format!("invalid year in '{}'", s),
)
})?;
let doy: u32 = parts[1].parse().map_err(|_| {
crate::ccsds::error::ccsds_parse_error(
"datetime",
&format!("invalid DOY in '{}'", s),
)
})?;
let time_parts: Vec<&str> = time_part.split(':').collect();
if time_parts.len() != 3 {
return Err(crate::ccsds::error::ccsds_parse_error(
"datetime",
&format!("invalid time format in '{}'", s),
));
}
let hour: u8 = time_parts[0].parse().map_err(|_| {
crate::ccsds::error::ccsds_parse_error(
"datetime",
&format!("invalid hour in '{}'", s),
)
})?;
let minute: u8 = time_parts[1].parse().map_err(|_| {
crate::ccsds::error::ccsds_parse_error(
"datetime",
&format!("invalid minute in '{}'", s),
)
})?;
let sec_str = time_parts[2];
let second: f64 = sec_str.parse().map_err(|_| {
crate::ccsds::error::ccsds_parse_error(
"datetime",
&format!("invalid second in '{}'", s),
)
})?;
let whole_second = second.floor();
let frac_second = second - whole_second;
let fractional_day = (doy as f64)
+ (hour as f64) / 24.0
+ (minute as f64) / 1440.0
+ whole_second / 86400.0
+ frac_second / 86400.0;
return Ok(Epoch::from_day_of_year(year, fractional_day, ts));
}
}
let normalized = s.replace('T', " ");
let parts: Vec<&str> = normalized.splitn(2, ' ').collect();
if parts.len() != 2 {
let date_parts: Vec<&str> = s.split('-').collect();
if date_parts.len() == 3 {
let year: u32 = date_parts[0].parse().map_err(|_| {
crate::ccsds::error::ccsds_parse_error(
"datetime",
&format!("invalid year in '{}'", s),
)
})?;
let month: u8 = date_parts[1].parse().map_err(|_| {
crate::ccsds::error::ccsds_parse_error(
"datetime",
&format!("invalid month in '{}'", s),
)
})?;
let day: u8 = date_parts[2].parse().map_err(|_| {
crate::ccsds::error::ccsds_parse_error(
"datetime",
&format!("invalid day in '{}'", s),
)
})?;
return Ok(Epoch::from_date(year, month, day, ts));
}
return Err(crate::ccsds::error::ccsds_parse_error(
"datetime",
&format!("unrecognized date format '{}'", s),
));
}
let date_part = parts[0];
let time_part = parts[1];
let date_parts: Vec<&str> = date_part.split('-').collect();
if date_parts.len() != 3 {
return Err(crate::ccsds::error::ccsds_parse_error(
"datetime",
&format!("invalid date format in '{}'", s),
));
}
let year: u32 = date_parts[0].parse().map_err(|_| {
crate::ccsds::error::ccsds_parse_error("datetime", &format!("invalid year in '{}'", s))
})?;
let month: u8 = date_parts[1].parse().map_err(|_| {
crate::ccsds::error::ccsds_parse_error("datetime", &format!("invalid month in '{}'", s))
})?;
let day: u8 = date_parts[2].parse().map_err(|_| {
crate::ccsds::error::ccsds_parse_error("datetime", &format!("invalid day in '{}'", s))
})?;
let time_parts: Vec<&str> = time_part.split(':').collect();
if time_parts.len() != 3 {
return Err(crate::ccsds::error::ccsds_parse_error(
"datetime",
&format!("invalid time format in '{}'", s),
));
}
let hour: u8 = time_parts[0].parse().map_err(|_| {
crate::ccsds::error::ccsds_parse_error("datetime", &format!("invalid hour in '{}'", s))
})?;
let minute: u8 = time_parts[1].parse().map_err(|_| {
crate::ccsds::error::ccsds_parse_error("datetime", &format!("invalid minute in '{}'", s))
})?;
let sec_str = time_parts[2];
let second: f64 = sec_str.parse().map_err(|_| {
crate::ccsds::error::ccsds_parse_error("datetime", &format!("invalid second in '{}'", s))
})?;
let whole_second = second.floor();
let frac_ns = (second - whole_second) * 1e9;
Ok(Epoch::from_datetime(
year,
month,
day,
hour,
minute,
whole_second,
frac_ns,
ts,
))
}
pub fn format_ccsds_datetime(epoch: &Epoch) -> String {
let (year, month, day, hour, minute, second, nanosecond) = epoch.to_datetime();
let total_seconds = second + nanosecond / 1e9;
if nanosecond == 0.0 {
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:06.3}",
year, month, day, hour, minute, total_seconds
)
} else {
let formatted = format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:013.10}",
year, month, day, hour, minute, total_seconds
);
let trimmed = formatted.trim_end_matches('0');
if trimmed.ends_with('.') {
format!("{}0", trimmed)
} else {
trimmed.to_string()
}
}
}
pub fn strip_units(value: &str) -> &str {
if let Some(bracket_pos) = value.find('[') {
value[..bracket_pos].trim()
} else {
value.trim()
}
}
pub fn covariance_from_lower_triangular(values: &[f64; 21], scale: f64) -> SMatrix<f64, 6, 6> {
let mut matrix = SMatrix::<f64, 6, 6>::zeros();
let mut idx = 0;
for row in 0..6 {
for col in 0..=row {
let val = values[idx] * scale;
matrix[(row, col)] = val;
matrix[(col, row)] = val;
idx += 1;
}
}
matrix
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CDMCovarianceDimension {
SixBySix,
SevenBySeven,
EightByEight,
NineByNine,
}
impl CDMCovarianceDimension {
pub fn size(&self) -> usize {
match self {
CDMCovarianceDimension::SixBySix => 6,
CDMCovarianceDimension::SevenBySeven => 7,
CDMCovarianceDimension::EightByEight => 8,
CDMCovarianceDimension::NineByNine => 9,
}
}
pub fn num_elements(&self) -> usize {
let n = self.size();
n * (n + 1) / 2
}
pub fn from_num_elements(n: usize) -> Result<Self, crate::utils::errors::BraheError> {
match n {
21 => Ok(CDMCovarianceDimension::SixBySix),
28 => Ok(CDMCovarianceDimension::SevenBySeven),
36 => Ok(CDMCovarianceDimension::EightByEight),
45 => Ok(CDMCovarianceDimension::NineByNine),
_ => Err(crate::ccsds::error::ccsds_parse_error(
"CDM",
&format!(
"invalid number of covariance elements: {} (expected 21, 28, 36, or 45)",
n
),
)),
}
}
}
pub fn covariance9x9_from_lower_triangular(
values: &[f64],
) -> Result<(SMatrix<f64, 9, 9>, CDMCovarianceDimension), crate::utils::errors::BraheError> {
let dim = CDMCovarianceDimension::from_num_elements(values.len())?;
let n = dim.size();
let mut matrix = SMatrix::<f64, 9, 9>::zeros();
let mut idx = 0;
for row in 0..n {
for col in 0..=row {
let val = values[idx];
matrix[(row, col)] = val;
matrix[(col, row)] = val;
idx += 1;
}
}
Ok((matrix, dim))
}
pub fn covariance9x9_to_lower_triangular(
matrix: &SMatrix<f64, 9, 9>,
dimension: CDMCovarianceDimension,
) -> Vec<f64> {
let n = dimension.size();
let mut values = Vec::with_capacity(dimension.num_elements());
for row in 0..n {
for col in 0..=row {
values.push(matrix[(row, col)]);
}
}
values
}
pub fn covariance_to_lower_triangular(matrix: &SMatrix<f64, 6, 6>, scale: f64) -> [f64; 21] {
let mut values = [0.0; 21];
let mut idx = 0;
for row in 0..6 {
for col in 0..=row {
values[idx] = matrix[(row, col)] * scale;
idx += 1;
}
}
values
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
#[test]
fn test_ccsds_format_display() {
assert_eq!(format!("{}", CCSDSFormat::KVN), "KVN");
assert_eq!(format!("{}", CCSDSFormat::XML), "XML");
assert_eq!(format!("{}", CCSDSFormat::JSON), "JSON");
}
#[test]
fn test_ccsds_time_system_parse() {
assert_eq!(CCSDSTimeSystem::parse("UTC").unwrap(), CCSDSTimeSystem::UTC);
assert_eq!(CCSDSTimeSystem::parse("TAI").unwrap(), CCSDSTimeSystem::TAI);
assert_eq!(CCSDSTimeSystem::parse("GPS").unwrap(), CCSDSTimeSystem::GPS);
assert_eq!(CCSDSTimeSystem::parse("TT").unwrap(), CCSDSTimeSystem::TT);
assert_eq!(CCSDSTimeSystem::parse("UT1").unwrap(), CCSDSTimeSystem::UT1);
assert_eq!(CCSDSTimeSystem::parse("TDB").unwrap(), CCSDSTimeSystem::TDB);
assert_eq!(CCSDSTimeSystem::parse("TCB").unwrap(), CCSDSTimeSystem::TCB);
assert_eq!(CCSDSTimeSystem::parse("TCG").unwrap(), CCSDSTimeSystem::TCG);
assert_eq!(CCSDSTimeSystem::parse("BDT").unwrap(), CCSDSTimeSystem::BDT);
assert_eq!(CCSDSTimeSystem::parse("GST").unwrap(), CCSDSTimeSystem::GST);
assert_eq!(CCSDSTimeSystem::parse("MET").unwrap(), CCSDSTimeSystem::MET);
assert_eq!(CCSDSTimeSystem::parse("MRT").unwrap(), CCSDSTimeSystem::MRT);
assert!(CCSDSTimeSystem::parse("INVALID").is_err());
}
#[test]
fn test_ccsds_time_system_to_brahe() {
assert!(CCSDSTimeSystem::UTC.to_time_system().is_some());
assert!(CCSDSTimeSystem::TAI.to_time_system().is_some());
assert!(CCSDSTimeSystem::GPS.to_time_system().is_some());
assert!(CCSDSTimeSystem::TT.to_time_system().is_some());
assert!(CCSDSTimeSystem::UT1.to_time_system().is_some());
assert!(CCSDSTimeSystem::TDB.to_time_system().is_some());
assert!(CCSDSTimeSystem::TCG.to_time_system().is_some());
assert!(CCSDSTimeSystem::TCB.to_time_system().is_some());
assert!(CCSDSTimeSystem::BDT.to_time_system().is_some());
assert!(CCSDSTimeSystem::GST.to_time_system().is_some());
assert!(CCSDSTimeSystem::MET.to_time_system().is_none());
}
#[test]
fn test_ccsds_ref_frame_parse() {
assert_eq!(CCSDSRefFrame::parse("EME2000"), CCSDSRefFrame::EME2000);
assert_eq!(CCSDSRefFrame::parse("GCRF"), CCSDSRefFrame::GCRF);
assert_eq!(CCSDSRefFrame::parse("ITRF2000"), CCSDSRefFrame::ITRF2000);
assert_eq!(CCSDSRefFrame::parse("ITRF-2000"), CCSDSRefFrame::ITRF2000);
assert_eq!(CCSDSRefFrame::parse("ITRF1997"), CCSDSRefFrame::ITRF97);
assert_eq!(CCSDSRefFrame::parse("TEME"), CCSDSRefFrame::TEME);
assert_eq!(CCSDSRefFrame::parse("RTN"), CCSDSRefFrame::RTN);
assert_eq!(
CCSDSRefFrame::parse("CUSTOM_FRAME"),
CCSDSRefFrame::Other("CUSTOM_FRAME".to_string())
);
}
#[test]
fn test_ccsds_ref_frame_display() {
assert_eq!(format!("{}", CCSDSRefFrame::EME2000), "EME2000");
assert_eq!(format!("{}", CCSDSRefFrame::RTN), "RTN");
assert_eq!(
format!("{}", CCSDSRefFrame::Other("CUSTOM".to_string())),
"CUSTOM"
);
}
#[test]
fn test_strip_units() {
assert_eq!(strip_units("6655.9942 [km]"), "6655.9942");
assert_eq!(strip_units("3.11548208 [km/s]"), "3.11548208");
assert_eq!(strip_units("0.020842611"), "0.020842611");
assert_eq!(strip_units(" 1913.000 [kg] "), "1913.000");
}
#[test]
fn test_covariance_round_trip() {
let values: [f64; 21] = [
3.331e-04, 4.619e-04, 6.782e-04, -3.070e-04, -4.221e-04, 3.232e-04, -3.349e-07,
-4.686e-07, 2.485e-07, 4.296e-10, -2.212e-07, -2.864e-07, 1.798e-07, 2.609e-10,
1.768e-10, -3.041e-07, -4.989e-07, 3.540e-07, 1.869e-10, 1.009e-10, 6.224e-10,
];
let matrix = covariance_from_lower_triangular(&values, 1.0);
let recovered = covariance_to_lower_triangular(&matrix, 1.0);
for i in 0..21 {
assert!((values[i] - recovered[i]).abs() < 1e-15);
}
for i in 0..6 {
for j in 0..6 {
assert_eq!(matrix[(i, j)], matrix[(j, i)]);
}
}
}
#[test]
fn test_parse_ccsds_datetime_calendar() {
let ts = CCSDSTimeSystem::UTC;
let epoch = parse_ccsds_datetime("1996-12-18T12:00:00.331", &ts).unwrap();
let (year, month, day, hour, minute, _second, _ns) = epoch.to_datetime();
assert_eq!(year, 1996);
assert_eq!(month, 12);
assert_eq!(day, 18);
assert_eq!(hour, 12);
assert_eq!(minute, 0);
}
#[test]
fn test_parse_ccsds_datetime_doy() {
let ts = CCSDSTimeSystem::UTC;
let epoch = parse_ccsds_datetime("1996-353T12:00:00.331", &ts).unwrap();
let (year, month, day, hour, minute, _second, _ns) = epoch.to_datetime();
assert_eq!(year, 1996);
assert_eq!(month, 12);
assert_eq!(day, 18);
assert_eq!(hour, 12);
assert_eq!(minute, 0);
}
#[test]
fn test_cdm_covariance_dimension() {
assert_eq!(CDMCovarianceDimension::SixBySix.size(), 6);
assert_eq!(CDMCovarianceDimension::SevenBySeven.size(), 7);
assert_eq!(CDMCovarianceDimension::EightByEight.size(), 8);
assert_eq!(CDMCovarianceDimension::NineByNine.size(), 9);
assert_eq!(CDMCovarianceDimension::SixBySix.num_elements(), 21);
assert_eq!(CDMCovarianceDimension::SevenBySeven.num_elements(), 28);
assert_eq!(CDMCovarianceDimension::EightByEight.num_elements(), 36);
assert_eq!(CDMCovarianceDimension::NineByNine.num_elements(), 45);
assert_eq!(
CDMCovarianceDimension::from_num_elements(21).unwrap(),
CDMCovarianceDimension::SixBySix
);
assert_eq!(
CDMCovarianceDimension::from_num_elements(45).unwrap(),
CDMCovarianceDimension::NineByNine
);
assert!(CDMCovarianceDimension::from_num_elements(10).is_err());
}
#[test]
fn test_covariance9x9_round_trip_6x6() {
let values: Vec<f64> = vec![
4.142e+01, -8.579e+00, 2.533e+03, -2.313e+01, 1.336e+01, 7.098e+01, 2.520e-03,
-5.476e+00, 8.626e-04, 5.744e-03, -1.006e-02, 4.041e-03, -1.359e-03, -1.502e-05,
1.049e-05, 1.053e-03, -3.412e-03, 1.213e-02, -3.004e-06, -1.091e-06, 5.529e-05,
];
let (matrix, dim) = covariance9x9_from_lower_triangular(&values).unwrap();
assert_eq!(dim, CDMCovarianceDimension::SixBySix);
let recovered = covariance9x9_to_lower_triangular(&matrix, dim);
for i in 0..21 {
assert!((values[i] - recovered[i]).abs() < 1e-15);
}
for i in 0..6 {
for j in 0..6 {
assert_eq!(matrix[(i, j)], matrix[(j, i)]);
}
}
for i in 6..9 {
for j in 0..9 {
assert_eq!(matrix[(i, j)], 0.0);
assert_eq!(matrix[(j, i)], 0.0);
}
}
}
#[test]
fn test_covariance9x9_round_trip_8x8() {
let mut values = vec![0.0; 36];
for (i, v) in values.iter_mut().enumerate() {
*v = (i + 1) as f64 * 0.1;
}
let (matrix, dim) = covariance9x9_from_lower_triangular(&values).unwrap();
assert_eq!(dim, CDMCovarianceDimension::EightByEight);
let recovered = covariance9x9_to_lower_triangular(&matrix, dim);
assert_eq!(values.len(), recovered.len());
for i in 0..36 {
assert!((values[i] - recovered[i]).abs() < 1e-15);
}
}
#[test]
fn test_parse_ccsds_datetime_no_fractional() {
let ts = CCSDSTimeSystem::UTC;
let epoch = parse_ccsds_datetime("1998-11-06T09:23:57", &ts).unwrap();
let (year, month, day, hour, minute, second, _ns) = epoch.to_datetime();
assert_eq!(year, 1998);
assert_eq!(month, 11);
assert_eq!(day, 6);
assert_eq!(hour, 9);
assert_eq!(minute, 23);
assert_eq!(second, 57.0);
}
#[test]
fn test_detect_format_kvn() {
assert_eq!(detect_format("CCSDS_OEM_VERS = 3.0\n"), CCSDSFormat::KVN);
}
#[test]
fn test_detect_format_xml() {
assert_eq!(
detect_format("<?xml version=\"1.0\"?>\n<oem>"),
CCSDSFormat::XML
);
assert_eq!(detect_format("<oem>"), CCSDSFormat::XML);
}
#[test]
fn test_detect_format_json() {
assert_eq!(detect_format("{\"header\": {}}"), CCSDSFormat::JSON);
assert_eq!(detect_format("[{\"header\": {}}]"), CCSDSFormat::JSON);
}
#[test]
fn test_detect_format_whitespace() {
assert_eq!(
detect_format(" \n CCSDS_OEM_VERS = 3.0"),
CCSDSFormat::KVN
);
assert_eq!(detect_format(" \n <?xml"), CCSDSFormat::XML);
}
#[test]
fn test_parse_ccsds_datetime_unsupported_time_system() {
let unsupported = [
CCSDSTimeSystem::TDR,
CCSDSTimeSystem::GMST,
CCSDSTimeSystem::MET,
CCSDSTimeSystem::MRT,
CCSDSTimeSystem::SCLK,
];
for ts in &unsupported {
let result = parse_ccsds_datetime("2024-01-15T12:00:00.000", ts);
assert!(result.is_err(), "Time system {} should return an error", ts);
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("not supported"),
"Error for {} should mention 'not supported': {}",
ts,
err_msg
);
}
}
#[test]
fn test_parse_ccsds_datetime_supported_time_systems() {
let supported = [
CCSDSTimeSystem::UTC,
CCSDSTimeSystem::TAI,
CCSDSTimeSystem::GPS,
CCSDSTimeSystem::TT,
];
for ts in &supported {
let result = parse_ccsds_datetime("2024-01-15T12:00:00.000", ts);
assert!(
result.is_ok(),
"Time system {} should succeed: {}",
ts,
result.unwrap_err()
);
}
}
#[test]
fn test_ccsds_time_system_display_exotic() {
assert_eq!(format!("{}", CCSDSTimeSystem::TDB), "TDB");
assert_eq!(format!("{}", CCSDSTimeSystem::TCB), "TCB");
assert_eq!(format!("{}", CCSDSTimeSystem::TDR), "TDR");
assert_eq!(format!("{}", CCSDSTimeSystem::TCG), "TCG");
assert_eq!(format!("{}", CCSDSTimeSystem::GMST), "GMST");
assert_eq!(format!("{}", CCSDSTimeSystem::MET), "MET");
assert_eq!(format!("{}", CCSDSTimeSystem::MRT), "MRT");
assert_eq!(format!("{}", CCSDSTimeSystem::SCLK), "SCLK");
}
#[test]
fn test_ccsds_time_system_parse_exotic() {
assert_eq!(CCSDSTimeSystem::parse("TCB").unwrap(), CCSDSTimeSystem::TCB);
assert_eq!(CCSDSTimeSystem::parse("TDR").unwrap(), CCSDSTimeSystem::TDR);
assert_eq!(CCSDSTimeSystem::parse("TCG").unwrap(), CCSDSTimeSystem::TCG);
assert_eq!(
CCSDSTimeSystem::parse("GMST").unwrap(),
CCSDSTimeSystem::GMST
);
assert_eq!(
CCSDSTimeSystem::parse("SCLK").unwrap(),
CCSDSTimeSystem::SCLK
);
}
#[test]
fn test_ccsds_ref_frame_display_all_variants() {
assert_eq!(format!("{}", CCSDSRefFrame::GCRF), "GCRF");
assert_eq!(format!("{}", CCSDSRefFrame::ITRF2000), "ITRF2000");
assert_eq!(format!("{}", CCSDSRefFrame::ITRF93), "ITRF93");
assert_eq!(format!("{}", CCSDSRefFrame::ITRF97), "ITRF97");
assert_eq!(format!("{}", CCSDSRefFrame::ITRF2005), "ITRF2005");
assert_eq!(format!("{}", CCSDSRefFrame::ITRF2008), "ITRF2008");
assert_eq!(format!("{}", CCSDSRefFrame::ITRF2014), "ITRF2014");
assert_eq!(format!("{}", CCSDSRefFrame::TEME), "TEME");
assert_eq!(format!("{}", CCSDSRefFrame::TDR), "TDR");
assert_eq!(format!("{}", CCSDSRefFrame::TNW), "TNW");
assert_eq!(format!("{}", CCSDSRefFrame::RSW), "RSW");
assert_eq!(format!("{}", CCSDSRefFrame::TOD), "TOD");
assert_eq!(format!("{}", CCSDSRefFrame::J2000), "J2000");
}
#[test]
fn test_ccsds_ref_frame_parse_alternative_formats() {
assert_eq!(CCSDSRefFrame::parse("ITRF-2005"), CCSDSRefFrame::ITRF2005);
assert_eq!(CCSDSRefFrame::parse("ITRF-2008"), CCSDSRefFrame::ITRF2008);
assert_eq!(CCSDSRefFrame::parse("ITRF-2014"), CCSDSRefFrame::ITRF2014);
assert_eq!(CCSDSRefFrame::parse("ITRF-97"), CCSDSRefFrame::ITRF97);
assert_eq!(CCSDSRefFrame::parse("ITRF-93"), CCSDSRefFrame::ITRF93);
assert_eq!(CCSDSRefFrame::parse("TDR"), CCSDSRefFrame::TDR);
assert_eq!(CCSDSRefFrame::parse("TOD"), CCSDSRefFrame::TOD);
assert_eq!(CCSDSRefFrame::parse("J2000"), CCSDSRefFrame::J2000);
assert_eq!(CCSDSRefFrame::parse("TNW"), CCSDSRefFrame::TNW);
assert_eq!(CCSDSRefFrame::parse("RSW"), CCSDSRefFrame::RSW);
}
#[test]
fn test_format_ccsds_datetime_zero_nanoseconds() {
let epoch = Epoch::from_date(2024, 1, 15, crate::time::TimeSystem::UTC);
let formatted = format_ccsds_datetime(&epoch);
assert_eq!(formatted, "2024-01-15T00:00:00.000");
}
#[test]
fn test_format_ccsds_datetime_trailing_zeros_trimmed() {
let epoch = Epoch::from_datetime(
2024,
1,
15,
12,
0,
0.0,
500_000_000.0,
crate::time::TimeSystem::UTC,
);
let formatted = format_ccsds_datetime(&epoch);
assert!(formatted.contains("T12:00:00.5"));
assert!(!formatted.ends_with('0') || formatted.ends_with(".0"));
}
#[test]
fn test_format_ccsds_datetime_integer_second_with_nanoseconds() {
let epoch =
Epoch::from_datetime(2024, 6, 1, 0, 0, 10.0, 100.0, crate::time::TimeSystem::UTC);
let formatted = format_ccsds_datetime(&epoch);
assert!(!formatted.ends_with('.'));
}
#[test]
fn test_parse_ccsds_datetime_date_only() {
let ts = CCSDSTimeSystem::UTC;
let epoch = parse_ccsds_datetime("2024-01-15", &ts).unwrap();
let (year, month, day, hour, minute, second, _ns) = epoch.to_datetime();
assert_eq!(year, 2024);
assert_eq!(month, 1);
assert_eq!(day, 15);
assert_eq!(hour, 0);
assert_eq!(minute, 0);
assert_eq!(second, 0.0);
}
#[test]
fn test_parse_ccsds_datetime_doy_high_precision() {
let ts = CCSDSTimeSystem::UTC;
let epoch = parse_ccsds_datetime("2024-032T06:30:15.123456789", &ts).unwrap();
let (year, month, day, hour, minute, _second, _ns) = epoch.to_datetime();
assert_eq!(year, 2024);
assert_eq!(month, 2); assert_eq!(day, 1);
assert_eq!(hour, 6);
assert_eq!(minute, 30);
}
#[test]
fn test_parse_ccsds_datetime_ut1() {
crate::utils::testing::setup_global_test_eop();
let ts = CCSDSTimeSystem::UT1;
let epoch = parse_ccsds_datetime("2020-06-15T00:00:00.000", &ts);
assert!(epoch.is_ok());
}
#[test]
fn test_covariance_from_lower_triangular_with_scale() {
let values: [f64; 21] = [
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
17.0, 18.0, 19.0, 20.0, 21.0,
];
let matrix = covariance_from_lower_triangular(&values, 1e6);
assert_eq!(matrix[(0, 0)], 1.0e6);
assert_eq!(matrix[(1, 0)], 2.0e6);
assert_eq!(matrix[(0, 1)], 2.0e6); assert_eq!(matrix[(5, 5)], 21.0e6);
}
#[test]
fn test_covariance_to_lower_triangular_with_scale() {
let values: [f64; 21] = [
1.0e6, 2.0e6, 3.0e6, 4.0e6, 5.0e6, 6.0e6, 7.0e6, 8.0e6, 9.0e6, 10.0e6, 11.0e6, 12.0e6,
13.0e6, 14.0e6, 15.0e6, 16.0e6, 17.0e6, 18.0e6, 19.0e6, 20.0e6, 21.0e6,
];
let matrix = covariance_from_lower_triangular(&values, 1.0);
let recovered = covariance_to_lower_triangular(&matrix, 1e-6);
for (i, val) in recovered.iter().enumerate() {
assert!((val - (i + 1) as f64).abs() < 1e-9);
}
}
#[test]
fn test_covariance_round_trip_with_scale() {
let values: [f64; 21] = [
3.331e-04, 4.619e-04, 6.782e-04, -3.070e-04, -4.221e-04, 3.232e-04, -3.349e-07,
-4.686e-07, 2.485e-07, 4.296e-10, -2.212e-07, -2.864e-07, 1.798e-07, 2.609e-10,
1.768e-10, -3.041e-07, -4.989e-07, 3.540e-07, 1.869e-10, 1.009e-10, 6.224e-10,
];
let matrix = covariance_from_lower_triangular(&values, 1e6);
let recovered = covariance_to_lower_triangular(&matrix, 1e-6);
for i in 0..21 {
assert!(
(values[i] - recovered[i]).abs() < 1e-15,
"Mismatch at index {}: {} vs {}",
i,
values[i],
recovered[i]
);
}
}
#[test]
fn test_covariance9x9_round_trip_7x7() {
let mut values = vec![0.0; 28];
for (i, v) in values.iter_mut().enumerate() {
*v = (i + 1) as f64 * 0.01;
}
let (matrix, dim) = covariance9x9_from_lower_triangular(&values).unwrap();
assert_eq!(dim, CDMCovarianceDimension::SevenBySeven);
let recovered = covariance9x9_to_lower_triangular(&matrix, dim);
assert_eq!(values.len(), recovered.len());
for i in 0..28 {
assert!(
(values[i] - recovered[i]).abs() < 1e-15,
"Mismatch at index {}: {} vs {}",
i,
values[i],
recovered[i]
);
}
for i in 0..7 {
for j in 0..7 {
assert_eq!(matrix[(i, j)], matrix[(j, i)]);
}
}
for i in 7..9 {
for j in 0..9 {
assert_eq!(matrix[(i, j)], 0.0);
}
}
}
#[test]
fn test_covariance9x9_round_trip_9x9() {
let mut values = vec![0.0; 45];
for (i, v) in values.iter_mut().enumerate() {
*v = (i + 1) as f64 * 0.001;
}
let (matrix, dim) = covariance9x9_from_lower_triangular(&values).unwrap();
assert_eq!(dim, CDMCovarianceDimension::NineByNine);
let recovered = covariance9x9_to_lower_triangular(&matrix, dim);
assert_eq!(values.len(), recovered.len());
for i in 0..45 {
assert!(
(values[i] - recovered[i]).abs() < 1e-15,
"Mismatch at index {}: {} vs {}",
i,
values[i],
recovered[i]
);
}
for i in 0..9 {
for j in 0..9 {
assert_eq!(matrix[(i, j)], matrix[(j, i)]);
}
}
}
#[test]
fn test_covariance9x9_invalid_element_count() {
let values = vec![0.0; 10]; let result = covariance9x9_from_lower_triangular(&values);
assert!(result.is_err());
}
#[test]
fn test_ccsds_time_system_display_all_standard() {
assert_eq!(format!("{}", CCSDSTimeSystem::UTC), "UTC");
assert_eq!(format!("{}", CCSDSTimeSystem::TAI), "TAI");
assert_eq!(format!("{}", CCSDSTimeSystem::GPS), "GPS");
assert_eq!(format!("{}", CCSDSTimeSystem::TT), "TT");
assert_eq!(format!("{}", CCSDSTimeSystem::UT1), "UT1");
}
#[test]
fn test_ccsds_time_system_to_brahe_exotic_none() {
assert!(CCSDSTimeSystem::TDR.to_time_system().is_none());
assert!(CCSDSTimeSystem::GMST.to_time_system().is_none());
assert!(CCSDSTimeSystem::MRT.to_time_system().is_none());
assert!(CCSDSTimeSystem::SCLK.to_time_system().is_none());
}
}