Skip to main content

pleiades_data/
lib.rs

1//! Packaged compressed ephemeris backend for the default 1900-2100 range.
2//!
3//! Wider coverage is available as an opt-in: regenerate the artifact over a
4//! custom window with the `generate-artifact <kernel> --out <path>
5//! [--start --end]` CLI subcommand.
6//!
7//! This crate now ships a small stage-5 draft artifact backed by the
8//! `pleiades-compression` codec. The bundled data is regenerated from the
9//! checked-in JPL reference snapshot and validated against a deterministic
10//! binary fixture that covers the comparison-body planetary set plus the
11//! source-backed custom asteroid `asteroid:433-Eros`, and the backend falls
12//! back to other providers when callers request bodies outside that packaged
13//! slice. The packaged artifact stores J2000 ecliptic coordinates directly,
14//! reconstructs equatorial coordinates from the stored channels and
15//! mean-obliquity transform when requested, and adds residual correction
16//! channels on high-curvature spans when they improve the fit. A
17//! maintainer-facing regeneration helper can rebuild the checked-in fixture
18//! from the bundled JPL reference snapshot without introducing any native
19//! tooling. When the `packaged-artifact-path` feature is
20//! enabled, callers can also load an explicit artifact file for larger or
21//! externally distributed packaged datasets. See `docs/time-observer-policy.md`
22//! for the explicit packaged request/lookup-epoch policy, and
23//! `spec/data-compression.md` for the stored-vs-derived artifact contract.
24//!
25//! # Examples
26//!
27//! ```
28//! use pleiades_backend::{CelestialBody, Instant, JulianDay, TimeScale};
29//! use pleiades_data::{packaged_backend, packaged_body_coverage_summary, packaged_lookup};
30//!
31//! let _backend = packaged_backend();
32//! let instant = Instant::new(JulianDay::from_days(2_451_545.0), TimeScale::Tt);
33//! let sun = packaged_lookup(&CelestialBody::Sun, instant)
34//!     .expect("Sun should be in the packaged artifact");
35//!
36//! assert!(sun.distance_au.is_some());
37//! assert!(packaged_body_coverage_summary().contains("433-Eros"));
38//! ```
39
40#![forbid(unsafe_code)]
41
42use std::sync::OnceLock;
43
44use pleiades_backend::{CelestialBody, CustomBodyId};
45use pleiades_jpl::SnapshotEntry;
46
47mod accuracy_baseline;
48mod backend;
49mod coverage;
50mod data;
51mod lookup;
52mod regenerate;
53pub mod thresholds;
54
55pub use accuracy_baseline::{
56    accuracy_baseline_against, eros_self_consistency_max_longitude_arcsec,
57    packaged_artifact_accuracy_baseline, packaged_artifact_accuracy_baseline_summary_for_report,
58    BodyChannelError,
59};
60pub use backend::*;
61pub use coverage::*;
62pub use data::*;
63pub use lookup::*;
64pub use regenerate::*;
65pub use thresholds::packaged_artifact_thresholds_summary_for_report;
66
67// Test-only re-exports: bring pub(crate) items into lib.rs scope so that
68// `use super::*` in the tests module can pick them up.
69#[cfg(test)]
70pub(crate) use coverage::{
71    channel_from_fit_samples_with_control_points, distance_channel_from_fit_samples,
72    distance_channel_from_samples, packaged_artifact_body_cadence,
73    packaged_artifact_fit_outlier_sample_fractions, packaged_artifact_fit_sample_fractions,
74    packaged_artifact_fit_sample_fractions_for_body, PackagedArtifactBodyCadence,
75};
76#[cfg(test)]
77pub(crate) use data::PACKAGED_ARTIFACT_FIXTURE;
78#[cfg(test)]
79pub(crate) use lookup::{
80    validate_packaged_artifact_access_summary_line, validate_packaged_artifact_storage_profile,
81    validate_packaged_artifact_storage_summary_line,
82    validate_packaged_frame_treatment_summary_line,
83};
84#[cfg(test)]
85pub(crate) use regenerate::{
86    best_residual_segment,
87    // other functions
88    body_segment_span_limit,
89    chebyshev_lobatto_fractions,
90    coordinates,
91    evaluate_polynomial_channel,
92    packaged_artifact_fit_sample_counts_for_body,
93    packaged_artifact_residual_sample_fractions_for_channel,
94    packaged_artifact_segment_validation_fractions_for_body,
95    packaged_artifact_split_fraction_for_interval,
96    segment_channel_value,
97    segment_error_prefers_candidate,
98    segment_fit_candidate_is_better,
99    segment_from_pair,
100    segment_from_pair_fallback,
101    snapshot_entry_from_ecliptic_coordinates,
102    validate_packaged_artifact_phase1_source_inputs,
103    PackagedArtifactFitCandidateScore,
104    PackagedArtifactSegmentFitError,
105    // structs
106    PackagedArtifactSplitCurvature,
107    PACKAGED_ARTIFACT_DENSE_FIT_SAMPLE_COUNTS,
108    PACKAGED_ARTIFACT_DENSE_RESIDUAL_SAMPLE_FRACTIONS,
109    PACKAGED_ARTIFACT_DENSE_VALIDATION_SAMPLE_FRACTIONS,
110    PACKAGED_ARTIFACT_FOUR_FIFTHS_SPLIT_FRACTION,
111    // split fraction constants
112    PACKAGED_ARTIFACT_LEFT_BIASED_SPLIT_FRACTION,
113    PACKAGED_ARTIFACT_LEFT_EXTREME_SPLIT_FRACTION,
114    PACKAGED_ARTIFACT_MEDIUM_FIT_SAMPLE_COUNTS,
115    PACKAGED_ARTIFACT_MEDIUM_VALIDATION_SAMPLE_FRACTIONS,
116    PACKAGED_ARTIFACT_ONE_EIGHTH_SPLIT_FRACTION,
117    PACKAGED_ARTIFACT_ONE_FIFTH_SPLIT_FRACTION,
118    PACKAGED_ARTIFACT_ONE_NINTH_SPLIT_FRACTION,
119    PACKAGED_ARTIFACT_ONE_SEVENTH_SPLIT_FRACTION,
120    PACKAGED_ARTIFACT_ONE_THIRD_SPLIT_FRACTION,
121    PACKAGED_ARTIFACT_RESIDUAL_SAMPLE_FRACTIONS,
122    PACKAGED_ARTIFACT_RIGHT_BIASED_SPLIT_FRACTION,
123    PACKAGED_ARTIFACT_RIGHT_EXTREME_SPLIT_FRACTION,
124    PACKAGED_ARTIFACT_SEVEN_EIGHTHS_SPLIT_FRACTION,
125    PACKAGED_ARTIFACT_SIX_SEVENTHS_SPLIT_FRACTION,
126};
127// External types needed by tests via `use super::*`
128#[cfg(test)]
129pub(crate) use pleiades_backend::{
130    Apparentness, BackendFamily, CoordinateFrame, EclipticCoordinates, EphemerisBackend,
131    EphemerisErrorKind, EphemerisRequest, Instant, JulianDay, QualityAnnotation, TimeRange,
132    TimeScale, ZodiacMode,
133};
134#[cfg(test)]
135pub(crate) use pleiades_compression::{
136    ArtifactOutput, ArtifactProfile, ChannelKind, CompressedArtifact, EndianPolicy,
137    PolynomialChannel, Segment, SpeedPolicy,
138};
139#[cfg(test)]
140pub(crate) use pleiades_jpl::{
141    production_generation_source_summary_for_report, production_holdout_corpus, reference_snapshot,
142    JplSnapshotBackend,
143};
144
145const PACKAGE_NAME: &str = "pleiades-data";
146const ARTIFACT_LABEL: &str = "stage-5 packaged-data draft";
147const ARTIFACT_PROFILE_ID: &str = "pleiades-packaged-artifact-profile/stage-5-draft";
148const PACKAGED_ARTIFACT_GENERATION_STRATEGY_TAIL: &str = "with 8-point and 10-point Chebyshev-Lobatto baseline candidates before the dense body-specific ladders and 12-point and 14-point candidates for inner and outer planets before fallback, with 10-point, 12-point, 14-point, 16-point, 18-point, and 20-point options for luminaries, lunar points, Pluto, selected asteroids, and custom bodies, and the best dense candidate wins before fallback, with equal-error, equal-sample-count ties preferring the simpler segment, residual correction channels on high-curvature spans when they improve the fit, residual-channel combinations and remaining channel-order permutations when composing those channels, preferring the smaller residual footprint on equal-error ties, higher-order reconstruction from fit samples when it quantizes cleanly, shared four-point control-point fallback across longitude, latitude, and distance channels when the higher-order fit does not quantize cleanly, quarter-biased splits on very long dense-body spans when quarter-point curvature is strongly asymmetric, a dense quarter-point control-point lattice before exact-third fallback on irregular spans, one-sixth and five-sixth probe fractions on very long dense-body spans when quarter-point curvature stays balanced, one-third and two-thirds probe fractions on long dense-body spans when quarter-point curvature stays balanced, a dense five-point fallback on the longest dense-body spans when one-fifth through four-fifth samples fit cleanly, a dense seven-point fallback on super-extreme dense-body spans when one-seventh through six-sevenths samples fit cleanly, one-ninth and eight-ninths probe fractions on super-extreme dense-body spans when the finer probes stay balanced, one-eighth and seven-eighths probe fractions on super-extreme dense-body spans when the ninth-point probes stay balanced, one-seventh and six-sevenths probe fractions on extreme dense-body spans when the super-extreme probes stay balanced, one-fifth and four-fifth probe fractions on the longest dense-body spans when the coarser probes stay balanced, and quadratic fallback otherwise";
149
150pub(crate) fn packaged_artifact_generation_policy_note_text() -> &'static str {
151    static NOTE: OnceLock<String> = OnceLock::new();
152    NOTE.get_or_init(|| {
153        format!(
154            "bodies with a single sampled epoch use point segments; bodies with two or more sampled epochs are recursively subdivided into quadratic windows using body-class span caps and measured-fit comparison against the fallback, {}",
155            PACKAGED_ARTIFACT_GENERATION_STRATEGY_TAIL
156        )
157    })
158    .as_str()
159}
160
161pub(crate) fn packaged_artifact_source_text() -> &'static str {
162    static SOURCE: OnceLock<String> = OnceLock::new();
163    SOURCE.get_or_init(|| {
164        format!(
165            "Quantized adjacent same-body quadratic windows with longitude-unwrapped planetary fits, with the comparison-body planetary set densely fit from the JPL de440 kernel over the default 1900-2100 coverage window and the constrained asteroid:433-Eros sourced from its committed reference corpus, with point segments only for single-epoch bodies and recursively subdivided quadratic spans for multi-epoch bodies using body-class span caps and measured-fit comparison against the fallback, {}.",
166            PACKAGED_ARTIFACT_GENERATION_STRATEGY_TAIL
167        )
168    })
169    .as_str()
170}
171
172const PACKAGED_BASE_BODIES: [CelestialBody; 10] = [
173    CelestialBody::Sun,
174    CelestialBody::Moon,
175    CelestialBody::Mercury,
176    CelestialBody::Venus,
177    CelestialBody::Mars,
178    CelestialBody::Jupiter,
179    CelestialBody::Saturn,
180    CelestialBody::Uranus,
181    CelestialBody::Neptune,
182    CelestialBody::Pluto,
183];
184
185const PACKAGED_REFERENCE_EPOCH_JD: f64 = 2_451_545.0;
186
187pub(crate) fn packaged_bodies() -> &'static [CelestialBody] {
188    static BODIES: OnceLock<Vec<CelestialBody>> = OnceLock::new();
189    BODIES.get_or_init(|| {
190        let mut bodies = PACKAGED_BASE_BODIES.to_vec();
191        bodies.push(CelestialBody::Custom(CustomBodyId::new(
192            "asteroid", "433-Eros",
193        )));
194        bodies
195    })
196}
197
198/// Returns the per-body release claims for the packaged artifact: every shipped
199/// body is release-grade, validated inside the artifact build against the corpus.
200pub fn packaged_body_claims() -> Vec<pleiades_backend::BodyClaim> {
201    use pleiades_backend::{AccuracyClass, BodyClaim, ClaimEvidence};
202    packaged_bodies()
203        .iter()
204        .cloned()
205        .map(|body| {
206            BodyClaim::release_grade(body, AccuracyClass::High, ClaimEvidence::ArtifactValidated)
207        })
208        .collect()
209}
210
211/// Release claims for the derived osculating lunar apsides (True Apogee /
212/// Perigee). These are computed from the packaged Moon state at lookup and
213/// validated against the Swiss Ephemeris `SE_OSCU_APOG` corpus by the
214/// `validate-lilith` gate, so their evidence is `CorpusValidated`.
215pub fn apsis_body_claims() -> Vec<pleiades_backend::BodyClaim> {
216    use pleiades_backend::{AccuracyClass, BodyClaim, ClaimEvidence};
217    let source = "Swiss Ephemeris 2.10.03 SE_OSCU_APOG (validate-lilith)".to_string();
218    vec![
219        BodyClaim::release_grade(
220            CelestialBody::TrueApogee,
221            AccuracyClass::High,
222            ClaimEvidence::CorpusValidated {
223                source: source.clone(),
224            },
225        ),
226        BodyClaim::release_grade(
227            CelestialBody::TruePerigee,
228            AccuracyClass::High,
229            ClaimEvidence::CorpusValidated { source },
230        ),
231    ]
232}
233
234pub(crate) fn packaged_reference_entry_for_body(
235    snapshot: &[SnapshotEntry],
236    body: &CelestialBody,
237) -> Option<SnapshotEntry> {
238    snapshot
239        .iter()
240        .find(|entry| {
241            entry.body == *body
242                && (entry.epoch.julian_day.days() - PACKAGED_REFERENCE_EPOCH_JD).abs()
243                    < f64::EPSILON
244        })
245        .cloned()
246        .or_else(|| snapshot.iter().find(|entry| entry.body == *body).cloned())
247}
248
249pub(crate) const AU_IN_KM: f64 = 149_597_870.7;
250
251/// Returns the canonical package name for this crate.
252pub const fn package_name() -> &'static str {
253    PACKAGE_NAME
254}
255
256#[cfg(test)]
257mod test_support;
258#[cfg(test)]
259mod tests;