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_for_report() -> String {
167 let summary = packaged_lookup_epoch_policy_summary_details();
168 match summary.validate() {
169 Ok(()) => summary.to_string(),
170 Err(error) => format!("Packaged lookup epoch policy: unavailable ({error})"),
171 }
172}
173
174pub fn packaged_lookup_epoch_policy_summary() -> &'static str {
176 static SUMMARY: OnceLock<String> = OnceLock::new();
177 SUMMARY
178 .get_or_init(|| {
179 let summary = packaged_lookup_epoch_policy_summary_details();
180 match summary.validate() {
181 Ok(()) => summary.to_string(),
182 Err(error) => format!("Packaged lookup epoch policy: unavailable ({error})"),
183 }
184 })
185 .as_str()
186}
187
188#[derive(Clone, Copy, Debug, PartialEq)]
190pub struct PackagedRequestPolicySummary {
191 pub geocentric_only: bool,
193 pub supported_frames: &'static [CoordinateFrame],
195 pub supported_time_scales: &'static [TimeScale],
197 pub supported_zodiac_modes: &'static [ZodiacMode],
199 pub supported_apparentness: &'static [Apparentness],
201 pub supports_topocentric_observer: bool,
203 pub lookup_epoch_policy: PackagedLookupEpochPolicy,
205}
206
207#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
209pub enum PackagedRequestPolicySummaryValidationError {
210 FieldOutOfSync { field: &'static str },
212}
213
214impl fmt::Display for PackagedRequestPolicySummaryValidationError {
215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216 match self {
217 Self::FieldOutOfSync { field } => write!(
218 f,
219 "the packaged request-policy summary field `{field}` is out of sync with the current posture"
220 ),
221 }
222 }
223}
224
225impl std::error::Error for PackagedRequestPolicySummaryValidationError {}
226
227impl PackagedRequestPolicySummary {
228 pub fn summary_line(&self) -> String {
230 format!(
231 "Packaged request policy: {}frames={}; time scales={}; zodiac modes={}; apparentness={}; topocentric observer={}; lookup epoch policy={}",
232 if self.geocentric_only {
233 "geocentric-only; "
234 } else {
235 ""
236 },
237 join_display(self.supported_frames),
238 join_display(self.supported_time_scales),
239 join_display(self.supported_zodiac_modes),
240 join_display(self.supported_apparentness),
241 self.supports_topocentric_observer,
242 self.lookup_epoch_policy.summary_line(),
243 )
244 }
245
246 pub fn validate(&self) -> Result<(), PackagedRequestPolicySummaryValidationError> {
248 if !self.geocentric_only {
249 return Err(
250 PackagedRequestPolicySummaryValidationError::FieldOutOfSync {
251 field: "geocentric_only",
252 },
253 );
254 }
255 if self.supported_frames != [CoordinateFrame::Ecliptic, CoordinateFrame::Equatorial] {
256 return Err(
257 PackagedRequestPolicySummaryValidationError::FieldOutOfSync {
258 field: "supported_frames",
259 },
260 );
261 }
262 if self.supported_time_scales != [TimeScale::Tt, TimeScale::Tdb] {
263 return Err(
264 PackagedRequestPolicySummaryValidationError::FieldOutOfSync {
265 field: "supported_time_scales",
266 },
267 );
268 }
269 if self.supported_zodiac_modes != [ZodiacMode::Tropical] {
270 return Err(
271 PackagedRequestPolicySummaryValidationError::FieldOutOfSync {
272 field: "supported_zodiac_modes",
273 },
274 );
275 }
276 if self.supported_apparentness != [Apparentness::Mean] {
277 return Err(
278 PackagedRequestPolicySummaryValidationError::FieldOutOfSync {
279 field: "supported_apparentness",
280 },
281 );
282 }
283 if self.supports_topocentric_observer {
284 return Err(
285 PackagedRequestPolicySummaryValidationError::FieldOutOfSync {
286 field: "supports_topocentric_observer",
287 },
288 );
289 }
290 if self.lookup_epoch_policy
291 != PackagedLookupEpochPolicy::RetagToTtGridWithoutRelativisticCorrection
292 {
293 return Err(
294 PackagedRequestPolicySummaryValidationError::FieldOutOfSync {
295 field: "lookup_epoch_policy",
296 },
297 );
298 }
299
300 Ok(())
301 }
302}
303
304impl fmt::Display for PackagedRequestPolicySummary {
305 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306 f.write_str(&self.summary_line())
307 }
308}
309
310const PACKAGED_REQUEST_POLICY_SUMMARY: PackagedRequestPolicySummary =
311 PackagedRequestPolicySummary {
312 geocentric_only: true,
313 supported_frames: &[CoordinateFrame::Ecliptic, CoordinateFrame::Equatorial],
314 supported_time_scales: &[TimeScale::Tt, TimeScale::Tdb],
315 supported_zodiac_modes: &[ZodiacMode::Tropical],
316 supported_apparentness: &[Apparentness::Mean],
317 supports_topocentric_observer: false,
318 lookup_epoch_policy: PackagedLookupEpochPolicy::RetagToTtGridWithoutRelativisticCorrection,
319 };
320
321pub fn packaged_request_policy_summary_details() -> PackagedRequestPolicySummary {
336 let summary = PACKAGED_REQUEST_POLICY_SUMMARY;
337 debug_assert!(summary.validate().is_ok());
338 summary
339}
340
341pub fn packaged_request_policy_summary_for_report() -> String {
343 let summary = packaged_request_policy_summary_details();
344 match summary.validate() {
345 Ok(()) => summary.to_string(),
346 Err(error) => format!("Packaged request policy: unavailable ({error})"),
347 }
348}
349
350pub fn packaged_request_policy_summary() -> &'static str {
352 static SUMMARY: OnceLock<String> = OnceLock::new();
353 SUMMARY
354 .get_or_init(|| {
355 let summary = packaged_request_policy_summary_details();
356 match summary.validate() {
357 Ok(()) => summary.to_string(),
358 Err(error) => format!("Packaged request policy: unavailable ({error})"),
359 }
360 })
361 .as_str()
362}
363
364#[derive(Clone, Copy, Debug, PartialEq, Eq)]
366pub struct PackagedFrameTreatmentSummary;
367
368#[derive(Clone, Copy, Debug, PartialEq, Eq)]
370pub enum PackagedFrameTreatmentSummaryValidationError {
371 BlankSummary,
373 WhitespacePaddedSummary,
375}
376
377impl fmt::Display for PackagedFrameTreatmentSummaryValidationError {
378 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
379 match self {
380 Self::BlankSummary => f.write_str("packaged frame-treatment summary is blank"),
381 Self::WhitespacePaddedSummary => {
382 f.write_str("packaged frame-treatment summary has surrounding whitespace")
383 }
384 }
385 }
386}
387
388impl std::error::Error for PackagedFrameTreatmentSummaryValidationError {}
389
390pub(crate) fn validate_packaged_frame_treatment_summary_line(
391 summary: &str,
392) -> Result<(), PackagedFrameTreatmentSummaryValidationError> {
393 if summary.trim().is_empty() {
394 Err(PackagedFrameTreatmentSummaryValidationError::BlankSummary)
395 } else if summary.trim() != summary {
396 Err(PackagedFrameTreatmentSummaryValidationError::WhitespacePaddedSummary)
397 } else {
398 Ok(())
399 }
400}
401
402impl PackagedFrameTreatmentSummary {
403 pub const fn summary_line(self) -> &'static str {
405 "checked-in compressed artifact stores J2000 ecliptic coordinates directly; equatorial coordinates are reconstructed from the stored channels and mean-obliquity transform"
406 }
407
408 pub fn validate(&self) -> Result<(), PackagedFrameTreatmentSummaryValidationError> {
410 validate_packaged_frame_treatment_summary_line(self.summary_line())
411 }
412
413 pub fn validated_summary_line(
415 &self,
416 ) -> Result<&'static str, PackagedFrameTreatmentSummaryValidationError> {
417 self.validate()?;
418 Ok(self.summary_line())
419 }
420}
421
422impl fmt::Display for PackagedFrameTreatmentSummary {
423 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
424 f.write_str(self.summary_line())
425 }
426}
427
428pub const fn packaged_frame_treatment_summary_details() -> PackagedFrameTreatmentSummary {
430 PackagedFrameTreatmentSummary
431}
432
433pub fn packaged_frame_treatment_summary_for_report() -> String {
435 let summary = packaged_frame_treatment_summary_details();
436 match summary.validated_summary_line() {
437 Ok(summary_line) => summary_line.to_string(),
438 Err(error) => format!("Packaged frame treatment unavailable ({error})"),
439 }
440}
441
442pub fn packaged_frame_treatment_summary() -> &'static str {
444 static SUMMARY: OnceLock<String> = OnceLock::new();
445 SUMMARY
446 .get_or_init(packaged_frame_treatment_summary_for_report)
447 .as_str()
448}
449
450#[derive(Clone, Copy, Debug, PartialEq, Eq)]
452pub struct PackagedArtifactStorageSummary;
453
454#[derive(Clone, Copy, Debug, PartialEq, Eq)]
456pub enum PackagedArtifactStorageSummaryValidationError {
457 BlankSummary,
459 WhitespacePaddedSummary,
461 ProfileOutOfSync { field: &'static str },
463}
464
465impl fmt::Display for PackagedArtifactStorageSummaryValidationError {
466 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
467 match self {
468 Self::BlankSummary => f.write_str("packaged artifact storage summary is blank"),
469 Self::WhitespacePaddedSummary => {
470 f.write_str("packaged artifact storage summary has surrounding whitespace")
471 }
472 Self::ProfileOutOfSync { field } => write!(
473 f,
474 "packaged artifact storage summary is out of sync with the bundled artifact profile field `{field}`"
475 ),
476 }
477 }
478}
479
480impl std::error::Error for PackagedArtifactStorageSummaryValidationError {}
481
482pub(crate) fn validate_packaged_artifact_storage_summary_line(
483 summary: &str,
484) -> Result<(), PackagedArtifactStorageSummaryValidationError> {
485 if summary.trim().is_empty() {
486 Err(PackagedArtifactStorageSummaryValidationError::BlankSummary)
487 } else if summary.trim() != summary {
488 Err(PackagedArtifactStorageSummaryValidationError::WhitespacePaddedSummary)
489 } else {
490 Ok(())
491 }
492}
493
494pub(crate) fn validate_packaged_artifact_storage_profile(
495 profile: &ArtifactProfile,
496) -> Result<(), PackagedArtifactStorageSummaryValidationError> {
497 const EXPECTED_STORED_CHANNELS: [ChannelKind; 3] = [
498 ChannelKind::Longitude,
499 ChannelKind::Latitude,
500 ChannelKind::DistanceAu,
501 ];
502 const EXPECTED_DERIVED_OUTPUTS: [ArtifactOutput; 3] = [
503 ArtifactOutput::EclipticCoordinates,
504 ArtifactOutput::EquatorialCoordinates,
505 ArtifactOutput::Motion,
506 ];
507 const EXPECTED_UNSUPPORTED_OUTPUTS: [ArtifactOutput; 3] = [
508 ArtifactOutput::ApparentCorrections,
509 ArtifactOutput::TopocentricCoordinates,
510 ArtifactOutput::SiderealCoordinates,
511 ];
512
513 if profile.stored_channels != EXPECTED_STORED_CHANNELS {
514 return Err(
515 PackagedArtifactStorageSummaryValidationError::ProfileOutOfSync {
516 field: "stored_channels",
517 },
518 );
519 }
520
521 if profile.derived_outputs.as_slice() != EXPECTED_DERIVED_OUTPUTS {
522 return Err(
523 PackagedArtifactStorageSummaryValidationError::ProfileOutOfSync {
524 field: "derived_outputs",
525 },
526 );
527 }
528
529 if profile.unsupported_outputs.as_slice() != EXPECTED_UNSUPPORTED_OUTPUTS {
530 return Err(
531 PackagedArtifactStorageSummaryValidationError::ProfileOutOfSync {
532 field: "unsupported_outputs",
533 },
534 );
535 }
536
537 Ok(())
538}
539
540impl PackagedArtifactStorageSummary {
541 pub const fn summary_line(self) -> &'static str {
543 "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"
544 }
545
546 pub fn validate(&self) -> Result<(), PackagedArtifactStorageSummaryValidationError> {
548 validate_packaged_artifact_storage_summary_line(self.summary_line())?;
549 validate_packaged_artifact_storage_profile(
550 &packaged_artifact_profile_summary_details().profile,
551 )
552 }
553}
554
555impl fmt::Display for PackagedArtifactStorageSummary {
556 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
557 f.write_str(self.summary_line())
558 }
559}
560
561pub const fn packaged_artifact_storage_summary_details() -> PackagedArtifactStorageSummary {
563 PackagedArtifactStorageSummary
564}
565
566pub fn packaged_artifact_storage_summary() -> &'static str {
568 static SUMMARY: OnceLock<String> = OnceLock::new();
569 SUMMARY
570 .get_or_init(|| {
571 let summary = packaged_artifact_storage_summary_details();
572 match summary.validate() {
573 Ok(()) => summary.to_string(),
574 Err(error) => format!("Packaged artifact storage unavailable ({error})"),
575 }
576 })
577 .as_str()
578}
579
580pub fn packaged_artifact_storage_summary_for_report() -> String {
582 let summary = packaged_artifact_storage_summary_details();
583 match summary.validate() {
584 Ok(()) => summary.to_string(),
585 Err(error) => format!("Packaged artifact storage/reconstruction: unavailable ({error})"),
586 }
587}
588
589#[derive(Clone, Copy, Debug, PartialEq, Eq)]
591pub struct PackagedArtifactAccessSummary {
592 pub explicit_path_loading: bool,
594}
595
596#[derive(Clone, Copy, Debug, PartialEq, Eq)]
598pub enum PackagedArtifactAccessSummaryValidationError {
599 BlankSummary,
601 WhitespacePaddedSummary,
603 FeatureStateOutOfSync { field: &'static str },
605}
606
607impl fmt::Display for PackagedArtifactAccessSummaryValidationError {
608 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
609 match self {
610 Self::BlankSummary => f.write_str("packaged artifact access summary is blank"),
611 Self::WhitespacePaddedSummary => {
612 f.write_str("packaged artifact access summary has surrounding whitespace")
613 }
614 Self::FeatureStateOutOfSync { field } => write!(
615 f,
616 "packaged artifact access summary is out of sync with the build feature field `{field}`"
617 ),
618 }
619 }
620}
621
622impl std::error::Error for PackagedArtifactAccessSummaryValidationError {}
623
624pub(crate) fn validate_packaged_artifact_access_summary_line(
625 summary: &str,
626) -> Result<(), PackagedArtifactAccessSummaryValidationError> {
627 if summary.trim().is_empty() {
628 Err(PackagedArtifactAccessSummaryValidationError::BlankSummary)
629 } else if summary.trim() != summary {
630 Err(PackagedArtifactAccessSummaryValidationError::WhitespacePaddedSummary)
631 } else {
632 Ok(())
633 }
634}
635
636impl PackagedArtifactAccessSummary {
637 pub const fn summary_line(self) -> &'static str {
639 if self.explicit_path_loading {
640 "packaged artifact access: checked-in fixture plus explicit artifact-path loading via `packaged-artifact-path` feature"
641 } else {
642 "packaged artifact access: checked-in fixture only; explicit artifact-path loading disabled"
643 }
644 }
645
646 pub fn validated_summary_line(
648 &self,
649 ) -> Result<&'static str, PackagedArtifactAccessSummaryValidationError> {
650 self.validate()?;
651 Ok(self.summary_line())
652 }
653
654 pub fn validate(&self) -> Result<(), PackagedArtifactAccessSummaryValidationError> {
656 validate_packaged_artifact_access_summary_line(self.summary_line())?;
657 if self.explicit_path_loading != packaged_artifact_path_loading_enabled() {
658 return Err(
659 PackagedArtifactAccessSummaryValidationError::FeatureStateOutOfSync {
660 field: "explicit_path_loading",
661 },
662 );
663 }
664 Ok(())
665 }
666}
667
668impl fmt::Display for PackagedArtifactAccessSummary {
669 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
670 f.write_str(self.summary_line())
671 }
672}
673
674pub const fn packaged_artifact_path_loading_enabled() -> bool {
676 cfg!(feature = "packaged-artifact-path")
677}
678
679pub const fn packaged_artifact_access_summary_details() -> PackagedArtifactAccessSummary {
696 PackagedArtifactAccessSummary {
697 explicit_path_loading: packaged_artifact_path_loading_enabled(),
698 }
699}
700
701pub fn packaged_artifact_access_summary() -> &'static str {
703 static SUMMARY: OnceLock<String> = OnceLock::new();
704 SUMMARY
705 .get_or_init(|| {
706 let summary = packaged_artifact_access_summary_details();
707 match summary.validated_summary_line() {
708 Ok(rendered) => rendered.to_string(),
709 Err(error) => format!("Packaged artifact access unavailable ({error})"),
710 }
711 })
712 .as_str()
713}
714
715pub fn packaged_artifact_access_summary_for_report() -> String {
717 let summary = packaged_artifact_access_summary_details();
718 match summary.validated_summary_line() {
719 Ok(rendered) => rendered.to_string(),
720 Err(error) => format!("Packaged artifact access: unavailable ({error})"),
721 }
722}
723
724#[derive(Clone, Debug, PartialEq)]
726pub struct PackagedBatchParitySummary {
727 pub request_count: usize,
729 pub body_count: usize,
731 pub ecliptic_request_count: usize,
733 pub equatorial_request_count: usize,
735 pub exact_count: usize,
737 pub interpolated_count: usize,
739 pub approximate_count: usize,
741 pub unknown_count: usize,
743 pub order_preserved: bool,
745 pub single_query_parity_preserved: bool,
747}
748
749#[derive(Clone, Debug, Eq, PartialEq)]
751pub enum PackagedBatchParitySummaryValidationError {
752 FieldOutOfSync { field: &'static str },
754}
755
756impl fmt::Display for PackagedBatchParitySummaryValidationError {
757 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
758 match self {
759 Self::FieldOutOfSync { field } => write!(
760 f,
761 "the packaged mixed-frame batch-parity summary field `{field}` is out of sync with the current packaged posture"
762 ),
763 }
764 }
765}
766
767impl std::error::Error for PackagedBatchParitySummaryValidationError {}
768
769impl PackagedBatchParitySummary {
770 pub fn validate(&self) -> Result<(), PackagedBatchParitySummaryValidationError> {
772 if self.request_count != self.body_count {
773 return Err(PackagedBatchParitySummaryValidationError::FieldOutOfSync {
774 field: "request_count/body_count",
775 });
776 }
777
778 if self.ecliptic_request_count + self.equatorial_request_count != self.request_count {
779 return Err(PackagedBatchParitySummaryValidationError::FieldOutOfSync {
780 field: "frame_counts",
781 });
782 }
783
784 if self.ecliptic_request_count == 0 || self.equatorial_request_count == 0 {
785 return Err(PackagedBatchParitySummaryValidationError::FieldOutOfSync {
786 field: "frame_mix",
787 });
788 }
789
790 if self.exact_count + self.interpolated_count + self.approximate_count + self.unknown_count
791 != self.request_count
792 {
793 return Err(PackagedBatchParitySummaryValidationError::FieldOutOfSync {
794 field: "quality_counts",
795 });
796 }
797
798 if !self.order_preserved {
799 return Err(PackagedBatchParitySummaryValidationError::FieldOutOfSync {
800 field: "order_preserved",
801 });
802 }
803
804 if !self.single_query_parity_preserved {
805 return Err(PackagedBatchParitySummaryValidationError::FieldOutOfSync {
806 field: "single_query_parity_preserved",
807 });
808 }
809
810 Ok(())
811 }
812
813 pub fn validated_summary_line(
815 &self,
816 ) -> Result<String, PackagedBatchParitySummaryValidationError> {
817 self.validate()?;
818 Ok(self.summary_line())
819 }
820}
821
822fn packaged_mixed_frame_batch_parity_request_entries(
823) -> Option<(Vec<EphemerisRequest>, Vec<SnapshotEntry>)> {
824 let snapshot = reference_snapshot();
825 let mut requests = Vec::with_capacity(packaged_bodies().len());
826 let mut entries = Vec::with_capacity(packaged_bodies().len());
827
828 for (index, body) in packaged_bodies().iter().cloned().enumerate() {
829 let entry = packaged_reference_entry_for_body(snapshot, &body)?;
830 entries.push(entry.clone());
831 requests.push(EphemerisRequest {
832 body,
833 instant: Instant::new(entry.epoch.julian_day, TimeScale::Tt),
834 observer: None,
835 frame: if index % 2 == 0 {
836 CoordinateFrame::Ecliptic
837 } else {
838 CoordinateFrame::Equatorial
839 },
840 zodiac_mode: ZodiacMode::Tropical,
841 apparent: Apparentness::Mean,
842 });
843 }
844
845 Some((requests, entries))
846}
847
848pub fn packaged_mixed_frame_batch_parity_requests() -> Option<Vec<EphemerisRequest>> {
850 packaged_mixed_frame_batch_parity_request_entries().map(|(requests, _)| requests)
851}
852
853#[doc(alias = "packaged_mixed_frame_batch_parity_requests")]
855pub fn packaged_mixed_frame_batch_parity_request_corpus() -> Option<Vec<EphemerisRequest>> {
856 packaged_mixed_frame_batch_parity_requests()
857}
858
859pub fn packaged_mixed_frame_batch_parity_summary() -> Option<PackagedBatchParitySummary> {
861 let backend = packaged_backend();
862 let (requests, entries) = packaged_mixed_frame_batch_parity_request_entries()?;
863
864 let results = backend.positions(&requests).ok()?;
865 if results.len() != requests.len() {
866 return None;
867 }
868
869 let mut ecliptic_request_count = 0usize;
870 let mut equatorial_request_count = 0usize;
871 let mut exact_count = 0usize;
872 let mut interpolated_count = 0usize;
873 let mut approximate_count = 0usize;
874 let mut unknown_count = 0usize;
875 let mut order_preserved = true;
876 let mut single_query_parity = true;
877
878 for ((request, result), entry) in requests.iter().zip(results.iter()).zip(entries.iter()) {
879 let single = backend.position(request).ok();
880 single_query_parity &= single.as_ref().is_some_and(|single| single == result);
881
882 order_preserved &= result.body == entry.body
883 && result.instant == request.instant
884 && result.frame == request.frame
885 && result.zodiac_mode == request.zodiac_mode
886 && result.apparent == request.apparent;
887
888 match request.frame {
889 CoordinateFrame::Ecliptic => ecliptic_request_count += 1,
890 CoordinateFrame::Equatorial => equatorial_request_count += 1,
891 _ => return None,
892 }
893
894 match result.quality {
895 QualityAnnotation::Exact => exact_count += 1,
896 QualityAnnotation::Interpolated => interpolated_count += 1,
897 QualityAnnotation::Approximate => approximate_count += 1,
898 QualityAnnotation::Unknown => unknown_count += 1,
899 _ => unknown_count += 1,
900 }
901 }
902
903 Some(PackagedBatchParitySummary {
904 request_count: requests.len(),
905 body_count: entries.len(),
906 ecliptic_request_count,
907 equatorial_request_count,
908 exact_count,
909 interpolated_count,
910 approximate_count,
911 unknown_count,
912 order_preserved,
913 single_query_parity_preserved: single_query_parity,
914 })
915}
916
917impl PackagedBatchParitySummary {
918 pub fn summary_line(&self) -> String {
920 let order = if self.order_preserved {
921 "preserved"
922 } else {
923 "needs attention"
924 };
925 let parity = if self.single_query_parity_preserved {
926 "preserved"
927 } else {
928 "needs attention"
929 };
930 format!(
931 "Packaged mixed frame batch parity: {} requests across {} bodies, ecliptic requests={}, equatorial requests={}; quality counts: Exact={}, Interpolated={}, Approximate={}, Unknown={}; order={}, single-query parity={}",
932 self.request_count,
933 self.body_count,
934 self.ecliptic_request_count,
935 self.equatorial_request_count,
936 self.exact_count,
937 self.interpolated_count,
938 self.approximate_count,
939 self.unknown_count,
940 order,
941 parity,
942 )
943 }
944}
945
946impl fmt::Display for PackagedBatchParitySummary {
947 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
948 f.write_str(&self.summary_line())
949 }
950}
951
952fn format_validated_packaged_mixed_frame_batch_parity_summary_for_report(
953 summary: &PackagedBatchParitySummary,
954) -> String {
955 match summary.validated_summary_line() {
956 Ok(line) => line,
957 Err(error) => format!("Packaged mixed frame batch parity: unavailable ({error})"),
958 }
959}
960
961pub fn packaged_mixed_frame_batch_parity_summary_for_report() -> String {
963 packaged_mixed_frame_batch_parity_summary()
964 .as_ref()
965 .map(format_validated_packaged_mixed_frame_batch_parity_summary_for_report)
966 .unwrap_or_else(|| "Packaged mixed frame batch parity: unavailable".to_string())
967}
968
969pub fn packaged_frame_parity_summary_for_report() -> String {
971 packaged_mixed_frame_batch_parity_summary_for_report()
972}
973
974#[derive(Clone, Debug, PartialEq)]
976pub struct PackagedTimeScaleBatchParitySummary {
977 pub request_count: usize,
979 pub body_count: usize,
981 pub tt_request_count: usize,
983 pub tdb_request_count: usize,
985 pub exact_count: usize,
987 pub interpolated_count: usize,
989 pub approximate_count: usize,
991 pub unknown_count: usize,
993 pub order_preserved: bool,
995 pub single_query_parity_preserved: bool,
997}
998
999#[derive(Clone, Debug, Eq, PartialEq)]
1001pub enum PackagedTimeScaleBatchParitySummaryValidationError {
1002 FieldOutOfSync { field: &'static str },
1004}
1005
1006impl fmt::Display for PackagedTimeScaleBatchParitySummaryValidationError {
1007 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1008 match self {
1009 Self::FieldOutOfSync { field } => write!(
1010 f,
1011 "the packaged mixed TT/TDB batch-parity summary field `{field}` is out of sync with the current packaged posture"
1012 ),
1013 }
1014 }
1015}
1016
1017impl std::error::Error for PackagedTimeScaleBatchParitySummaryValidationError {}
1018
1019fn packaged_mixed_tt_tdb_batch_parity_request_entries(
1020) -> Option<(Vec<EphemerisRequest>, Vec<SnapshotEntry>)> {
1021 let snapshot = reference_snapshot();
1022 let mut requests = Vec::with_capacity(packaged_bodies().len());
1023 let mut entries = Vec::with_capacity(packaged_bodies().len());
1024
1025 for (index, body) in packaged_bodies().iter().cloned().enumerate() {
1026 let entry = packaged_reference_entry_for_body(snapshot, &body)?;
1027 entries.push(entry.clone());
1028 requests.push(EphemerisRequest {
1029 body,
1030 instant: Instant::new(
1031 entry.epoch.julian_day,
1032 if index % 2 == 0 {
1033 TimeScale::Tt
1034 } else {
1035 TimeScale::Tdb
1036 },
1037 ),
1038 observer: None,
1039 frame: CoordinateFrame::Ecliptic,
1040 zodiac_mode: ZodiacMode::Tropical,
1041 apparent: Apparentness::Mean,
1042 });
1043 }
1044
1045 Some((requests, entries))
1046}
1047
1048pub fn packaged_mixed_tt_tdb_batch_parity_requests() -> Option<Vec<EphemerisRequest>> {
1050 packaged_mixed_tt_tdb_batch_parity_request_entries().map(|(requests, _)| requests)
1051}
1052
1053#[doc(alias = "packaged_mixed_tt_tdb_batch_parity_requests")]
1055pub fn packaged_mixed_tt_tdb_batch_parity_request_corpus() -> Option<Vec<EphemerisRequest>> {
1056 packaged_mixed_tt_tdb_batch_parity_requests()
1057}
1058
1059pub fn packaged_mixed_tt_tdb_batch_parity_summary() -> Option<PackagedTimeScaleBatchParitySummary> {
1061 let backend = packaged_backend();
1062 let (requests, entries) = packaged_mixed_tt_tdb_batch_parity_request_entries()?;
1063
1064 let results = backend.positions(&requests).ok()?;
1065 if results.len() != requests.len() {
1066 return None;
1067 }
1068
1069 let mut tt_request_count = 0usize;
1070 let mut tdb_request_count = 0usize;
1071 let mut exact_count = 0usize;
1072 let mut interpolated_count = 0usize;
1073 let mut approximate_count = 0usize;
1074 let mut unknown_count = 0usize;
1075 let mut order_preserved = true;
1076 let mut single_query_parity = true;
1077
1078 for ((request, result), entry) in requests.iter().zip(results.iter()).zip(entries.iter()) {
1079 let single = backend.position(request).ok();
1080 single_query_parity &= single.as_ref().is_some_and(|single| single == result);
1081
1082 order_preserved &= result.body == entry.body
1083 && result.instant == request.instant
1084 && result.frame == request.frame
1085 && result.zodiac_mode == request.zodiac_mode
1086 && result.apparent == request.apparent;
1087
1088 match request.instant.scale {
1089 TimeScale::Tt => tt_request_count += 1,
1090 TimeScale::Tdb => tdb_request_count += 1,
1091 _ => return None,
1092 }
1093
1094 match result.quality {
1095 QualityAnnotation::Exact => exact_count += 1,
1096 QualityAnnotation::Interpolated => interpolated_count += 1,
1097 QualityAnnotation::Approximate => approximate_count += 1,
1098 QualityAnnotation::Unknown => unknown_count += 1,
1099 _ => unknown_count += 1,
1100 }
1101 }
1102
1103 Some(PackagedTimeScaleBatchParitySummary {
1104 request_count: requests.len(),
1105 body_count: entries.len(),
1106 tt_request_count,
1107 tdb_request_count,
1108 exact_count,
1109 interpolated_count,
1110 approximate_count,
1111 unknown_count,
1112 order_preserved,
1113 single_query_parity_preserved: single_query_parity,
1114 })
1115}
1116
1117impl PackagedTimeScaleBatchParitySummary {
1118 pub fn validate(&self) -> Result<(), PackagedTimeScaleBatchParitySummaryValidationError> {
1120 if self.request_count != self.body_count {
1121 return Err(
1122 PackagedTimeScaleBatchParitySummaryValidationError::FieldOutOfSync {
1123 field: "request_count/body_count",
1124 },
1125 );
1126 }
1127
1128 if self.tt_request_count + self.tdb_request_count != self.request_count {
1129 return Err(
1130 PackagedTimeScaleBatchParitySummaryValidationError::FieldOutOfSync {
1131 field: "time_scale_counts",
1132 },
1133 );
1134 }
1135
1136 if self.tt_request_count == 0 || self.tdb_request_count == 0 {
1137 return Err(
1138 PackagedTimeScaleBatchParitySummaryValidationError::FieldOutOfSync {
1139 field: "time_scale_mix",
1140 },
1141 );
1142 }
1143
1144 if self.exact_count + self.interpolated_count + self.approximate_count + self.unknown_count
1145 != self.request_count
1146 {
1147 return Err(
1148 PackagedTimeScaleBatchParitySummaryValidationError::FieldOutOfSync {
1149 field: "quality_counts",
1150 },
1151 );
1152 }
1153
1154 if !self.order_preserved {
1155 return Err(
1156 PackagedTimeScaleBatchParitySummaryValidationError::FieldOutOfSync {
1157 field: "order_preserved",
1158 },
1159 );
1160 }
1161
1162 if !self.single_query_parity_preserved {
1163 return Err(
1164 PackagedTimeScaleBatchParitySummaryValidationError::FieldOutOfSync {
1165 field: "single_query_parity_preserved",
1166 },
1167 );
1168 }
1169
1170 Ok(())
1171 }
1172
1173 pub fn validated_summary_line(
1175 &self,
1176 ) -> Result<String, PackagedTimeScaleBatchParitySummaryValidationError> {
1177 self.validate()?;
1178 Ok(self.summary_line())
1179 }
1180
1181 pub fn summary_line(&self) -> String {
1183 let order = if self.order_preserved {
1184 "preserved"
1185 } else {
1186 "needs attention"
1187 };
1188 let parity = if self.single_query_parity_preserved {
1189 "preserved"
1190 } else {
1191 "needs attention"
1192 };
1193 format!(
1194 "Packaged mixed TT/TDB batch parity: {} requests across {} bodies, TT requests={}, TDB requests={}; quality counts: Exact={}, Interpolated={}, Approximate={}, Unknown={}; order={}, single-query parity={}",
1195 self.request_count,
1196 self.body_count,
1197 self.tt_request_count,
1198 self.tdb_request_count,
1199 self.exact_count,
1200 self.interpolated_count,
1201 self.approximate_count,
1202 self.unknown_count,
1203 order,
1204 parity,
1205 )
1206 }
1207}
1208
1209impl fmt::Display for PackagedTimeScaleBatchParitySummary {
1210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1211 f.write_str(&self.summary_line())
1212 }
1213}
1214
1215fn format_validated_packaged_mixed_tt_tdb_batch_parity_summary_for_report(
1216 summary: &PackagedTimeScaleBatchParitySummary,
1217) -> String {
1218 match summary.validated_summary_line() {
1219 Ok(line) => line,
1220 Err(error) => format!("Packaged mixed TT/TDB batch parity: unavailable ({error})"),
1221 }
1222}
1223
1224pub fn packaged_mixed_tt_tdb_batch_parity_summary_for_report() -> String {
1226 packaged_mixed_tt_tdb_batch_parity_summary()
1227 .as_ref()
1228 .map(format_validated_packaged_mixed_tt_tdb_batch_parity_summary_for_report)
1229 .unwrap_or_else(|| "Packaged mixed TT/TDB batch parity: unavailable".to_string())
1230}
1231
1232pub fn packaged_lookup(
1247 body: &CelestialBody,
1248 instant: Instant,
1249) -> Result<EclipticCoordinates, pleiades_compression::CompressionError> {
1250 packaged_artifact().lookup_ecliptic(body, normalize_lookup_instant(instant))
1251}
1252
1253pub fn packaged_backend() -> PackagedDataBackend {
1270 PackagedDataBackend::new()
1271}
1272
1273pub fn packaged_backend_from_artifact(artifact: CompressedArtifact) -> PackagedDataBackend {
1275 PackagedDataBackend::from_artifact(artifact)
1276}
1277
1278pub fn packaged_backend_from_bytes(
1296 bytes: &[u8],
1297) -> Result<PackagedDataBackend, PackagedArtifactLoadError> {
1298 PackagedDataBackend::from_bytes(bytes)
1299}
1300
1301#[cfg(feature = "packaged-artifact-path")]
1302pub fn packaged_backend_from_path(
1325 path: impl AsRef<Path>,
1326) -> Result<PackagedDataBackend, PackagedArtifactLoadError> {
1327 PackagedDataBackend::from_path(path)
1328}