1use pleiades_backend::CelestialBody;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum BodyClass {
9 Luminary,
10 InnerPlanet,
11 OuterPlanet,
12 Asteroid,
13}
14
15pub fn body_class(body: &CelestialBody) -> BodyClass {
16 match body {
17 CelestialBody::Sun | CelestialBody::Moon => BodyClass::Luminary,
18 CelestialBody::Mercury | CelestialBody::Venus | CelestialBody::Mars => {
19 BodyClass::InnerPlanet
20 }
21 CelestialBody::Jupiter
22 | CelestialBody::Saturn
23 | CelestialBody::Uranus
24 | CelestialBody::Neptune
25 | CelestialBody::Pluto => BodyClass::OuterPlanet,
26 _ => BodyClass::Asteroid,
27 }
28}
29
30#[derive(Clone, Copy, Debug)]
31pub struct AccuracyCeiling {
32 pub lon_arcsec: f64,
33 pub lat_arcsec: f64,
34 pub dist_km: f64,
35 pub lon_speed_arcsec_per_day: f64,
36 pub lat_speed_arcsec_per_day: f64,
37 pub radial_speed_au_per_day: f64,
38}
39
40pub fn accuracy_ceiling(body: &CelestialBody) -> AccuracyCeiling {
58 match body_class(body) {
59 BodyClass::Luminary | BodyClass::InnerPlanet => AccuracyCeiling {
60 lon_arcsec: 1.0,
61 lat_arcsec: 1.0,
62 dist_km: 50.0,
63 lon_speed_arcsec_per_day: 0.5,
64 lat_speed_arcsec_per_day: 0.5,
65 radial_speed_au_per_day: 1.0e-4,
66 },
67 BodyClass::OuterPlanet => AccuracyCeiling {
68 lon_arcsec: 5.0,
69 lat_arcsec: 5.0,
70 dist_km: 1_000.0,
71 lon_speed_arcsec_per_day: 0.05,
72 lat_speed_arcsec_per_day: 0.05,
73 radial_speed_au_per_day: 1.0e-4,
74 },
75 BodyClass::Asteroid => AccuracyCeiling {
76 lon_arcsec: 30.0,
77 lat_arcsec: 30.0,
78 dist_km: 5_000_000.0,
79 lon_speed_arcsec_per_day: 120.0,
80 lat_speed_arcsec_per_day: 120.0,
81 radial_speed_au_per_day: 1.0e-2,
82 },
83 }
84}
85
86#[derive(Clone, Copy, Debug)]
87pub struct ArtifactBudgets {
88 pub max_encoded_bytes: usize,
89 pub decode_latency_target_ms: f64,
90 pub single_lookup_target_ms: f64,
91 pub batch_throughput_target_per_s: f64,
92 pub chart_workload_target_ms: f64,
93}
94
95pub const PACKAGED_BUDGETS: ArtifactBudgets = ArtifactBudgets {
96 max_encoded_bytes: 12_000_000, decode_latency_target_ms: 400.0, single_lookup_target_ms: 6.0, batch_throughput_target_per_s: 1_000.0,
100 chart_workload_target_ms: 50.0,
101};
102
103pub fn packaged_artifact_thresholds_summary_for_report() -> String {
111 let baseline = crate::accuracy_baseline::packaged_artifact_accuracy_baseline();
112 let mut lines = Vec::new();
113 for e in &baseline {
114 let c = accuracy_ceiling(&e.body);
115 let lon_status = if e.max_longitude_arcsec <= c.lon_arcsec {
116 "PASS"
117 } else {
118 "FAIL"
119 };
120 let lat_status = if e.max_latitude_arcsec <= c.lat_arcsec {
121 "PASS"
122 } else {
123 "FAIL"
124 };
125 let dist_status = if e.max_distance_km <= c.dist_km {
126 "PASS"
127 } else {
128 "FAIL"
129 };
130 let spd_status = if e.max_lon_speed_arcsec_per_day <= c.lon_speed_arcsec_per_day {
131 "PASS"
132 } else {
133 "FAIL"
134 };
135 lines.push(format!(
136 "{:?}: lon {:.4}\"/{}\"={} lat {:.4}\"/{}\"={} dist {:.1}/{} km={} lon_spd {:.4}/{} \"/d={}",
137 e.body,
138 e.max_longitude_arcsec, c.lon_arcsec, lon_status,
139 e.max_latitude_arcsec, c.lat_arcsec, lat_status,
140 e.max_distance_km, c.dist_km, dist_status,
141 e.max_lon_speed_arcsec_per_day, c.lon_speed_arcsec_per_day, spd_status,
142 ));
143 }
144 format!(
145 "Packaged-artifact thresholds ({} bodies); size budget {} bytes:\n{}",
146 baseline.len(),
147 PACKAGED_BUDGETS.max_encoded_bytes,
148 lines.join("\n")
149 )
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use pleiades_backend::CelestialBody;
156
157 #[test]
158 fn classes_map_bodies_correctly() {
159 assert_eq!(body_class(&CelestialBody::Sun), BodyClass::Luminary);
160 assert_eq!(body_class(&CelestialBody::Moon), BodyClass::Luminary);
161 assert_eq!(body_class(&CelestialBody::Mercury), BodyClass::InnerPlanet);
162 assert_eq!(body_class(&CelestialBody::Pluto), BodyClass::OuterPlanet);
163 }
164
165 #[test]
166 fn outer_planets_have_looser_longitude_ceiling_than_inner() {
167 assert!(
168 accuracy_ceiling(&CelestialBody::Uranus).lon_arcsec
169 > accuracy_ceiling(&CelestialBody::Mercury).lon_arcsec
170 );
171 }
172
173 #[test]
174 fn size_budget_exceeds_current_artifact() {
175 let max_encoded_bytes = PACKAGED_BUDGETS.max_encoded_bytes;
179 assert!(max_encoded_bytes >= 10_000_000);
180 assert!(max_encoded_bytes <= 16_000_000);
181 }
182
183 #[test]
184 #[ignore = "maintainer helper: prints the thresholds summary to regenerate the golden"]
185 fn print_packaged_artifact_thresholds_summary() {
186 eprintln!("{}", packaged_artifact_thresholds_summary_for_report());
187 }
188
189 #[test]
194 fn packaged_artifact_thresholds_summary_matches_committed_golden() {
195 let report = packaged_artifact_thresholds_summary_for_report();
196
197 assert!(
199 report.contains("Packaged-artifact thresholds (10 bodies)"),
200 "thresholds report header drift: {report}"
201 );
202 assert!(
203 report.contains("size budget 12000000 bytes"),
204 "thresholds report size-budget line drift: {report}"
205 );
206
207 assert!(
210 report.contains("Moon:") && report.contains("/1\"=PASS"),
211 "Moon lon/ceiling PASS line drift: {report}"
212 );
213
214 for body_name in &[
217 "Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune",
218 "Pluto",
219 ] {
220 assert!(
221 report.contains(body_name),
222 "{body_name} missing from thresholds report: {report}"
223 );
224 assert!(
226 report.contains(&format!("{body_name}:")) && {
227 report
229 .lines()
230 .find(|l| l.starts_with(body_name))
231 .map(|l| l.contains("PASS"))
232 .unwrap_or(false)
233 },
234 "{body_name} line does not contain PASS: {report}"
235 );
236 }
237 }
238}