use crate::astro::earth_rotation::jd_ut1_from_tt_eop;
use crate::astro::earth_rotation_provider::itrs_to_equatorial_mean_j2000_rotation;
use crate::astro::nutation::nutation_iau2000b;
use crate::astro::precession::precession_matrix_iau2006;
use crate::astro::sidereal::gast_iau2006;
use crate::calculus::ephemeris::Ephemeris;
use crate::coordinates::centers::Geodetic;
use crate::coordinates::frames::ECEF;
use crate::coordinates::transform::context::DefaultEphemeris;
use crate::coordinates::transform::AstroContext;
use crate::time::JulianDate;
use cheby;
use qtty::*;
const CHEB_DEGREE: usize = 8;
const CHEB_NODES: usize = CHEB_DEGREE + 1;
const SEGMENT_DAYS: Days = Days::new(4.0);
const J2000_OBLIQUITY_RAD: qtty::Quantity<Radian> =
qtty::Quantity::<Radian>::new(84381.406 / 3600.0 * std::f64::consts::PI / 180.0);
const NUT_STEP_DAYS: Days = Hours::new(2.0).to_const::<Day>();
pub struct MoonPositionCache {
mjd_start: ModifiedJulianDate,
num_segments: usize,
cx: Vec<[f64; CHEB_NODES]>,
cy: Vec<[f64; CHEB_NODES]>,
cz: Vec<[f64; CHEB_NODES]>,
}
impl MoonPositionCache {
pub fn new(mjd_start: ModifiedJulianDate, mjd_end: ModifiedJulianDate) -> Self {
let pad = Days::new(1.0); let t0 = mjd_start - pad;
let span = mjd_end + pad - t0;
let num_segments = ((span / SEGMENT_DAYS).value().ceil() as usize).max(1);
let nodes: [f64; CHEB_NODES] = cheby::nodes();
let mut cx = Vec::with_capacity(num_segments);
let mut cy = Vec::with_capacity(num_segments);
let mut cz = Vec::with_capacity(num_segments);
for seg in 0..num_segments {
let seg_start = t0 + seg as f64 * SEGMENT_DAYS;
let seg_mid = seg_start + SEGMENT_DAYS * 0.5;
let seg_half = SEGMENT_DAYS * 0.5;
let mut vx = [0.0; CHEB_NODES];
let mut vy = [0.0; CHEB_NODES];
let mut vz = [0.0; CHEB_NODES];
for k in 0..CHEB_NODES {
let mjd_k = seg_mid + seg_half * nodes[k];
let pos = DefaultEphemeris::moon_geocentric(mjd_k.into());
vx[k] = pos.x().value();
vy[k] = pos.y().value();
vz[k] = pos.z().value();
}
cx.push(cheby::fit_coeffs(&vx));
cy.push(cheby::fit_coeffs(&vy));
cz.push(cheby::fit_coeffs(&vz));
}
Self {
mjd_start: t0,
num_segments,
cx,
cy,
cz,
}
}
#[inline]
pub fn get_position_km(&self, mjd: ModifiedJulianDate) -> (Kilometers, Kilometers, Kilometers) {
let offset = mjd - self.mjd_start;
let seg_idx = (offset / SEGMENT_DAYS).value() as usize;
if seg_idx >= self.num_segments {
let pos = DefaultEphemeris::moon_geocentric(mjd.into());
return (pos.x(), pos.y(), pos.z());
}
let seg_start = self.mjd_start + seg_idx as f64 * SEGMENT_DAYS;
let seg_mid = seg_start + SEGMENT_DAYS * 0.5;
let x = (mjd - seg_mid) / (SEGMENT_DAYS * 0.5);
let x = x.value(); let px = Kilometers::new(cheby::evaluate(&self.cx[seg_idx], x));
let py = Kilometers::new(cheby::evaluate(&self.cy[seg_idx], x));
let pz = Kilometers::new(cheby::evaluate(&self.cz[seg_idx], x));
(px, py, pz)
}
}
pub struct NutationCache {
mjd_start: ModifiedJulianDate,
num_entries: usize,
values: Vec<[Radians; 3]>,
}
impl NutationCache {
pub fn new(mjd_start: ModifiedJulianDate, mjd_end: ModifiedJulianDate) -> Self {
let pad = Days::new(1.0); let t0 = mjd_start - pad;
let t1 = mjd_end + pad;
let span = t1 - t0;
let num_entries = ((span / NUT_STEP_DAYS).value().ceil() as usize) + 1;
let mut values = Vec::with_capacity(num_entries);
for i in 0..num_entries {
let mjd = t0 + i as f64 * NUT_STEP_DAYS;
let nut = nutation_iau2000b(mjd.into());
values.push([nut.dpsi, nut.deps, nut.mean_obliquity]);
}
Self {
mjd_start: t0,
num_entries,
values,
}
}
#[inline]
pub fn get_nutation_rad(&self, mjd: ModifiedJulianDate) -> (Radians, Radians, Radians) {
let offset = mjd - self.mjd_start;
let frac = (offset / NUT_STEP_DAYS).value();
let idx = frac as usize;
if idx + 1 >= self.num_entries {
let nut = nutation_iau2000b(mjd.into());
return (nut.dpsi, nut.deps, nut.mean_obliquity);
}
let t = frac - idx as f64; let a = &self.values[idx];
let b = &self.values[idx + 1];
(
a[0] + t * (b[0] - a[0]),
a[1] + t * (b[1] - a[1]),
a[2] + t * (b[2] - a[2]),
)
}
#[inline]
pub fn nutation_rotation(&self, mjd: ModifiedJulianDate) -> affn::Rotation3 {
let (dpsi, deps, eps0) = self.get_nutation_rad(mjd);
affn::Rotation3::rx(eps0 + deps) * affn::Rotation3::rz(dpsi) * affn::Rotation3::rx(-eps0)
}
}
pub struct MoonAltitudeContext {
pos_cache: MoonPositionCache,
nut_cache: NutationCache,
site_itrf_km: [Kilometers; 3],
lat: qtty::Radians,
lon_rad: qtty::Radians,
}
impl MoonAltitudeContext {
pub fn new(
mjd_start: ModifiedJulianDate,
mjd_end: ModifiedJulianDate,
site: Geodetic<ECEF>,
) -> Self {
let pos_cache = MoonPositionCache::new(mjd_start, mjd_end);
let nut_cache = NutationCache::new(mjd_start, mjd_end);
let site_ecef = site.to_cartesian::<Kilometer>();
let site_itrf_km = [site_ecef.x(), site_ecef.y(), site_ecef.z()];
Self {
pos_cache,
nut_cache,
site_itrf_km,
lat: site.lat.to::<Radian>(),
lon_rad: site.lon.to::<Radian>(),
}
}
#[inline]
pub fn altitude_rad(&self, mjd: ModifiedJulianDate) -> Quantity<Radian> {
let jd: JulianDate = mjd.into();
let ctx: AstroContext = AstroContext::default();
let (x_ecl, y_ecl, z_ecl) = self.pos_cache.get_position_km(mjd);
let (sin_e, cos_e) = J2000_OBLIQUITY_RAD.sin_cos();
let x_eq = x_ecl;
let y_eq = cos_e * y_ecl - sin_e * z_ecl;
let z_eq = sin_e * y_ecl + cos_e * z_ecl;
let sx = self.site_itrf_km[0];
let sy = self.site_itrf_km[1];
let sz = self.site_itrf_km[2];
let rot_itrs = itrs_to_equatorial_mean_j2000_rotation(jd, &ctx);
let [site_eq_x, site_eq_y, site_eq_z] = rot_itrs * [sx, sy, sz];
let x_topo = x_eq - site_eq_x;
let y_topo = y_eq - site_eq_y;
let z_topo = z_eq - site_eq_z;
let rot_prec = precession_matrix_iau2006(mjd.into());
let [x_mod, y_mod, z_mod] =
rot_prec.apply_array([x_topo.value(), y_topo.value(), z_topo.value()]);
let (dpsi, deps, eps0) = self.nut_cache.get_nutation_rad(mjd);
let rot_nut = self.nut_cache.nutation_rotation(mjd);
let [x_tod, y_tod, z_tod] = rot_nut.apply_array([x_mod, y_mod, z_mod]);
let ra_rad = y_tod.atan2(x_tod);
let r_xy = (x_tod * x_tod + y_tod * y_tod).sqrt();
let dec_rad = z_tod.atan2(r_xy);
let eop = ctx.eop_at(jd);
let jd_ut1 = jd_ut1_from_tt_eop(jd, &eop);
let gast = gast_iau2006(jd_ut1, jd, dpsi, eps0 + deps);
let lst_rad = gast + self.lon_rad;
let ha = (lst_rad.value() - ra_rad).rem_euclid(std::f64::consts::TAU);
let sin_alt = dec_rad.sin() * self.lat.sin() + dec_rad.cos() * self.lat.cos() * ha.cos();
Quantity::<Radian>::new(sin_alt.asin())
}
}
use crate::calculus::math_core::intervals::LabeledCrossing;
use crate::calculus::math_core::root_finding;
use crate::time::{ModifiedJulianDate, Period, MJD};
type Mjd = ModifiedJulianDate;
type Days = qtty::Quantity<qtty::Day>;
const DEDUPE_EPS: Days = Days::new(1e-8);
pub fn find_and_label_crossings<V, F>(
period: Period<MJD>,
step: Days,
f: &F,
threshold: qtty::Quantity<V>,
) -> (Vec<LabeledCrossing>, bool)
where
V: qtty::Unit,
F: Fn(ModifiedJulianDate) -> qtty::Quantity<V>,
{
let g = |t: Mjd| -> qtty::Quantity<V> { f(t) - threshold };
let t_start = period.start;
let t_end = period.end;
let start_val = g(t_start);
let start_above = start_val > 0.0;
let mut labeled = Vec::new();
let mut t = t_start;
let mut prev = start_val;
while t < t_end {
let next_t = (t + step).min(t_end);
let next_v = g(next_t);
if prev.signum() * next_v.signum() < 0.0 {
if let Some(root) =
root_finding::brent_with_values(Period::new(t, next_t), prev, next_v, g)
{
if root >= t_start && root <= t_end {
let direction = if prev < 0.0 { 1 } else { -1 };
labeled.push(LabeledCrossing { t: root, direction });
}
}
}
t = next_t;
prev = next_v;
}
labeled.sort_by(|a, b| a.t.partial_cmp(&b.t).unwrap());
labeled.dedup_by(|a, b| {
let dup = (a.t - b.t).abs() < DEDUPE_EPS;
if dup {
}
dup
});
(labeled, start_above)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::calculus::lunar::moon_altitude_rad;
use crate::observatories::ROQUE_DE_LOS_MUCHACHOS;
use crate::time::JulianDate;
use qtty::Radians;
#[test]
fn chebyshev_position_accuracy() {
let mjd_start: ModifiedJulianDate = JulianDate::J2000.into();
let mjd_end = mjd_start + Days::new(30.0);
let cache = MoonPositionCache::new(mjd_start, mjd_end);
for i in 0..100 {
let mjd = mjd_start + Days::new((i as f64) * 0.3 + 0.1); let (cx, cy, cz) = cache.get_position_km(mjd);
let direct = DefaultEphemeris::moon_geocentric(JulianDate::from(mjd));
let (dx, dy, dz) = (direct.x(), direct.y(), direct.z());
let err = (cx - dx).abs().max((cy - dy).abs()).max((cz - dz).abs());
assert!(
err < Kilometers::new(1.0),
"Chebyshev max-axis error at MJD {mjd}: {err} (x:{} vs {}, y:{} vs {}, z:{} vs {})",
cx,
dx,
cy,
dy,
cz,
dz
);
}
}
#[test]
fn nutation_cache_accuracy() {
let mjd_start: ModifiedJulianDate = JulianDate::J2000.into();
let mjd_end: ModifiedJulianDate = mjd_start + Days::new(30.0);
let cache = NutationCache::new(mjd_start, mjd_end);
for i in 0..100 {
let mjd = mjd_start + Days::new((i as f64) * 0.3 + 0.05);
let (dpsi, deps, eps0) = cache.get_nutation_rad(mjd);
let direct = nutation_iau2000b(mjd.into());
let d_dpsi = direct.dpsi;
let d_deps = direct.deps;
let d_eps0 = direct.mean_obliquity;
let err_dpsi = (dpsi - d_dpsi).abs();
let err_deps = (deps - d_deps).abs();
let err_eps0 = (eps0 - d_eps0).abs();
assert!(
err_dpsi < Radians::new(5e-10),
"Nutation dpsi error at MJD {mjd}: {err_dpsi}"
);
assert!(
err_deps < Radians::new(5e-10),
"Nutation deps error at MJD {mjd}: {err_deps}"
);
assert!(
err_eps0 < Radians::new(5e-10),
"Nutation eps0 error at MJD {mjd}: {err_eps0}"
);
}
}
#[test]
fn cached_altitude_matches_direct() {
let site = ROQUE_DE_LOS_MUCHACHOS;
let mjd_start: ModifiedJulianDate = JulianDate::J2000.into();
let mjd_end = mjd_start + Days::new(7.0);
let ctx = MoonAltitudeContext::new(mjd_start, mjd_end, site);
for i in 0..50 {
let mjd = mjd_start + Days::new((i as f64) * 0.14 + 0.01);
let cached_alt = ctx.altitude_rad(mjd);
let direct_alt = moon_altitude_rad(mjd, &site);
let err_deg = (cached_alt - direct_alt).abs().to::<Degree>();
assert!(
err_deg < Degrees::new(0.02),
"Altitude error at MJD {}: cached={} direct={} err={}",
mjd,
cached_alt.to::<Degree>(),
direct_alt.to::<Degree>(),
err_deg
);
}
}
#[test]
fn find_and_label_crossings_sine_wave() {
let f = |t: Mjd| Radians::new((2.0 * std::f64::consts::PI * (t.value() + 0.05)).sin());
let period = Period::new(Mjd::new(0.0), Mjd::new(1.0));
let step = Days::new(0.01);
let (labeled, _start_above) = find_and_label_crossings(period, step, &f, Radians::new(0.0));
assert_eq!(labeled.len(), 2, "Expected 2 crossings, got {:?}", labeled);
assert_eq!(labeled[0].direction, -1);
assert_eq!(labeled[1].direction, 1);
}
}