1use super::*;
2
3#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4pub(crate) enum PackagedArtifactBodyCadence {
5 Luminaries,
6 InnerPlanets,
7 OuterPlanets,
8 Pluto,
9 LunarPoints,
10 SelectedAsteroids,
11 CustomBodies,
12}
13
14impl PackagedArtifactBodyCadence {
15 pub(crate) fn uses_dense_sampling(self) -> bool {
16 matches!(
17 self,
18 Self::Luminaries
19 | Self::Pluto
20 | Self::LunarPoints
21 | Self::SelectedAsteroids
22 | Self::CustomBodies
23 )
24 }
25
26 pub(crate) fn uses_dense_validation_sampling(self) -> bool {
27 matches!(
28 self,
29 Self::Luminaries
30 | Self::InnerPlanets
31 | Self::OuterPlanets
32 | Self::Pluto
33 | Self::LunarPoints
34 | Self::SelectedAsteroids
35 | Self::CustomBodies
36 )
37 }
38
39 pub(crate) fn uses_dense_residual_sample_lattice(self, kind: ChannelKind) -> bool {
40 match kind {
41 ChannelKind::Longitude | ChannelKind::Latitude => self.uses_dense_sampling(),
42 ChannelKind::DistanceAu => {
43 matches!(
44 self,
45 Self::InnerPlanets
46 | Self::OuterPlanets
47 | Self::Pluto
48 | Self::LunarPoints
49 | Self::SelectedAsteroids
50 | Self::CustomBodies
51 )
52 }
53 _ => false,
54 }
55 }
56}
57
58pub(crate) fn packaged_artifact_body_cadence(body: &CelestialBody) -> PackagedArtifactBodyCadence {
59 match body {
60 CelestialBody::Sun | CelestialBody::Moon => PackagedArtifactBodyCadence::Luminaries,
61 CelestialBody::Mercury | CelestialBody::Venus | CelestialBody::Mars => {
62 PackagedArtifactBodyCadence::InnerPlanets
63 }
64 CelestialBody::Jupiter
65 | CelestialBody::Saturn
66 | CelestialBody::Uranus
67 | CelestialBody::Neptune => PackagedArtifactBodyCadence::OuterPlanets,
68 CelestialBody::Pluto => PackagedArtifactBodyCadence::Pluto,
69 CelestialBody::MeanNode
70 | CelestialBody::TrueNode
71 | CelestialBody::MeanApogee
72 | CelestialBody::TrueApogee
73 | CelestialBody::MeanPerigee
74 | CelestialBody::TruePerigee => PackagedArtifactBodyCadence::LunarPoints,
75 CelestialBody::Ceres
76 | CelestialBody::Pallas
77 | CelestialBody::Juno
78 | CelestialBody::Vesta => PackagedArtifactBodyCadence::SelectedAsteroids,
79 CelestialBody::Custom(custom) if custom.catalog.eq_ignore_ascii_case("asteroid") => {
80 PackagedArtifactBodyCadence::SelectedAsteroids
81 }
82 CelestialBody::Custom(_) => PackagedArtifactBodyCadence::CustomBodies,
83 _ => PackagedArtifactBodyCadence::CustomBodies,
84 }
85}
86
87pub(crate) fn packaged_artifact_target_threshold_scope_envelope_summary_details(
88 scope: &'static str,
89) -> PackagedArtifactTargetThresholdScopeSummary {
90 let artifact = packaged_artifact();
91 let bodies: Vec<CelestialBody> = artifact
92 .bodies
93 .iter()
94 .filter(|body| packaged_artifact_body_scope(&body.body) == scope)
95 .map(|body| body.body.clone())
96 .collect();
97 let body_count = bodies.len();
98 let samples = packaged_artifact_fit_samples_for_current_artifact()
99 .iter()
100 .filter(|sample| packaged_artifact_body_scope(&sample.body) == scope)
101 .cloned()
102 .collect::<Vec<_>>();
103 let expected_sample_count =
104 packaged_artifact_fit_expected_sample_count_with_filter(artifact, |body| {
105 packaged_artifact_body_scope(body) == scope
106 });
107 let fit_envelope =
108 packaged_artifact_fit_envelope_summary_from_samples(&samples, expected_sample_count);
109 PackagedArtifactTargetThresholdScopeSummary {
110 scope,
111 bodies,
112 body_count,
113 fit_envelope,
114 }
115}
116
117fn packaged_artifact_target_threshold_scope_envelope_summaries_details(
118) -> Vec<PackagedArtifactTargetThresholdScopeSummary> {
119 PACKAGED_ARTIFACT_TARGET_THRESHOLD_SCOPES
120 .iter()
121 .copied()
122 .map(packaged_artifact_target_threshold_scope_envelope_summary_details)
123 .collect()
124}
125
126pub fn packaged_artifact_target_threshold_scope_envelopes_summary_details(
128) -> PackagedArtifactTargetThresholdScopeEnvelopesSummary {
129 PackagedArtifactTargetThresholdScopeEnvelopesSummary {
130 scope_envelopes: packaged_artifact_target_threshold_scope_envelope_summaries_details(),
131 }
132}
133
134pub(crate) fn format_scope_bodies(bodies: &[CelestialBody]) -> String {
135 match bodies {
136 [] => "0 (none)".to_string(),
137 [single] => format!("1 ({single})"),
138 _ => format!("{} ({})", bodies.len(), join_display(bodies)),
139 }
140}
141
142pub(crate) fn packaged_artifact_quantization_scales_line() -> String {
143 let artifact = packaged_artifact();
144 let stored = packaged_artifact_channel_quantization_scales(artifact, false);
145 let residual = packaged_artifact_channel_quantization_scales(artifact, true);
146
147 if residual.is_empty() {
148 format!("quantization scales: stored={stored}")
149 } else {
150 format!("quantization scales: stored={stored}; residual={residual}")
151 }
152}
153
154fn packaged_artifact_channel_quantization_scales(
155 artifact: &CompressedArtifact,
156 residual_channels: bool,
157) -> String {
158 let entries = [
159 ChannelKind::Longitude,
160 ChannelKind::Latitude,
161 ChannelKind::DistanceAu,
162 ]
163 .into_iter()
164 .filter_map(|kind| {
165 let mut exponents = artifact
166 .bodies
167 .iter()
168 .flat_map(|body| body.segments.iter())
169 .flat_map(|segment| {
170 let channels = if residual_channels {
171 &segment.residual_channels
172 } else {
173 &segment.channels
174 };
175
176 channels
177 .iter()
178 .filter(move |channel| channel.kind == kind)
179 .map(|channel| channel.scale_exponent)
180 .collect::<Vec<_>>()
181 })
182 .collect::<Vec<_>>();
183
184 if exponents.is_empty() {
185 None
186 } else {
187 exponents.sort_unstable();
188 exponents.dedup();
189 Some(format!("{kind}={}", join_display(&exponents)))
190 }
191 })
192 .collect::<Vec<_>>();
193
194 if entries.is_empty() {
195 "none".to_string()
196 } else {
197 entries.join(", ")
198 }
199}
200
201pub(crate) fn packaged_artifact_segment_span_bounds(artifact: &CompressedArtifact) -> (f64, f64) {
202 let mut min_span_days: f64 = f64::INFINITY;
203 let mut max_span_days: f64 = 0.0;
204
205 for body in &artifact.bodies {
206 for segment in &body.segments {
207 let span_days = segment.end.julian_day.days() - segment.start.julian_day.days();
208 min_span_days = min_span_days.min(span_days);
209 max_span_days = max_span_days.max(span_days);
210 }
211 }
212
213 if min_span_days.is_infinite() {
214 (0.0, 0.0)
215 } else {
216 (min_span_days, max_span_days)
217 }
218}
219
220pub(crate) fn packaged_artifact_channel_count(
221 artifact: &CompressedArtifact,
222 residual_channels: bool,
223) -> usize {
224 artifact
225 .bodies
226 .iter()
227 .flat_map(|body| body.segments.iter())
228 .map(|segment| {
229 if residual_channels {
230 segment.residual_channels.len()
231 } else {
232 segment.channels.len()
233 }
234 })
235 .sum()
236}
237
238pub(crate) fn packaged_artifact_encoded_bytes(artifact: &CompressedArtifact) -> usize {
239 artifact
240 .encode()
241 .expect("packaged artifact should be encodable")
242 .len()
243}
244
245#[derive(Clone, Debug, PartialEq)]
247pub struct PackagedArtifactNormalizedIntermediateSummary {
248 pub label: &'static str,
250 pub artifact_version: u16,
252 pub source: &'static str,
254 pub source_revision: String,
256 pub profile_id: &'static str,
258 pub time_range: TimeRange,
260 pub generation_policy: PackagedArtifactGenerationPolicy,
262 pub quantization_scales: String,
264 pub checksum: u64,
266 pub body_count: usize,
268 pub segment_count: usize,
270 pub residual_segment_count: usize,
272 pub stored_channel_count: usize,
274 pub residual_channel_count: usize,
276 pub min_segment_span_days: f64,
278 pub max_segment_span_days: f64,
280}
281
282impl PackagedArtifactNormalizedIntermediateSummary {
283 pub(crate) fn summary_payload_line(&self) -> String {
285 format!(
286 "label={}; profile id={}; version={}; time range={}; source={}; source revision={}; body count={}; segments={}; residual-bearing segments={}; stored channels={}; residual channels={}; segment span days={:.12}..{:.12}; segment strategy={}; {}",
287 self.label,
288 self.profile_id,
289 self.artifact_version,
290 self.time_range,
291 self.source,
292 self.source_revision,
293 self.body_count,
294 self.segment_count,
295 self.residual_segment_count,
296 self.stored_channel_count,
297 self.residual_channel_count,
298 self.min_segment_span_days,
299 self.max_segment_span_days,
300 self.generation_policy.segment_strategy(),
301 self.quantization_scales,
302 )
303 }
304
305 pub fn summary_fields_line(&self) -> String {
307 format!(
308 "{}; checksum=0x{:016x}",
309 self.summary_payload_line(),
310 self.checksum,
311 )
312 }
313
314 pub fn summary_line(&self) -> String {
316 format!(
317 "Packaged artifact normalized intermediates: {}",
318 self.summary_fields_line()
319 )
320 }
321
322 pub fn validate(&self) -> Result<(), pleiades_compression::CompressionError> {
324 let artifact = packaged_artifact();
325 if self.label != ARTIFACT_LABEL {
326 return Err(pleiades_compression::CompressionError::new(
327 pleiades_compression::CompressionErrorKind::InvalidFormat,
328 "packaged artifact normalized intermediate summary label does not match the checked-in artifact label",
329 ));
330 }
331 if self.artifact_version != artifact.header.version {
332 return Err(pleiades_compression::CompressionError::new(
333 pleiades_compression::CompressionErrorKind::InvalidFormat,
334 "packaged artifact normalized intermediate summary artifact version does not match the checked-in packaged artifact version",
335 ));
336 }
337 if self.source != packaged_artifact_source_text() {
338 return Err(pleiades_compression::CompressionError::new(
339 pleiades_compression::CompressionErrorKind::InvalidFormat,
340 "packaged artifact normalized intermediate summary source does not match the checked-in artifact source",
341 ));
342 }
343 if self.source_revision != production_generation_source_summary_for_report() {
344 return Err(pleiades_compression::CompressionError::new(
345 pleiades_compression::CompressionErrorKind::InvalidFormat,
346 "packaged artifact normalized intermediate summary source revision does not match the checked-in production-generation source summary",
347 ));
348 }
349 if self.profile_id != ARTIFACT_PROFILE_ID {
350 return Err(pleiades_compression::CompressionError::new(
351 pleiades_compression::CompressionErrorKind::InvalidFormat,
352 "packaged artifact normalized intermediate summary profile id does not match the checked-in artifact profile id",
353 ));
354 }
355 if self.time_range != artifact_time_range(artifact) {
356 return Err(pleiades_compression::CompressionError::new(
357 pleiades_compression::CompressionErrorKind::InvalidFormat,
358 "packaged artifact normalized intermediate summary time range does not match the checked-in packaged artifact",
359 ));
360 }
361 if self.generation_policy
362 != PackagedArtifactGenerationPolicy::AdjacentSameBodyQuadraticWindows
363 {
364 return Err(pleiades_compression::CompressionError::new(
365 pleiades_compression::CompressionErrorKind::InvalidFormat,
366 "packaged artifact normalized intermediate summary generation policy does not match the checked-in packaged artifact",
367 ));
368 }
369 if self.quantization_scales != packaged_artifact_quantization_scales_line() {
370 return Err(pleiades_compression::CompressionError::new(
371 pleiades_compression::CompressionErrorKind::InvalidFormat,
372 "packaged artifact normalized intermediate summary quantization scales do not match the checked-in packaged artifact",
373 ));
374 }
375 if self.body_count != artifact.bodies.len() {
376 return Err(pleiades_compression::CompressionError::new(
377 pleiades_compression::CompressionErrorKind::InvalidFormat,
378 "packaged artifact normalized intermediate summary body count does not match the checked-in packaged artifact",
379 ));
380 }
381 if self.segment_count != artifact.segment_count() {
382 return Err(pleiades_compression::CompressionError::new(
383 pleiades_compression::CompressionErrorKind::InvalidFormat,
384 "packaged artifact normalized intermediate summary segment count does not match the checked-in packaged artifact",
385 ));
386 }
387 if self.residual_segment_count != artifact.residual_segment_count() {
388 return Err(pleiades_compression::CompressionError::new(
389 pleiades_compression::CompressionErrorKind::InvalidFormat,
390 "packaged artifact normalized intermediate summary residual segment count does not match the checked-in packaged artifact",
391 ));
392 }
393 if self.stored_channel_count != packaged_artifact_channel_count(artifact, false) {
394 return Err(pleiades_compression::CompressionError::new(
395 pleiades_compression::CompressionErrorKind::InvalidFormat,
396 "packaged artifact normalized intermediate summary stored channel count does not match the checked-in packaged artifact",
397 ));
398 }
399 if self.residual_channel_count != packaged_artifact_channel_count(artifact, true) {
400 return Err(pleiades_compression::CompressionError::new(
401 pleiades_compression::CompressionErrorKind::InvalidFormat,
402 "packaged artifact normalized intermediate summary residual channel count does not match the checked-in packaged artifact",
403 ));
404 }
405 let (expected_min_segment_span_days, expected_max_segment_span_days) =
406 packaged_artifact_segment_span_bounds(artifact);
407 if self.min_segment_span_days != expected_min_segment_span_days {
408 return Err(pleiades_compression::CompressionError::new(
409 pleiades_compression::CompressionErrorKind::InvalidFormat,
410 "packaged artifact normalized intermediate summary minimum segment span does not match the checked-in packaged artifact",
411 ));
412 }
413 if self.max_segment_span_days != expected_max_segment_span_days {
414 return Err(pleiades_compression::CompressionError::new(
415 pleiades_compression::CompressionErrorKind::InvalidFormat,
416 "packaged artifact normalized intermediate summary maximum segment span does not match the checked-in packaged artifact",
417 ));
418 }
419
420 let expected_checksum = fnv1a64(self.summary_payload_line().as_bytes());
421 if self.checksum != expected_checksum {
422 return Err(pleiades_compression::CompressionError::new(
423 pleiades_compression::CompressionErrorKind::InvalidFormat,
424 format!(
425 "packaged artifact normalized intermediate summary checksum 0x{:016x} does not match the current normalized-intermediate checksum 0x{:016x}",
426 self.checksum,
427 expected_checksum
428 ),
429 ));
430 }
431
432 Ok(())
433 }
434
435 pub fn validated_summary_line(&self) -> Result<String, pleiades_compression::CompressionError> {
437 self.validate()?;
438 Ok(self.summary_line())
439 }
440}
441
442impl fmt::Display for PackagedArtifactNormalizedIntermediateSummary {
443 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
444 f.write_str(&self.summary_line())
445 }
446}
447
448#[derive(Clone, Debug, PartialEq)]
450pub struct PackagedArtifactRegenerationSummary {
451 pub label: &'static str,
453 pub artifact_version: u16,
455 pub source: &'static str,
457 pub source_revision: String,
459 pub profile_id: &'static str,
461 pub checksum: u64,
463 pub artifact_size_bytes: usize,
465 pub generation_policy: PackagedArtifactGenerationPolicy,
467 pub quantization_scales: String,
469 pub residual_bodies: Vec<CelestialBody>,
471 pub bodies: Vec<CelestialBody>,
473 pub normalized_intermediates: PackagedArtifactNormalizedIntermediateSummary,
475 pub fit_envelope: PackagedArtifactFitEnvelopeSummary,
477 pub reference_snapshot: Option<ReferenceSnapshotSummary>,
479}
480
481impl PackagedArtifactRegenerationSummary {
482 pub fn body_coverage_line(&self) -> String {
484 format!(
485 "{} bundled bodies ({})",
486 self.bodies.len(),
487 join_display(&self.bodies)
488 )
489 }
490
491 pub fn reference_snapshot_line(&self) -> String {
493 self.reference_snapshot
494 .map(|summary| format_reference_snapshot_summary(&summary))
495 .unwrap_or_else(|| "Reference snapshot coverage: unavailable".to_string())
496 }
497
498 pub fn normalized_intermediates_line(&self) -> String {
500 self.normalized_intermediates.summary_fields_line()
501 }
502
503 pub fn residual_body_coverage_summary(&self) -> ArtifactResidualBodyCoverageSummary {
505 ArtifactResidualBodyCoverageSummary::new(self.residual_bodies.clone())
506 }
507
508 pub fn residual_body_line(&self) -> String {
510 self.residual_body_coverage_summary()
511 .summary_line_with_body_count()
512 }
513
514 pub fn generation_policy_line(&self) -> String {
516 format!(
517 "generation policy: {}",
518 self.generation_policy.summary_line()
519 )
520 }
521
522 pub(crate) fn validate_residual_body_subset(
524 &self,
525 ) -> Result<(), pleiades_compression::CompressionError> {
526 for body in &self.residual_bodies {
527 if !self.bodies.contains(body) {
528 return Err(pleiades_compression::CompressionError::new(
529 pleiades_compression::CompressionErrorKind::InvalidFormat,
530 format!(
531 "packaged artifact regeneration summary residual body {body} is not covered by the bundled body list"
532 ),
533 ));
534 }
535 }
536
537 Ok(())
538 }
539
540 pub fn validate(&self) -> Result<(), pleiades_compression::CompressionError> {
544 let artifact = packaged_artifact();
545
546 if self.label != ARTIFACT_LABEL {
547 return Err(pleiades_compression::CompressionError::new(
548 pleiades_compression::CompressionErrorKind::InvalidFormat,
549 "packaged artifact regeneration summary label does not match the checked-in artifact label",
550 ));
551 }
552 if self.source != packaged_artifact_source_text() {
553 return Err(pleiades_compression::CompressionError::new(
554 pleiades_compression::CompressionErrorKind::InvalidFormat,
555 "packaged artifact regeneration summary source does not match the checked-in artifact source",
556 ));
557 }
558 if self.source_revision != production_generation_source_summary_for_report() {
559 return Err(pleiades_compression::CompressionError::new(
560 pleiades_compression::CompressionErrorKind::InvalidFormat,
561 "packaged artifact regeneration summary source revision does not match the checked-in production-generation source summary",
562 ));
563 }
564 self.normalized_intermediates.validate().map_err(|error| {
565 pleiades_compression::CompressionError::new(
566 pleiades_compression::CompressionErrorKind::InvalidFormat,
567 format!(
568 "packaged artifact regeneration summary normalized intermediates are invalid: {error}"
569 ),
570 )
571 })?;
572 if self.profile_id != ARTIFACT_PROFILE_ID {
573 return Err(pleiades_compression::CompressionError::new(
574 pleiades_compression::CompressionErrorKind::InvalidFormat,
575 "packaged artifact regeneration summary profile id does not match the checked-in artifact profile id",
576 ));
577 }
578 if self.artifact_version != artifact.header.version {
579 return Err(pleiades_compression::CompressionError::new(
580 pleiades_compression::CompressionErrorKind::InvalidFormat,
581 format!(
582 "packaged artifact regeneration summary artifact version {} does not match the checked-in packaged artifact version {}",
583 self.artifact_version,
584 artifact.header.version
585 ),
586 ));
587 }
588 if self.checksum != artifact.checksum {
589 return Err(pleiades_compression::CompressionError::new(
590 pleiades_compression::CompressionErrorKind::InvalidFormat,
591 format!(
592 "packaged artifact regeneration summary checksum 0x{:016x} does not match the checked-in packaged artifact checksum 0x{:016x}",
593 self.checksum,
594 artifact.checksum
595 ),
596 ));
597 }
598 let expected_artifact_size_bytes = packaged_artifact_encoded_bytes(artifact);
599 if self.artifact_size_bytes != expected_artifact_size_bytes {
600 return Err(pleiades_compression::CompressionError::new(
601 pleiades_compression::CompressionErrorKind::InvalidFormat,
602 format!(
603 "packaged artifact regeneration summary artifact size {} bytes does not match the checked-in packaged artifact size {} bytes",
604 self.artifact_size_bytes,
605 expected_artifact_size_bytes
606 ),
607 ));
608 }
609 self.generation_policy.validate().map_err(|error| {
610 pleiades_compression::CompressionError::new(
611 pleiades_compression::CompressionErrorKind::InvalidFormat,
612 format!(
613 "packaged artifact regeneration summary generation policy is invalid: {error}"
614 ),
615 )
616 })?;
617
618 if self.quantization_scales != packaged_artifact_quantization_scales_line() {
619 return Err(pleiades_compression::CompressionError::new(
620 pleiades_compression::CompressionErrorKind::InvalidFormat,
621 "packaged artifact regeneration summary quantization scales do not match the checked-in packaged artifact",
622 ));
623 }
624
625 self.residual_body_coverage_summary()
626 .validate(artifact)
627 .map_err(|error| {
628 pleiades_compression::CompressionError::new(
629 pleiades_compression::CompressionErrorKind::InvalidFormat,
630 format!(
631 "packaged artifact regeneration summary residual body coverage is invalid: {error}"
632 ),
633 )
634 })?;
635
636 if self.reference_snapshot.is_none() {
637 return Err(pleiades_compression::CompressionError::new(
638 pleiades_compression::CompressionErrorKind::InvalidFormat,
639 "packaged artifact regeneration summary is missing reference snapshot coverage",
640 ));
641 }
642
643 for (index, body) in self.bodies.iter().enumerate() {
644 if self.bodies[..index].iter().any(|other| other == body) {
645 return Err(pleiades_compression::CompressionError::new(
646 pleiades_compression::CompressionErrorKind::InvalidFormat,
647 format!("packaged artifact regeneration summary contains duplicate body entry {body}"),
648 ));
649 }
650 }
651
652 let expected_bodies = packaged_bodies();
653 if self.bodies.as_slice() != expected_bodies {
654 return Err(pleiades_compression::CompressionError::new(
655 pleiades_compression::CompressionErrorKind::InvalidFormat,
656 format!(
657 "packaged artifact regeneration summary body list does not match the checked-in packaged body set: expected [{}]; got [{}]",
658 join_display(expected_bodies),
659 join_display(&self.bodies)
660 ),
661 ));
662 }
663
664 self.validate_residual_body_subset()?;
665
666 self.fit_envelope.validate().map_err(|error| {
667 pleiades_compression::CompressionError::new(
668 pleiades_compression::CompressionErrorKind::InvalidFormat,
669 format!("packaged artifact regeneration fit envelope is invalid: {error}"),
670 )
671 })?;
672
673 if let Some(reference_snapshot) = self.reference_snapshot {
674 reference_snapshot.validate().map_err(|error| {
675 pleiades_compression::CompressionError::new(
676 pleiades_compression::CompressionErrorKind::InvalidFormat,
677 format!(
678 "packaged artifact regeneration reference snapshot is invalid: {error}"
679 ),
680 )
681 })?;
682 for body in &self.bodies {
683 if !reference_snapshot.bodies.contains(body) {
684 return Err(pleiades_compression::CompressionError::new(
685 pleiades_compression::CompressionErrorKind::InvalidFormat,
686 format!("packaged artifact regeneration body {body} is not covered by the reference snapshot"),
687 ));
688 }
689 }
690 if self.reference_snapshot != reference_snapshot_summary() {
691 return Err(pleiades_compression::CompressionError::new(
692 pleiades_compression::CompressionErrorKind::InvalidFormat,
693 "packaged artifact regeneration summary reference snapshot does not match the checked-in reference snapshot summary",
694 ));
695 }
696 }
697
698 Ok(())
699 }
700
701 pub fn summary_line(&self) -> String {
703 format!(
704 "Packaged artifact regeneration source: label={}; profile id={}; source={}; source revision={}; normalized intermediates: {}; checksum=0x{:016x}; artifact size={} bytes; {}; segment strategy={}; {}; {}; bundled bodies: {}; {}; fit envelope: {}; artifact version={}",
705 self.label,
706 self.profile_id,
707 self.source,
708 self.source_revision,
709 self.normalized_intermediates_line(),
710 self.checksum,
711 self.artifact_size_bytes,
712 self.generation_policy_line(),
713 self.generation_policy.segment_strategy(),
714 self.quantization_scales,
715 self.residual_body_line(),
716 self.body_coverage_line(),
717 self.reference_snapshot_line(),
718 self.fit_envelope.summary_line(),
719 self.artifact_version,
720 )
721 }
722
723 pub fn validated_summary_line(&self) -> Result<String, pleiades_compression::CompressionError> {
725 self.validate()?;
726 Ok(self.summary_line())
727 }
728}
729
730impl fmt::Display for PackagedArtifactRegenerationSummary {
731 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
732 f.write_str(&self.summary_line())
733 }
734}
735
736pub fn packaged_artifact_regeneration_summary_details() -> PackagedArtifactRegenerationSummary {
738 static SUMMARY: OnceLock<PackagedArtifactRegenerationSummary> = OnceLock::new();
739 SUMMARY
740 .get_or_init(|| {
741 let artifact = packaged_artifact();
742 let summary = PackagedArtifactRegenerationSummary {
743 label: ARTIFACT_LABEL,
744 artifact_version: artifact.header.version,
745 source: packaged_artifact_source_text(),
746 source_revision: production_generation_source_summary_for_report(),
747 profile_id: ARTIFACT_PROFILE_ID,
748 checksum: artifact.checksum,
749 artifact_size_bytes: packaged_artifact_encoded_bytes(artifact),
750 generation_policy:
751 PackagedArtifactGenerationPolicy::AdjacentSameBodyQuadraticWindows,
752 quantization_scales: packaged_artifact_quantization_scales_line(),
753 residual_bodies: artifact.residual_bodies(),
754 bodies: packaged_bodies().to_vec(),
755 normalized_intermediates: packaged_artifact_normalized_intermediate_summary_details(
756 ),
757 fit_envelope: packaged_artifact_fit_envelope_summary_details(),
758 reference_snapshot: reference_snapshot_summary(),
759 };
760 debug_assert!(summary.validate().is_ok());
761 summary
762 })
763 .clone()
764}
765
766pub fn packaged_artifact_normalized_intermediate_summary_details(
768) -> PackagedArtifactNormalizedIntermediateSummary {
769 let artifact = packaged_artifact();
770 let payload_checksum = fnv1a64(
771 format!(
772 "label={}; profile id={}; version={}; time range={}; source={}; source revision={}; body count={}; segments={}; residual-bearing segments={}; stored channels={}; residual channels={}; segment span days={:.12}..{:.12}; segment strategy={}; {}",
773 ARTIFACT_LABEL,
774 ARTIFACT_PROFILE_ID,
775 artifact.header.version,
776 artifact_time_range(artifact),
777 packaged_artifact_source_text(),
778 production_generation_source_summary_for_report(),
779 artifact.bodies.len(),
780 artifact.segment_count(),
781 artifact.residual_segment_count(),
782 packaged_artifact_channel_count(artifact, false),
783 packaged_artifact_channel_count(artifact, true),
784 packaged_artifact_segment_span_bounds(artifact).0,
785 packaged_artifact_segment_span_bounds(artifact).1,
786 PackagedArtifactGenerationPolicy::AdjacentSameBodyQuadraticWindows.segment_strategy(),
787 packaged_artifact_quantization_scales_line(),
788 )
789 .as_bytes(),
790 );
791 let summary = PackagedArtifactNormalizedIntermediateSummary {
792 label: ARTIFACT_LABEL,
793 artifact_version: artifact.header.version,
794 source: packaged_artifact_source_text(),
795 source_revision: production_generation_source_summary_for_report(),
796 profile_id: ARTIFACT_PROFILE_ID,
797 time_range: artifact_time_range(artifact),
798 generation_policy: PackagedArtifactGenerationPolicy::AdjacentSameBodyQuadraticWindows,
799 quantization_scales: packaged_artifact_quantization_scales_line(),
800 checksum: payload_checksum,
801 body_count: artifact.bodies.len(),
802 segment_count: artifact.segment_count(),
803 residual_segment_count: artifact.residual_segment_count(),
804 stored_channel_count: packaged_artifact_channel_count(artifact, false),
805 residual_channel_count: packaged_artifact_channel_count(artifact, true),
806 min_segment_span_days: packaged_artifact_segment_span_bounds(artifact).0,
807 max_segment_span_days: packaged_artifact_segment_span_bounds(artifact).1,
808 };
809 debug_assert!(summary.validate().is_ok());
810 summary
811}