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. The backend serves the Sun, the Moon and Mercury through
11//! Pluto, and falls back to other providers when callers request bodies
12//! outside that packaged slice.
13//!
14//! The artifact also carries segments for `asteroid:433-Eros`, fitted to 17
15//! reference rows. They are not served: outside those rows the fit is wrong by
16//! tens of degrees. `PackagedDataBackend` reports the body unsupported, and
17//! `packaged_lookup` refuses it too.
18//!
19//! The packaged artifact stores J2000 ecliptic coordinates directly,
20//! reconstructs equatorial coordinates from the stored channels and
21//! mean-obliquity transform when requested, and adds residual correction
22//! channels on high-curvature spans when they improve the fit. A
23//! maintainer-facing regeneration helper can rebuild the checked-in fixture
24//! from the bundled JPL reference snapshot without introducing any native
25//! tooling. When the `packaged-artifact-path` feature is
26//! enabled, callers can also load an explicit artifact file for larger or
27//! externally distributed packaged datasets. See `docs/time-observer-policy.md`
28//! for the explicit packaged request/lookup-epoch policy, and
29//! `spec/data-compression.md` for the stored-vs-derived artifact contract.
30//!
31//! # Examples
32//!
33//! ```
34//! use pleiades_backend::{CelestialBody, Instant, JulianDay, TimeScale};
35//! use pleiades_data::{packaged_backend, packaged_lookup};
36//!
37//! let _backend = packaged_backend();
38//! let instant = Instant::new(JulianDay::from_days(2_451_545.0), TimeScale::Tt);
39//! let sun = packaged_lookup(&CelestialBody::Sun, instant)
40//!     .expect("Sun should be in the packaged artifact");
41//!
42//! assert!(sun.distance_au.is_some());
43//! ```
44
45#![forbid(unsafe_code)]
46
47use std::sync::OnceLock;
48
49use pleiades_backend::{CelestialBody, CustomBodyId};
50use pleiades_jpl::SnapshotEntry;
51
52mod accuracy_baseline;
53mod backend;
54mod coverage;
55mod data;
56mod lookup;
57mod regenerate;
58pub mod thresholds;
59
60pub use accuracy_baseline::{
61    accuracy_baseline_against, packaged_artifact_accuracy_baseline, BodyChannelError,
62};
63pub use backend::*;
64pub use coverage::*;
65pub use data::*;
66pub use lookup::*;
67pub use regenerate::*;
68
69// Test-only re-exports: bring pub(crate) items into lib.rs scope so that
70// `use super::*` in the tests module can pick them up.
71#[cfg(test)]
72pub(crate) use coverage::{
73    channel_from_fit_samples_with_control_points, distance_channel_from_fit_samples,
74    distance_channel_from_samples, packaged_artifact_body_cadence,
75    packaged_artifact_fit_outlier_sample_fractions, packaged_artifact_fit_sample_fractions,
76    packaged_artifact_fit_sample_fractions_for_body, PackagedArtifactBodyCadence,
77};
78#[cfg(test)]
79pub(crate) use data::PACKAGED_ARTIFACT_FIXTURE;
80#[cfg(test)]
81pub(crate) use lookup::{
82    validate_packaged_artifact_access_summary_line, validate_packaged_artifact_storage_profile,
83    validate_packaged_artifact_storage_summary_line,
84    validate_packaged_frame_treatment_summary_line,
85};
86#[cfg(test)]
87pub(crate) use regenerate::{
88    best_residual_segment,
89    // other functions
90    body_segment_span_limit,
91    chebyshev_lobatto_fractions,
92    coordinates,
93    evaluate_polynomial_channel,
94    packaged_artifact_fit_sample_counts_for_body,
95    packaged_artifact_residual_sample_fractions_for_channel,
96    packaged_artifact_segment_validation_fractions_for_body,
97    packaged_artifact_split_fraction_for_interval,
98    segment_channel_value,
99    segment_error_prefers_candidate,
100    segment_fit_candidate_is_better,
101    segment_from_pair,
102    segment_from_pair_fallback,
103    snapshot_entry_from_ecliptic_coordinates,
104    validate_packaged_artifact_phase1_source_inputs,
105    PackagedArtifactFitCandidateScore,
106    PackagedArtifactSegmentFitError,
107    // structs
108    PackagedArtifactSplitCurvature,
109    PACKAGED_ARTIFACT_DENSE_FIT_SAMPLE_COUNTS,
110    PACKAGED_ARTIFACT_DENSE_RESIDUAL_SAMPLE_FRACTIONS,
111    PACKAGED_ARTIFACT_DENSE_VALIDATION_SAMPLE_FRACTIONS,
112    PACKAGED_ARTIFACT_FOUR_FIFTHS_SPLIT_FRACTION,
113    // split fraction constants
114    PACKAGED_ARTIFACT_LEFT_BIASED_SPLIT_FRACTION,
115    PACKAGED_ARTIFACT_LEFT_EXTREME_SPLIT_FRACTION,
116    PACKAGED_ARTIFACT_MEDIUM_FIT_SAMPLE_COUNTS,
117    PACKAGED_ARTIFACT_MEDIUM_VALIDATION_SAMPLE_FRACTIONS,
118    PACKAGED_ARTIFACT_ONE_EIGHTH_SPLIT_FRACTION,
119    PACKAGED_ARTIFACT_ONE_FIFTH_SPLIT_FRACTION,
120    PACKAGED_ARTIFACT_ONE_NINTH_SPLIT_FRACTION,
121    PACKAGED_ARTIFACT_ONE_SEVENTH_SPLIT_FRACTION,
122    PACKAGED_ARTIFACT_ONE_THIRD_SPLIT_FRACTION,
123    PACKAGED_ARTIFACT_RESIDUAL_SAMPLE_FRACTIONS,
124    PACKAGED_ARTIFACT_RIGHT_BIASED_SPLIT_FRACTION,
125    PACKAGED_ARTIFACT_RIGHT_EXTREME_SPLIT_FRACTION,
126    PACKAGED_ARTIFACT_SEVEN_EIGHTHS_SPLIT_FRACTION,
127    PACKAGED_ARTIFACT_SIX_SEVENTHS_SPLIT_FRACTION,
128};
129// External types needed by tests via `use super::*`
130#[cfg(test)]
131pub(crate) use pleiades_backend::{
132    Apparentness, BackendFamily, CoordinateFrame, EclipticCoordinates, EphemerisBackend,
133    EphemerisErrorKind, EphemerisRequest, Instant, JulianDay, QualityAnnotation, TimeRange,
134    TimeScale, ZodiacMode,
135};
136#[cfg(test)]
137pub(crate) use pleiades_compression::{
138    ArtifactOutput, ArtifactProfile, ChannelKind, CompressedArtifact, EndianPolicy,
139    PolynomialChannel, Segment, SpeedPolicy,
140};
141#[cfg(test)]
142pub(crate) use pleiades_jpl::{
143    production_generation_source_summary_for_report, production_holdout_corpus, reference_snapshot,
144};
145
146const PACKAGE_NAME: &str = "pleiades-data";
147const ARTIFACT_LABEL: &str = "stage-5 packaged-data draft";
148const ARTIFACT_PROFILE_ID: &str = "pleiades-packaged-artifact-profile/stage-5-draft";
149const 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";
150
151pub(crate) fn packaged_artifact_generation_policy_note_text() -> &'static str {
152    static NOTE: OnceLock<String> = OnceLock::new();
153    NOTE.get_or_init(|| {
154        format!(
155            "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, {}",
156            PACKAGED_ARTIFACT_GENERATION_STRATEGY_TAIL
157        )
158    })
159    .as_str()
160}
161
162pub(crate) fn packaged_artifact_source_text() -> &'static str {
163    static SOURCE: OnceLock<String> = OnceLock::new();
164    SOURCE.get_or_init(|| {
165        format!(
166            "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, {}.",
167            PACKAGED_ARTIFACT_GENERATION_STRATEGY_TAIL
168        )
169    })
170    .as_str()
171}
172
173const PACKAGED_BASE_BODIES: [CelestialBody; 10] = [
174    CelestialBody::Sun,
175    CelestialBody::Moon,
176    CelestialBody::Mercury,
177    CelestialBody::Venus,
178    CelestialBody::Mars,
179    CelestialBody::Jupiter,
180    CelestialBody::Saturn,
181    CelestialBody::Uranus,
182    CelestialBody::Neptune,
183    CelestialBody::Pluto,
184];
185
186const PACKAGED_REFERENCE_EPOCH_JD: f64 = 2_451_545.0;
187
188pub(crate) fn packaged_bodies() -> &'static [CelestialBody] {
189    static BODIES: OnceLock<Vec<CelestialBody>> = OnceLock::new();
190    BODIES.get_or_init(|| {
191        let mut bodies = PACKAGED_BASE_BODIES.to_vec();
192        bodies.push(CelestialBody::Custom(CustomBodyId::new(
193            "asteroid", "433-Eros",
194        )));
195        bodies
196    })
197}
198
199/// Whether the artifact carries `body` without the backend serving it.
200///
201/// `asteroid:433-Eros` is fitted to 17 reference rows that lie decades apart
202/// outside one nine-day cluster, so its segments are wrong by tens of degrees
203/// on almost every date (issue #158). The artifact keeps them, so its bytes
204/// and checksum are unchanged, and [`PackagedDataBackend`] declines the body.
205/// The dense-data follow-up (issue #201) removes or replaces the segments.
206pub(crate) fn is_carried_but_unserved(body: &CelestialBody) -> bool {
207    matches!(
208        body,
209        CelestialBody::Custom(id) if id.catalog == "asteroid" && id.designation == "433-Eros"
210    )
211}
212
213/// Returns the per-body release claims for the packaged artifact: every body
214/// the backend serves is release-grade, validated inside the artifact build
215/// against the corpus. A body the artifact carries but the backend does not
216/// serve (`asteroid:433-Eros`, issue #201) has no claim.
217pub fn packaged_body_claims() -> Vec<pleiades_backend::BodyClaim> {
218    use pleiades_backend::{AccuracyClass, BodyClaim, ClaimEvidence};
219    packaged_bodies()
220        .iter()
221        .filter(|body| !is_carried_but_unserved(body))
222        .cloned()
223        .map(|body| {
224            BodyClaim::release_grade(body, AccuracyClass::High, ClaimEvidence::ArtifactValidated)
225        })
226        .collect()
227}
228
229/// Release claims for the derived osculating lunar apsides (True Apogee /
230/// Perigee). These are computed from the packaged Moon state at lookup and
231/// validated against the Swiss Ephemeris `SE_OSCU_APOG` corpus by the
232/// `validate-lilith` gate, so their evidence is `CorpusValidated`.
233pub fn apsis_body_claims() -> Vec<pleiades_backend::BodyClaim> {
234    use pleiades_backend::{AccuracyClass, BodyClaim, ClaimEvidence};
235    let source = "Swiss Ephemeris 2.10.03 SE_OSCU_APOG (validate-lilith)".to_string();
236    vec![
237        BodyClaim::release_grade(
238            CelestialBody::TrueApogee,
239            AccuracyClass::High,
240            ClaimEvidence::CorpusValidated {
241                source: source.clone(),
242            },
243        ),
244        BodyClaim::release_grade(
245            CelestialBody::TruePerigee,
246            AccuracyClass::High,
247            ClaimEvidence::CorpusValidated { source },
248        ),
249    ]
250}
251
252/// Release claim for the derived osculating lunar ascending node
253/// (`TrueNode`). Computed from the packaged Moon state at lookup (formed in
254/// the mean ecliptic of date, emitted in J2000) and validated against the
255/// Swiss Ephemeris `SE_TRUE_NODE` corpus by the `validate-true-node` gate, so
256/// its evidence is `CorpusValidated`. Supersedes, in the routed chart chain,
257/// the `pleiades-elp` Meeus periodic-term approximation (issue #58).
258pub fn true_node_body_claims() -> Vec<pleiades_backend::BodyClaim> {
259    use pleiades_backend::{AccuracyClass, BodyClaim, ClaimEvidence};
260    vec![BodyClaim::release_grade(
261        CelestialBody::TrueNode,
262        AccuracyClass::High,
263        ClaimEvidence::CorpusValidated {
264            source: "Swiss Ephemeris 2.10.03 SE_TRUE_NODE (validate-true-node)".to_string(),
265        },
266    )]
267}
268
269/// Release claims for the mean lunar points (`MeanNode`, `MeanApogee`,
270/// `MeanPerigee`). Formed at lookup from the mean lunar elements in the mean
271/// ecliptic of date (the apsides on the inclined mean orbit, as Swiss
272/// Ephemeris' `SE_MEAN_APOG` is), emitted in J2000, and validated against the
273/// Swiss Ephemeris `SE_MEAN_NODE` / `SE_MEAN_APOG` corpus by the
274/// `validate-mean-lunar-points` gate. Supersede, in the routed chart chain,
275/// the `pleiades-elp` mean-element channels (issue #90).
276pub fn mean_lunar_point_body_claims() -> Vec<pleiades_backend::BodyClaim> {
277    use pleiades_backend::{AccuracyClass, BodyClaim, ClaimEvidence};
278    let source = "Swiss Ephemeris 2.10.03 SE_MEAN_NODE / SE_MEAN_APOG (validate-mean-lunar-points)";
279    [
280        CelestialBody::MeanNode,
281        CelestialBody::MeanApogee,
282        CelestialBody::MeanPerigee,
283    ]
284    .into_iter()
285    .map(|body| {
286        BodyClaim::release_grade(
287            body,
288            AccuracyClass::High,
289            ClaimEvidence::CorpusValidated {
290                source: source.to_string(),
291            },
292        )
293    })
294    .collect()
295}
296
297pub(crate) fn packaged_reference_entry_for_body(
298    snapshot: &[SnapshotEntry],
299    body: &CelestialBody,
300) -> Option<SnapshotEntry> {
301    snapshot
302        .iter()
303        .find(|entry| {
304            entry.body == *body
305                && (entry.epoch.julian_day.days() - PACKAGED_REFERENCE_EPOCH_JD).abs()
306                    < f64::EPSILON
307        })
308        .cloned()
309        .or_else(|| snapshot.iter().find(|entry| entry.body == *body).cloned())
310}
311
312pub(crate) const AU_IN_KM: f64 = 149_597_870.7;
313
314/// Returns the canonical package name for this crate.
315pub const fn package_name() -> &'static str {
316    PACKAGE_NAME
317}
318
319#[cfg(test)]
320mod test_support;
321#[cfg(test)]
322mod tests;