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_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//! ```
38
39#![forbid(unsafe_code)]
40
41use std::sync::OnceLock;
42
43use pleiades_backend::{CelestialBody, CustomBodyId};
44use pleiades_jpl::SnapshotEntry;
45
46mod accuracy_baseline;
47mod backend;
48mod coverage;
49mod data;
50mod lookup;
51mod regenerate;
52pub mod thresholds;
53
54pub use accuracy_baseline::{
55    accuracy_baseline_against, packaged_artifact_accuracy_baseline, BodyChannelError,
56};
57pub use backend::*;
58pub use coverage::*;
59pub use data::*;
60pub use lookup::*;
61pub use regenerate::*;
62
63// Test-only re-exports: bring pub(crate) items into lib.rs scope so that
64// `use super::*` in the tests module can pick them up.
65#[cfg(test)]
66pub(crate) use coverage::{
67    channel_from_fit_samples_with_control_points, distance_channel_from_fit_samples,
68    distance_channel_from_samples, packaged_artifact_body_cadence,
69    packaged_artifact_fit_outlier_sample_fractions, packaged_artifact_fit_sample_fractions,
70    packaged_artifact_fit_sample_fractions_for_body, PackagedArtifactBodyCadence,
71};
72#[cfg(test)]
73pub(crate) use data::PACKAGED_ARTIFACT_FIXTURE;
74#[cfg(test)]
75pub(crate) use lookup::{
76    validate_packaged_artifact_access_summary_line, validate_packaged_artifact_storage_profile,
77    validate_packaged_artifact_storage_summary_line,
78    validate_packaged_frame_treatment_summary_line,
79};
80#[cfg(test)]
81pub(crate) use regenerate::{
82    best_residual_segment,
83    // other functions
84    body_segment_span_limit,
85    chebyshev_lobatto_fractions,
86    coordinates,
87    evaluate_polynomial_channel,
88    packaged_artifact_fit_sample_counts_for_body,
89    packaged_artifact_residual_sample_fractions_for_channel,
90    packaged_artifact_segment_validation_fractions_for_body,
91    packaged_artifact_split_fraction_for_interval,
92    segment_channel_value,
93    segment_error_prefers_candidate,
94    segment_fit_candidate_is_better,
95    segment_from_pair,
96    segment_from_pair_fallback,
97    snapshot_entry_from_ecliptic_coordinates,
98    validate_packaged_artifact_phase1_source_inputs,
99    PackagedArtifactFitCandidateScore,
100    PackagedArtifactSegmentFitError,
101    // structs
102    PackagedArtifactSplitCurvature,
103    PACKAGED_ARTIFACT_DENSE_FIT_SAMPLE_COUNTS,
104    PACKAGED_ARTIFACT_DENSE_RESIDUAL_SAMPLE_FRACTIONS,
105    PACKAGED_ARTIFACT_DENSE_VALIDATION_SAMPLE_FRACTIONS,
106    PACKAGED_ARTIFACT_FOUR_FIFTHS_SPLIT_FRACTION,
107    // split fraction constants
108    PACKAGED_ARTIFACT_LEFT_BIASED_SPLIT_FRACTION,
109    PACKAGED_ARTIFACT_LEFT_EXTREME_SPLIT_FRACTION,
110    PACKAGED_ARTIFACT_MEDIUM_FIT_SAMPLE_COUNTS,
111    PACKAGED_ARTIFACT_MEDIUM_VALIDATION_SAMPLE_FRACTIONS,
112    PACKAGED_ARTIFACT_ONE_EIGHTH_SPLIT_FRACTION,
113    PACKAGED_ARTIFACT_ONE_FIFTH_SPLIT_FRACTION,
114    PACKAGED_ARTIFACT_ONE_NINTH_SPLIT_FRACTION,
115    PACKAGED_ARTIFACT_ONE_SEVENTH_SPLIT_FRACTION,
116    PACKAGED_ARTIFACT_ONE_THIRD_SPLIT_FRACTION,
117    PACKAGED_ARTIFACT_RESIDUAL_SAMPLE_FRACTIONS,
118    PACKAGED_ARTIFACT_RIGHT_BIASED_SPLIT_FRACTION,
119    PACKAGED_ARTIFACT_RIGHT_EXTREME_SPLIT_FRACTION,
120    PACKAGED_ARTIFACT_SEVEN_EIGHTHS_SPLIT_FRACTION,
121    PACKAGED_ARTIFACT_SIX_SEVENTHS_SPLIT_FRACTION,
122};
123// External types needed by tests via `use super::*`
124#[cfg(test)]
125pub(crate) use pleiades_backend::{
126    Apparentness, BackendFamily, CoordinateFrame, EclipticCoordinates, EphemerisBackend,
127    EphemerisErrorKind, EphemerisRequest, Instant, JulianDay, QualityAnnotation, TimeRange,
128    TimeScale, ZodiacMode,
129};
130#[cfg(test)]
131pub(crate) use pleiades_compression::{
132    ArtifactOutput, ArtifactProfile, ChannelKind, CompressedArtifact, EndianPolicy,
133    PolynomialChannel, Segment, SpeedPolicy,
134};
135#[cfg(test)]
136pub(crate) use pleiades_jpl::{
137    production_generation_source_summary_for_report, production_holdout_corpus, reference_snapshot,
138    JplSnapshotBackend,
139};
140
141const PACKAGE_NAME: &str = "pleiades-data";
142const ARTIFACT_LABEL: &str = "stage-5 packaged-data draft";
143const ARTIFACT_PROFILE_ID: &str = "pleiades-packaged-artifact-profile/stage-5-draft";
144const 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";
145
146pub(crate) fn packaged_artifact_generation_policy_note_text() -> &'static str {
147    static NOTE: OnceLock<String> = OnceLock::new();
148    NOTE.get_or_init(|| {
149        format!(
150            "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, {}",
151            PACKAGED_ARTIFACT_GENERATION_STRATEGY_TAIL
152        )
153    })
154    .as_str()
155}
156
157pub(crate) fn packaged_artifact_source_text() -> &'static str {
158    static SOURCE: OnceLock<String> = OnceLock::new();
159    SOURCE.get_or_init(|| {
160        format!(
161            "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, {}.",
162            PACKAGED_ARTIFACT_GENERATION_STRATEGY_TAIL
163        )
164    })
165    .as_str()
166}
167
168const PACKAGED_BASE_BODIES: [CelestialBody; 10] = [
169    CelestialBody::Sun,
170    CelestialBody::Moon,
171    CelestialBody::Mercury,
172    CelestialBody::Venus,
173    CelestialBody::Mars,
174    CelestialBody::Jupiter,
175    CelestialBody::Saturn,
176    CelestialBody::Uranus,
177    CelestialBody::Neptune,
178    CelestialBody::Pluto,
179];
180
181const PACKAGED_REFERENCE_EPOCH_JD: f64 = 2_451_545.0;
182
183pub(crate) fn packaged_bodies() -> &'static [CelestialBody] {
184    static BODIES: OnceLock<Vec<CelestialBody>> = OnceLock::new();
185    BODIES.get_or_init(|| {
186        let mut bodies = PACKAGED_BASE_BODIES.to_vec();
187        bodies.push(CelestialBody::Custom(CustomBodyId::new(
188            "asteroid", "433-Eros",
189        )));
190        bodies
191    })
192}
193
194/// Returns the per-body release claims for the packaged artifact: every shipped
195/// body is release-grade, validated inside the artifact build against the corpus.
196pub fn packaged_body_claims() -> Vec<pleiades_backend::BodyClaim> {
197    use pleiades_backend::{AccuracyClass, BodyClaim, ClaimEvidence};
198    packaged_bodies()
199        .iter()
200        .cloned()
201        .map(|body| {
202            BodyClaim::release_grade(body, AccuracyClass::High, ClaimEvidence::ArtifactValidated)
203        })
204        .collect()
205}
206
207/// Release claims for the derived osculating lunar apsides (True Apogee /
208/// Perigee). These are computed from the packaged Moon state at lookup and
209/// validated against the Swiss Ephemeris `SE_OSCU_APOG` corpus by the
210/// `validate-lilith` gate, so their evidence is `CorpusValidated`.
211pub fn apsis_body_claims() -> Vec<pleiades_backend::BodyClaim> {
212    use pleiades_backend::{AccuracyClass, BodyClaim, ClaimEvidence};
213    let source = "Swiss Ephemeris 2.10.03 SE_OSCU_APOG (validate-lilith)".to_string();
214    vec![
215        BodyClaim::release_grade(
216            CelestialBody::TrueApogee,
217            AccuracyClass::High,
218            ClaimEvidence::CorpusValidated {
219                source: source.clone(),
220            },
221        ),
222        BodyClaim::release_grade(
223            CelestialBody::TruePerigee,
224            AccuracyClass::High,
225            ClaimEvidence::CorpusValidated { source },
226        ),
227    ]
228}
229
230/// Release claim for the derived osculating lunar ascending node
231/// (`TrueNode`). Computed from the packaged Moon state at lookup (formed in
232/// the mean ecliptic of date, emitted in J2000) and validated against the
233/// Swiss Ephemeris `SE_TRUE_NODE` corpus by the `validate-true-node` gate, so
234/// its evidence is `CorpusValidated`. Supersedes, in the routed chart chain,
235/// the `pleiades-elp` Meeus periodic-term approximation (issue #58).
236pub fn true_node_body_claims() -> Vec<pleiades_backend::BodyClaim> {
237    use pleiades_backend::{AccuracyClass, BodyClaim, ClaimEvidence};
238    vec![BodyClaim::release_grade(
239        CelestialBody::TrueNode,
240        AccuracyClass::High,
241        ClaimEvidence::CorpusValidated {
242            source: "Swiss Ephemeris 2.10.03 SE_TRUE_NODE (validate-true-node)".to_string(),
243        },
244    )]
245}
246
247pub(crate) fn packaged_reference_entry_for_body(
248    snapshot: &[SnapshotEntry],
249    body: &CelestialBody,
250) -> Option<SnapshotEntry> {
251    snapshot
252        .iter()
253        .find(|entry| {
254            entry.body == *body
255                && (entry.epoch.julian_day.days() - PACKAGED_REFERENCE_EPOCH_JD).abs()
256                    < f64::EPSILON
257        })
258        .cloned()
259        .or_else(|| snapshot.iter().find(|entry| entry.body == *body).cloned())
260}
261
262pub(crate) const AU_IN_KM: f64 = 149_597_870.7;
263
264/// Returns the canonical package name for this crate.
265pub const fn package_name() -> &'static str {
266    PACKAGE_NAME
267}
268
269#[cfg(test)]
270mod test_support;
271#[cfg(test)]
272mod tests;