use super::{EphemerisError, OEMTimeParsingSnafu};
use crate::math::{Matrix6, Vector6};
use crate::naif::daf::data_types::DataType;
use crate::prelude::{Frame, Orbit};
use hifitime::{
efmt::{Format, Formatter},
Epoch,
};
use log::warn;
use snafu::ResultExt;
use std::collections::BTreeMap;
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::Path;
use std::str::FromStr;
use super::{Covariance, Ephemeris, EphemerisRecord, LocalFrame};
impl Ephemeris {
pub fn from_ccsds_oem_file<P: AsRef<Path>>(path: P) -> Result<Self, EphemerisError> {
let file = File::open(path).map_err(|e| EphemerisError::OEMParsingError {
lno: 0,
details: format!("could not open file: {e}"),
})?;
let reader = BufReader::new(file);
let mut in_state_data = false;
let mut in_cov_data = false;
let mut time_system = String::new();
let mut center_name = None;
let mut orient_name = None;
let mut interpolation = DataType::Type9LagrangeUnequalStep;
let mut degree = 5;
let mut object_id: Option<String> = None;
let mut cov_epoch = None;
let mut cov_mat = None;
let mut cov_frame = None;
let mut cov_row = 0;
let mut state_data = BTreeMap::new();
let parse_one_val = |lno: usize, line: &str, err: &str| -> Result<String, EphemerisError> {
let parts: Vec<&str> = line.split('=').collect();
match parts.get(1) {
Some(val_str) => Ok(val_str.trim().to_string()),
None => Err(EphemerisError::OEMParsingError {
lno,
details: err.to_string(),
}),
}
};
for (lno, line) in reader.lines().enumerate() {
let line = line.map_err(|e| EphemerisError::OEMParsingError {
lno,
details: format!("could not read line: {e}"),
})?;
let line = line.trim();
if line.is_empty() {
continue;
}
if line.starts_with("CCSDS_OEM_VERS") {
let version_str = parse_one_val(lno, line, "no value for CCSDS_OEM_VERS")?;
match version_str.parse::<f32>() {
Ok(version_val) => match version_val as i16 {
1 | 2 => {}
_ => {
return Err(EphemerisError::OEMParsingError {
lno,
details: "CCSDS OEM version {version_val} not supported"
.to_string(),
})
}
},
Err(_) => {
return Err(EphemerisError::OEMParsingError {
lno,
details: format!("could not parse OEM version `{version_str}`"),
})
}
}
}
if line.starts_with("OBJECT_ID") {
let oem_obj_id = parse_one_val(lno, line, "no value for OBJECT_ID")?;
if let Some(prev_obj_id) = &object_id {
if oem_obj_id != *prev_obj_id {
return Err(EphemerisError::OEMParsingError {
lno,
details: format!(
"OEM must have only one object: `{prev_obj_id}` != `{oem_obj_id}`"
),
});
}
} else {
object_id = Some(oem_obj_id);
}
} else if line.starts_with("CENTER_NAME") {
center_name = Some(parse_one_val(lno, line, "no value for CENTER")?);
} else if line.starts_with("REF_FRAME") {
orient_name = Some(parse_one_val(lno, line, "no value for REF_FRAME")?);
} else if line.starts_with("TIME_SYSTEM") {
time_system = parse_one_val(lno, line, "no value for TIME_SYSTEM")?;
} else if line.starts_with("INTERPOLATION_DEGREE") {
let interp_str =
parse_one_val(lno, line, "no value for INTERPOLATION_DEGREE")?.to_lowercase();
match interp_str.parse::<usize>() {
Ok(ideg) => degree = ideg,
Err(_) => {
return Err(EphemerisError::OEMParsingError {
lno,
details: format!("could not parse `{interp_str}` as float"),
})
}
}
} else if line.starts_with("INTERPOLATION") {
let interp_str =
parse_one_val(lno, line, "no value for INTERPOLATION")?.to_lowercase();
match interp_str.as_str() {
"lagrange" => interpolation = DataType::Type9LagrangeUnequalStep,
"hermite" => interpolation = DataType::Type13HermiteUnequalStep,
_ => {
warn!("unsupported interpolation `{interp_str}` using Hermite")
}
};
} else if line.starts_with("META_STOP") {
in_state_data = true;
in_cov_data = false;
} else if line.starts_with("META_START") {
in_state_data = false;
in_cov_data = false;
} else if line.starts_with("COVARIANCE_START") {
in_state_data = false;
in_cov_data = true;
} else if line.starts_with("COVARIANCE_STOP") {
in_state_data = false;
in_cov_data = false;
} else if line.starts_with("COMMENT") {
} else if in_state_data {
let center_name_str =
center_name
.as_ref()
.ok_or_else(|| EphemerisError::OEMParsingError {
lno,
details: "CENTER_NAME not found in metadata".to_string(),
})?;
let center_name = center_name_str
.split_whitespace()
.map(|word| {
let word = word.to_lowercase();
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => {
first.to_uppercase().collect::<String>() + chars.as_str()
}
}
})
.collect::<Vec<String>>()
.join(" ");
let orient_name_str =
orient_name
.as_ref()
.ok_or_else(|| EphemerisError::OEMParsingError {
lno,
details: "REF_FRAME not found in metadata".to_string(),
})?;
let frame =
Frame::from_name(center_name.as_str(), orient_name_str).map_err(|e| {
EphemerisError::OEMParsingError {
lno,
details: format!("frame error `{center_name:?} {orient_name:?}`: {e}"),
}
})?;
let parts: Vec<&str> = line.split_whitespace().collect();
let mut state_vec = Vector6::zeros();
let epoch = match parts.first() {
Some(state_epoch) => {
let epoch_str = format!("{state_epoch} {time_system}");
Epoch::from_str(epoch_str.trim()).context(OEMTimeParsingSnafu {
line: lno,
details: format!("`{epoch_str}` for state epoch"),
})?
}
None => {
return Err(EphemerisError::OEMParsingError {
lno,
details: "no `=` sign for covariance epoch".to_string(),
})
}
};
for i in 0..6 {
match parts.get(i + 1) {
Some(val_str) => match val_str.trim().parse::<f64>() {
Ok(val_f64) => {
state_vec[i] = val_f64;
}
Err(_) => {
return Err(EphemerisError::OEMParsingError {
lno,
details: format!(
"could not parse `{}` as float",
val_str.trim()
),
})
}
},
None => {
return Err(EphemerisError::OEMParsingError {
lno,
details: format!("missing float in position {}", i + 1),
})
}
};
}
let orbit = Orbit::from_cartesian_pos_vel(state_vec, epoch, frame);
state_data.insert(epoch, EphemerisRecord { orbit, covar: None });
} else if in_cov_data {
if line.starts_with("EPOCH") {
let state_epoch = parse_one_val(lno, line, "no `=` sign for covariance epoch")?;
let epoch_str = format!("{state_epoch} {time_system}");
let epoch = Epoch::from_str(epoch_str.trim()).context(OEMTimeParsingSnafu {
line: lno,
details: format!("`{epoch_str}` for covariance epoch"),
})?;
if !state_data.contains_key(&epoch) {
return Err(EphemerisError::OEMParsingError { lno, details: format!("cannot have covariance data at {epoch} because no orbit data at that epoch")});
}
cov_epoch = Some(epoch);
cov_mat = Some(Matrix6::zeros());
cov_row = 0;
} else if line.starts_with("COV_REF_FRAME") {
let cov_frame_str = parse_one_val(lno, line, "invalid COV_REF_FRAME token")?;
match cov_frame_str.as_str() {
"EME2000" | "ICRF" => cov_frame = Some(LocalFrame::Inertial),
"RSW" | "RTN" => cov_frame = Some(LocalFrame::RIC),
"TNW" => cov_frame = Some(LocalFrame::VNC),
_ => {
return Err(EphemerisError::OEMParsingError {
lno,
details: format!("invalid COV_REF_FRAME `{cov_frame_str}`"),
})
}
};
} else {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() != cov_row + 1 {
return Err(EphemerisError::OEMParsingError {
lno,
details: format!(
"expected {} values for covariance row {cov_row} but got {}",
cov_row + 1,
parts.len()
),
});
}
for col in 0..cov_row + 1 {
match parts.get(col) {
Some(val_str) => match val_str.trim().parse::<f64>() {
Ok(val_f64) => {
cov_mat.as_mut().unwrap()[(col, cov_row)] = val_f64;
cov_mat.as_mut().unwrap()[(cov_row, col)] = val_f64;
}
Err(_) => {
return Err(EphemerisError::OEMParsingError {
lno,
details: format!(
"could not parse `{}` as float",
val_str.trim()
),
})
}
},
None => {
return Err(EphemerisError::OEMParsingError {
lno,
details: format!(
"missing float in covariance data position {col}"
),
})
}
};
}
cov_row += 1;
if cov_row == 6 {
match cov_epoch {
Some(cov_epoch) => {
let covar = cov_mat.map(|mat| Covariance {
matrix: mat,
local_frame: cov_frame.unwrap_or(LocalFrame::Inertial),
});
state_data
.get_mut(&cov_epoch)
.expect("epoch was valid but now no?")
.covar = covar;
}
None => {
return Err(EphemerisError::OEMParsingError {
lno,
details: "no cov epoch ever found?!".to_string(),
})
}
}
}
}
}
}
if state_data.is_empty() {
return Err(EphemerisError::OEMParsingError {
lno: 0,
details: "ephemeris file contains no state data".to_string(),
});
}
if let Some(object_id) = object_id {
Ok(Ephemeris {
object_id,
degree,
interpolation,
state_data,
})
} else {
Err(EphemerisError::OEMParsingError {
lno: 0,
details: "no OBJECT_ID found throughout the file".to_string(),
})
}
}
pub fn write_ccsds_oem<P: AsRef<Path>>(
&self,
path: P,
originator: Option<String>,
object_name: Option<String>,
) -> Result<(), EphemerisError> {
if self.state_data.is_empty() {
return Err(EphemerisError::OEMParsingError {
lno: 0,
details: "ephemeris file contains no state data".to_string(),
});
}
let file = File::create(&path).map_err(|e| EphemerisError::OEMWritingError {
details: format!("could not create file: {e}"),
})?;
let mut writer = BufWriter::new(file);
let err_hdlr = |e| EphemerisError::OEMWritingError {
details: format!("{e}"),
};
let iso8601_no_ts = Format::from_str("%Y-%m-%dT%H:%M:%S.%f").unwrap();
writeln!(writer, "CCSDS_OEM_VERS = 2.0\n").map_err(err_hdlr)?;
writeln!(
writer,
"COMMENT Built by ANISE, a modern rewrite of NASA/NAIF SPICE (https://nyxspace.com/anise)",
)
.map_err(err_hdlr)?;
writeln!(
writer,
"COMMENT ANISE is open-source software provided under the Mozilla Public License 2.0 (https://github.com/nyx-space/anise)\n"
)
.map_err(err_hdlr)?;
writeln!(
writer,
"CREATION_DATE = {}",
Formatter::new(Epoch::now().unwrap(), iso8601_no_ts)
)
.map_err(err_hdlr)?;
writeln!(
writer,
"ORIGINATOR = {}\n",
originator.unwrap_or("Nyx Space ANISE".to_string())
)
.map_err(err_hdlr)?;
writeln!(writer, "META_START").map_err(err_hdlr)?;
writeln!(writer, "\tOBJECT_ID = {}", self.object_id).map_err(err_hdlr)?;
if let Some(object_name) = object_name {
writeln!(writer, "\tOBJECT_NAME = {object_name}").map_err(err_hdlr)?;
}
let first_orbit = self.state_data.first_key_value().unwrap().1.orbit;
let first_frame = first_orbit.frame;
let last_orbit = self.state_data.last_key_value().unwrap().1.orbit;
let center = format!("{first_frame:e}");
let ref_frame = format!("{first_frame:o}");
writeln!(
writer,
"\tREF_FRAME = {}",
match ref_frame.trim() {
"J2000" => "EME2000",
_ => ref_frame.trim(),
}
)
.map_err(err_hdlr)?;
writeln!(writer, "\tCENTER_NAME = {center}",).map_err(err_hdlr)?;
writeln!(writer, "\tTIME_SYSTEM = {}", first_orbit.epoch.time_scale).map_err(err_hdlr)?;
writeln!(
writer,
"\tINTERPOLATION = {}",
match self.interpolation {
DataType::Type9LagrangeUnequalStep => "LAGRANGE",
DataType::Type13HermiteUnequalStep => "HERMITE",
_ => unreachable!(),
}
)
.map_err(err_hdlr)?;
writeln!(writer, "\tINTERPOLATION_DEGREE = {}", self.degree).map_err(err_hdlr)?;
writeln!(
writer,
"\tSTART_TIME = {}",
Formatter::new(first_orbit.epoch, iso8601_no_ts),
)
.map_err(err_hdlr)?;
writeln!(
writer,
"\tUSEABLE_START_TIME = {}",
Formatter::new(first_orbit.epoch, iso8601_no_ts),
)
.map_err(err_hdlr)?;
writeln!(
writer,
"\tUSEABLE_STOP_TIME = {}",
Formatter::new(last_orbit.epoch, iso8601_no_ts),
)
.map_err(err_hdlr)?;
writeln!(
writer,
"\tSTOP_TIME = {}",
Formatter::new(last_orbit.epoch, iso8601_no_ts),
)
.map_err(err_hdlr)?;
writeln!(writer, "META_STOP\n").map_err(err_hdlr)?;
for (epoch, entry) in self.state_data.iter() {
let orbit = entry.orbit;
writeln!(
writer,
"{} {:E} {:E} {:E} {:E} {:E} {:E}",
Formatter::new(*epoch, iso8601_no_ts),
orbit.radius_km.x,
orbit.radius_km.y,
orbit.radius_km.z,
orbit.velocity_km_s.x,
orbit.velocity_km_s.y,
orbit.velocity_km_s.z
)
.map_err(err_hdlr)?;
}
#[allow(clippy::writeln_empty_string)]
writeln!(writer, "").map_err(err_hdlr)?;
let mut cov_started = false;
for (epoch, entry) in self.state_data.iter() {
if let Some(covar) = &entry.covar {
if !cov_started {
writeln!(writer, "COVARIANCE_START").map_err(err_hdlr)?;
cov_started = true;
}
writeln!(writer, "EPOCH = {}", Formatter::new(*epoch, iso8601_no_ts),)
.map_err(err_hdlr)?;
writeln!(
writer,
"COV_REF_FRAME = {}",
match covar.local_frame {
LocalFrame::Inertial => "EME2000",
LocalFrame::RIC => "RTN",
LocalFrame::VNC => "TNW",
LocalFrame::RCN =>
return Err(EphemerisError::OEMWritingError {
details: "RCN frame is not supported for OEM covariance export"
.to_string(),
}),
}
)
.map_err(err_hdlr)?;
for row in 0..6 {
let mut line = String::new();
for col in 0..row + 1 {
line.push_str(&format!("{:E} ", covar.matrix[(col, row)]));
}
writeln!(writer, "{}", line.trim()).map_err(err_hdlr)?;
}
#[allow(clippy::writeln_empty_string)]
writeln!(writer, "").map_err(err_hdlr)?;
}
}
if cov_started {
writeln!(writer, "COVARIANCE_STOP").map_err(err_hdlr)?;
}
Ok(())
}
}