use super::*;
#[derive(Clone, Debug, PartialEq)]
pub struct PackagedArtifactTargetThresholdScopeSummary {
pub scope: &'static str,
pub bodies: Vec<CelestialBody>,
pub body_count: usize,
pub fit_envelope: PackagedArtifactFitEnvelopeSummary,
}
impl PackagedArtifactTargetThresholdScopeSummary {
pub fn summary_line(&self) -> String {
format!(
"scope={}; bodies={}; {}",
self.scope,
format_scope_bodies(&self.bodies),
self.fit_envelope.summary_line(),
)
}
pub fn validated_summary_line(
&self,
) -> Result<String, PackagedArtifactFitEnvelopeSummaryValidationError> {
self.validate()?;
Ok(self.summary_line())
}
pub fn validate(&self) -> Result<(), PackagedArtifactFitEnvelopeSummaryValidationError> {
let expected =
packaged_artifact_target_threshold_scope_envelope_summary_details(self.scope);
if self != &expected {
return Err(
PackagedArtifactFitEnvelopeSummaryValidationError::FieldOutOfSync {
field: "scope fit envelope",
},
);
}
Ok(())
}
}
impl fmt::Display for PackagedArtifactTargetThresholdScopeSummary {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.summary_line())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct PackagedArtifactTargetThresholdScopeEnvelopesSummary {
pub scope_envelopes: Vec<PackagedArtifactTargetThresholdScopeSummary>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PackagedArtifactTargetThresholdScopeEnvelopesSummaryValidationError {
FieldOutOfSync { field: &'static str },
}
impl fmt::Display for PackagedArtifactTargetThresholdScopeEnvelopesSummaryValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::FieldOutOfSync { field } => write!(
f,
"the packaged-artifact target-threshold scope envelopes summary field `{field}` is out of sync with the current posture"
),
}
}
}
impl std::error::Error for PackagedArtifactTargetThresholdScopeEnvelopesSummaryValidationError {}
impl PackagedArtifactTargetThresholdScopeEnvelopesSummary {
pub fn summary_line(&self) -> String {
format!("scope envelopes: {}", join_display(&self.scope_envelopes))
}
pub fn validate(
&self,
) -> Result<(), PackagedArtifactTargetThresholdScopeEnvelopesSummaryValidationError> {
let expected = packaged_artifact_target_threshold_scope_envelopes_summary_details();
if self != &expected {
return Err(
PackagedArtifactTargetThresholdScopeEnvelopesSummaryValidationError::FieldOutOfSync {
field: "scope_envelopes",
},
);
}
for scope_envelope in &self.scope_envelopes {
scope_envelope.validate().map_err(|_| {
PackagedArtifactTargetThresholdScopeEnvelopesSummaryValidationError::FieldOutOfSync {
field: "scope_envelopes",
}
})?;
}
Ok(())
}
pub fn validated_summary_line(
&self,
) -> Result<String, PackagedArtifactTargetThresholdScopeEnvelopesSummaryValidationError> {
self.validate()?;
Ok(self.summary_line())
}
}
impl fmt::Display for PackagedArtifactTargetThresholdScopeEnvelopesSummary {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.summary_line())
}
}
impl PackagedArtifactFitEnvelopeSummary {
pub fn summary_line(&self) -> String {
format!(
"fit envelope: {}/{} segment samples across {} bundled bodies; mean Δlon={:.12}°, mean Δlat={:.12}°, mean Δdist={:.12} AU; max Δlon={:.12}°, max Δlat={:.12}°, max Δdist={:.12} AU",
self.sample_count,
self.expected_sample_count,
self.body_count,
self.mean_longitude_delta_degrees,
self.mean_latitude_delta_degrees,
self.mean_distance_delta_au,
self.max_longitude_delta_degrees,
self.max_latitude_delta_degrees,
self.max_distance_delta_au,
)
}
pub fn validate(&self) -> Result<(), PackagedArtifactFitEnvelopeSummaryValidationError> {
let artifact = packaged_artifact();
let expected = packaged_artifact_fit_envelope_summary_details();
let expected_sample_count = packaged_artifact_fit_expected_sample_count(artifact);
let expected_body_count = artifact.bodies.len();
if self.expected_sample_count != expected_sample_count {
return Err(
PackagedArtifactFitEnvelopeSummaryValidationError::FieldOutOfSync {
field: "expected_sample_count",
},
);
}
if self.sample_count != expected_sample_count {
return Err(
PackagedArtifactFitEnvelopeSummaryValidationError::FieldOutOfSync {
field: "sample_count",
},
);
}
if self.body_count != expected_body_count {
return Err(
PackagedArtifactFitEnvelopeSummaryValidationError::FieldOutOfSync {
field: "body_count",
},
);
}
if self != &expected {
for (field, matches) in [
(
"mean_longitude_delta_degrees",
self.mean_longitude_delta_degrees == expected.mean_longitude_delta_degrees,
),
(
"mean_latitude_delta_degrees",
self.mean_latitude_delta_degrees == expected.mean_latitude_delta_degrees,
),
(
"mean_distance_delta_au",
self.mean_distance_delta_au == expected.mean_distance_delta_au,
),
(
"max_longitude_delta_degrees",
self.max_longitude_delta_degrees == expected.max_longitude_delta_degrees,
),
(
"max_latitude_delta_degrees",
self.max_latitude_delta_degrees == expected.max_latitude_delta_degrees,
),
(
"max_distance_delta_au",
self.max_distance_delta_au == expected.max_distance_delta_au,
),
] {
if !matches {
return Err(
PackagedArtifactFitEnvelopeSummaryValidationError::FieldOutOfSync { field },
);
}
}
}
Ok(())
}
}
impl fmt::Display for PackagedArtifactFitEnvelopeSummary {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.summary_line())
}
}
#[cfg(test)]
pub(crate) fn packaged_artifact_fit_sample_fractions(segment: &Segment) -> &'static [f64] {
if segment.start.julian_day.days() == segment.end.julian_day.days() {
&[0.0]
} else {
&[0.25, 0.5, 0.75]
}
}
pub(crate) fn packaged_artifact_fit_sample_fractions_for_body(
body: &CelestialBody,
segment: &Segment,
) -> &'static [f64] {
if segment.start.julian_day.days() == segment.end.julian_day.days() {
&[0.0]
} else {
match packaged_artifact_body_cadence(body) {
PackagedArtifactBodyCadence::Luminaries
| PackagedArtifactBodyCadence::LunarPoints
| PackagedArtifactBodyCadence::SelectedAsteroids
| PackagedArtifactBodyCadence::Pluto
| PackagedArtifactBodyCadence::CustomBodies => {
packaged_artifact_segment_validation_fractions_for_body(body)
}
PackagedArtifactBodyCadence::InnerPlanets
| PackagedArtifactBodyCadence::OuterPlanets => {
PACKAGED_ARTIFACT_MEDIUM_VALIDATION_SAMPLE_FRACTIONS
}
}
}
}
pub(crate) fn distance_channel_from_samples(
start: f64,
midpoint: Option<f64>,
end: f64,
) -> PolynomialChannel {
midpoint
.map(|midpoint| {
PolynomialChannel::quadratic(ChannelKind::DistanceAu, 10, start, midpoint, end, 0.5)
})
.unwrap_or_else(|| PolynomialChannel::linear(ChannelKind::DistanceAu, 10, start, end))
}
pub(crate) fn distance_channel_from_four_point_control_points(
start: f64,
first_third: f64,
second_third: f64,
end: f64,
) -> Option<PolynomialChannel> {
polynomial_channel_from_samples(
ChannelKind::DistanceAu,
10,
&[
(0.0, start),
(1.0 / 3.0, first_third),
(2.0 / 3.0, second_third),
(1.0, end),
],
)
}
fn channel_from_fit_control_points(
kind: ChannelKind,
scale_exponent: u8,
samples: &[(f64, f64)],
) -> Option<PolynomialChannel> {
const TARGET_FRACTIONS: [f64; 4] = [0.0, 1.0 / 3.0, 2.0 / 3.0, 1.0];
if samples.len() < TARGET_FRACTIONS.len() {
return None;
}
let mut selected_samples = Vec::with_capacity(TARGET_FRACTIONS.len());
let mut used_indices = vec![false; samples.len()];
for target_fraction in TARGET_FRACTIONS {
let mut best_index = None;
let mut best_distance = f64::INFINITY;
for (index, (fraction, _)) in samples.iter().enumerate() {
if used_indices[index] {
continue;
}
let distance = (*fraction - target_fraction).abs();
if distance < best_distance {
best_distance = distance;
best_index = Some(index);
}
}
let index = best_index?;
used_indices[index] = true;
selected_samples.push(samples[index]);
}
polynomial_channel_from_samples(kind, scale_exponent, &selected_samples)
}
pub(crate) fn channel_from_fit_samples_with_control_points(
kind: ChannelKind,
scale_exponent: u8,
samples: &[(f64, f64)],
) -> Option<PolynomialChannel> {
polynomial_channel_from_samples(kind, scale_exponent, samples)
.or_else(|| channel_from_fit_control_points(kind, scale_exponent, samples))
}
pub(crate) fn channel_from_dense_fit_samples_with_control_points(
kind: ChannelKind,
scale_exponent: u8,
samples: &[(f64, f64)],
) -> Option<PolynomialChannel> {
channel_from_fit_control_points(kind, scale_exponent, samples)
.or_else(|| polynomial_channel_from_samples(kind, scale_exponent, samples))
}
pub(crate) fn distance_channel_from_dense_fit_samples(
samples: &[(f64, f64)],
start: f64,
midpoint: Option<f64>,
end: f64,
) -> PolynomialChannel {
channel_from_fit_control_points(ChannelKind::DistanceAu, 10, samples)
.or_else(|| {
channel_from_fit_samples_with_control_points(ChannelKind::DistanceAu, 10, samples)
})
.unwrap_or_else(|| distance_channel_from_samples(start, midpoint, end))
}
pub(crate) fn distance_channel_from_fit_samples(
samples: &[(f64, f64)],
start: f64,
midpoint: Option<f64>,
end: f64,
) -> PolynomialChannel {
channel_from_fit_samples_with_control_points(ChannelKind::DistanceAu, 10, samples)
.unwrap_or_else(|| distance_channel_from_samples(start, midpoint, end))
}
fn packaged_artifact_fit_sample_request(
body: &CelestialBody,
segment: &Segment,
fraction: f64,
) -> (Instant, EphemerisRequest) {
let start = segment.start.julian_day.days();
let span = segment.end.julian_day.days() - start;
let instant = Instant::new(
JulianDay::from_days(start + span * fraction),
segment.start.scale,
);
let request = EphemerisRequest {
body: body.clone(),
instant,
observer: None,
frame: CoordinateFrame::Ecliptic,
zodiac_mode: ZodiacMode::Tropical,
apparent: Apparentness::Mean,
};
(instant, request)
}
pub(crate) fn packaged_artifact_fit_expected_sample_count_with_filter<F>(
_artifact: &CompressedArtifact,
mut include_body: F,
) -> usize
where
F: FnMut(&CelestialBody) -> bool,
{
packaged_artifact_fit_samples_for_current_artifact()
.iter()
.filter(|sample| include_body(&sample.body))
.count()
}
fn packaged_artifact_fit_expected_sample_count(artifact: &CompressedArtifact) -> usize {
packaged_artifact_fit_expected_sample_count_with_filter(artifact, |_| true)
}
struct FitTruthBackend {
corpus: SnapshotCorpusBackend,
snapshot: JplSnapshotBackend,
}
impl FitTruthBackend {
fn fits_from_snapshot(body: &CelestialBody) -> bool {
use crate::coverage::PackagedArtifactBodyCadence;
matches!(
crate::coverage::packaged_artifact_body_cadence(body),
PackagedArtifactBodyCadence::SelectedAsteroids
| PackagedArtifactBodyCadence::CustomBodies
)
}
}
impl EphemerisBackend for FitTruthBackend {
fn metadata(&self) -> pleiades_backend::BackendMetadata {
self.corpus.metadata()
}
fn supports_body(&self, body: CelestialBody) -> bool {
if Self::fits_from_snapshot(&body) {
self.snapshot.supports_body(body)
} else {
self.corpus.supports_body(body)
}
}
fn position(&self, req: &EphemerisRequest) -> Result<EphemerisResult, EphemerisError> {
if Self::fits_from_snapshot(&req.body) {
self.snapshot.position(req)
} else {
self.corpus.position(req)
}
}
}
fn fit_truth_backend() -> &'static FitTruthBackend {
static BACKEND: OnceLock<FitTruthBackend> = OnceLock::new();
BACKEND.get_or_init(|| {
let mut seen = std::collections::HashSet::new();
let entries = production_reference_corpus()
.iter()
.filter(|entry| {
seen.insert((entry.body.clone(), entry.epoch.julian_day.days().to_bits()))
})
.cloned()
.collect::<Vec<_>>();
FitTruthBackend {
corpus: SnapshotCorpusBackend::from_entries(entries),
snapshot: JplSnapshotBackend,
}
})
}
fn packaged_artifact_fit_samples_with_filter<F>(
artifact: &CompressedArtifact,
mut include_body: F,
) -> Vec<PackagedArtifactFitSample>
where
F: FnMut(&CelestialBody) -> bool,
{
let reference_backend = fit_truth_backend();
let packaged_backend = packaged_backend();
let mut samples = Vec::new();
for body_artifact in &artifact.bodies {
if !include_body(&body_artifact.body) {
continue;
}
for segment in &body_artifact.segments {
for fraction in
packaged_artifact_fit_sample_fractions_for_body(&body_artifact.body, segment)
{
let (instant, request) =
packaged_artifact_fit_sample_request(&body_artifact.body, segment, *fraction);
let expected = match reference_backend.position(&request) {
Ok(result) => result,
Err(_) => continue,
};
let actual = match packaged_backend.position(&request) {
Ok(result) => result,
Err(_) => continue,
};
let (Some(expected_ecliptic), Some(actual_ecliptic)) =
(expected.ecliptic, actual.ecliptic)
else {
continue;
};
let (Some(expected_distance), Some(actual_distance)) =
(expected_ecliptic.distance_au, actual_ecliptic.distance_au)
else {
continue;
};
samples.push(PackagedArtifactFitSample {
body: body_artifact.body.clone(),
segment_start: segment.start,
segment_end: segment.end,
sample_instant: instant,
sample_fraction: *fraction,
longitude_delta_degrees: Angle::from_degrees(
actual_ecliptic.longitude.degrees() - expected_ecliptic.longitude.degrees(),
)
.normalized_signed()
.degrees()
.abs(),
latitude_delta_degrees: (actual_ecliptic.latitude.degrees()
- expected_ecliptic.latitude.degrees())
.abs(),
distance_delta_au: (actual_distance - expected_distance).abs(),
});
}
}
}
samples
}
pub(crate) fn packaged_artifact_fit_samples_for_current_artifact(
) -> &'static [PackagedArtifactFitSample] {
static SAMPLES: OnceLock<Vec<PackagedArtifactFitSample>> = OnceLock::new();
SAMPLES
.get_or_init(|| {
let artifact = packaged_artifact();
packaged_artifact_fit_samples_with_filter(artifact, |_| true)
})
.as_slice()
}
pub(crate) fn packaged_artifact_fit_outlier_sample_fractions(
body: &CelestialBody,
segment: &Segment,
) -> &'static [f64] {
if segment.start.julian_day.days() == segment.end.julian_day.days() {
&[0.0]
} else {
packaged_artifact_segment_validation_fractions_for_body(body)
}
}
fn packaged_artifact_fit_outlier_samples_with_filter<F>(
artifact: &CompressedArtifact,
mut include_body: F,
) -> Vec<PackagedArtifactFitSample>
where
F: FnMut(&CelestialBody) -> bool,
{
let reference_backend = fit_truth_backend();
let packaged_backend = packaged_backend();
let mut samples = Vec::new();
for body_artifact in &artifact.bodies {
if !include_body(&body_artifact.body) {
continue;
}
for segment in &body_artifact.segments {
for fraction in
packaged_artifact_fit_outlier_sample_fractions(&body_artifact.body, segment)
{
let (instant, request) =
packaged_artifact_fit_sample_request(&body_artifact.body, segment, *fraction);
let expected = match reference_backend.position(&request) {
Ok(result) => result,
Err(_) => continue,
};
let actual = match packaged_backend.position(&request) {
Ok(result) => result,
Err(_) => continue,
};
let (Some(expected_ecliptic), Some(actual_ecliptic)) =
(expected.ecliptic, actual.ecliptic)
else {
continue;
};
let (Some(expected_distance), Some(actual_distance)) =
(expected_ecliptic.distance_au, actual_ecliptic.distance_au)
else {
continue;
};
samples.push(PackagedArtifactFitSample {
body: body_artifact.body.clone(),
segment_start: segment.start,
segment_end: segment.end,
sample_instant: instant,
sample_fraction: *fraction,
longitude_delta_degrees: Angle::from_degrees(
actual_ecliptic.longitude.degrees() - expected_ecliptic.longitude.degrees(),
)
.normalized_signed()
.degrees()
.abs(),
latitude_delta_degrees: (actual_ecliptic.latitude.degrees()
- expected_ecliptic.latitude.degrees())
.abs(),
distance_delta_au: (actual_distance - expected_distance).abs(),
});
}
}
}
samples
}
pub(crate) fn packaged_artifact_fit_outlier_samples_for_current_artifact(
) -> &'static [PackagedArtifactFitSample] {
static SAMPLES: OnceLock<Vec<PackagedArtifactFitSample>> = OnceLock::new();
SAMPLES
.get_or_init(|| {
let artifact = packaged_artifact();
packaged_artifact_fit_outlier_samples_with_filter(artifact, |_| true)
})
.as_slice()
}
pub(crate) fn packaged_artifact_fit_envelope_summary_from_samples(
samples: &[PackagedArtifactFitSample],
expected_sample_count: usize,
) -> PackagedArtifactFitEnvelopeSummary {
let sample_count = samples.len();
let mut observed_bodies = Vec::new();
let mut mean_longitude_delta_degrees: f64 = 0.0;
let mut mean_latitude_delta_degrees: f64 = 0.0;
let mut mean_distance_delta_au: f64 = 0.0;
let mut max_longitude_delta_degrees: f64 = 0.0;
let mut max_latitude_delta_degrees: f64 = 0.0;
let mut max_distance_delta_au: f64 = 0.0;
for sample in samples {
if !observed_bodies.contains(&sample.body) {
observed_bodies.push(sample.body.clone());
}
mean_longitude_delta_degrees += sample.longitude_delta_degrees;
mean_latitude_delta_degrees += sample.latitude_delta_degrees;
mean_distance_delta_au += sample.distance_delta_au;
max_longitude_delta_degrees =
max_longitude_delta_degrees.max(sample.longitude_delta_degrees);
max_latitude_delta_degrees = max_latitude_delta_degrees.max(sample.latitude_delta_degrees);
max_distance_delta_au = max_distance_delta_au.max(sample.distance_delta_au);
}
if sample_count > 0 {
let sample_count = sample_count as f64;
mean_longitude_delta_degrees /= sample_count;
mean_latitude_delta_degrees /= sample_count;
mean_distance_delta_au /= sample_count;
}
PackagedArtifactFitEnvelopeSummary {
sample_count,
expected_sample_count,
body_count: observed_bodies.len(),
mean_longitude_delta_degrees,
mean_latitude_delta_degrees,
mean_distance_delta_au,
max_longitude_delta_degrees,
max_latitude_delta_degrees,
max_distance_delta_au,
}
}
pub fn packaged_artifact_fit_envelope_summary_details() -> PackagedArtifactFitEnvelopeSummary {
static SUMMARY: OnceLock<PackagedArtifactFitEnvelopeSummary> = OnceLock::new();
SUMMARY
.get_or_init(|| {
let artifact = packaged_artifact();
let samples = packaged_artifact_fit_samples_for_current_artifact();
packaged_artifact_fit_envelope_summary_from_samples(
samples,
packaged_artifact_fit_expected_sample_count(artifact),
)
})
.clone()
}
fn packaged_artifact_fit_channel_rank(channel: ChannelKind) -> usize {
match channel {
ChannelKind::DistanceAu => 0,
ChannelKind::Longitude => 1,
ChannelKind::Latitude => 2,
_ => unreachable!("unsupported packaged-artifact channel kind"),
}
}
pub(crate) fn packaged_artifact_fit_channel_delta(
sample: &PackagedArtifactFitSample,
channel: ChannelKind,
) -> f64 {
match channel {
ChannelKind::Longitude => sample.longitude_delta_degrees,
ChannelKind::Latitude => sample.latitude_delta_degrees,
ChannelKind::DistanceAu => sample.distance_delta_au,
_ => unreachable!("unsupported packaged-artifact channel kind"),
}
}
fn packaged_artifact_fit_outlier_summary_from_samples(
samples: &[PackagedArtifactFitSample],
) -> PackagedArtifactFitOutlierSummary {
let mut families: HashMap<
(
CelestialBody,
ChannelKind,
PackagedArtifactFitSegmentFamilyKey,
),
PackagedArtifactFitChannelFamilyAccumulator,
> = HashMap::new();
for sample in samples {
let family_key = PackagedArtifactFitSegmentFamilyKey::from_sample(sample);
for channel in [
ChannelKind::DistanceAu,
ChannelKind::Longitude,
ChannelKind::Latitude,
] {
let entry = families
.entry((sample.body.clone(), channel, family_key))
.or_insert_with(PackagedArtifactFitChannelFamilyAccumulator::new);
entry.push(sample, channel);
}
}
let mut body_channel_outliers: HashMap<
CelestialBody,
[Option<PackagedArtifactFitChannelOutlier>; 3],
> = HashMap::new();
for ((body, channel, _family_key), family) in families {
let Some(outlier) = family.finish(channel) else {
continue;
};
let entry = body_channel_outliers
.entry(body)
.or_insert_with(|| [None, None, None]);
let channel_index = packaged_artifact_fit_channel_rank(channel);
let should_replace = entry[channel_index]
.as_ref()
.map(|existing| {
outlier.delta > existing.delta
|| (outlier.delta == existing.delta
&& outlier.segment_span_days < existing.segment_span_days)
})
.unwrap_or(true);
if should_replace {
entry[channel_index] = Some(outlier);
}
}
let mut body_summaries = body_channel_outliers
.into_iter()
.map(|(body, outliers)| {
let mut channel_outliers = Vec::new();
for channel in [
ChannelKind::DistanceAu,
ChannelKind::Longitude,
ChannelKind::Latitude,
] {
if let Some(outlier) = outliers[packaged_artifact_fit_channel_rank(channel)].clone()
{
channel_outliers.push(outlier);
}
}
PackagedArtifactFitBodyOutlierSummary {
body,
channel_outliers,
}
})
.collect::<Vec<_>>();
body_summaries.sort_by_key(|summary| summary.body.to_string());
PackagedArtifactFitOutlierSummary {
body_count: body_summaries.len(),
body_summaries,
}
}
pub fn packaged_artifact_fit_outlier_summary_details() -> PackagedArtifactFitOutlierSummary {
static SUMMARY: OnceLock<PackagedArtifactFitOutlierSummary> = OnceLock::new();
SUMMARY
.get_or_init(|| {
let samples = packaged_artifact_fit_outlier_samples_for_current_artifact();
packaged_artifact_fit_outlier_summary_from_samples(samples)
})
.clone()
}
pub fn packaged_artifact_fit_threshold_summary_details() -> PackagedArtifactFitThresholdSummary {
PACKAGED_ARTIFACT_FIT_THRESHOLD_SUMMARY
}
pub fn packaged_artifact_fit_margin_summary_details() -> PackagedArtifactFitMarginSummary {
let summary = PackagedArtifactFitMarginSummary {
envelope: packaged_artifact_fit_envelope_summary_details(),
thresholds: packaged_artifact_fit_threshold_summary_details(),
};
debug_assert!(summary.validate().is_ok());
summary
}
pub fn packaged_artifact_fit_threshold_violation_summary_details(
) -> PackagedArtifactFitThresholdViolationsSummary {
let envelope = packaged_artifact_fit_envelope_summary_details();
let thresholds = packaged_artifact_fit_threshold_summary_details();
let violations = packaged_artifact_fit_threshold_violations_from_envelope_and_thresholds(
&envelope,
&thresholds,
);
PackagedArtifactFitThresholdViolationsSummary { violations }
}
pub(crate) fn packaged_artifact_body_scope(body: &CelestialBody) -> &'static str {
match body {
CelestialBody::Sun | CelestialBody::Moon => "luminaries",
CelestialBody::Mercury
| CelestialBody::Venus
| CelestialBody::Mars
| CelestialBody::Jupiter
| CelestialBody::Saturn
| CelestialBody::Uranus
| CelestialBody::Neptune => "major planets",
CelestialBody::Pluto => "pluto",
CelestialBody::MeanNode
| CelestialBody::TrueNode
| CelestialBody::MeanApogee
| CelestialBody::TrueApogee
| CelestialBody::MeanPerigee
| CelestialBody::TruePerigee => "lunar points",
CelestialBody::Ceres
| CelestialBody::Pallas
| CelestialBody::Juno
| CelestialBody::Vesta => "selected asteroids",
CelestialBody::Custom(custom) if custom.catalog.eq_ignore_ascii_case("asteroid") => {
"selected asteroids"
}
CelestialBody::Custom(_) => "custom bodies",
_ => "custom bodies",
}
}