use core::fmt;
use pleiades_types::{CelestialBody, Instant};
use crate::codec::{validate_body_segments, validate_segment};
use crate::error::{CompressionError, CompressionErrorKind};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
#[repr(u8)]
#[non_exhaustive]
pub enum ChannelKind {
Longitude,
Latitude,
DistanceAu,
}
impl ChannelKind {
pub const fn label(self) -> &'static str {
match self {
Self::Longitude => "Longitude",
Self::Latitude => "Latitude",
Self::DistanceAu => "DistanceAu",
}
}
}
impl fmt::Display for ChannelKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq)]
pub struct PolynomialChannel {
pub kind: ChannelKind,
pub scale_exponent: u8,
pub coefficients: Vec<f64>,
}
impl PolynomialChannel {
pub fn new(kind: ChannelKind, scale_exponent: u8, coefficients: Vec<f64>) -> Self {
Self {
kind,
scale_exponent,
coefficients,
}
}
pub fn linear(kind: ChannelKind, scale_exponent: u8, start: f64, end: f64) -> Self {
Self::new(kind, scale_exponent, vec![start, end - start])
}
pub fn quadratic(
kind: ChannelKind,
scale_exponent: u8,
start: f64,
midpoint: f64,
end: f64,
midpoint_x: f64,
) -> Self {
let linear_delta = end - start;
let midpoint_residual = midpoint - (start + linear_delta * midpoint_x);
let curvature_scale = midpoint_x * (1.0 - midpoint_x);
if curvature_scale == 0.0 {
return Self::linear(kind, scale_exponent, start, end);
}
let curvature = midpoint_residual / curvature_scale;
Self::new(
kind,
scale_exponent,
vec![start, linear_delta + curvature, -curvature],
)
}
pub fn validate(&self) -> Result<(), CompressionError> {
for (index, coefficient) in self.coefficients.iter().enumerate() {
if !coefficient.is_finite() {
return Err(CompressionError::new(
CompressionErrorKind::InvalidFormat,
format!(
"polynomial channel {:?} contains a non-finite coefficient at index {index}",
self.kind
),
));
}
}
Ok(())
}
pub(crate) fn evaluate(&self, x: f64) -> f64 {
let mut result = 0.0;
let mut power = 1.0;
for coefficient in &self.coefficients {
result += coefficient * power;
power *= x;
}
result
}
pub(crate) fn evaluate_derivative(&self, x: f64) -> f64 {
let mut result = 0.0;
let mut power = 1.0; for (i, coefficient) in self.coefficients.iter().enumerate().skip(1) {
result += (i as f64) * coefficient * power;
power *= x;
}
result
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq)]
pub struct Segment {
pub start: Instant,
pub end: Instant,
pub channels: Vec<PolynomialChannel>,
pub residual_channels: Vec<PolynomialChannel>,
}
impl Segment {
pub fn new(start: Instant, end: Instant, channels: Vec<PolynomialChannel>) -> Self {
Self {
start,
end,
channels,
residual_channels: Vec::new(),
}
}
pub fn with_residual_channels(
start: Instant,
end: Instant,
channels: Vec<PolynomialChannel>,
residual_channels: Vec<PolynomialChannel>,
) -> Self {
Self {
start,
end,
channels,
residual_channels,
}
}
pub fn validate(&self) -> Result<(), CompressionError> {
validate_segment(self)
}
pub fn summary_line(&self) -> String {
let stored_channels = self
.channels
.iter()
.map(|channel| channel.kind)
.collect::<Vec<_>>();
let residual_channels = self
.residual_channels
.iter()
.map(|channel| channel.kind)
.collect::<Vec<_>>();
format!(
"start: {}; end: {}; stored channels: {}; residual channels: {}",
self.start,
self.end,
crate::format::format_bracketed_labels(&stored_channels),
crate::format::format_bracketed_labels(&residual_channels),
)
}
pub(crate) fn contains(&self, instant: Instant) -> bool {
self.start.scale == instant.scale
&& self.end.scale == instant.scale
&& self.start.julian_day.days() <= instant.julian_day.days()
&& instant.julian_day.days() <= self.end.julian_day.days()
}
pub(crate) fn span_days(&self) -> f64 {
self.end.julian_day.days() - self.start.julian_day.days()
}
pub(crate) fn channel(&self, kind: ChannelKind) -> Option<&PolynomialChannel> {
self.channels.iter().find(|channel| channel.kind == kind)
}
fn residual_channel(&self, kind: ChannelKind) -> Option<&PolynomialChannel> {
self.residual_channels
.iter()
.find(|channel| channel.kind == kind)
}
pub(crate) fn evaluate_channel(
&self,
kind: ChannelKind,
x: f64,
) -> Result<f64, CompressionError> {
let base = self
.channel(kind)
.map(|channel| channel.evaluate(x))
.ok_or_else(|| {
CompressionError::new(
CompressionErrorKind::MissingChannel,
format!("missing {kind:?} channel"),
)
})?;
let residual = self
.residual_channel(kind)
.map(|channel| channel.evaluate(x))
.unwrap_or(0.0);
Ok(base + residual)
}
pub(crate) fn evaluate_channel_derivative(
&self,
kind: ChannelKind,
x: f64,
) -> Result<f64, CompressionError> {
let base = self
.channel(kind)
.map(|channel| channel.evaluate_derivative(x))
.ok_or_else(|| {
CompressionError::new(
CompressionErrorKind::MissingChannel,
format!("missing {kind:?} channel"),
)
})?;
let residual = self
.residual_channel(kind)
.map(|channel| channel.evaluate_derivative(x))
.unwrap_or(0.0);
Ok(base + residual)
}
}
impl fmt::Display for Segment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.summary_line())
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub enum StoredFrame {
Geocentric,
Heliocentric,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq)]
pub struct BodyArtifact {
pub body: CelestialBody,
pub segments: Vec<Segment>,
pub frame: StoredFrame,
}
impl BodyArtifact {
pub fn new(body: CelestialBody, segments: Vec<Segment>) -> Self {
Self {
body,
segments,
frame: StoredFrame::Geocentric,
}
}
pub fn with_frame(body: CelestialBody, segments: Vec<Segment>, frame: StoredFrame) -> Self {
Self {
body,
segments,
frame,
}
}
pub fn validate(&self) -> Result<(), CompressionError> {
for segment in &self.segments {
segment.validate()?;
}
validate_body_segments(&self.segments)
}
pub fn summary_line(&self) -> String {
let residual_segment_count = self
.segments
.iter()
.filter(|segment| !segment.residual_channels.is_empty())
.count();
format!(
"body: {}; segments: {}; residual-bearing segments: {}",
self.body,
self.segments.len(),
residual_segment_count,
)
}
pub fn segment_at(&self, instant: Instant) -> Option<&Segment> {
self.segments
.iter()
.rev()
.find(|segment| segment.contains(instant))
}
}
impl fmt::Display for BodyArtifact {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.summary_line())
}
}
#[cfg(test)]
mod frame_field_tests {
use super::*;
use pleiades_types::CelestialBody;
#[test]
fn new_defaults_to_geocentric() {
let b = BodyArtifact::new(CelestialBody::Sun, vec![]);
assert_eq!(b.frame, StoredFrame::Geocentric);
}
#[test]
fn with_frame_sets_heliocentric() {
let b = BodyArtifact::with_frame(CelestialBody::Jupiter, vec![], StoredFrame::Heliocentric);
assert_eq!(b.frame, StoredFrame::Heliocentric);
}
}
#[cfg(test)]
mod derivative_tests {
use super::*;
use pleiades_types::{JulianDay, TimeScale};
#[test]
fn polynomial_derivative_matches_power_rule() {
let ch = PolynomialChannel::new(ChannelKind::Longitude, 9, vec![2.0, 3.0, 4.0]);
assert!((ch.evaluate_derivative(0.0) - 3.0).abs() < 1e-12);
assert!((ch.evaluate_derivative(1.0) - 11.0).abs() < 1e-12);
assert!((ch.evaluate_derivative(0.5) - 7.0).abs() < 1e-12);
}
#[test]
fn segment_channel_derivative_includes_residual() {
let start = Instant::new(JulianDay::from_days(0.0), TimeScale::Tt);
let end = Instant::new(JulianDay::from_days(1.0), TimeScale::Tt);
let seg = Segment::with_residual_channels(
start,
end,
vec![PolynomialChannel::new(
ChannelKind::Longitude,
9,
vec![0.0, 2.0],
)], vec![PolynomialChannel::new(
ChannelKind::Longitude,
9,
vec![0.0, 0.0, 5.0], )],
);
let d = seg
.evaluate_channel_derivative(ChannelKind::Longitude, 1.0)
.unwrap();
assert!((d - 12.0).abs() < 1e-12);
}
}