1use std::fmt;
2use std::sync::OnceLock;
3
4#[cfg(feature = "packaged-artifact-path")]
5use std::path::Path;
6
7use pleiades_backend::{
8 Apparentness, CelestialBody, CoordinateFrame, EclipticCoordinates, EphemerisBackend,
9 EphemerisRequest, Instant, QualityAnnotation, TimeScale, ZodiacMode,
10};
11use pleiades_compression::{
12 join_display, ArtifactOutput, ArtifactProfile, ChannelKind, CompressedArtifact,
13};
14use pleiades_jpl::{reference_snapshot, SnapshotEntry};
15
16use crate::backend::PackagedDataBackend;
17use crate::coverage::packaged_artifact_profile_summary_details;
18use crate::data::{packaged_artifact, PackagedArtifactLoadError};
19use crate::regenerate::normalize_lookup_instant;
20use crate::{packaged_bodies, packaged_reference_entry_for_body};
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum PackagedLookupEpochPolicy {
25 RetagToTtGridWithoutRelativisticCorrection,
27}
28
29impl PackagedLookupEpochPolicy {
30 pub const fn label(self) -> &'static str {
32 match self {
33 Self::RetagToTtGridWithoutRelativisticCorrection => {
34 "TT-grid retag without relativistic correction"
35 }
36 }
37 }
38
39 pub const fn note(self) -> &'static str {
41 match self {
42 Self::RetagToTtGridWithoutRelativisticCorrection => {
43 "TDB lookup epochs are re-tagged onto the TT grid without applying a relativistic correction"
44 }
45 }
46 }
47
48 pub fn validate(self) -> Result<(), PackagedLookupEpochPolicyValidationError> {
50 if self != Self::RetagToTtGridWithoutRelativisticCorrection {
51 return Err(PackagedLookupEpochPolicyValidationError::FieldOutOfSync {
52 field: "policy",
53 });
54 }
55
56 Ok(())
57 }
58
59 pub fn summary_line(self) -> String {
61 format!("{}; {}", self.label(), self.note())
62 }
63}
64
65impl fmt::Display for PackagedLookupEpochPolicy {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 f.write_str(self.label())
68 }
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
73pub enum PackagedLookupEpochPolicyValidationError {
74 FieldOutOfSync { field: &'static str },
76}
77
78impl fmt::Display for PackagedLookupEpochPolicyValidationError {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 Self::FieldOutOfSync { field } => write!(
82 f,
83 "the packaged lookup-epoch policy field `{field}` is out of sync with the current posture"
84 ),
85 }
86 }
87}
88
89impl std::error::Error for PackagedLookupEpochPolicyValidationError {}
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub struct PackagedLookupEpochPolicySummary {
94 pub policy: PackagedLookupEpochPolicy,
96}
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
100pub enum PackagedLookupEpochPolicySummaryValidationError {
101 FieldOutOfSync { field: &'static str },
103}
104
105impl fmt::Display for PackagedLookupEpochPolicySummaryValidationError {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 match self {
108 Self::FieldOutOfSync { field } => write!(
109 f,
110 "the packaged lookup-epoch policy summary field `{field}` is out of sync with the current posture"
111 ),
112 }
113 }
114}
115
116impl std::error::Error for PackagedLookupEpochPolicySummaryValidationError {}
117
118impl PackagedLookupEpochPolicySummary {
119 pub fn summary_line(self) -> String {
121 self.policy.summary_line()
122 }
123
124 pub fn validate(&self) -> Result<(), PackagedLookupEpochPolicySummaryValidationError> {
126 self.policy.validate().map_err(|error| match error {
127 PackagedLookupEpochPolicyValidationError::FieldOutOfSync { field } => {
128 PackagedLookupEpochPolicySummaryValidationError::FieldOutOfSync { field }
129 }
130 })
131 }
132}
133
134impl fmt::Display for PackagedLookupEpochPolicySummary {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 f.write_str(&self.summary_line())
137 }
138}
139
140const PACKAGED_LOOKUP_EPOCH_POLICY_SUMMARY: PackagedLookupEpochPolicySummary =
141 PackagedLookupEpochPolicySummary {
142 policy: PackagedLookupEpochPolicy::RetagToTtGridWithoutRelativisticCorrection,
143 };
144
145pub fn packaged_lookup_epoch_policy_summary_details() -> PackagedLookupEpochPolicySummary {
160 let summary = PACKAGED_LOOKUP_EPOCH_POLICY_SUMMARY;
161 debug_assert!(summary.validate().is_ok());
162 summary
163}
164
165pub fn packaged_lookup_epoch_policy_summary() -> &'static str {
167 static SUMMARY: OnceLock<String> = OnceLock::new();
168 SUMMARY
169 .get_or_init(|| {
170 let summary = packaged_lookup_epoch_policy_summary_details();
171 match summary.validate() {
172 Ok(()) => summary.to_string(),
173 Err(error) => format!("Packaged lookup epoch policy: unavailable ({error})"),
174 }
175 })
176 .as_str()
177}
178
179#[derive(Clone, Copy, Debug, PartialEq)]
181pub struct PackagedRequestPolicySummary {
182 pub geocentric_only: bool,
184 pub supported_frames: &'static [CoordinateFrame],
186 pub supported_time_scales: &'static [TimeScale],
188 pub supported_zodiac_modes: &'static [ZodiacMode],
190 pub supported_apparentness: &'static [Apparentness],
192 pub supports_topocentric_observer: bool,
194 pub lookup_epoch_policy: PackagedLookupEpochPolicy,
196}
197
198#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
200pub enum PackagedRequestPolicySummaryValidationError {
201 FieldOutOfSync { field: &'static str },
203}
204
205impl fmt::Display for PackagedRequestPolicySummaryValidationError {
206 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207 match self {
208 Self::FieldOutOfSync { field } => write!(
209 f,
210 "the packaged request-policy summary field `{field}` is out of sync with the current posture"
211 ),
212 }
213 }
214}
215
216impl std::error::Error for PackagedRequestPolicySummaryValidationError {}
217
218impl PackagedRequestPolicySummary {
219 pub fn summary_line(&self) -> String {
221 format!(
222 "Packaged request policy: {}frames={}; time scales={}; zodiac modes={}; apparentness={}; topocentric observer={}; lookup epoch policy={}",
223 if self.geocentric_only {
224 "geocentric-only; "
225 } else {
226 ""
227 },
228 join_display(self.supported_frames),
229 join_display(self.supported_time_scales),
230 join_display(self.supported_zodiac_modes),
231 join_display(self.supported_apparentness),
232 self.supports_topocentric_observer,
233 self.lookup_epoch_policy.summary_line(),
234 )
235 }
236
237 pub fn validate(&self) -> Result<(), PackagedRequestPolicySummaryValidationError> {
239 if !self.geocentric_only {
240 return Err(
241 PackagedRequestPolicySummaryValidationError::FieldOutOfSync {
242 field: "geocentric_only",
243 },
244 );
245 }
246 if self.supported_frames != [CoordinateFrame::Ecliptic, CoordinateFrame::Equatorial] {
247 return Err(
248 PackagedRequestPolicySummaryValidationError::FieldOutOfSync {
249 field: "supported_frames",
250 },
251 );
252 }
253 if self.supported_time_scales != [TimeScale::Tt, TimeScale::Tdb] {
254 return Err(
255 PackagedRequestPolicySummaryValidationError::FieldOutOfSync {
256 field: "supported_time_scales",
257 },
258 );
259 }
260 if self.supported_zodiac_modes != [ZodiacMode::Tropical] {
261 return Err(
262 PackagedRequestPolicySummaryValidationError::FieldOutOfSync {
263 field: "supported_zodiac_modes",
264 },
265 );
266 }
267 if self.supported_apparentness != [Apparentness::Mean] {
268 return Err(
269 PackagedRequestPolicySummaryValidationError::FieldOutOfSync {
270 field: "supported_apparentness",
271 },
272 );
273 }
274 if self.supports_topocentric_observer {
275 return Err(
276 PackagedRequestPolicySummaryValidationError::FieldOutOfSync {
277 field: "supports_topocentric_observer",
278 },
279 );
280 }
281 if self.lookup_epoch_policy
282 != PackagedLookupEpochPolicy::RetagToTtGridWithoutRelativisticCorrection
283 {
284 return Err(
285 PackagedRequestPolicySummaryValidationError::FieldOutOfSync {
286 field: "lookup_epoch_policy",
287 },
288 );
289 }
290
291 Ok(())
292 }
293}
294
295impl fmt::Display for PackagedRequestPolicySummary {
296 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297 f.write_str(&self.summary_line())
298 }
299}
300
301const PACKAGED_REQUEST_POLICY_SUMMARY: PackagedRequestPolicySummary =
302 PackagedRequestPolicySummary {
303 geocentric_only: true,
304 supported_frames: &[CoordinateFrame::Ecliptic, CoordinateFrame::Equatorial],
305 supported_time_scales: &[TimeScale::Tt, TimeScale::Tdb],
306 supported_zodiac_modes: &[ZodiacMode::Tropical],
307 supported_apparentness: &[Apparentness::Mean],
308 supports_topocentric_observer: false,
309 lookup_epoch_policy: PackagedLookupEpochPolicy::RetagToTtGridWithoutRelativisticCorrection,
310 };
311
312pub fn packaged_request_policy_summary_details() -> PackagedRequestPolicySummary {
327 let summary = PACKAGED_REQUEST_POLICY_SUMMARY;
328 debug_assert!(summary.validate().is_ok());
329 summary
330}
331
332pub fn packaged_request_policy_summary() -> &'static str {
334 static SUMMARY: OnceLock<String> = OnceLock::new();
335 SUMMARY
336 .get_or_init(|| {
337 let summary = packaged_request_policy_summary_details();
338 match summary.validate() {
339 Ok(()) => summary.to_string(),
340 Err(error) => format!("Packaged request policy: unavailable ({error})"),
341 }
342 })
343 .as_str()
344}
345
346#[derive(Clone, Copy, Debug, PartialEq, Eq)]
348pub struct PackagedFrameTreatmentSummary;
349
350#[derive(Clone, Copy, Debug, PartialEq, Eq)]
352pub enum PackagedFrameTreatmentSummaryValidationError {
353 BlankSummary,
355 WhitespacePaddedSummary,
357}
358
359impl fmt::Display for PackagedFrameTreatmentSummaryValidationError {
360 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361 match self {
362 Self::BlankSummary => f.write_str("packaged frame-treatment summary is blank"),
363 Self::WhitespacePaddedSummary => {
364 f.write_str("packaged frame-treatment summary has surrounding whitespace")
365 }
366 }
367 }
368}
369
370impl std::error::Error for PackagedFrameTreatmentSummaryValidationError {}
371
372pub(crate) fn validate_packaged_frame_treatment_summary_line(
373 summary: &str,
374) -> Result<(), PackagedFrameTreatmentSummaryValidationError> {
375 if summary.trim().is_empty() {
376 Err(PackagedFrameTreatmentSummaryValidationError::BlankSummary)
377 } else if summary.trim() != summary {
378 Err(PackagedFrameTreatmentSummaryValidationError::WhitespacePaddedSummary)
379 } else {
380 Ok(())
381 }
382}
383
384impl PackagedFrameTreatmentSummary {
385 pub const fn summary_line(self) -> &'static str {
387 "checked-in compressed artifact stores J2000 ecliptic coordinates directly; equatorial coordinates are reconstructed from the stored channels and mean-obliquity transform"
388 }
389
390 pub fn validate(&self) -> Result<(), PackagedFrameTreatmentSummaryValidationError> {
392 validate_packaged_frame_treatment_summary_line(self.summary_line())
393 }
394
395 pub fn validated_summary_line(
397 &self,
398 ) -> Result<&'static str, PackagedFrameTreatmentSummaryValidationError> {
399 self.validate()?;
400 Ok(self.summary_line())
401 }
402}
403
404impl fmt::Display for PackagedFrameTreatmentSummary {
405 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
406 f.write_str(self.summary_line())
407 }
408}
409
410pub const fn packaged_frame_treatment_summary_details() -> PackagedFrameTreatmentSummary {
412 PackagedFrameTreatmentSummary
413}
414
415pub fn packaged_frame_treatment_summary() -> &'static str {
417 static SUMMARY: OnceLock<String> = OnceLock::new();
418 SUMMARY
419 .get_or_init(|| {
420 let summary = packaged_frame_treatment_summary_details();
421 match summary.validated_summary_line() {
422 Ok(summary_line) => summary_line.to_string(),
423 Err(error) => format!("Packaged frame treatment unavailable ({error})"),
424 }
425 })
426 .as_str()
427}
428
429#[derive(Clone, Copy, Debug, PartialEq, Eq)]
431pub struct PackagedArtifactStorageSummary;
432
433#[derive(Clone, Copy, Debug, PartialEq, Eq)]
435pub enum PackagedArtifactStorageSummaryValidationError {
436 BlankSummary,
438 WhitespacePaddedSummary,
440 ProfileOutOfSync { field: &'static str },
442}
443
444impl fmt::Display for PackagedArtifactStorageSummaryValidationError {
445 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
446 match self {
447 Self::BlankSummary => f.write_str("packaged artifact storage summary is blank"),
448 Self::WhitespacePaddedSummary => {
449 f.write_str("packaged artifact storage summary has surrounding whitespace")
450 }
451 Self::ProfileOutOfSync { field } => write!(
452 f,
453 "packaged artifact storage summary is out of sync with the bundled artifact profile field `{field}`"
454 ),
455 }
456 }
457}
458
459impl std::error::Error for PackagedArtifactStorageSummaryValidationError {}
460
461pub(crate) fn validate_packaged_artifact_storage_summary_line(
462 summary: &str,
463) -> Result<(), PackagedArtifactStorageSummaryValidationError> {
464 if summary.trim().is_empty() {
465 Err(PackagedArtifactStorageSummaryValidationError::BlankSummary)
466 } else if summary.trim() != summary {
467 Err(PackagedArtifactStorageSummaryValidationError::WhitespacePaddedSummary)
468 } else {
469 Ok(())
470 }
471}
472
473pub(crate) fn validate_packaged_artifact_storage_profile(
474 profile: &ArtifactProfile,
475) -> Result<(), PackagedArtifactStorageSummaryValidationError> {
476 const EXPECTED_STORED_CHANNELS: [ChannelKind; 3] = [
477 ChannelKind::Longitude,
478 ChannelKind::Latitude,
479 ChannelKind::DistanceAu,
480 ];
481 const EXPECTED_DERIVED_OUTPUTS: [ArtifactOutput; 3] = [
482 ArtifactOutput::EclipticCoordinates,
483 ArtifactOutput::EquatorialCoordinates,
484 ArtifactOutput::Motion,
485 ];
486 const EXPECTED_UNSUPPORTED_OUTPUTS: [ArtifactOutput; 3] = [
487 ArtifactOutput::ApparentCorrections,
488 ArtifactOutput::TopocentricCoordinates,
489 ArtifactOutput::SiderealCoordinates,
490 ];
491
492 if profile.stored_channels != EXPECTED_STORED_CHANNELS {
493 return Err(
494 PackagedArtifactStorageSummaryValidationError::ProfileOutOfSync {
495 field: "stored_channels",
496 },
497 );
498 }
499
500 if profile.derived_outputs.as_slice() != EXPECTED_DERIVED_OUTPUTS {
501 return Err(
502 PackagedArtifactStorageSummaryValidationError::ProfileOutOfSync {
503 field: "derived_outputs",
504 },
505 );
506 }
507
508 if profile.unsupported_outputs.as_slice() != EXPECTED_UNSUPPORTED_OUTPUTS {
509 return Err(
510 PackagedArtifactStorageSummaryValidationError::ProfileOutOfSync {
511 field: "unsupported_outputs",
512 },
513 );
514 }
515
516 Ok(())
517}
518
519impl PackagedArtifactStorageSummary {
520 pub const fn summary_line(self) -> &'static str {
522 "Quantized linear segments stored in pleiades-compression artifact format; body-indexed segment tables support random access by body and lookup time across the advertised range; ecliptic and equatorial coordinates are reconstructed at runtime from stored channels; apparent, topocentric, and sidereal outputs remain unsupported; motion/speed is derived from fitted segment derivatives"
523 }
524
525 pub fn validate(&self) -> Result<(), PackagedArtifactStorageSummaryValidationError> {
527 validate_packaged_artifact_storage_summary_line(self.summary_line())?;
528 validate_packaged_artifact_storage_profile(
529 &packaged_artifact_profile_summary_details().profile,
530 )
531 }
532}
533
534impl fmt::Display for PackagedArtifactStorageSummary {
535 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
536 f.write_str(self.summary_line())
537 }
538}
539
540pub const fn packaged_artifact_storage_summary_details() -> PackagedArtifactStorageSummary {
542 PackagedArtifactStorageSummary
543}
544
545pub fn packaged_artifact_storage_summary() -> &'static str {
547 static SUMMARY: OnceLock<String> = OnceLock::new();
548 SUMMARY
549 .get_or_init(|| {
550 let summary = packaged_artifact_storage_summary_details();
551 match summary.validate() {
552 Ok(()) => summary.to_string(),
553 Err(error) => format!("Packaged artifact storage unavailable ({error})"),
554 }
555 })
556 .as_str()
557}
558
559#[derive(Clone, Copy, Debug, PartialEq, Eq)]
561pub struct PackagedArtifactAccessSummary {
562 pub explicit_path_loading: bool,
564}
565
566#[derive(Clone, Copy, Debug, PartialEq, Eq)]
568pub enum PackagedArtifactAccessSummaryValidationError {
569 BlankSummary,
571 WhitespacePaddedSummary,
573 FeatureStateOutOfSync { field: &'static str },
575}
576
577impl fmt::Display for PackagedArtifactAccessSummaryValidationError {
578 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
579 match self {
580 Self::BlankSummary => f.write_str("packaged artifact access summary is blank"),
581 Self::WhitespacePaddedSummary => {
582 f.write_str("packaged artifact access summary has surrounding whitespace")
583 }
584 Self::FeatureStateOutOfSync { field } => write!(
585 f,
586 "packaged artifact access summary is out of sync with the build feature field `{field}`"
587 ),
588 }
589 }
590}
591
592impl std::error::Error for PackagedArtifactAccessSummaryValidationError {}
593
594pub(crate) fn validate_packaged_artifact_access_summary_line(
595 summary: &str,
596) -> Result<(), PackagedArtifactAccessSummaryValidationError> {
597 if summary.trim().is_empty() {
598 Err(PackagedArtifactAccessSummaryValidationError::BlankSummary)
599 } else if summary.trim() != summary {
600 Err(PackagedArtifactAccessSummaryValidationError::WhitespacePaddedSummary)
601 } else {
602 Ok(())
603 }
604}
605
606impl PackagedArtifactAccessSummary {
607 pub const fn summary_line(self) -> &'static str {
609 if self.explicit_path_loading {
610 "packaged artifact access: checked-in fixture plus explicit artifact-path loading via `packaged-artifact-path` feature"
611 } else {
612 "packaged artifact access: checked-in fixture only; explicit artifact-path loading disabled"
613 }
614 }
615
616 pub fn validated_summary_line(
618 &self,
619 ) -> Result<&'static str, PackagedArtifactAccessSummaryValidationError> {
620 self.validate()?;
621 Ok(self.summary_line())
622 }
623
624 pub fn validate(&self) -> Result<(), PackagedArtifactAccessSummaryValidationError> {
626 validate_packaged_artifact_access_summary_line(self.summary_line())?;
627 if self.explicit_path_loading != packaged_artifact_path_loading_enabled() {
628 return Err(
629 PackagedArtifactAccessSummaryValidationError::FeatureStateOutOfSync {
630 field: "explicit_path_loading",
631 },
632 );
633 }
634 Ok(())
635 }
636}
637
638impl fmt::Display for PackagedArtifactAccessSummary {
639 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
640 f.write_str(self.summary_line())
641 }
642}
643
644pub const fn packaged_artifact_path_loading_enabled() -> bool {
646 cfg!(feature = "packaged-artifact-path")
647}
648
649pub const fn packaged_artifact_access_summary_details() -> PackagedArtifactAccessSummary {
664 PackagedArtifactAccessSummary {
665 explicit_path_loading: packaged_artifact_path_loading_enabled(),
666 }
667}
668
669pub fn packaged_artifact_access_summary() -> &'static str {
671 static SUMMARY: OnceLock<String> = OnceLock::new();
672 SUMMARY
673 .get_or_init(|| {
674 let summary = packaged_artifact_access_summary_details();
675 match summary.validated_summary_line() {
676 Ok(rendered) => rendered.to_string(),
677 Err(error) => format!("Packaged artifact access unavailable ({error})"),
678 }
679 })
680 .as_str()
681}
682
683#[derive(Clone, Debug, PartialEq)]
685pub struct PackagedBatchParitySummary {
686 pub request_count: usize,
688 pub body_count: usize,
690 pub ecliptic_request_count: usize,
692 pub equatorial_request_count: usize,
694 pub exact_count: usize,
696 pub interpolated_count: usize,
698 pub approximate_count: usize,
700 pub unknown_count: usize,
702 pub order_preserved: bool,
704 pub single_query_parity_preserved: bool,
706}
707
708#[derive(Clone, Debug, Eq, PartialEq)]
710pub enum PackagedBatchParitySummaryValidationError {
711 FieldOutOfSync { field: &'static str },
713}
714
715impl fmt::Display for PackagedBatchParitySummaryValidationError {
716 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
717 match self {
718 Self::FieldOutOfSync { field } => write!(
719 f,
720 "the packaged mixed-frame batch-parity summary field `{field}` is out of sync with the current packaged posture"
721 ),
722 }
723 }
724}
725
726impl std::error::Error for PackagedBatchParitySummaryValidationError {}
727
728impl PackagedBatchParitySummary {
729 pub fn validate(&self) -> Result<(), PackagedBatchParitySummaryValidationError> {
731 if self.request_count != self.body_count {
732 return Err(PackagedBatchParitySummaryValidationError::FieldOutOfSync {
733 field: "request_count/body_count",
734 });
735 }
736
737 if self.ecliptic_request_count + self.equatorial_request_count != self.request_count {
738 return Err(PackagedBatchParitySummaryValidationError::FieldOutOfSync {
739 field: "frame_counts",
740 });
741 }
742
743 if self.ecliptic_request_count == 0 || self.equatorial_request_count == 0 {
744 return Err(PackagedBatchParitySummaryValidationError::FieldOutOfSync {
745 field: "frame_mix",
746 });
747 }
748
749 if self.exact_count + self.interpolated_count + self.approximate_count + self.unknown_count
750 != self.request_count
751 {
752 return Err(PackagedBatchParitySummaryValidationError::FieldOutOfSync {
753 field: "quality_counts",
754 });
755 }
756
757 if !self.order_preserved {
758 return Err(PackagedBatchParitySummaryValidationError::FieldOutOfSync {
759 field: "order_preserved",
760 });
761 }
762
763 if !self.single_query_parity_preserved {
764 return Err(PackagedBatchParitySummaryValidationError::FieldOutOfSync {
765 field: "single_query_parity_preserved",
766 });
767 }
768
769 Ok(())
770 }
771
772 pub fn validated_summary_line(
774 &self,
775 ) -> Result<String, PackagedBatchParitySummaryValidationError> {
776 self.validate()?;
777 Ok(self.summary_line())
778 }
779}
780
781fn packaged_mixed_frame_batch_parity_request_entries(
782) -> Option<(Vec<EphemerisRequest>, Vec<SnapshotEntry>)> {
783 let snapshot = reference_snapshot();
784 let mut requests = Vec::with_capacity(packaged_bodies().len());
785 let mut entries = Vec::with_capacity(packaged_bodies().len());
786
787 for (index, body) in packaged_bodies().iter().cloned().enumerate() {
788 let entry = packaged_reference_entry_for_body(snapshot, &body)?;
789 entries.push(entry.clone());
790 requests.push(EphemerisRequest {
791 body,
792 instant: Instant::new(entry.epoch.julian_day, TimeScale::Tt),
793 observer: None,
794 frame: if index % 2 == 0 {
795 CoordinateFrame::Ecliptic
796 } else {
797 CoordinateFrame::Equatorial
798 },
799 zodiac_mode: ZodiacMode::Tropical,
800 apparent: Apparentness::Mean,
801 });
802 }
803
804 Some((requests, entries))
805}
806
807pub fn packaged_mixed_frame_batch_parity_requests() -> Option<Vec<EphemerisRequest>> {
809 packaged_mixed_frame_batch_parity_request_entries().map(|(requests, _)| requests)
810}
811
812#[doc(alias = "packaged_mixed_frame_batch_parity_requests")]
814pub fn packaged_mixed_frame_batch_parity_request_corpus() -> Option<Vec<EphemerisRequest>> {
815 packaged_mixed_frame_batch_parity_requests()
816}
817
818pub fn packaged_mixed_frame_batch_parity_summary() -> Option<PackagedBatchParitySummary> {
820 let backend = packaged_backend();
821 let (requests, entries) = packaged_mixed_frame_batch_parity_request_entries()?;
822
823 let results = backend.positions(&requests).ok()?;
824 if results.len() != requests.len() {
825 return None;
826 }
827
828 let mut ecliptic_request_count = 0usize;
829 let mut equatorial_request_count = 0usize;
830 let mut exact_count = 0usize;
831 let mut interpolated_count = 0usize;
832 let mut approximate_count = 0usize;
833 let mut unknown_count = 0usize;
834 let mut order_preserved = true;
835 let mut single_query_parity = true;
836
837 for ((request, result), entry) in requests.iter().zip(results.iter()).zip(entries.iter()) {
838 let single = backend.position(request).ok();
839 single_query_parity &= single.as_ref().is_some_and(|single| single == result);
840
841 order_preserved &= result.body == entry.body
842 && result.instant == request.instant
843 && result.frame == request.frame
844 && result.zodiac_mode == request.zodiac_mode
845 && result.apparent == request.apparent;
846
847 match request.frame {
848 CoordinateFrame::Ecliptic => ecliptic_request_count += 1,
849 CoordinateFrame::Equatorial => equatorial_request_count += 1,
850 _ => return None,
851 }
852
853 match result.quality {
854 QualityAnnotation::Exact => exact_count += 1,
855 QualityAnnotation::Interpolated => interpolated_count += 1,
856 QualityAnnotation::Approximate => approximate_count += 1,
857 QualityAnnotation::Unknown => unknown_count += 1,
858 _ => unknown_count += 1,
859 }
860 }
861
862 Some(PackagedBatchParitySummary {
863 request_count: requests.len(),
864 body_count: entries.len(),
865 ecliptic_request_count,
866 equatorial_request_count,
867 exact_count,
868 interpolated_count,
869 approximate_count,
870 unknown_count,
871 order_preserved,
872 single_query_parity_preserved: single_query_parity,
873 })
874}
875
876impl PackagedBatchParitySummary {
877 pub fn summary_line(&self) -> String {
879 let order = if self.order_preserved {
880 "preserved"
881 } else {
882 "needs attention"
883 };
884 let parity = if self.single_query_parity_preserved {
885 "preserved"
886 } else {
887 "needs attention"
888 };
889 format!(
890 "Packaged mixed frame batch parity: {} requests across {} bodies, ecliptic requests={}, equatorial requests={}; quality counts: Exact={}, Interpolated={}, Approximate={}, Unknown={}; order={}, single-query parity={}",
891 self.request_count,
892 self.body_count,
893 self.ecliptic_request_count,
894 self.equatorial_request_count,
895 self.exact_count,
896 self.interpolated_count,
897 self.approximate_count,
898 self.unknown_count,
899 order,
900 parity,
901 )
902 }
903}
904
905impl fmt::Display for PackagedBatchParitySummary {
906 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
907 f.write_str(&self.summary_line())
908 }
909}
910
911#[allow(dead_code)]
912fn format_validated_packaged_mixed_frame_batch_parity_summary_for_report(
913 summary: &PackagedBatchParitySummary,
914) -> String {
915 match summary.validated_summary_line() {
916 Ok(line) => line,
917 Err(error) => format!("Packaged mixed frame batch parity: unavailable ({error})"),
918 }
919}
920
921#[allow(dead_code)]
923pub(crate) fn packaged_mixed_frame_batch_parity_summary_for_report() -> String {
924 packaged_mixed_frame_batch_parity_summary()
925 .as_ref()
926 .map(format_validated_packaged_mixed_frame_batch_parity_summary_for_report)
927 .unwrap_or_else(|| "Packaged mixed frame batch parity: unavailable".to_string())
928}
929
930#[derive(Clone, Debug, PartialEq)]
932pub struct PackagedTimeScaleBatchParitySummary {
933 pub request_count: usize,
935 pub body_count: usize,
937 pub tt_request_count: usize,
939 pub tdb_request_count: usize,
941 pub exact_count: usize,
943 pub interpolated_count: usize,
945 pub approximate_count: usize,
947 pub unknown_count: usize,
949 pub order_preserved: bool,
951 pub single_query_parity_preserved: bool,
953}
954
955#[derive(Clone, Debug, Eq, PartialEq)]
957pub enum PackagedTimeScaleBatchParitySummaryValidationError {
958 FieldOutOfSync { field: &'static str },
960}
961
962impl fmt::Display for PackagedTimeScaleBatchParitySummaryValidationError {
963 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
964 match self {
965 Self::FieldOutOfSync { field } => write!(
966 f,
967 "the packaged mixed TT/TDB batch-parity summary field `{field}` is out of sync with the current packaged posture"
968 ),
969 }
970 }
971}
972
973impl std::error::Error for PackagedTimeScaleBatchParitySummaryValidationError {}
974
975fn packaged_mixed_tt_tdb_batch_parity_request_entries(
976) -> Option<(Vec<EphemerisRequest>, Vec<SnapshotEntry>)> {
977 let snapshot = reference_snapshot();
978 let mut requests = Vec::with_capacity(packaged_bodies().len());
979 let mut entries = Vec::with_capacity(packaged_bodies().len());
980
981 for (index, body) in packaged_bodies().iter().cloned().enumerate() {
982 let entry = packaged_reference_entry_for_body(snapshot, &body)?;
983 entries.push(entry.clone());
984 requests.push(EphemerisRequest {
985 body,
986 instant: Instant::new(
987 entry.epoch.julian_day,
988 if index % 2 == 0 {
989 TimeScale::Tt
990 } else {
991 TimeScale::Tdb
992 },
993 ),
994 observer: None,
995 frame: CoordinateFrame::Ecliptic,
996 zodiac_mode: ZodiacMode::Tropical,
997 apparent: Apparentness::Mean,
998 });
999 }
1000
1001 Some((requests, entries))
1002}
1003
1004pub fn packaged_mixed_tt_tdb_batch_parity_requests() -> Option<Vec<EphemerisRequest>> {
1006 packaged_mixed_tt_tdb_batch_parity_request_entries().map(|(requests, _)| requests)
1007}
1008
1009#[doc(alias = "packaged_mixed_tt_tdb_batch_parity_requests")]
1011pub fn packaged_mixed_tt_tdb_batch_parity_request_corpus() -> Option<Vec<EphemerisRequest>> {
1012 packaged_mixed_tt_tdb_batch_parity_requests()
1013}
1014
1015pub fn packaged_mixed_tt_tdb_batch_parity_summary() -> Option<PackagedTimeScaleBatchParitySummary> {
1017 let backend = packaged_backend();
1018 let (requests, entries) = packaged_mixed_tt_tdb_batch_parity_request_entries()?;
1019
1020 let results = backend.positions(&requests).ok()?;
1021 if results.len() != requests.len() {
1022 return None;
1023 }
1024
1025 let mut tt_request_count = 0usize;
1026 let mut tdb_request_count = 0usize;
1027 let mut exact_count = 0usize;
1028 let mut interpolated_count = 0usize;
1029 let mut approximate_count = 0usize;
1030 let mut unknown_count = 0usize;
1031 let mut order_preserved = true;
1032 let mut single_query_parity = true;
1033
1034 for ((request, result), entry) in requests.iter().zip(results.iter()).zip(entries.iter()) {
1035 let single = backend.position(request).ok();
1036 single_query_parity &= single.as_ref().is_some_and(|single| single == result);
1037
1038 order_preserved &= result.body == entry.body
1039 && result.instant == request.instant
1040 && result.frame == request.frame
1041 && result.zodiac_mode == request.zodiac_mode
1042 && result.apparent == request.apparent;
1043
1044 match request.instant.scale {
1045 TimeScale::Tt => tt_request_count += 1,
1046 TimeScale::Tdb => tdb_request_count += 1,
1047 _ => return None,
1048 }
1049
1050 match result.quality {
1051 QualityAnnotation::Exact => exact_count += 1,
1052 QualityAnnotation::Interpolated => interpolated_count += 1,
1053 QualityAnnotation::Approximate => approximate_count += 1,
1054 QualityAnnotation::Unknown => unknown_count += 1,
1055 _ => unknown_count += 1,
1056 }
1057 }
1058
1059 Some(PackagedTimeScaleBatchParitySummary {
1060 request_count: requests.len(),
1061 body_count: entries.len(),
1062 tt_request_count,
1063 tdb_request_count,
1064 exact_count,
1065 interpolated_count,
1066 approximate_count,
1067 unknown_count,
1068 order_preserved,
1069 single_query_parity_preserved: single_query_parity,
1070 })
1071}
1072
1073impl PackagedTimeScaleBatchParitySummary {
1074 pub fn validate(&self) -> Result<(), PackagedTimeScaleBatchParitySummaryValidationError> {
1076 if self.request_count != self.body_count {
1077 return Err(
1078 PackagedTimeScaleBatchParitySummaryValidationError::FieldOutOfSync {
1079 field: "request_count/body_count",
1080 },
1081 );
1082 }
1083
1084 if self.tt_request_count + self.tdb_request_count != self.request_count {
1085 return Err(
1086 PackagedTimeScaleBatchParitySummaryValidationError::FieldOutOfSync {
1087 field: "time_scale_counts",
1088 },
1089 );
1090 }
1091
1092 if self.tt_request_count == 0 || self.tdb_request_count == 0 {
1093 return Err(
1094 PackagedTimeScaleBatchParitySummaryValidationError::FieldOutOfSync {
1095 field: "time_scale_mix",
1096 },
1097 );
1098 }
1099
1100 if self.exact_count + self.interpolated_count + self.approximate_count + self.unknown_count
1101 != self.request_count
1102 {
1103 return Err(
1104 PackagedTimeScaleBatchParitySummaryValidationError::FieldOutOfSync {
1105 field: "quality_counts",
1106 },
1107 );
1108 }
1109
1110 if !self.order_preserved {
1111 return Err(
1112 PackagedTimeScaleBatchParitySummaryValidationError::FieldOutOfSync {
1113 field: "order_preserved",
1114 },
1115 );
1116 }
1117
1118 if !self.single_query_parity_preserved {
1119 return Err(
1120 PackagedTimeScaleBatchParitySummaryValidationError::FieldOutOfSync {
1121 field: "single_query_parity_preserved",
1122 },
1123 );
1124 }
1125
1126 Ok(())
1127 }
1128
1129 pub fn validated_summary_line(
1131 &self,
1132 ) -> Result<String, PackagedTimeScaleBatchParitySummaryValidationError> {
1133 self.validate()?;
1134 Ok(self.summary_line())
1135 }
1136
1137 pub fn summary_line(&self) -> String {
1139 let order = if self.order_preserved {
1140 "preserved"
1141 } else {
1142 "needs attention"
1143 };
1144 let parity = if self.single_query_parity_preserved {
1145 "preserved"
1146 } else {
1147 "needs attention"
1148 };
1149 format!(
1150 "Packaged mixed TT/TDB batch parity: {} requests across {} bodies, TT requests={}, TDB requests={}; quality counts: Exact={}, Interpolated={}, Approximate={}, Unknown={}; order={}, single-query parity={}",
1151 self.request_count,
1152 self.body_count,
1153 self.tt_request_count,
1154 self.tdb_request_count,
1155 self.exact_count,
1156 self.interpolated_count,
1157 self.approximate_count,
1158 self.unknown_count,
1159 order,
1160 parity,
1161 )
1162 }
1163}
1164
1165impl fmt::Display for PackagedTimeScaleBatchParitySummary {
1166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1167 f.write_str(&self.summary_line())
1168 }
1169}
1170
1171pub fn packaged_lookup(
1186 body: &CelestialBody,
1187 instant: Instant,
1188) -> Result<EclipticCoordinates, pleiades_compression::CompressionError> {
1189 packaged_artifact().lookup_ecliptic(body, normalize_lookup_instant(instant))
1190}
1191
1192pub fn packaged_backend() -> PackagedDataBackend {
1209 PackagedDataBackend::new()
1210}
1211
1212pub fn packaged_backend_from_artifact(artifact: CompressedArtifact) -> PackagedDataBackend {
1214 PackagedDataBackend::from_artifact(artifact)
1215}
1216
1217pub fn packaged_backend_from_bytes(
1235 bytes: &[u8],
1236) -> Result<PackagedDataBackend, PackagedArtifactLoadError> {
1237 PackagedDataBackend::from_bytes(bytes)
1238}
1239
1240#[cfg(feature = "packaged-artifact-path")]
1241pub fn packaged_backend_from_path(
1264 path: impl AsRef<Path>,
1265) -> Result<PackagedDataBackend, PackagedArtifactLoadError> {
1266 PackagedDataBackend::from_path(path)
1267}