Skip to main content

astroceleste_engine/ephemeris/
kernels.rs

1//! A set of JPL kernels in preference order, and barycentric states of bodies.
2
3use crate::constants::{AU_KM, DAY_S, T0};
4use crate::frames::Vec3;
5use crate::time::Time;
6
7use super::spk::{Segment, Spk, SpkError};
8
9/// NAIF code of the Solar System Barycenter.
10pub const SSB: i32 = 0;
11
12/// One loaded kernel and the Julian-date span over which it has every body (a chart needs
13/// all of them, so the usable span is the intersection of the bodies' spans). A body may
14/// be split into several segments covering consecutive time ranges.
15#[derive(Debug)]
16pub struct Kernel {
17    /// Label used in error messages and diagnostics, e.g. "de440s.bsp".
18    pub name: String,
19    spk: Spk,
20    /// First Julian date (TDB) at which every body is covered.
21    pub start_jd: f64,
22    /// Last Julian date (TDB) at which every body is covered.
23    pub end_jd: f64,
24}
25
26impl Kernel {
27    /// Wrap a loaded SPK file; fails if it has no segments or if its bodies share no
28    /// common time span.
29    pub fn new(name: impl Into<String>, spk: Spk) -> Result<Self, SpkError> {
30        let segments = spk.segments();
31        if segments.is_empty() {
32            return Err(SpkError::Format("kernel has no segments".into()));
33        }
34        let (mut start_et, mut end_et) = (f64::NEG_INFINITY, f64::INFINITY);
35        let mut targets: Vec<i32> = segments.iter().map(|s| s.target).collect();
36        targets.sort_unstable();
37        targets.dedup();
38        for target in targets {
39            let (start, end) = body_span(segments, target);
40            start_et = start_et.max(start);
41            end_et = end_et.min(end);
42        }
43        if start_et.is_nan() || end_et.is_nan() || start_et > end_et {
44            return Err(SpkError::Format(
45                "the kernel's bodies share no common time span".into(),
46            ));
47        }
48        let (start_jd, end_jd) = (start_et / DAY_S + T0, end_et / DAY_S + T0);
49        Ok(Kernel {
50            name: name.into(),
51            spk,
52            start_jd,
53            end_jd,
54        })
55    }
56
57    /// Whether every segment covers Julian date `jd`.
58    pub fn covers(&self, jd: f64) -> bool {
59        self.start_jd <= jd && jd <= self.end_jd
60    }
61
62    /// Whether the kernel has a segment for this body (Skyfield `code in ephemeris`).
63    pub fn contains(&self, code: i32) -> bool {
64        self.spk
65            .segments()
66            .iter()
67            .any(|s| s.target == code || s.center == code)
68    }
69
70    /// The segment for `target` covering `et` (TDB seconds past J2000): the first one in
71    /// file order that covers it, else the body's first segment (which then reports the
72    /// date out of range).
73    fn segment_for(&self, target: i32, et: f64) -> Option<&Segment> {
74        let mut segments = self.spk.segments().iter().filter(|s| s.target == target);
75        let first = segments.next()?;
76        if first.covers(et) {
77            return Some(first);
78        }
79        segments.find(|s| s.covers(et)).or(Some(first))
80    }
81
82    /// Position (au) and velocity (au/day) of `code` relative to the Solar System
83    /// Barycenter, summing the chain of segments outward from the SSB like Skyfield's
84    /// `VectorSum`.
85    #[doc(hidden)] // takes the internal `Time`
86    pub fn barycentric(&self, code: i32, t: &Time) -> Result<(Vec3, Vec3), SpkError> {
87        const MAX_CHAIN: usize = 8;
88        let et = (t.whole - T0 + t.tdb_fraction) * DAY_S;
89        let mut chain: [Option<&Segment>; MAX_CHAIN] = [None; MAX_CHAIN];
90        let mut len = 0;
91        let mut current = code;
92        while current != SSB {
93            if len == MAX_CHAIN {
94                return Err(SpkError::Format(format!("segment chain for {code} loops")));
95            }
96            let segment = self.segment_for(current, et).ok_or(SpkError::NoSegment {
97                target: current,
98                center: SSB,
99            })?;
100            chain[len] = Some(segment);
101            len += 1;
102            current = segment.center;
103        }
104        let mut position = [0.0; 3];
105        let mut velocity = [0.0; 3];
106        for segment in chain[..len].iter().rev().flatten() {
107            let (p, v) = self
108                .spk
109                .segment_state_split(segment, t.whole, t.tdb_fraction)?;
110            for k in 0..3 {
111                position[k] += p[k] / AU_KM;
112                velocity[k] += v[k] * DAY_S / AU_KM;
113            }
114        }
115        Ok((position, velocity))
116    }
117}
118
119/// The longest contiguous span (TDB seconds past J2000) covered by `target`'s segments.
120fn body_span(segments: &[Segment], target: i32) -> (f64, f64) {
121    let mut spans: Vec<(f64, f64)> = segments
122        .iter()
123        .filter(|s| s.target == target)
124        .map(|s| (s.start_et, s.end_et))
125        .collect();
126    spans.sort_by(|a, b| a.0.total_cmp(&b.0));
127    let mut best = (f64::NAN, f64::NAN);
128    let mut run = spans[0];
129    for &(start, end) in &spans[1..] {
130        if start <= run.1 {
131            run.1 = run.1.max(end);
132        } else {
133            best = longer(best, run);
134            run = (start, end);
135        }
136    }
137    longer(best, run)
138}
139
140fn longer(a: (f64, f64), b: (f64, f64)) -> (f64, f64) {
141    if a.1 - a.0 >= b.1 - b.0 {
142        a
143    } else {
144        b
145    }
146}
147
148/// Kernels in preference order: a date is computed with the first kernel covering it.
149#[derive(Debug, Default)]
150pub struct KernelSet {
151    kernels: Vec<Kernel>,
152}
153
154impl KernelSet {
155    /// An empty set.
156    pub fn new() -> Self {
157        Self::default()
158    }
159
160    /// Add a kernel, after (so less preferred than) the ones already loaded.
161    pub fn push(&mut self, kernel: Kernel) {
162        self.kernels.push(kernel);
163    }
164
165    /// The kernels, in preference order.
166    pub fn kernels(&self) -> &[Kernel] {
167        &self.kernels
168    }
169
170    /// Whether no kernel is loaded.
171    pub fn is_empty(&self) -> bool {
172        self.kernels.is_empty()
173    }
174
175    /// The preferred kernel covering this Julian date.
176    pub fn for_jd(&self, jd: f64) -> Option<&Kernel> {
177        self.kernels.iter().find(|k| k.covers(jd))
178    }
179
180    /// Combined span of every loaded kernel, as Julian dates.
181    pub fn coverage(&self) -> Option<(f64, f64)> {
182        if self.kernels.is_empty() {
183            return None;
184        }
185        let start = self
186            .kernels
187            .iter()
188            .map(|k| k.start_jd)
189            .fold(f64::INFINITY, f64::min);
190        let end = self
191            .kernels
192            .iter()
193            .map(|k| k.end_jd)
194            .fold(f64::NEG_INFINITY, f64::max);
195        Some((start, end))
196    }
197}