pub mod fit;
pub mod solver;
mod embedded;
use crate::coordinates::cartesian::Position;
use crate::coordinates::centers::Barycentric;
use crate::coordinates::frames::EclipticMeanJ2000;
use crate::ephemeris::EphemerisError;
use crate::qtty::{AstronomicalUnit, Days, Kilometer, Kilometers, Meters, Second};
use crate::time::JulianDate;
const SECONDS_PER_DAY: f64 = crate::qtty::time::SECONDS_PER_DAY;
const J2000_JD: f64 = tempoch::J2000_JD_TT_DAY.value();
pub const FIT_FROM_JD: f64 = 2_415_020.5;
pub const FIT_TO_JD: f64 = 2_488_070.5;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SunEarthLagrangePoint {
L1,
L2,
L3,
L4,
L5,
}
impl SunEarthLagrangePoint {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::L1 => "L1",
Self::L2 => "L2",
Self::L3 => "L3",
Self::L4 => "L4",
Self::L5 => "L5",
}
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct LagrangeMetadata {
pub source: &'static str,
pub valid_from: Days,
pub valid_to: Days,
pub frame_name: &'static str,
pub time_scale_name: &'static str,
pub length_unit_name: &'static str,
pub block: Second,
pub validation_step: Second,
pub max_abs_error: Meters,
pub generator_version: &'static str,
pub generated_at: &'static str,
pub checksum: &'static str,
}
pub const SUN_EARTH_LAGRANGE_METADATA: LagrangeMetadata = LagrangeMetadata {
source: "siderust-analytic-sun-earth-v1",
valid_from: Days::new(2_415_020.5),
valid_to: Days::new(2_488_070.5),
frame_name: "EclipticMeanJ2000",
time_scale_name: "TDB-compatible Julian Date",
length_unit_name: "Kilometer",
block: Second::new(32.0 * SECONDS_PER_DAY),
validation_step: Second::new(6.0 * 3_600.0),
max_abs_error: Meters::new(1.121),
generator_version: "siderust-analytic-sun-earth-v1",
generated_at: "2026-05-24",
checksum: embedded::CHECKSUM,
};
pub fn try_sun_earth_lagrange_barycentric(
point: SunEarthLagrangePoint,
jd: JulianDate,
) -> Result<Position<Barycentric, EclipticMeanJ2000, AstronomicalUnit>, EphemerisError> {
evaluate_embedded(point, jd)
}
pub fn sun_earth_lagrange_barycentric(
point: SunEarthLagrangePoint,
jd: JulianDate,
) -> Position<Barycentric, EclipticMeanJ2000, AstronomicalUnit> {
try_sun_earth_lagrange_barycentric(point, jd)
.expect("Sun-Earth Lagrange archive epoch outside coverage")
}
pub(crate) fn evaluate_records(
records: &[f64],
ncoeff: usize,
jd: JulianDate,
) -> Result<Position<Barycentric, EclipticMeanJ2000, AstronomicalUnit>, EphemerisError> {
let record_len = 2 + 3 * ncoeff;
let jd_value = jd.raw().value();
if records.is_empty()
|| ncoeff == 0
|| record_len == 2
|| !records.len().is_multiple_of(record_len)
{
return Err(EphemerisError::OutOfRange {
jd: jd_value,
start_jd: SUN_EARTH_LAGRANGE_METADATA.valid_from.value(),
end_jd: SUN_EARTH_LAGRANGE_METADATA.valid_to.value(),
});
}
let seconds = (jd_value - J2000_JD) * SECONDS_PER_DAY;
let mut first = f64::INFINITY;
let mut last = f64::NEG_INFINITY;
for record in records.chunks_exact(record_len) {
let start = record[0] - record[1];
let end = record[0] + record[1];
first = first.min(start);
last = last.max(end);
if seconds >= start && seconds <= end {
let tau = (seconds - record[0]) / record[1];
let cx = &record[2..2 + ncoeff];
let cy = &record[2 + ncoeff..2 + 2 * ncoeff];
let cz = &record[2 + 2 * ncoeff..2 + 3 * ncoeff];
let km = Position::<Barycentric, EclipticMeanJ2000, Kilometer>::new(
Kilometers::new(cheby::evaluate(cx, tau)),
Kilometers::new(cheby::evaluate(cy, tau)),
Kilometers::new(cheby::evaluate(cz, tau)),
);
return Ok(km.to_unit::<AstronomicalUnit>());
}
}
Err(EphemerisError::OutOfRange {
jd: jd_value,
start_jd: J2000_JD + first / SECONDS_PER_DAY,
end_jd: J2000_JD + last / SECONDS_PER_DAY,
})
}
fn evaluate_embedded(
point: SunEarthLagrangePoint,
jd: JulianDate,
) -> Result<Position<Barycentric, EclipticMeanJ2000, AstronomicalUnit>, EphemerisError> {
evaluate_records(records_for(point), embedded::NCOEFF, jd)
}
fn records_for(point: SunEarthLagrangePoint) -> &'static [f64] {
match point {
SunEarthLagrangePoint::L1 => embedded::records_l1(),
SunEarthLagrangePoint::L2 => embedded::records_l2(),
SunEarthLagrangePoint::L3 => embedded::records_l3(),
SunEarthLagrangePoint::L4 => embedded::records_l4(),
SunEarthLagrangePoint::L5 => embedded::records_l5(),
}
}
pub(crate) fn fallback_or_solve<Eph: crate::ephemeris::Ephemeris>(
point: SunEarthLagrangePoint,
jd: JulianDate,
) -> Position<Barycentric, EclipticMeanJ2000, AstronomicalUnit> {
try_sun_earth_lagrange_barycentric(point, jd).unwrap_or_else(|_| {
solver::solve_sun_earth_lagrange::<Eph>(point, jd)
.expect("Sun-Earth Lagrange solver failed")
.position
.to_unit::<AstronomicalUnit>()
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::time::J2000;
#[test]
fn embedded_archive_evaluates_j2000() {
let pos = try_sun_earth_lagrange_barycentric(SunEarthLagrangePoint::L1, J2000)
.expect("generated archive covers J2000");
assert!(pos.x().value().is_finite());
assert!(pos.y().value().is_finite());
assert!(pos.z().value().is_finite());
}
#[test]
fn point_labels_are_stable() {
assert_eq!(SunEarthLagrangePoint::L5.label(), "L5");
}
#[test]
fn astronomical_units_constructor_is_available() {
let zero = crate::qtty::AstronomicalUnits::new(0.0);
assert_eq!(zero.value(), 0.0);
}
}