1use std::fmt;
10use std::fs::File;
11use std::io::{Read, Seek, SeekFrom};
12use std::path::Path;
13use std::sync::Mutex;
14
15const RECORD_BYTES: usize = 1024;
16const WORD_BYTES: usize = 8;
17const J2000_JD: f64 = 2_451_545.0;
18const SECONDS_PER_DAY: f64 = 86_400.0;
19
20pub fn jd_tdb_to_et(jd_tdb: f64) -> f64 {
22 (jd_tdb - J2000_JD) * SECONDS_PER_DAY
23}
24
25#[derive(Debug, Clone, PartialEq)]
27#[non_exhaustive]
28pub enum SpkError {
29 Io(String),
31 Format(String),
33 UnsupportedType(i32),
35 OutOfRange {
37 target: i32,
39 center: i32,
41 et: f64,
43 },
44 NoSegment {
46 target: i32,
48 center: i32,
50 },
51}
52
53impl fmt::Display for SpkError {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 match self {
56 SpkError::Io(msg) => write!(f, "SPK I/O error: {msg}"),
57 SpkError::Format(msg) => write!(f, "malformed SPK file: {msg}"),
58 SpkError::UnsupportedType(t) => write!(f, "unsupported SPK segment type {t}"),
59 SpkError::OutOfRange { target, center, et } => write!(
60 f,
61 "no SPK segment for {center} -> {target} covers ET {et:.1} s"
62 ),
63 SpkError::NoSegment { target, center } => {
64 write!(f, "kernel has no segment for {center} -> {target}")
65 }
66 }
67 }
68}
69
70impl std::error::Error for SpkError {}
71
72impl From<std::io::Error> for SpkError {
73 fn from(err: std::io::Error) -> Self {
74 SpkError::Io(err.to_string())
75 }
76}
77
78enum Storage {
81 Memory(Vec<u8>),
82 File(Mutex<File>),
83}
84
85impl Storage {
86 fn read_bytes(&self, offset: usize, buf: &mut [u8]) -> Result<(), SpkError> {
87 match self {
88 Storage::Memory(data) => {
89 let end = offset
90 .checked_add(buf.len())
91 .filter(|end| *end <= data.len())
92 .ok_or_else(|| SpkError::Format("read past end of file".into()))?;
93 buf.copy_from_slice(&data[offset..end]);
94 Ok(())
95 }
96 Storage::File(file) => {
97 let mut file = file
98 .lock()
99 .map_err(|_| SpkError::Io("poisoned lock".into()))?;
100 file.seek(SeekFrom::Start(offset as u64))?;
101 file.read_exact(buf)?;
102 Ok(())
103 }
104 }
105 }
106}
107
108#[derive(Debug, Clone, PartialEq)]
110pub struct Segment {
111 pub name: String,
113 pub center: i32,
115 pub target: i32,
117 pub frame: i32,
119 pub data_type: i32,
121 pub start_et: f64,
123 pub end_et: f64,
125 start_word: usize,
127 init: f64,
129 interval: f64,
130 record_size: usize,
131 record_count: usize,
132}
133
134impl Segment {
135 pub fn covers(&self, et: f64) -> bool {
137 et >= self.start_et && et <= self.end_et
138 }
139
140 fn components(&self) -> usize {
141 if self.data_type == 3 {
142 6
143 } else {
144 3
145 }
146 }
147}
148
149pub struct Spk {
151 storage: Storage,
152 little_endian: bool,
153 segments: Vec<Segment>,
154}
155
156impl fmt::Debug for Spk {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158 f.debug_struct("Spk")
159 .field("segments", &self.segments)
160 .finish()
161 }
162}
163
164pub type State = ([f64; 3], [f64; 3]);
166
167impl Spk {
168 pub fn from_bytes(data: Vec<u8>) -> Result<Self, SpkError> {
170 Self::load(Storage::Memory(data))
171 }
172
173 pub fn open(path: impl AsRef<Path>) -> Result<Self, SpkError> {
175 Self::load(Storage::File(Mutex::new(File::open(path)?)))
176 }
177
178 pub fn segments(&self) -> &[Segment] {
180 &self.segments
181 }
182
183 fn load(storage: Storage) -> Result<Self, SpkError> {
184 let mut header = [0u8; RECORD_BYTES];
185 storage.read_bytes(0, &mut header)?;
186 if &header[0..7] != b"DAF/SPK" {
187 return Err(SpkError::Format("not a DAF/SPK file".into()));
188 }
189 let little_endian = match &header[88..96] {
190 b"LTL-IEEE" => true,
191 b"BIG-IEEE" => false,
192 _ => i32::from_le_bytes(header[8..12].try_into().unwrap()) == 2,
194 };
195 let mut spk = Spk {
196 storage,
197 little_endian,
198 segments: Vec::new(),
199 };
200 let nd = spk.int_at(&header, 8) as usize;
201 let ni = spk.int_at(&header, 12) as usize;
202 if nd != 2 || ni != 6 {
203 return Err(SpkError::Format(format!("unexpected ND={nd} NI={ni}")));
204 }
205 let summary_words = nd + ni.div_ceil(2);
206
207 let mut record_number = spk.int_at(&header, 76) as usize;
208 let mut visited = 0;
209 while record_number != 0 {
210 visited += 1;
211 if visited > 100_000 {
212 return Err(SpkError::Format("summary record chain does not end".into()));
213 }
214 let mut record = [0u8; RECORD_BYTES];
215 spk.storage
216 .read_bytes((record_number - 1) * RECORD_BYTES, &mut record)?;
217 let next = spk.double_at(&record, 0) as usize;
218 let count = spk.double_at(&record, 16) as usize;
219 let mut names = [0u8; RECORD_BYTES];
221 spk.storage
222 .read_bytes(record_number * RECORD_BYTES, &mut names)?;
223 let name_chars = summary_words * WORD_BYTES;
224 for i in 0..count {
225 let base = 24 + i * summary_words * WORD_BYTES;
226 if base + summary_words * WORD_BYTES > RECORD_BYTES {
227 return Err(SpkError::Format("summary overflows its record".into()));
228 }
229 let mut segment = spk.read_segment(&record, base)?;
230 let raw = &names[i * name_chars..(i + 1) * name_chars];
231 segment.name = String::from_utf8_lossy(raw).trim_end().to_string();
232 spk.segments.push(segment);
233 }
234 record_number = next;
235 }
236 Ok(spk)
237 }
238
239 fn read_segment(&self, record: &[u8], base: usize) -> Result<Segment, SpkError> {
240 let start_et = self.double_at(record, base);
241 let end_et = self.double_at(record, base + 8);
242 let ints = base + 16;
243 let target = self.int_at(record, ints);
244 let center = self.int_at(record, ints + 4);
245 let frame = self.int_at(record, ints + 8);
246 let data_type = self.int_at(record, ints + 12);
247 let start_word = self.int_at(record, ints + 16) as usize;
248 let end_word = self.int_at(record, ints + 20) as usize;
249
250 let mut segment = Segment {
251 name: String::new(),
252 center,
253 target,
254 frame,
255 data_type,
256 start_et,
257 end_et,
258 start_word,
259 init: 0.0,
260 interval: 0.0,
261 record_size: 0,
262 record_count: 0,
263 };
264 if data_type == 2 || data_type == 3 {
265 if end_word < start_word.max(4) {
266 return Err(SpkError::Format(format!(
267 "bad word range for {center} -> {target}"
268 )));
269 }
270 let dir = self.read_words(end_word - 3, 4)?;
271 segment.init = dir[0];
272 segment.interval = dir[1];
273 segment.record_size = dir[2] as usize;
274 segment.record_count = dir[3] as usize;
275 let coefficients = segment.record_size.saturating_sub(2);
276 if segment.interval <= 0.0
277 || segment.record_count == 0
278 || coefficients == 0
279 || coefficients % segment.components() != 0
280 {
281 return Err(SpkError::Format(format!(
282 "bad Chebyshev directory for {center} -> {target}"
283 )));
284 }
285 }
286 Ok(segment)
287 }
288
289 pub fn state(&self, target: i32, center: i32, et: f64) -> Result<State, SpkError> {
291 let mut found_pair = false;
292 for segment in &self.segments {
293 if segment.target != target || segment.center != center {
294 continue;
295 }
296 found_pair = true;
297 if segment.covers(et) {
298 return self.segment_state(segment, et);
299 }
300 }
301 if found_pair {
302 Err(SpkError::OutOfRange { target, center, et })
303 } else {
304 Err(SpkError::NoSegment { target, center })
305 }
306 }
307
308 pub fn segment_state(&self, segment: &Segment, et: f64) -> Result<State, SpkError> {
310 if !segment.covers(et) {
311 return Err(SpkError::OutOfRange {
312 target: segment.target,
313 center: segment.center,
314 et,
315 });
316 }
317 self.check_type(segment)?;
318 let index = (((et - segment.init) / segment.interval).floor().max(0.0) as usize)
319 .min(segment.record_count - 1);
320 let words = self.record(segment, index)?;
321 let (mid, radius) = (words[0], words[1]);
322 Ok(self.evaluate(segment, &words, (et - mid) / radius))
323 }
324
325 pub fn segment_state_split(
329 &self,
330 segment: &Segment,
331 whole: f64,
332 fraction: f64,
333 ) -> Result<State, SpkError> {
334 self.check_type(segment)?;
335 let et = (whole - J2000_JD + fraction) * SECONDS_PER_DAY;
336 let out_of_range = SpkError::OutOfRange {
337 target: segment.target,
338 center: segment.center,
339 et,
340 };
341 let intlen = segment.interval;
342 let a = (whole - J2000_JD) * SECONDS_PER_DAY - segment.init;
343 let (index1, offset1) = (a.div_euclid(intlen), a.rem_euclid(intlen));
344 let b = fraction * SECONDS_PER_DAY;
345 let (index2, offset2) = (b.div_euclid(intlen), b.rem_euclid(intlen));
346 let c = offset1 + offset2;
347 let (index3, mut offset) = (c.div_euclid(intlen), c.rem_euclid(intlen));
348 let mut index = index1 + index2 + index3;
349 let count = segment.record_count as f64;
350 if index == count {
351 index -= 1.0;
352 offset += intlen;
353 }
354 if index < 0.0 || index >= count {
355 return Err(out_of_range);
356 }
357 let words = self.record(segment, index as usize)?;
358 Ok(self.evaluate(segment, &words, 2.0 * offset / intlen - 1.0))
359 }
360
361 fn check_type(&self, segment: &Segment) -> Result<(), SpkError> {
362 if segment.data_type == 2 || segment.data_type == 3 {
363 Ok(())
364 } else {
365 Err(SpkError::UnsupportedType(segment.data_type))
366 }
367 }
368
369 fn record(&self, segment: &Segment, index: usize) -> Result<Vec<f64>, SpkError> {
370 self.read_words(
371 segment.start_word + index * segment.record_size,
372 segment.record_size,
373 )
374 }
375
376 fn evaluate(&self, segment: &Segment, words: &[f64], s: f64) -> State {
378 let radius = words[1];
379 let n = (segment.record_size - 2) / segment.components();
380 let coeffs = &words[2..];
381 let mut position = [0.0; 3];
382 let mut velocity = [0.0; 3];
383 for axis in 0..3 {
384 let c = &coeffs[axis * n..(axis + 1) * n];
385 let (value, derivative) = chebyshev(c, s);
386 position[axis] = value;
387 velocity[axis] = derivative / radius;
388 }
389 if segment.data_type == 3 {
390 for (axis, v) in velocity.iter_mut().enumerate() {
392 let c = &coeffs[(axis + 3) * n..(axis + 4) * n];
393 *v = chebyshev(c, s).0;
394 }
395 }
396 (position, velocity)
397 }
398
399 pub fn excerpt(&self, start_jd: f64, end_jd: f64) -> Result<Vec<u8>, SpkError> {
404 const SUMMARIES_PER_RECORD: usize = 25; const WORDS_PER_RECORD: usize = RECORD_BYTES / WORD_BYTES;
406 if end_jd <= start_jd {
407 return Err(SpkError::Format("excerpt range is empty".into()));
408 }
409 let (start_et, end_et) = (jd_tdb_to_et(start_jd), jd_tdb_to_et(end_jd));
410
411 struct Kept<'a> {
412 segment: &'a Segment,
413 start_et: f64,
414 end_et: f64,
415 first_word: usize,
416 last_word: usize,
417 }
418 let overlapping: Vec<&Segment> = self
419 .segments
420 .iter()
421 .filter(|s| s.start_et <= end_et && s.end_et >= start_et)
422 .collect();
423 let summary_records = overlapping.len().div_ceil(SUMMARIES_PER_RECORD).max(1);
424 let mut word = (1 + 2 * summary_records) * WORDS_PER_RECORD + 1;
426 let mut data: Vec<f64> = Vec::new();
427 let mut kept = Vec::new();
428 for segment in overlapping {
429 self.check_type(segment)?;
430 let last = segment.record_count as i64 - 1;
431 let first_index =
432 (((start_et - segment.init) / segment.interval).floor() as i64).clamp(0, last);
433 let last_index =
434 (((end_et - segment.init) / segment.interval).floor() as i64).clamp(0, last);
435 let (i0, i1) = (first_index as usize, last_index as usize);
436 let count = i1 - i0 + 1;
437 let records = self.read_words(
438 segment.start_word + i0 * segment.record_size,
439 count * segment.record_size,
440 )?;
441 let init = segment.init + i0 as f64 * segment.interval;
442 let first_word = word;
443 data.extend_from_slice(&records);
444 data.extend_from_slice(&[
445 init,
446 segment.interval,
447 segment.record_size as f64,
448 count as f64,
449 ]);
450 word += records.len() + 4;
451 kept.push(Kept {
452 segment,
453 start_et: segment.start_et.max(init),
454 end_et: segment.end_et.min(init + count as f64 * segment.interval),
455 first_word,
456 last_word: word - 1,
457 });
458 }
459
460 let data_records = data.len().div_ceil(WORDS_PER_RECORD);
461 let mut out = vec![0u8; (1 + 2 * summary_records + data_records) * RECORD_BYTES];
462 let put_i32 =
463 |out: &mut [u8], at: usize, v: i32| out[at..at + 4].copy_from_slice(&v.to_le_bytes());
464 let put_f64 =
465 |out: &mut [u8], at: usize, v: f64| out[at..at + 8].copy_from_slice(&v.to_le_bytes());
466
467 out[0..8].copy_from_slice(b"DAF/SPK ");
469 put_i32(&mut out, 8, 2);
470 put_i32(&mut out, 12, 6);
471 let ifname = format!("{:<60}", "astroceleste-engine excerpt");
472 out[16..76].copy_from_slice(&ifname.as_bytes()[..60]);
473 put_i32(&mut out, 76, 2);
474 put_i32(&mut out, 80, (2 + 2 * (summary_records - 1)) as i32);
475 put_i32(&mut out, 84, word as i32);
476 out[88..96].copy_from_slice(b"LTL-IEEE");
477 out[699..727].copy_from_slice(b"FTPSTR:\r:\n:\r\n:\r\x00:\x81:\x10\xce:ENDFTP");
478
479 let chunks: Vec<&[Kept]> = if kept.is_empty() {
481 vec![&[]]
482 } else {
483 kept.chunks(SUMMARIES_PER_RECORD).collect()
484 };
485 for (k, chunk) in chunks.iter().enumerate() {
486 let record = 2 + 2 * k;
487 let base = (record - 1) * RECORD_BYTES;
488 let next = if k + 1 < chunks.len() { record + 2 } else { 0 };
489 let prev = if k > 0 { record - 2 } else { 0 };
490 put_f64(&mut out, base, next as f64);
491 put_f64(&mut out, base + 8, prev as f64);
492 put_f64(&mut out, base + 16, chunk.len() as f64);
493 let names = base + RECORD_BYTES;
494 out[names..names + RECORD_BYTES].fill(b' ');
495 for (i, kept) in chunk.iter().enumerate() {
496 let at = base + 24 + i * 40;
497 put_f64(&mut out, at, kept.start_et);
498 put_f64(&mut out, at + 8, kept.end_et);
499 let s = kept.segment;
500 for (j, v) in [
501 s.target,
502 s.center,
503 s.frame,
504 s.data_type,
505 kept.first_word as i32,
506 kept.last_word as i32,
507 ]
508 .into_iter()
509 .enumerate()
510 {
511 put_i32(&mut out, at + 16 + 4 * j, v);
512 }
513 let name = s.name.as_bytes();
514 let n = name.len().min(40);
515 out[names + i * 40..names + i * 40 + n].copy_from_slice(&name[..n]);
516 }
517 }
518
519 let data_base = (1 + 2 * summary_records) * RECORD_BYTES;
521 for (i, v) in data.iter().enumerate() {
522 put_f64(&mut out, data_base + i * WORD_BYTES, *v);
523 }
524 Ok(out)
525 }
526
527 fn read_words(&self, first_word: usize, count: usize) -> Result<Vec<f64>, SpkError> {
528 if first_word == 0 {
529 return Err(SpkError::Format("word address 0".into()));
530 }
531 let mut buf = vec![0u8; count * WORD_BYTES];
532 self.storage
533 .read_bytes((first_word - 1) * WORD_BYTES, &mut buf)?;
534 Ok(buf
535 .chunks_exact(WORD_BYTES)
536 .map(|b| self.to_f64(b.try_into().unwrap()))
537 .collect())
538 }
539
540 fn double_at(&self, bytes: &[u8], offset: usize) -> f64 {
541 self.to_f64(bytes[offset..offset + 8].try_into().unwrap())
542 }
543
544 fn int_at(&self, bytes: &[u8], offset: usize) -> i32 {
545 let raw: [u8; 4] = bytes[offset..offset + 4].try_into().unwrap();
546 if self.little_endian {
547 i32::from_le_bytes(raw)
548 } else {
549 i32::from_be_bytes(raw)
550 }
551 }
552
553 fn to_f64(&self, raw: [u8; 8]) -> f64 {
554 if self.little_endian {
555 f64::from_le_bytes(raw)
556 } else {
557 f64::from_be_bytes(raw)
558 }
559 }
560}
561
562fn chebyshev(coeffs: &[f64], s: f64) -> (f64, f64) {
564 let (mut t_prev, mut t) = (1.0, s);
565 let (mut d_prev, mut d) = (0.0, 1.0);
566 let mut value = coeffs[0];
567 let mut derivative = 0.0;
568 if coeffs.len() > 1 {
569 value += coeffs[1] * s;
570 derivative += coeffs[1];
571 }
572 for &c in &coeffs[2.min(coeffs.len())..] {
573 let t_next = 2.0 * s * t - t_prev;
574 let d_next = 2.0 * t + 2.0 * s * d - d_prev;
575 value += c * t_next;
576 derivative += c * d_next;
577 (t_prev, t) = (t, t_next);
578 (d_prev, d) = (d, d_next);
579 }
580 (value, derivative)
581}
582
583#[cfg(test)]
584mod tests {
585 use super::chebyshev;
586
587 #[test]
588 fn chebyshev_matches_closed_form() {
589 let s: f64 = 0.37;
591 let expected =
592 1.0 + 2.0 * s + 3.0 * (2.0 * s * s - 1.0) + 4.0 * (4.0 * s.powi(3) - 3.0 * s);
593 let expected_d = 2.0 + 12.0 * s + 4.0 * (12.0 * s * s - 3.0);
594 let (v, d) = chebyshev(&[1.0, 2.0, 3.0, 4.0], s);
595 assert!((v - expected).abs() < 1e-12);
596 assert!((d - expected_d).abs() < 1e-12);
597 }
598}