#![cfg(feature = "ades")]
pub mod ades_structure;
use ahash::AHashMap;
use camino::Utf8Path;
use quick_xml::de::from_str;
use thiserror::Error;
use crate::{
Arcseconds, Degrees, TrajId,
constants::ARCSEC_TO_DEG,
coordinates::equatorial::EquCoord,
observation_dataset::ObsId,
observation_dataset::{
ObsDataset,
index::{ObsMapIndex, TrajIndexMap},
observation::ObservationInput,
},
observer::dataset::ObserverId,
photometry::{Filter, Photometry},
};
use ades_structure::{FlatAdes, OpticalObs, StructuredAdes};
#[derive(Debug, Error)]
pub enum AdesError {
#[error("I/O error reading ADES file: {0}")]
Io(#[from] std::io::Error),
#[error(
"failed to parse ADES document:\n flat error: {flat_err}\n structured error: {structured_err}"
)]
ParseXml {
flat_err: String,
structured_err: String,
},
#[error("ADES observation #{index} has no permID, provID, or trkSub")]
MissingTrajId { index: usize },
#[error(
"ADES observation #{index} has no RA uncertainty (rmsRA / precRA) and no fallback was provided"
)]
MissingRaError { index: usize },
#[error(
"ADES observation #{index} has no Dec uncertainty (rmsDec / precDec) and no fallback was provided"
)]
MissingDecError { index: usize },
}
pub(crate) fn parse_ades_file(
ades_path: &Utf8Path,
error_ra: Option<f64>,
error_dec: Option<f64>,
start_id: ObsId,
) -> Result<ObsDataset, AdesError> {
let xml = std::fs::read_to_string(ades_path)?;
parse_ades_xml(&xml, error_ra, error_dec, start_id)
}
pub(crate) fn parse_ades_xml(
xml: &str,
error_ra: Option<f64>,
error_dec: Option<f64>,
start_id: ObsId,
) -> Result<ObsDataset, AdesError> {
match from_str::<FlatAdes>(xml) {
Ok(flat) => build_from_flat(&flat.opticals, error_ra, error_dec, start_id),
Err(flat_err) => match from_str::<StructuredAdes>(xml) {
Ok(structured) => build_from_structured(&structured, error_ra, error_dec, start_id),
Err(structured_err) => Err(AdesError::ParseXml {
flat_err: flat_err.to_string(),
structured_err: structured_err.to_string(),
}),
},
}
}
fn build_from_flat(
opticals: &[OpticalObs],
error_ra: Option<f64>,
error_dec: Option<f64>,
start_id: ObsId,
) -> Result<ObsDataset, AdesError> {
let records: Vec<(&OpticalObs, Option<[u8; 3]>)> = opticals.iter().map(|o| (o, None)).collect();
build_dataset(records, error_ra, error_dec, start_id)
}
fn build_from_structured(
structured: &StructuredAdes,
error_ra: Option<f64>,
error_dec: Option<f64>,
start_id: ObsId,
) -> Result<ObsDataset, AdesError> {
let mut records: Vec<(&OpticalObs, Option<[u8; 3]>)> = Vec::new();
for block in &structured.obs_blocks {
let ctx_bytes = mpc_str_to_bytes(&block.obs_context.observatory.mpc_code);
for optical in &block.obs_data.opticals {
records.push((optical, Some(ctx_bytes)));
}
}
build_dataset(records, error_ra, error_dec, start_id)
}
fn build_dataset(
records: Vec<(&OpticalObs, Option<[u8; 3]>)>,
error_ra: Option<Arcseconds>,
error_dec: Option<Arcseconds>,
start_id: ObsId,
) -> Result<ObsDataset, AdesError> {
let mut observations: Vec<ObservationInput> = Vec::with_capacity(records.len());
let mut traj_index: AHashMap<TrajId, Vec<usize>> =
AHashMap::with_capacity(records.len() / 4 + 1);
for (idx, (optical, ctx_mpc)) in records.iter().enumerate() {
let mpc_code = ctx_mpc.unwrap_or_else(|| optical.mpc_code_bytes());
let traj_id = optical
.traj_id()
.ok_or(AdesError::MissingTrajId { index: idx })?;
let ra_err_arcsec = optical
.ra_error_arcsec(error_ra)
.ok_or(AdesError::MissingRaError { index: idx })?;
let dec_err_arcsec = optical
.dec_error_arcsec(error_dec)
.ok_or(AdesError::MissingDecError { index: idx })?;
let ra_err_deg: Degrees = ra_err_arcsec * ARCSEC_TO_DEG;
let dec_err_deg: Degrees = dec_err_arcsec * ARCSEC_TO_DEG;
let equ_coord = EquCoord::from_degrees(optical.ra, ra_err_deg, optical.dec, dec_err_deg);
let photometry = Photometry {
magnitude: optical.mag.unwrap_or(0.0),
error: optical.rms_mag.unwrap_or(0.0),
filter: optical
.band
.as_deref()
.map(|b| Filter::String(b.to_string()))
.unwrap_or(Filter::String("unknown".to_string())),
};
observations.push(ObservationInput {
id: start_id + idx as u64,
equ_coord,
photometry,
mjd_tt: optical.obs_time,
observer: Some(ObserverId::MpcCode(mpc_code)),
});
traj_index.entry(traj_id).or_default().push(idx);
}
let traj_index_map: TrajIndexMap = traj_index
.into_iter()
.map(|(traj_id, indices)| (traj_id, ObsMapIndex::Split(indices)))
.collect();
Ok(ObsDataset::new(
observations,
vec![], None, None, Some(traj_index_map),
))
}
fn mpc_str_to_bytes(code: &str) -> [u8; 3] {
let bytes = code.as_bytes();
let mut arr = [b' '; 3];
let len = bytes.len().min(3);
arr[..len].copy_from_slice(&bytes[..len]);
arr
}