use crate::qtty::*;
use tempoch::{JulianDate, UTC};
#[derive(Debug, Clone, Copy)]
pub struct EopValues {
pub dut1: Seconds,
pub xp: Arcseconds,
pub yp: Arcseconds,
pub dx: MilliArcseconds,
pub dy: MilliArcseconds,
pub lod: Seconds,
}
impl Default for EopValues {
fn default() -> Self {
Self {
dut1: Seconds::new(0.0),
xp: Arcseconds::new(0.0),
yp: Arcseconds::new(0.0),
dx: MilliArcseconds::new(0.0),
dy: MilliArcseconds::new(0.0),
lod: Seconds::new(0.0),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum EopError {
TimeConversion(tempoch::ConversionError),
NoData {
jd_utc: f64,
mjd_utc: f64,
},
}
impl core::fmt::Display for EopError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match *self {
Self::TimeConversion(e) => write!(f, "EOP time conversion: {e}"),
Self::NoData { jd_utc, mjd_utc } => write!(
f,
"no Earth Orientation Parameters cover UTC JD {jd_utc} (MJD {mjd_utc})"
),
}
}
}
impl core::error::Error for EopError {}
impl From<tempoch::ConversionError> for EopError {
fn from(error: tempoch::ConversionError) -> Self {
Self::TimeConversion(error)
}
}
pub trait EopProvider {
#[inline]
fn try_eop_at(&self, jd_utc: JulianDate<UTC>) -> Result<EopValues, EopError> {
Ok(self.eop_at(jd_utc))
}
fn eop_at(&self, jd_utc: JulianDate<UTC>) -> EopValues;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NullEop;
impl EopProvider for NullEop {
#[inline]
fn try_eop_at(&self, _jd_utc: JulianDate<UTC>) -> Result<EopValues, EopError> {
Ok(EopValues::default())
}
#[inline]
fn eop_at(&self, _jd_utc: JulianDate<UTC>) -> EopValues {
EopValues::default()
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct IersEop;
impl IersEop {
#[inline]
pub fn new() -> Self {
Self
}
pub fn mjd_range(&self) -> Option<(Days, Days)> {
let start = tempoch::eop::eop_start()?;
let end = tempoch::eop::eop_end()?;
Some((start, end))
}
#[inline]
pub fn len(&self) -> usize {
match self.mjd_range() {
Some((start, end)) => (end - start).value().floor() as usize + 1,
None => 0,
}
}
#[inline]
pub fn is_empty(&self) -> bool {
self.mjd_range().is_none()
}
}
impl EopProvider for IersEop {
fn try_eop_at(&self, jd_utc: JulianDate<UTC>) -> Result<EopValues, EopError> {
let ctx = tempoch::TimeContext::with_builtin_eop();
let mjd = jd_utc.to_with::<tempoch::MJD>(&ctx)?.try_raw_with(&ctx)?;
let e = ctx.eop_at(mjd).ok_or(EopError::NoData {
jd_utc: jd_utc.try_raw_with(&ctx)?.value(),
mjd_utc: mjd.value(),
})?;
Ok(EopValues {
dut1: e.ut1_minus_utc,
xp: Arcseconds::new(e.pm_xp.map(|v| v.value()).unwrap_or(0.0)),
yp: Arcseconds::new(e.pm_yp.map(|v| v.value()).unwrap_or(0.0)),
dx: MilliArcseconds::new(e.dx.map(|v| v.value()).unwrap_or(0.0)),
dy: MilliArcseconds::new(e.dy.map(|v| v.value()).unwrap_or(0.0)),
lod: Seconds::new(e.lod.map(|v| v.value() / 1_000.0).unwrap_or(0.0)),
})
}
fn eop_at(&self, jd_utc: JulianDate<UTC>) -> EopValues {
if let Ok(observed) = self.try_eop_at(jd_utc) {
return observed;
}
let ctx = tempoch::TimeContext::new().allow_pre_definition_utc();
let model = jd_utc
.to::<tempoch::TT>()
.to_with::<tempoch::UT1>(&ctx)
.expect("monthly UT1 model must cover an infallible EOP request");
let offset = ctx
.tai_minus_utc(jd_utc)
.expect("UTC offset must be representable for an infallible EOP request");
let (model_hi, model_lo) = model.raw_seconds_pair();
let (tai_hi, tai_lo) = jd_utc.to::<tempoch::TAI>().raw_seconds_pair();
EopValues {
dut1: (model_hi - tai_hi) + (model_lo - tai_lo) + offset,
..EopValues::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn null_eop_returns_zeros() {
let eop = NullEop;
let vals = eop.eop_at(crate::J2000.to::<UTC>());
assert_eq!(vals.dut1.value(), 0.0);
assert_eq!(vals.xp.value(), 0.0);
assert_eq!(vals.yp.value(), 0.0);
assert_eq!(vals.dx.value(), 0.0);
assert_eq!(vals.dy.value(), 0.0);
}
#[test]
fn eop_unit_conversions() {
let as2rad = core::f64::consts::PI / (180.0 * 3600.0);
let vals = EopValues {
xp: Arcseconds::new(0.1),
yp: Arcseconds::new(0.2),
dx: MilliArcseconds::new(0.3),
dy: MilliArcseconds::new(0.4),
..Default::default()
};
assert!((vals.xp.to::<Radian>().value() - 0.1 * as2rad).abs() < 1e-20);
assert!((vals.yp.to::<Radian>().value() - 0.2 * as2rad).abs() < 1e-20);
assert!((vals.dx.to::<Radian>().value() - 0.3e-3 * as2rad).abs() < 1e-20);
assert!((vals.dy.to::<Radian>().value() - 0.4e-3 * as2rad).abs() < 1e-20);
}
#[test]
fn iers_eop_returns_no_data_without_runtime_bundle() {
let eop = IersEop::new();
let result = eop.try_eop_at(crate::J2000.to::<UTC>());
assert!(
matches!(result, Err(EopError::NoData { .. })),
"expected NoData without runtime EOP bundle, got {result:?}"
);
}
#[test]
fn iers_eop_falls_back_to_monthly_ut1_without_runtime_bundle() {
let eop = IersEop::new();
let vals = eop.eop_at(crate::J2000.to::<UTC>());
let ctx = tempoch::TimeContext::new();
let supplied = crate::astro::earth_rotation::jd_ut1_from_tt_eop(crate::J2000, &vals);
let model = crate::J2000.to_with::<tempoch::UT1>(&ctx).unwrap();
let (s_hi, s_lo) = supplied.raw_seconds_pair();
let (m_hi, m_lo) = model.raw_seconds_pair();
assert!(((s_hi - m_hi) + (s_lo - m_lo)).value().abs() < 1e-8);
assert_eq!(vals.xp.value(), 0.0);
}
#[test]
fn iers_eop_mjd_range_is_none_without_runtime_bundle() {
let eop = IersEop::new();
assert!(eop.mjd_range().is_none());
assert!(eop.is_empty());
}
}