1use core::fmt;
4
5use pleiades_types::{CelestialBody, Instant};
6
7use crate::codec::{validate_body_segments, validate_segment};
8use crate::error::{CompressionError, CompressionErrorKind};
9
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
13#[repr(u8)]
14#[non_exhaustive]
15pub enum ChannelKind {
16 Longitude,
18 Latitude,
20 DistanceAu,
22}
23
24impl ChannelKind {
25 pub const fn label(self) -> &'static str {
27 match self {
28 Self::Longitude => "Longitude",
29 Self::Latitude => "Latitude",
30 Self::DistanceAu => "DistanceAu",
31 }
32 }
33}
34
35impl fmt::Display for ChannelKind {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 f.write_str(self.label())
38 }
39}
40
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43#[derive(Clone, Debug, PartialEq)]
44pub struct PolynomialChannel {
45 pub kind: ChannelKind,
47 pub scale_exponent: u8,
49 pub coefficients: Vec<f64>,
51}
52
53impl PolynomialChannel {
54 pub fn new(kind: ChannelKind, scale_exponent: u8, coefficients: Vec<f64>) -> Self {
56 Self {
57 kind,
58 scale_exponent,
59 coefficients,
60 }
61 }
62
63 pub fn linear(kind: ChannelKind, scale_exponent: u8, start: f64, end: f64) -> Self {
65 Self::new(kind, scale_exponent, vec![start, end - start])
66 }
67
68 pub fn quadratic(
71 kind: ChannelKind,
72 scale_exponent: u8,
73 start: f64,
74 midpoint: f64,
75 end: f64,
76 midpoint_x: f64,
77 ) -> Self {
78 let linear_delta = end - start;
79 let midpoint_residual = midpoint - (start + linear_delta * midpoint_x);
80 let curvature_scale = midpoint_x * (1.0 - midpoint_x);
81
82 if curvature_scale == 0.0 {
83 return Self::linear(kind, scale_exponent, start, end);
84 }
85
86 let curvature = midpoint_residual / curvature_scale;
87 Self::new(
88 kind,
89 scale_exponent,
90 vec![start, linear_delta + curvature, -curvature],
91 )
92 }
93
94 pub fn validate(&self) -> Result<(), CompressionError> {
96 for (index, coefficient) in self.coefficients.iter().enumerate() {
97 if !coefficient.is_finite() {
98 return Err(CompressionError::new(
99 CompressionErrorKind::InvalidFormat,
100 format!(
101 "polynomial channel {:?} contains a non-finite coefficient at index {index}",
102 self.kind
103 ),
104 ));
105 }
106 }
107
108 Ok(())
109 }
110
111 pub(crate) fn evaluate(&self, x: f64) -> f64 {
112 let mut result = 0.0;
113 let mut power = 1.0;
114 for coefficient in &self.coefficients {
115 result += coefficient * power;
116 power *= x;
117 }
118 result
119 }
120
121 pub(crate) fn evaluate_derivative(&self, x: f64) -> f64 {
124 let mut result = 0.0;
125 let mut power = 1.0; for (i, coefficient) in self.coefficients.iter().enumerate().skip(1) {
127 result += (i as f64) * coefficient * power;
128 power *= x;
129 }
130 result
131 }
132}
133
134#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
136#[derive(Clone, Debug, PartialEq)]
137pub struct Segment {
138 pub start: Instant,
140 pub end: Instant,
142 pub channels: Vec<PolynomialChannel>,
144 pub residual_channels: Vec<PolynomialChannel>,
146}
147
148impl Segment {
149 pub fn new(start: Instant, end: Instant, channels: Vec<PolynomialChannel>) -> Self {
151 Self {
152 start,
153 end,
154 channels,
155 residual_channels: Vec::new(),
156 }
157 }
158
159 pub fn with_residual_channels(
161 start: Instant,
162 end: Instant,
163 channels: Vec<PolynomialChannel>,
164 residual_channels: Vec<PolynomialChannel>,
165 ) -> Self {
166 Self {
167 start,
168 end,
169 channels,
170 residual_channels,
171 }
172 }
173
174 pub fn validate(&self) -> Result<(), CompressionError> {
179 validate_segment(self)
180 }
181
182 pub fn summary_line(&self) -> String {
184 let stored_channels = self
185 .channels
186 .iter()
187 .map(|channel| channel.kind)
188 .collect::<Vec<_>>();
189 let residual_channels = self
190 .residual_channels
191 .iter()
192 .map(|channel| channel.kind)
193 .collect::<Vec<_>>();
194
195 format!(
196 "start: {}; end: {}; stored channels: {}; residual channels: {}",
197 self.start,
198 self.end,
199 crate::format::format_bracketed_labels(&stored_channels),
200 crate::format::format_bracketed_labels(&residual_channels),
201 )
202 }
203
204 pub(crate) fn contains(&self, instant: Instant) -> bool {
205 self.start.scale == instant.scale
206 && self.end.scale == instant.scale
207 && self.start.julian_day.days() <= instant.julian_day.days()
208 && instant.julian_day.days() <= self.end.julian_day.days()
209 }
210
211 pub(crate) fn span_days(&self) -> f64 {
212 self.end.julian_day.days() - self.start.julian_day.days()
213 }
214
215 pub(crate) fn channel(&self, kind: ChannelKind) -> Option<&PolynomialChannel> {
216 self.channels.iter().find(|channel| channel.kind == kind)
217 }
218
219 fn residual_channel(&self, kind: ChannelKind) -> Option<&PolynomialChannel> {
220 self.residual_channels
221 .iter()
222 .find(|channel| channel.kind == kind)
223 }
224
225 pub(crate) fn evaluate_channel(
226 &self,
227 kind: ChannelKind,
228 x: f64,
229 ) -> Result<f64, CompressionError> {
230 let base = self
231 .channel(kind)
232 .map(|channel| channel.evaluate(x))
233 .ok_or_else(|| {
234 CompressionError::new(
235 CompressionErrorKind::MissingChannel,
236 format!("missing {kind:?} channel"),
237 )
238 })?;
239
240 let residual = self
241 .residual_channel(kind)
242 .map(|channel| channel.evaluate(x))
243 .unwrap_or(0.0);
244
245 Ok(base + residual)
246 }
247
248 pub(crate) fn evaluate_channel_derivative(
250 &self,
251 kind: ChannelKind,
252 x: f64,
253 ) -> Result<f64, CompressionError> {
254 let base = self
255 .channel(kind)
256 .map(|channel| channel.evaluate_derivative(x))
257 .ok_or_else(|| {
258 CompressionError::new(
259 CompressionErrorKind::MissingChannel,
260 format!("missing {kind:?} channel"),
261 )
262 })?;
263 let residual = self
264 .residual_channel(kind)
265 .map(|channel| channel.evaluate_derivative(x))
266 .unwrap_or(0.0);
267 Ok(base + residual)
268 }
269}
270
271impl fmt::Display for Segment {
272 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273 f.write_str(&self.summary_line())
274 }
275}
276
277#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
283#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
284#[non_exhaustive]
285pub enum StoredFrame {
286 Geocentric,
288 Heliocentric,
290}
291
292#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
294#[derive(Clone, Debug, PartialEq)]
295pub struct BodyArtifact {
296 pub body: CelestialBody,
298 pub segments: Vec<Segment>,
300 pub frame: StoredFrame,
302}
303
304impl BodyArtifact {
305 pub fn new(body: CelestialBody, segments: Vec<Segment>) -> Self {
307 Self {
308 body,
309 segments,
310 frame: StoredFrame::Geocentric,
311 }
312 }
313
314 pub fn with_frame(body: CelestialBody, segments: Vec<Segment>, frame: StoredFrame) -> Self {
316 Self {
317 body,
318 segments,
319 frame,
320 }
321 }
322
323 pub fn validate(&self) -> Result<(), CompressionError> {
329 for segment in &self.segments {
330 segment.validate()?;
331 }
332
333 validate_body_segments(&self.segments)
334 }
335
336 pub fn summary_line(&self) -> String {
338 let residual_segment_count = self
339 .segments
340 .iter()
341 .filter(|segment| !segment.residual_channels.is_empty())
342 .count();
343
344 format!(
345 "body: {}; segments: {}; residual-bearing segments: {}",
346 self.body,
347 self.segments.len(),
348 residual_segment_count,
349 )
350 }
351
352 pub fn segment_at(&self, instant: Instant) -> Option<&Segment> {
358 self.segments
359 .iter()
360 .rev()
361 .find(|segment| segment.contains(instant))
362 }
363}
364
365impl fmt::Display for BodyArtifact {
366 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367 f.write_str(&self.summary_line())
368 }
369}
370
371#[cfg(test)]
372mod frame_field_tests {
373 use super::*;
374 use pleiades_types::CelestialBody;
375
376 #[test]
377 fn new_defaults_to_geocentric() {
378 let b = BodyArtifact::new(CelestialBody::Sun, vec![]);
379 assert_eq!(b.frame, StoredFrame::Geocentric);
380 }
381
382 #[test]
383 fn with_frame_sets_heliocentric() {
384 let b = BodyArtifact::with_frame(CelestialBody::Jupiter, vec![], StoredFrame::Heliocentric);
385 assert_eq!(b.frame, StoredFrame::Heliocentric);
386 }
387}
388
389#[cfg(test)]
390mod derivative_tests {
391 use super::*;
392 use pleiades_types::{JulianDay, TimeScale};
393
394 #[test]
395 fn polynomial_derivative_matches_power_rule() {
396 let ch = PolynomialChannel::new(ChannelKind::Longitude, 9, vec![2.0, 3.0, 4.0]);
398 assert!((ch.evaluate_derivative(0.0) - 3.0).abs() < 1e-12);
399 assert!((ch.evaluate_derivative(1.0) - 11.0).abs() < 1e-12);
400 assert!((ch.evaluate_derivative(0.5) - 7.0).abs() < 1e-12);
401 }
402
403 #[test]
404 fn segment_channel_derivative_includes_residual() {
405 let start = Instant::new(JulianDay::from_days(0.0), TimeScale::Tt);
406 let end = Instant::new(JulianDay::from_days(1.0), TimeScale::Tt);
407 let seg = Segment::with_residual_channels(
408 start,
409 end,
410 vec![PolynomialChannel::new(
411 ChannelKind::Longitude,
412 9,
413 vec![0.0, 2.0],
414 )], vec![PolynomialChannel::new(
416 ChannelKind::Longitude,
417 9,
418 vec![0.0, 0.0, 5.0], )],
420 );
421 let d = seg
423 .evaluate_channel_derivative(ChannelKind::Longitude, 1.0)
424 .unwrap();
425 assert!((d - 12.0).abs() < 1e-12);
426 }
427}