Skip to main content

pleiades_data/
backend.rs

1use std::sync::Arc;
2
3#[cfg(feature = "packaged-artifact-path")]
4use std::path::Path;
5
6use pleiades_apsides::{apsides, MU_EARTH_MOON_AU3_PER_DAY2};
7use pleiades_backend::{
8    validate_observer_policy, validate_request_policy, validate_zodiac_policy, AccuracyClass,
9    BackendCapabilities, BackendFamily, BackendId, BackendMetadata, BackendProvenance,
10    CelestialBody, CoordinateFrame, EclipticCoordinates, EphemerisBackend, EphemerisError,
11    EphemerisErrorKind, EphemerisRequest, EphemerisResult, Instant, JulianDay, Latitude, Longitude,
12    Motion, QualityAnnotation, TimeScale, ZodiacMode,
13};
14use pleiades_compression::{spherical_state_to_cartesian, CompressedArtifact, SphericalState};
15
16use crate::coverage::packaged_body_coverage_summary;
17#[cfg(feature = "packaged-artifact-path")]
18use crate::data::packaged_artifact_from_path;
19use crate::data::{packaged_artifact, packaged_artifact_from_bytes, PackagedArtifactLoadError};
20use crate::lookup::{
21    packaged_artifact_access_summary_for_report, packaged_artifact_storage_summary_for_report,
22    packaged_frame_treatment_summary_for_report, packaged_request_policy_summary_for_report,
23};
24use crate::regenerate::{artifact_time_range, map_artifact_error, normalize_lookup_instant};
25use crate::PACKAGE_NAME;
26
27/// A packaged compressed-data backend.
28#[derive(Debug, Clone)]
29pub struct PackagedDataBackend {
30    artifact: Arc<CompressedArtifact>,
31}
32
33impl Default for PackagedDataBackend {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39impl PackagedDataBackend {
40    /// Creates a new packaged-data backend backed by the checked-in fixture.
41    pub fn new() -> Self {
42        Self::from_artifact(packaged_artifact().clone())
43    }
44
45    /// Creates a packaged-data backend from an explicit artifact.
46    pub fn from_artifact(artifact: CompressedArtifact) -> Self {
47        Self {
48            artifact: Arc::new(artifact),
49        }
50    }
51
52    /// Creates a packaged-data backend from decoded artifact bytes.
53    ///
54    /// See [`crate::packaged_backend_from_bytes`] for an end-to-end example that
55    /// encodes the checked-in artifact and reloads it through this constructor.
56    pub fn from_bytes(bytes: &[u8]) -> Result<Self, PackagedArtifactLoadError> {
57        Ok(Self::from_artifact(
58            packaged_artifact_from_bytes(bytes).map_err(PackagedArtifactLoadError::Decode)?,
59        ))
60    }
61
62    #[cfg(feature = "packaged-artifact-path")]
63    /// Creates a packaged-data backend from an artifact file.
64    ///
65    /// See [`crate::packaged_backend_from_path`] for an end-to-end example that writes
66    /// the checked-in artifact to a temporary file and reloads it through this
67    /// constructor.
68    pub fn from_path(path: impl AsRef<Path>) -> Result<Self, PackagedArtifactLoadError> {
69        Ok(Self::from_artifact(packaged_artifact_from_path(path)?))
70    }
71
72    fn artifact(&self) -> &CompressedArtifact {
73        &self.artifact
74    }
75
76    fn osculating_apsis_position(
77        &self,
78        req: &EphemerisRequest,
79    ) -> Result<EphemerisResult, EphemerisError> {
80        let ecliptic = self.osculating_apsis_ecliptic(&req.body, req.instant)?;
81        let equatorial = ecliptic.to_equatorial(req.instant.mean_obliquity());
82        let motion = self.osculating_apsis_motion(&req.body, req.instant)?;
83
84        let mut result = EphemerisResult::new(
85            BackendId::new(PACKAGE_NAME),
86            req.body.clone(),
87            req.instant,
88            req.frame,
89            req.zodiac_mode.clone(),
90            req.apparent,
91        );
92        result.ecliptic = Some(ecliptic);
93        result.equatorial = Some(equatorial);
94        result.motion = Some(motion);
95        result.quality = QualityAnnotation::Interpolated;
96        Ok(result)
97    }
98
99    fn osculating_apsis_ecliptic(
100        &self,
101        body: &CelestialBody,
102        instant: Instant,
103    ) -> Result<EclipticCoordinates, EphemerisError> {
104        let li = normalize_lookup_instant(instant);
105        let ecl = self
106            .artifact
107            .lookup_ecliptic(&CelestialBody::Moon, li)
108            .map_err(map_artifact_error)?;
109        let mot = self
110            .artifact
111            .lookup_motion(&CelestialBody::Moon, li)
112            .map_err(map_artifact_error)?;
113        let dist = ecl.distance_au.ok_or_else(|| {
114            EphemerisError::new(
115                EphemerisErrorKind::InvalidRequest,
116                "packaged Moon lacks distance for osculating apsis",
117            )
118        })?;
119        let state = SphericalState {
120            lon_rad: ecl.longitude.degrees().to_radians(),
121            lat_rad: ecl.latitude.degrees().to_radians(),
122            dist_au: dist,
123            lon_rate_rad_per_day: mot.longitude_deg_per_day.unwrap_or(0.0).to_radians(),
124            lat_rate_rad_per_day: mot.latitude_deg_per_day.unwrap_or(0.0).to_radians(),
125            dist_rate_au_per_day: mot.distance_au_per_day.unwrap_or(0.0),
126        };
127        let cart = spherical_state_to_cartesian(state);
128        let aps = apsides(cart.pos_au, cart.vel_au_per_day, MU_EARTH_MOON_AU3_PER_DAY2).map_err(
129            |_| {
130                EphemerisError::new(
131                    EphemerisErrorKind::InvalidRequest,
132                    "osculating apsis undefined for the lunar state at this instant",
133                )
134            },
135        )?;
136        let point = match body {
137            CelestialBody::TrueApogee => aps.apogee,
138            CelestialBody::TruePerigee => aps.perigee,
139            _ => {
140                return Err(EphemerisError::new(
141                    EphemerisErrorKind::InvalidRequest,
142                    "not an osculating-apsis body",
143                ))
144            }
145        };
146        Ok(EclipticCoordinates::new(
147            Longitude::from_degrees(point.longitude_deg),
148            Latitude::from_degrees(point.latitude_deg),
149            Some(point.distance_au),
150        ))
151    }
152
153    fn osculating_apsis_motion(
154        &self,
155        body: &CelestialBody,
156        instant: Instant,
157    ) -> Result<Motion, EphemerisError> {
158        const HALF_SPAN_DAYS: f64 = 0.5;
159        let shift = |days: f64| {
160            Instant::new(
161                JulianDay::from_days(instant.julian_day.days() + days),
162                instant.scale,
163            )
164        };
165        let before = match self.osculating_apsis_ecliptic(body, shift(-HALF_SPAN_DAYS)) {
166            Ok(e) => e,
167            Err(ref e) if e.kind == EphemerisErrorKind::OutOfRangeInstant => {
168                return Ok(Motion::new(None, None, None));
169            }
170            Err(e) => return Err(e),
171        };
172        let after = match self.osculating_apsis_ecliptic(body, shift(HALF_SPAN_DAYS)) {
173            Ok(e) => e,
174            Err(ref e) if e.kind == EphemerisErrorKind::OutOfRangeInstant => {
175                return Ok(Motion::new(None, None, None));
176            }
177            Err(e) => return Err(e),
178        };
179        let span = 2.0 * HALF_SPAN_DAYS;
180
181        let mut dlon = after.longitude.degrees() - before.longitude.degrees();
182        while dlon > 180.0 {
183            dlon -= 360.0;
184        }
185        while dlon < -180.0 {
186            dlon += 360.0;
187        }
188        let dlon_per_day = dlon / span;
189        let dlat_per_day = (after.latitude.degrees() - before.latitude.degrees()) / span;
190        let ddist_per_day = match (before.distance_au, after.distance_au) {
191            (Some(b), Some(a)) => Some((a - b) / span),
192            _ => None,
193        };
194        Ok(Motion::new(
195            Some(dlon_per_day),
196            Some(dlat_per_day),
197            ddist_per_day,
198        ))
199    }
200}
201
202impl EphemerisBackend for PackagedDataBackend {
203    fn metadata(&self) -> BackendMetadata {
204        let artifact = self.artifact();
205        let bodies = artifact
206            .bodies
207            .iter()
208            .map(|series| series.body.clone())
209            .collect::<Vec<_>>();
210        let range = artifact_time_range(artifact);
211
212        BackendMetadata {
213            id: BackendId::new(PACKAGE_NAME),
214            version: format!(
215                "{} checksum:{:016x}",
216                artifact.header.version, artifact.checksum
217            ),
218            family: BackendFamily::CompressedData,
219            provenance: BackendProvenance {
220                summary: artifact.header.source.clone(),
221                data_sources: vec![
222                    packaged_body_coverage_summary(),
223                    packaged_request_policy_summary_for_report(),
224                    packaged_frame_treatment_summary_for_report(),
225                    packaged_artifact_storage_summary_for_report(),
226                    packaged_artifact_access_summary_for_report(),
227                ],
228            },
229            nominal_range: range,
230            supported_time_scales: vec![TimeScale::Tt, TimeScale::Tdb],
231            body_claims: {
232                let declared = crate::packaged_body_claims();
233                let mut claims: Vec<_> = bodies
234                    .iter()
235                    .map(|body| {
236                        declared
237                            .iter()
238                            .find(|c| &c.body == body)
239                            .cloned()
240                            .unwrap_or_else(|| pleiades_backend::BodyClaim::from(body.clone()))
241                    })
242                    .collect();
243                claims.extend(crate::apsis_body_claims());
244                claims
245            },
246            supported_frames: vec![CoordinateFrame::Ecliptic, CoordinateFrame::Equatorial],
247            capabilities: BackendCapabilities {
248                geocentric: true,
249                topocentric: false,
250                apparent: false,
251                mean: true,
252                batch: true,
253                native_sidereal: false,
254            },
255            accuracy: AccuracyClass::Approximate,
256            deterministic: true,
257            offline: true,
258        }
259    }
260
261    fn supports_body(&self, body: CelestialBody) -> bool {
262        matches!(body, CelestialBody::TrueApogee | CelestialBody::TruePerigee)
263            || self
264                .artifact
265                .bodies
266                .iter()
267                .any(|series| series.body == body)
268    }
269
270    fn position(&self, req: &EphemerisRequest) -> Result<EphemerisResult, EphemerisError> {
271        if !matches!(req.instant.scale, TimeScale::Tt | TimeScale::Tdb) {
272            return Err(EphemerisError::new(
273                EphemerisErrorKind::UnsupportedTimeScale,
274                "packaged data only supports TT or TDB requests",
275            ));
276        }
277
278        validate_request_policy(
279            req,
280            "packaged data",
281            &[TimeScale::Tt, TimeScale::Tdb],
282            &[CoordinateFrame::Ecliptic, CoordinateFrame::Equatorial],
283            true,
284            false,
285        )?;
286
287        validate_zodiac_policy(req, "packaged data", &[ZodiacMode::Tropical])?;
288
289        validate_observer_policy(req, "packaged data", false)?;
290
291        if matches!(
292            req.body,
293            CelestialBody::TrueApogee | CelestialBody::TruePerigee
294        ) {
295            return self.osculating_apsis_position(req);
296        }
297
298        let lookup_instant = normalize_lookup_instant(req.instant);
299        let ecliptic = self
300            .artifact
301            .lookup_ecliptic(&req.body, lookup_instant)
302            .map_err(map_artifact_error)?;
303        let equatorial = ecliptic.to_equatorial(req.instant.mean_obliquity());
304        let motion = self
305            .artifact
306            .lookup_motion(&req.body, lookup_instant)
307            .map_err(map_artifact_error)?;
308
309        let mut result = EphemerisResult::new(
310            BackendId::new(PACKAGE_NAME),
311            req.body.clone(),
312            req.instant,
313            req.frame,
314            req.zodiac_mode.clone(),
315            req.apparent,
316        );
317        result.ecliptic = Some(ecliptic);
318        result.equatorial = Some(equatorial);
319        result.motion = Some(motion);
320        result.quality = QualityAnnotation::Interpolated;
321        Ok(result)
322    }
323}