Skip to main content

pleiades_data/coverage/
fit.rs

1use super::*;
2
3/// Structured fit envelope for the packaged artifact.
4#[derive(Clone, Debug, PartialEq)]
5pub struct PackagedArtifactFitEnvelopeSummary {
6    /// Number of successfully measured segment samples.
7    pub sample_count: usize,
8    /// Number of planned segment samples for the current artifact layout.
9    pub expected_sample_count: usize,
10    /// Number of bundled bodies covered by the measured sample set.
11    pub body_count: usize,
12    /// Mean absolute longitude delta in degrees.
13    pub mean_longitude_delta_degrees: f64,
14    /// Mean absolute latitude delta in degrees.
15    pub mean_latitude_delta_degrees: f64,
16    /// Mean absolute distance delta in AU.
17    pub mean_distance_delta_au: f64,
18    /// Maximum absolute longitude delta in degrees.
19    pub max_longitude_delta_degrees: f64,
20    /// Maximum absolute latitude delta in degrees.
21    pub max_latitude_delta_degrees: f64,
22    /// Maximum absolute distance delta in AU.
23    pub max_distance_delta_au: f64,
24}
25
26/// A packaged-artifact fit threshold violation.
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct PackagedArtifactFitThresholdViolation {
29    /// The field that exceeds the calibrated threshold.
30    pub field: &'static str,
31    /// The measured value, encoded as raw bits so the summary stays lossless.
32    pub measured_bits: u64,
33    /// The calibrated threshold, encoded as raw bits so the summary stays lossless.
34    pub threshold_bits: u64,
35    /// The amount by which the measured value exceeds the calibrated threshold.
36    pub overage_bits: u64,
37}
38
39impl PackagedArtifactFitThresholdViolation {
40    fn summary_line(&self) -> String {
41        format!(
42            "`{}` measured={:.12}, threshold={:.12}, overage={:+.12}",
43            self.field,
44            f64::from_bits(self.measured_bits),
45            f64::from_bits(self.threshold_bits),
46            f64::from_bits(self.overage_bits),
47        )
48    }
49}
50
51pub(crate) fn packaged_artifact_fit_threshold_violations_from_envelope_and_thresholds(
52    envelope: &PackagedArtifactFitEnvelopeSummary,
53    thresholds: &PackagedArtifactFitThresholdSummary,
54) -> Vec<PackagedArtifactFitThresholdViolation> {
55    let mut violations = Vec::new();
56
57    macro_rules! check_threshold {
58        ($field:literal, $measured:expr, $threshold:expr) => {
59            if $measured > $threshold {
60                violations.push(PackagedArtifactFitThresholdViolation {
61                    field: $field,
62                    measured_bits: $measured.to_bits(),
63                    threshold_bits: $threshold.to_bits(),
64                    overage_bits: ($measured - $threshold).to_bits(),
65                });
66            }
67        };
68    }
69
70    check_threshold!(
71        "mean_longitude_delta_degrees",
72        envelope.mean_longitude_delta_degrees,
73        thresholds.max_mean_longitude_delta_degrees
74    );
75    check_threshold!(
76        "mean_latitude_delta_degrees",
77        envelope.mean_latitude_delta_degrees,
78        thresholds.max_mean_latitude_delta_degrees
79    );
80    check_threshold!(
81        "mean_distance_delta_au",
82        envelope.mean_distance_delta_au,
83        thresholds.max_mean_distance_delta_au
84    );
85    check_threshold!(
86        "max_longitude_delta_degrees",
87        envelope.max_longitude_delta_degrees,
88        thresholds.max_longitude_delta_degrees
89    );
90    check_threshold!(
91        "max_latitude_delta_degrees",
92        envelope.max_latitude_delta_degrees,
93        thresholds.max_latitude_delta_degrees
94    );
95    check_threshold!(
96        "max_distance_delta_au",
97        envelope.max_distance_delta_au,
98        thresholds.max_distance_delta_au
99    );
100
101    violations
102}
103
104/// Validation error for a packaged-artifact fit envelope that drifted from the current posture.
105#[derive(Clone, Debug, Eq, PartialEq)]
106pub enum PackagedArtifactFitEnvelopeSummaryValidationError {
107    /// A rendered summary field no longer matches the current packaged-artifact fit envelope.
108    FieldOutOfSync { field: &'static str },
109    /// One or more measured fit fields exceed the calibrated packaged-artifact fit thresholds.
110    ThresholdExceeded {
111        violations: Vec<PackagedArtifactFitThresholdViolation>,
112    },
113}
114
115impl PackagedArtifactFitEnvelopeSummaryValidationError {
116    /// Returns the number of threshold violations captured by the validation error.
117    pub fn violation_count(&self) -> usize {
118        match self {
119            Self::FieldOutOfSync { .. } => 0,
120            Self::ThresholdExceeded { violations } => violations.len(),
121        }
122    }
123
124    /// Returns the compact release-facing summary for the validation error.
125    pub fn summary_line(&self) -> String {
126        match self {
127            Self::FieldOutOfSync { field } => format!(
128                "the packaged artifact fit envelope summary field `{field}` is out of sync with the current posture"
129            ),
130            Self::ThresholdExceeded { violations } => {
131                let rendered = violations
132                    .iter()
133                    .map(PackagedArtifactFitThresholdViolation::summary_line)
134                    .collect::<Vec<_>>()
135                    .join("; ");
136                let violation_count = violations.len();
137                let violation_label = if violation_count == 1 {
138                    "violation"
139                } else {
140                    "violations"
141                };
142                format!(
143                    "the packaged artifact fit envelope summary exceeds the calibrated fit thresholds ({violation_count} {violation_label}): {rendered}"
144                )
145            }
146        }
147    }
148}
149
150impl fmt::Display for PackagedArtifactFitEnvelopeSummaryValidationError {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        f.write_str(&self.summary_line())
153    }
154}
155
156impl std::error::Error for PackagedArtifactFitEnvelopeSummaryValidationError {}
157
158/// Calibrated fit thresholds for the packaged artifact.
159#[derive(Clone, Debug, PartialEq)]
160pub struct PackagedArtifactFitThresholdSummary {
161    /// Maximum allowed mean absolute longitude delta in degrees.
162    pub max_mean_longitude_delta_degrees: f64,
163    /// Maximum allowed mean absolute latitude delta in degrees.
164    pub max_mean_latitude_delta_degrees: f64,
165    /// Maximum allowed mean absolute distance delta in AU.
166    pub max_mean_distance_delta_au: f64,
167    /// Maximum allowed absolute longitude delta in degrees.
168    pub max_longitude_delta_degrees: f64,
169    /// Maximum allowed absolute latitude delta in degrees.
170    pub max_latitude_delta_degrees: f64,
171    /// Maximum allowed absolute distance delta in AU.
172    pub max_distance_delta_au: f64,
173}
174
175/// Validation error for a packaged-artifact fit threshold summary that drifted from the current posture.
176#[derive(Clone, Debug, Eq, PartialEq)]
177pub enum PackagedArtifactFitThresholdSummaryValidationError {
178    /// A summary field is out of sync with the current packaged-artifact fit threshold posture.
179    FieldOutOfSync { field: &'static str },
180}
181
182impl PackagedArtifactFitThresholdSummaryValidationError {
183    /// Returns the compact release-facing summary for the validation error.
184    pub fn summary_line(&self) -> String {
185        match self {
186            Self::FieldOutOfSync { field } => format!(
187                "the packaged artifact fit threshold summary field `{field}` is out of sync with the current posture"
188            ),
189        }
190    }
191}
192
193impl fmt::Display for PackagedArtifactFitThresholdSummaryValidationError {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        f.write_str(&self.summary_line())
196    }
197}
198
199impl std::error::Error for PackagedArtifactFitThresholdSummaryValidationError {}
200
201impl PackagedArtifactFitThresholdSummary {
202    /// Returns the calibrated fit thresholds as a compact human-readable line.
203    pub fn summary_line(&self) -> String {
204        format!(
205            "fit thresholds: mean Δlon≤{:.12}°, mean Δlat≤{:.12}°, mean Δdist≤{:.12} AU; max Δlon≤{:.12}°, max Δlat≤{:.12}°, max Δdist≤{:.12} AU",
206            self.max_mean_longitude_delta_degrees,
207            self.max_mean_latitude_delta_degrees,
208            self.max_mean_distance_delta_au,
209            self.max_longitude_delta_degrees,
210            self.max_latitude_delta_degrees,
211            self.max_distance_delta_au,
212        )
213    }
214
215    /// Returns `Ok(())` when the summary still matches the current packaged-artifact fit thresholds.
216    pub fn validate(&self) -> Result<(), PackagedArtifactFitThresholdSummaryValidationError> {
217        if self != &PACKAGED_ARTIFACT_FIT_THRESHOLD_SUMMARY {
218            return Err(
219                PackagedArtifactFitThresholdSummaryValidationError::FieldOutOfSync {
220                    field: "thresholds",
221                },
222            );
223        }
224
225        Ok(())
226    }
227
228    /// Returns the validated calibrated fit thresholds as a compact human-readable line.
229    pub fn validated_summary_line(
230        &self,
231    ) -> Result<String, PackagedArtifactFitThresholdSummaryValidationError> {
232        self.validate()?;
233        Ok(self.summary_line())
234    }
235}
236
237impl fmt::Display for PackagedArtifactFitThresholdSummary {
238    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239        f.write_str(&self.summary_line())
240    }
241}
242
243/// Structured fit margins for the packaged artifact.
244#[derive(Clone, Debug, PartialEq)]
245pub struct PackagedArtifactFitMarginSummary {
246    /// Measured fit envelope for the current packaged artifact.
247    pub envelope: PackagedArtifactFitEnvelopeSummary,
248    /// Calibrated thresholds used to compute the current margins.
249    pub thresholds: PackagedArtifactFitThresholdSummary,
250}
251
252impl PackagedArtifactFitMarginSummary {
253    /// Returns the fit margins relative to the calibrated thresholds as a compact human-readable line.
254    pub fn summary_line(&self) -> String {
255        format!(
256            "fit margins: mean Δlon={:+.12}°, mean Δlat={:+.12}°, mean Δdist={:+.12} AU; max Δlon={:+.12}°, max Δlat={:+.12}°, max Δdist={:+.12} AU",
257            self.thresholds.max_mean_longitude_delta_degrees - self.envelope.mean_longitude_delta_degrees,
258            self.thresholds.max_mean_latitude_delta_degrees - self.envelope.mean_latitude_delta_degrees,
259            self.thresholds.max_mean_distance_delta_au - self.envelope.mean_distance_delta_au,
260            self.thresholds.max_longitude_delta_degrees - self.envelope.max_longitude_delta_degrees,
261            self.thresholds.max_latitude_delta_degrees - self.envelope.max_latitude_delta_degrees,
262            self.thresholds.max_distance_delta_au - self.envelope.max_distance_delta_au,
263        )
264    }
265
266    /// Returns `Ok(())` when the summary still matches the current packaged-artifact fit posture.
267    pub fn validate(&self) -> Result<(), PackagedArtifactFitEnvelopeSummaryValidationError> {
268        let current_envelope = packaged_artifact_fit_envelope_summary_details();
269        if self.envelope != current_envelope {
270            return Err(
271                PackagedArtifactFitEnvelopeSummaryValidationError::FieldOutOfSync {
272                    field: "envelope",
273                },
274            );
275        }
276
277        let current_thresholds = packaged_artifact_fit_threshold_summary_details();
278        if self.thresholds != current_thresholds {
279            return Err(
280                PackagedArtifactFitEnvelopeSummaryValidationError::FieldOutOfSync {
281                    field: "thresholds",
282                },
283            );
284        }
285
286        self.envelope.validate_against_thresholds(&self.thresholds)
287    }
288
289    /// Returns the validated fit margins relative to the calibrated thresholds as a compact human-readable line.
290    pub fn validated_summary_line(
291        &self,
292    ) -> Result<String, PackagedArtifactFitEnvelopeSummaryValidationError> {
293        self.validate()?;
294        Ok(self.summary_line())
295    }
296}
297
298impl fmt::Display for PackagedArtifactFitMarginSummary {
299    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300        f.write_str(&self.summary_line())
301    }
302}
303
304/// Packaged-artifact fit threshold violations captured for the current posture.
305#[derive(Clone, Debug, PartialEq, Eq)]
306pub struct PackagedArtifactFitThresholdViolationsSummary {
307    /// Threshold violations ordered by field as they appear in the envelope.
308    pub violations: Vec<PackagedArtifactFitThresholdViolation>,
309}
310
311/// Validation error for a packaged-artifact fit threshold violation summary that drifted from the current posture.
312#[derive(Clone, Debug, Eq, PartialEq)]
313pub enum PackagedArtifactFitThresholdViolationsSummaryValidationError {
314    /// A summary field is out of sync with the current packaged-artifact fit threshold violation posture.
315    FieldOutOfSync { field: &'static str },
316}
317
318impl PackagedArtifactFitThresholdViolationsSummaryValidationError {
319    /// Returns the compact release-facing summary for the validation error.
320    pub fn summary_line(&self) -> String {
321        match self {
322            Self::FieldOutOfSync { field } => format!(
323                "the packaged artifact fit threshold violation summary field `{field}` is out of sync with the current posture"
324            ),
325        }
326    }
327}
328
329impl fmt::Display for PackagedArtifactFitThresholdViolationsSummaryValidationError {
330    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331        f.write_str(&self.summary_line())
332    }
333}
334
335impl std::error::Error for PackagedArtifactFitThresholdViolationsSummaryValidationError {}
336
337impl PackagedArtifactFitThresholdViolationsSummary {
338    /// Returns the packaged-artifact fit threshold violations as a compact human-readable line.
339    pub fn summary_line(&self) -> String {
340        let violation_count = self.violations.len();
341        if violation_count == 0 {
342            return "fit threshold violations: 0; details: none".to_string();
343        }
344
345        let rendered = self
346            .violations
347            .iter()
348            .map(PackagedArtifactFitThresholdViolation::summary_line)
349            .collect::<Vec<_>>()
350            .join("; ");
351        let violation_label = if violation_count == 1 {
352            "violation"
353        } else {
354            "violations"
355        };
356        format!(
357            "fit threshold violations: {violation_count} {violation_label}; details: {rendered}"
358        )
359    }
360
361    /// Returns `Ok(())` when the summary still matches the current packaged-artifact fit threshold violation posture.
362    pub fn validate(
363        &self,
364    ) -> Result<(), PackagedArtifactFitThresholdViolationsSummaryValidationError> {
365        let current = packaged_artifact_fit_threshold_violation_summary_details();
366        if self == &current {
367            Ok(())
368        } else {
369            Err(
370                PackagedArtifactFitThresholdViolationsSummaryValidationError::FieldOutOfSync {
371                    field: "violations",
372                },
373            )
374        }
375    }
376
377    /// Returns the validated packaged-artifact fit threshold violations as a compact human-readable line.
378    pub fn validated_summary_line(
379        &self,
380    ) -> Result<String, PackagedArtifactFitThresholdViolationsSummaryValidationError> {
381        self.validate()?;
382        Ok(self.summary_line())
383    }
384}
385
386impl fmt::Display for PackagedArtifactFitThresholdViolationsSummary {
387    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388        f.write_str(&self.summary_line())
389    }
390}
391
392impl PackagedArtifactFitEnvelopeSummary {
393    /// Returns `Ok(())` when the measured fit envelope stays within the calibrated thresholds.
394    pub fn validate_against_thresholds(
395        &self,
396        thresholds: &PackagedArtifactFitThresholdSummary,
397    ) -> Result<(), PackagedArtifactFitEnvelopeSummaryValidationError> {
398        let violations = packaged_artifact_fit_threshold_violations_from_envelope_and_thresholds(
399            self, thresholds,
400        );
401
402        if violations.is_empty() {
403            Ok(())
404        } else {
405            Err(PackagedArtifactFitEnvelopeSummaryValidationError::ThresholdExceeded { violations })
406        }
407    }
408}
409
410#[derive(Clone, Debug, PartialEq)]
411pub(crate) struct PackagedArtifactFitSample {
412    pub(crate) body: CelestialBody,
413    pub(crate) segment_start: Instant,
414    pub(crate) segment_end: Instant,
415    pub(crate) sample_instant: Instant,
416    pub(crate) sample_fraction: f64,
417    pub(crate) longitude_delta_degrees: f64,
418    pub(crate) latitude_delta_degrees: f64,
419    pub(crate) distance_delta_au: f64,
420}
421
422#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
423pub(crate) struct PackagedArtifactFitSegmentFamilyKey {
424    segment_start_days_bits: u64,
425    segment_start_scale: TimeScale,
426    segment_end_days_bits: u64,
427    segment_end_scale: TimeScale,
428}
429
430impl PackagedArtifactFitSegmentFamilyKey {
431    pub(crate) fn from_sample(sample: &PackagedArtifactFitSample) -> Self {
432        Self {
433            segment_start_days_bits: sample.segment_start.julian_day.days().to_bits(),
434            segment_start_scale: sample.segment_start.scale,
435            segment_end_days_bits: sample.segment_end.julian_day.days().to_bits(),
436            segment_end_scale: sample.segment_end.scale,
437        }
438    }
439}
440
441#[derive(Clone, Debug)]
442pub(crate) struct PackagedArtifactFitChannelFamilyAccumulator {
443    sample_count: usize,
444    worst_sample: Option<PackagedArtifactFitSample>,
445}
446
447impl PackagedArtifactFitChannelFamilyAccumulator {
448    pub(crate) fn new() -> Self {
449        Self {
450            sample_count: 0,
451            worst_sample: None,
452        }
453    }
454
455    pub(crate) fn push(&mut self, sample: &PackagedArtifactFitSample, channel: ChannelKind) {
456        self.sample_count += 1;
457        let should_replace = self
458            .worst_sample
459            .as_ref()
460            .map(|existing| {
461                let existing_delta = packaged_artifact_fit_channel_delta(existing, channel);
462                let candidate_delta = packaged_artifact_fit_channel_delta(sample, channel);
463                candidate_delta > existing_delta
464                    || (candidate_delta == existing_delta
465                        && (sample.segment_end.julian_day.days()
466                            - sample.segment_start.julian_day.days())
467                            < (existing.segment_end.julian_day.days()
468                                - existing.segment_start.julian_day.days()))
469            })
470            .unwrap_or(true);
471
472        if should_replace {
473            self.worst_sample = Some(sample.clone());
474        }
475    }
476
477    pub(crate) fn finish(self, channel: ChannelKind) -> Option<PackagedArtifactFitChannelOutlier> {
478        self.worst_sample.as_ref().map(|sample| {
479            PackagedArtifactFitChannelOutlier::from_sample(sample, channel, self.sample_count)
480        })
481    }
482}
483
484#[derive(Clone, Debug, PartialEq)]
485pub struct PackagedArtifactFitChannelOutlier {
486    /// Channel whose fit error is being tracked.
487    pub channel: ChannelKind,
488    /// Absolute delta for the channel.
489    pub delta: f64,
490    /// Inclusive segment start for the source interval.
491    pub segment_start: Instant,
492    /// Inclusive segment end for the source interval.
493    pub segment_end: Instant,
494    /// Segment span in days.
495    pub segment_span_days: f64,
496    /// Sample instant that produced the tracked delta.
497    pub sample_instant: Instant,
498    /// Sample position inside the segment, expressed as a normalized fraction.
499    pub sample_fraction: f64,
500    /// Number of fit samples that shared the same body/channel/segment family.
501    pub sample_count: usize,
502}
503
504impl PackagedArtifactFitChannelOutlier {
505    fn from_sample(
506        sample: &PackagedArtifactFitSample,
507        channel: ChannelKind,
508        sample_count: usize,
509    ) -> Self {
510        Self {
511            channel,
512            delta: packaged_artifact_fit_channel_delta(sample, channel),
513            segment_start: sample.segment_start,
514            segment_end: sample.segment_end,
515            segment_span_days: sample.segment_end.julian_day.days()
516                - sample.segment_start.julian_day.days(),
517            sample_instant: sample.sample_instant,
518            sample_fraction: sample.sample_fraction,
519            sample_count,
520        }
521    }
522
523    fn delta_unit(&self) -> &'static str {
524        match self.channel {
525            ChannelKind::Longitude | ChannelKind::Latitude => "°",
526            ChannelKind::DistanceAu => " AU",
527            _ => unreachable!("unsupported packaged-artifact channel kind"),
528        }
529    }
530
531    fn summary_line(&self) -> String {
532        format!(
533            "{}={:.12}{} @ {} (segment {} → {}, span={:.12} d, x={:.3}, samples={})",
534            self.channel,
535            self.delta,
536            self.delta_unit(),
537            self.sample_instant,
538            self.segment_start,
539            self.segment_end,
540            self.segment_span_days,
541            self.sample_fraction,
542            self.sample_count,
543        )
544    }
545}
546
547impl fmt::Display for PackagedArtifactFitChannelOutlier {
548    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
549        f.write_str(&self.summary_line())
550    }
551}
552
553#[derive(Clone, Debug, PartialEq)]
554pub struct PackagedArtifactFitBodyOutlierSummary {
555    /// Bundled body whose fit outliers are summarized.
556    pub body: CelestialBody,
557    /// Worst sampled delta for each stored channel.
558    pub channel_outliers: Vec<PackagedArtifactFitChannelOutlier>,
559}
560
561impl PackagedArtifactFitBodyOutlierSummary {
562    pub(crate) fn summary_line(&self) -> String {
563        format!("{}{{{}}}", self.body, join_display(&self.channel_outliers))
564    }
565}
566
567impl fmt::Display for PackagedArtifactFitBodyOutlierSummary {
568    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
569        f.write_str(&self.summary_line())
570    }
571}
572
573#[derive(Clone, Debug, PartialEq)]
574pub struct PackagedArtifactFitOutlierSummary {
575    /// Number of bundled bodies represented in the outlier report.
576    pub body_count: usize,
577    /// Body-level summaries for the worst sampled fit deltas.
578    pub body_summaries: Vec<PackagedArtifactFitBodyOutlierSummary>,
579}
580
581impl PackagedArtifactFitOutlierSummary {
582    /// Returns the body/channel fit outliers as a compact human-readable line.
583    pub fn summary_line(&self) -> String {
584        format!(
585            "fit outliers: {} bundled bodies; {}",
586            self.body_count,
587            join_display(&self.body_summaries)
588        )
589    }
590
591    /// Returns `Ok(())` when the summary still matches the current packaged artifact.
592    pub fn validate(&self) -> Result<(), PackagedArtifactFitEnvelopeSummaryValidationError> {
593        let expected = packaged_artifact_fit_outlier_summary_details();
594        if self != &expected {
595            return Err(
596                PackagedArtifactFitEnvelopeSummaryValidationError::FieldOutOfSync {
597                    field: "outlier summary",
598                },
599            );
600        }
601
602        Ok(())
603    }
604
605    /// Returns the validated body/channel fit outliers as a compact human-readable line.
606    pub fn validated_summary_line(
607        &self,
608    ) -> Result<String, PackagedArtifactFitEnvelopeSummaryValidationError> {
609        self.validate()?;
610        Ok(self.summary_line())
611    }
612}
613
614impl fmt::Display for PackagedArtifactFitOutlierSummary {
615    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
616        f.write_str(&self.summary_line())
617    }
618}
619
620#[derive(Clone, Debug, PartialEq)]
621pub struct PackagedArtifactFitChannelOutlierSummary {
622    /// Channel-level summaries for the worst sampled fit deltas.
623    pub channel_summaries: Vec<String>,
624}
625
626/// Validation error for a packaged-artifact fit outlier-by-channel summary that drifted from the current posture.
627#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
628pub enum PackagedArtifactFitChannelOutlierSummaryValidationError {
629    /// A summary field is out of sync with the current packaged-artifact posture.
630    FieldOutOfSync { field: &'static str },
631}
632
633impl PackagedArtifactFitChannelOutlierSummaryValidationError {
634    /// Returns the compact release-facing summary for the validation error.
635    pub fn summary_line(&self) -> String {
636        match self {
637            Self::FieldOutOfSync { field } => format!(
638                "the packaged artifact fit outlier-by-channel summary field `{field}` is out of sync with the current posture"
639            ),
640        }
641    }
642}
643
644impl fmt::Display for PackagedArtifactFitChannelOutlierSummaryValidationError {
645    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
646        f.write_str(&self.summary_line())
647    }
648}
649
650impl std::error::Error for PackagedArtifactFitChannelOutlierSummaryValidationError {}
651
652impl PackagedArtifactFitChannelOutlierSummary {
653    /// Returns the channel-level fit outliers as a compact human-readable line.
654    pub fn summary_line(&self) -> String {
655        if self.channel_summaries.is_empty() {
656            "fit outliers by channel: none".to_string()
657        } else {
658            format!(
659                "fit outliers by channel: {}",
660                self.channel_summaries.join("; ")
661            )
662        }
663    }
664
665    /// Returns `Ok(())` when the summary still matches the current packaged artifact.
666    pub fn validate(&self) -> Result<(), PackagedArtifactFitChannelOutlierSummaryValidationError> {
667        let expected = packaged_artifact_fit_channel_outlier_summary_details();
668        if self != &expected {
669            return Err(
670                PackagedArtifactFitChannelOutlierSummaryValidationError::FieldOutOfSync {
671                    field: "channel_summaries",
672                },
673            );
674        }
675
676        Ok(())
677    }
678
679    /// Returns the validated channel-level fit outliers as a compact human-readable line.
680    pub fn validated_summary_line(
681        &self,
682    ) -> Result<String, PackagedArtifactFitChannelOutlierSummaryValidationError> {
683        self.validate()?;
684        Ok(self.summary_line())
685    }
686}
687
688impl fmt::Display for PackagedArtifactFitChannelOutlierSummary {
689    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
690        f.write_str(&self.summary_line())
691    }
692}
693
694pub(crate) fn packaged_artifact_fit_channel_outlier_summary_for_channel(
695    samples: &[PackagedArtifactFitSample],
696    channel: ChannelKind,
697) -> Option<String> {
698    let mut families: HashMap<
699        (CelestialBody, PackagedArtifactFitSegmentFamilyKey),
700        PackagedArtifactFitChannelFamilyAccumulator,
701    > = HashMap::new();
702
703    for sample in samples {
704        let family_key = PackagedArtifactFitSegmentFamilyKey::from_sample(sample);
705        let entry = families
706            .entry((sample.body.clone(), family_key))
707            .or_insert_with(PackagedArtifactFitChannelFamilyAccumulator::new);
708        entry.push(sample, channel);
709    }
710
711    let mut body_outliers: HashMap<CelestialBody, PackagedArtifactFitChannelOutlier> =
712        HashMap::new();
713
714    for ((body, _family_key), family) in families {
715        let Some(candidate) = family.finish(channel) else {
716            continue;
717        };
718        match body_outliers.get_mut(&body) {
719            Some(existing) if existing.delta > candidate.delta => {}
720            Some(existing)
721                if existing.delta == candidate.delta
722                    && existing.segment_span_days <= candidate.segment_span_days => {}
723            Some(existing) => *existing = candidate,
724            None => {
725                body_outliers.insert(body, candidate);
726            }
727        }
728    }
729
730    if body_outliers.is_empty() {
731        return None;
732    }
733
734    let mut body_entries = body_outliers
735        .into_iter()
736        .map(|(body, outlier)| format!("{body}{{{outlier}}}"))
737        .collect::<Vec<_>>();
738    body_entries.sort();
739
740    Some(format!("{channel}{{{}}}", body_entries.join(", ")))
741}
742
743/// Returns the current packaged-artifact fit outliers by channel as a structured summary record.
744pub fn packaged_artifact_fit_channel_outlier_summary_details(
745) -> PackagedArtifactFitChannelOutlierSummary {
746    let samples = packaged_artifact_fit_outlier_samples_for_current_artifact();
747    let mut channel_summaries = Vec::new();
748
749    for channel in [
750        ChannelKind::DistanceAu,
751        ChannelKind::Longitude,
752        ChannelKind::Latitude,
753    ] {
754        if let Some(entry) =
755            packaged_artifact_fit_channel_outlier_summary_for_channel(samples, channel)
756        {
757            channel_summaries.push(entry);
758        }
759    }
760
761    PackagedArtifactFitChannelOutlierSummary { channel_summaries }
762}
763
764// SP1 draft baseline: thresholds are pinned to the measured worst case (overall
765// envelope and the worst per-scope envelope) of the regenerated dense de440-backed
766// draft artifact. Each body is measured against the SAME source it was fit from:
767// major bodies against the dense de440 production reference corpus (1900–2100, ≥3
768// entries/body, no extrapolation), selected-asteroid/custom bodies against the
769// JplSnapshotBackend reference snapshot they were fit against. So the posture
770// reflects measured reality, not an enforced target. These are sample-residual
771// envelopes (not the per-body hold-out accuracy); they are finite and bounded.
772// SP2 tunes these toward real accuracy goals.
773pub(crate) const PACKAGED_ARTIFACT_FIT_MAX_MEAN_LONGITUDE_DELTA_DEGREES: f64 = 79.29937281518961;
774pub(crate) const PACKAGED_ARTIFACT_FIT_MAX_MEAN_LATITUDE_DELTA_DEGREES: f64 =
775    3.320_919_159_432_106_6;
776pub(crate) const PACKAGED_ARTIFACT_FIT_MAX_MEAN_DISTANCE_DELTA_AU: f64 = 5240.2473102557;
777pub(crate) const PACKAGED_ARTIFACT_FIT_MAX_LONGITUDE_DELTA_DEGREES: f64 = 179.999_799_204_804_48;
778pub(crate) const PACKAGED_ARTIFACT_FIT_MAX_LATITUDE_DELTA_DEGREES: f64 = 69.9571184739231;
779pub(crate) const PACKAGED_ARTIFACT_FIT_MAX_DISTANCE_DELTA_AU: f64 = 10227288.989857685;
780
781pub(crate) const PACKAGED_ARTIFACT_FIT_THRESHOLD_SUMMARY: PackagedArtifactFitThresholdSummary =
782    PackagedArtifactFitThresholdSummary {
783        max_mean_longitude_delta_degrees: PACKAGED_ARTIFACT_FIT_MAX_MEAN_LONGITUDE_DELTA_DEGREES,
784        max_mean_latitude_delta_degrees: PACKAGED_ARTIFACT_FIT_MAX_MEAN_LATITUDE_DELTA_DEGREES,
785        max_mean_distance_delta_au: PACKAGED_ARTIFACT_FIT_MAX_MEAN_DISTANCE_DELTA_AU,
786        max_longitude_delta_degrees: PACKAGED_ARTIFACT_FIT_MAX_LONGITUDE_DELTA_DEGREES,
787        max_latitude_delta_degrees: PACKAGED_ARTIFACT_FIT_MAX_LATITUDE_DELTA_DEGREES,
788        max_distance_delta_au: PACKAGED_ARTIFACT_FIT_MAX_DISTANCE_DELTA_AU,
789    };