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<FileStorage>),
83}
84
85const CACHED_READS: usize = 64;
89
90struct FileStorage {
91 file: File,
92 cache: Vec<(usize, Box<[u8]>)>,
94 next: usize,
96}
97
98impl Storage {
99 fn file(file: File) -> Self {
100 Storage::File(Mutex::new(FileStorage {
101 file,
102 cache: Vec::with_capacity(CACHED_READS),
103 next: 0,
104 }))
105 }
106
107 fn len(&self) -> Result<u64, SpkError> {
109 match self {
110 Storage::Memory(data) => Ok(data.len() as u64),
111 Storage::File(file) => Ok(file
112 .lock()
113 .map_err(|_| SpkError::Io("poisoned lock".into()))?
114 .file
115 .metadata()?
116 .len()),
117 }
118 }
119
120 fn read_bytes(&self, offset: usize, buf: &mut [u8]) -> Result<(), SpkError> {
121 match self {
122 Storage::Memory(data) => {
123 let end = offset
124 .checked_add(buf.len())
125 .filter(|end| *end <= data.len())
126 .ok_or_else(|| SpkError::Format("read past end of file".into()))?;
127 buf.copy_from_slice(&data[offset..end]);
128 Ok(())
129 }
130 Storage::File(storage) => {
131 let mut storage = storage
132 .lock()
133 .map_err(|_| SpkError::Io("poisoned lock".into()))?;
134 let FileStorage { file, cache, next } = &mut *storage;
135 if let Some((_, bytes)) = cache
136 .iter()
137 .find(|(at, bytes)| *at == offset && bytes.len() == buf.len())
138 {
139 buf.copy_from_slice(bytes);
140 return Ok(());
141 }
142 file.seek(SeekFrom::Start(offset as u64))?;
143 file.read_exact(buf)?;
144 let entry = (offset, Box::from(&*buf));
145 if cache.len() < CACHED_READS {
146 cache.push(entry);
147 } else {
148 cache[*next] = entry;
149 *next = (*next + 1) % CACHED_READS;
150 }
151 Ok(())
152 }
153 }
154 }
155}
156
157#[derive(Debug, Clone, PartialEq)]
159pub struct Segment {
160 pub name: String,
162 pub center: i32,
164 pub target: i32,
166 pub frame: i32,
168 pub data_type: i32,
170 pub start_et: f64,
172 pub end_et: f64,
174 start_word: usize,
176 init: f64,
178 interval: f64,
179 record_size: usize,
180 record_count: usize,
181}
182
183impl Segment {
184 pub fn covers(&self, et: f64) -> bool {
186 et >= self.start_et && et <= self.end_et
187 }
188
189 fn components(&self) -> usize {
190 if self.data_type == 3 {
191 6
192 } else {
193 3
194 }
195 }
196}
197
198pub struct Spk {
200 storage: Storage,
201 words: usize,
204 little_endian: bool,
205 segments: Vec<Segment>,
206}
207
208impl fmt::Debug for Spk {
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 f.debug_struct("Spk")
211 .field("segments", &self.segments)
212 .finish()
213 }
214}
215
216pub type State = ([f64; 3], [f64; 3]);
218
219impl Spk {
220 pub fn from_bytes(data: Vec<u8>) -> Result<Self, SpkError> {
222 Self::load(Storage::Memory(data))
223 }
224
225 pub fn open(path: impl AsRef<Path>) -> Result<Self, SpkError> {
227 Self::load(Storage::file(File::open(path)?))
228 }
229
230 pub fn segments(&self) -> &[Segment] {
232 &self.segments
233 }
234
235 fn load(storage: Storage) -> Result<Self, SpkError> {
236 let mut header = [0u8; RECORD_BYTES];
237 storage.read_bytes(0, &mut header)?;
238 if &header[0..7] != b"DAF/SPK" {
239 return Err(SpkError::Format("not a DAF/SPK file".into()));
240 }
241 let little_endian = match &header[88..96] {
242 b"LTL-IEEE" => true,
243 b"BIG-IEEE" => false,
244 _ => i32::from_le_bytes(header[8..12].try_into().unwrap()) == 2,
246 };
247 let words = usize::try_from(storage.len()? / WORD_BYTES as u64).unwrap_or(usize::MAX);
248 let mut spk = Spk {
249 storage,
250 words,
251 little_endian,
252 segments: Vec::new(),
253 };
254 let nd = spk.int_at(&header, 8) as usize;
255 let ni = spk.int_at(&header, 12) as usize;
256 if nd != 2 || ni != 6 {
257 return Err(SpkError::Format(format!("unexpected ND={nd} NI={ni}")));
258 }
259 let summary_words = nd + ni.div_ceil(2);
260
261 let mut record_number = spk.address_at(&header, 76)?;
262 let mut visited = 0;
263 while record_number != 0 {
264 visited += 1;
265 if visited > 100_000 {
266 return Err(SpkError::Format("summary record chain does not end".into()));
267 }
268 let offset = (record_number - 1)
269 .checked_mul(RECORD_BYTES)
270 .ok_or_else(|| SpkError::Format("summary record out of range".into()))?;
271 let mut record = [0u8; RECORD_BYTES];
272 spk.storage.read_bytes(offset, &mut record)?;
273 let next = spk.double_at(&record, 0);
274 if !(next >= 0.0 && next.fract() == 0.0 && next <= i32::MAX as f64) {
275 return Err(SpkError::Format("bad next summary record".into()));
276 }
277 let count = spk.double_at(&record, 16) as usize;
278 let mut names = [0u8; RECORD_BYTES];
280 spk.storage.read_bytes(offset + RECORD_BYTES, &mut names)?;
281 let name_chars = summary_words * WORD_BYTES;
282 for i in 0..count {
283 let base = 24 + i * summary_words * WORD_BYTES;
284 if base + summary_words * WORD_BYTES > RECORD_BYTES {
285 return Err(SpkError::Format("summary overflows its record".into()));
286 }
287 let mut segment = spk.read_segment(&record, base)?;
288 let raw = &names[i * name_chars..(i + 1) * name_chars];
289 segment.name = String::from_utf8_lossy(raw).trim_end().to_string();
290 spk.segments.push(segment);
291 }
292 record_number = next as usize;
293 }
294 Ok(spk)
295 }
296
297 fn read_segment(&self, record: &[u8], base: usize) -> Result<Segment, SpkError> {
298 let start_et = self.double_at(record, base);
299 let end_et = self.double_at(record, base + 8);
300 let ints = base + 16;
301 let target = self.int_at(record, ints);
302 let center = self.int_at(record, ints + 4);
303 let frame = self.int_at(record, ints + 8);
304 let data_type = self.int_at(record, ints + 12);
305 let start_word = self.address_at(record, ints + 16)?;
306 let end_word = self.address_at(record, ints + 20)?;
307
308 let mut segment = Segment {
309 name: String::new(),
310 center,
311 target,
312 frame,
313 data_type,
314 start_et,
315 end_et,
316 start_word,
317 init: 0.0,
318 interval: 0.0,
319 record_size: 0,
320 record_count: 0,
321 };
322 if data_type == 2 || data_type == 3 {
323 let bad =
324 || SpkError::Format(format!("bad Chebyshev directory for {center} -> {target}"));
325 if end_word < start_word.max(4) || end_word > self.words {
326 return Err(SpkError::Format(format!(
327 "bad word range for {center} -> {target}"
328 )));
329 }
330 let dir = self.read_words(end_word - 3, 4)?;
331 let count = |x: f64| {
332 (x >= 1.0 && x.fract() == 0.0 && x <= self.words as f64)
333 .then_some(x as usize)
334 .ok_or_else(bad)
335 };
336 segment.init = dir[0];
337 segment.interval = dir[1];
338 segment.record_size = count(dir[2])?;
339 segment.record_count = count(dir[3])?;
340 let data_words = segment
342 .record_size
343 .checked_mul(segment.record_count)
344 .and_then(|w| w.checked_add(4))
345 .ok_or_else(bad)?;
346 let coefficients = segment.record_size.saturating_sub(2);
347 if !segment.init.is_finite()
348 || !(segment.interval > 0.0 && segment.interval.is_finite())
349 || data_words > end_word - start_word + 1
350 || coefficients == 0
351 || coefficients % segment.components() != 0
352 {
353 return Err(SpkError::Format(format!(
354 "bad Chebyshev directory for {center} -> {target}"
355 )));
356 }
357 }
358 Ok(segment)
359 }
360
361 pub fn state(&self, target: i32, center: i32, et: f64) -> Result<State, SpkError> {
363 let mut found_pair = false;
364 for segment in &self.segments {
365 if segment.target != target || segment.center != center {
366 continue;
367 }
368 found_pair = true;
369 if segment.covers(et) {
370 return self.segment_state(segment, et);
371 }
372 }
373 if found_pair {
374 Err(SpkError::OutOfRange { target, center, et })
375 } else {
376 Err(SpkError::NoSegment { target, center })
377 }
378 }
379
380 pub fn segment_state(&self, segment: &Segment, et: f64) -> Result<State, SpkError> {
382 if !segment.covers(et) {
383 return Err(SpkError::OutOfRange {
384 target: segment.target,
385 center: segment.center,
386 et,
387 });
388 }
389 self.check_type(segment)?;
390 let index = (((et - segment.init) / segment.interval).floor().max(0.0) as usize)
391 .min(segment.record_count - 1);
392 self.with_record(segment, index, |words| {
393 let (mid, radius) = (words[0], words[1]);
394 self.evaluate(segment, words, (et - mid) / radius)
395 })
396 }
397
398 pub fn segment_state_split(
402 &self,
403 segment: &Segment,
404 whole: f64,
405 fraction: f64,
406 ) -> Result<State, SpkError> {
407 self.check_type(segment)?;
408 let et = (whole - J2000_JD + fraction) * SECONDS_PER_DAY;
409 let out_of_range = SpkError::OutOfRange {
410 target: segment.target,
411 center: segment.center,
412 et,
413 };
414 let intlen = segment.interval;
415 let a = (whole - J2000_JD) * SECONDS_PER_DAY - segment.init;
416 let (index1, offset1) = (a.div_euclid(intlen), a.rem_euclid(intlen));
417 let b = fraction * SECONDS_PER_DAY;
418 let (index2, offset2) = (b.div_euclid(intlen), b.rem_euclid(intlen));
419 let c = offset1 + offset2;
420 let (index3, mut offset) = (c.div_euclid(intlen), c.rem_euclid(intlen));
421 let mut index = index1 + index2 + index3;
422 let count = segment.record_count as f64;
423 if index == count {
424 index -= 1.0;
425 offset += intlen;
426 }
427 if index < 0.0 || index >= count {
428 return Err(out_of_range);
429 }
430 self.with_record(segment, index as usize, |words| {
431 self.evaluate(segment, words, 2.0 * offset / intlen - 1.0)
432 })
433 }
434
435 fn check_type(&self, segment: &Segment) -> Result<(), SpkError> {
436 if segment.data_type == 2 || segment.data_type == 3 {
437 Ok(())
438 } else {
439 Err(SpkError::UnsupportedType(segment.data_type))
440 }
441 }
442
443 fn with_record<R>(
446 &self,
447 segment: &Segment,
448 index: usize,
449 f: impl FnOnce(&[f64]) -> R,
450 ) -> Result<R, SpkError> {
451 const STACK_WORDS: usize = 128;
452 let (first, count) = (
453 segment.start_word + index * segment.record_size,
454 segment.record_size,
455 );
456 if count > STACK_WORDS {
457 return Ok(f(&self.read_words(first, count)?));
458 }
459 self.check_words(first, count)?;
460 let mut bytes = [0u8; STACK_WORDS * WORD_BYTES];
461 let bytes = &mut bytes[..count * WORD_BYTES];
462 self.storage.read_bytes((first - 1) * WORD_BYTES, bytes)?;
463 let mut words = [0.0; STACK_WORDS];
464 for (word, raw) in words.iter_mut().zip(bytes.chunks_exact(WORD_BYTES)) {
465 *word = self.to_f64(raw.try_into().unwrap());
466 }
467 Ok(f(&words[..count]))
468 }
469
470 fn evaluate(&self, segment: &Segment, words: &[f64], s: f64) -> State {
472 let radius = words[1];
473 let n = (segment.record_size - 2) / segment.components();
474 let coeffs = &words[2..];
475 let mut position = [0.0; 3];
476 let mut velocity = [0.0; 3];
477 for axis in 0..3 {
478 let c = &coeffs[axis * n..(axis + 1) * n];
479 let (value, derivative) = chebyshev(c, s);
480 position[axis] = value;
481 velocity[axis] = derivative / radius;
482 }
483 if segment.data_type == 3 {
484 for (axis, v) in velocity.iter_mut().enumerate() {
486 let c = &coeffs[(axis + 3) * n..(axis + 4) * n];
487 *v = chebyshev(c, s).0;
488 }
489 }
490 (position, velocity)
491 }
492
493 pub fn excerpt(&self, start_jd: f64, end_jd: f64) -> Result<Vec<u8>, SpkError> {
498 const SUMMARIES_PER_RECORD: usize = 25; const WORDS_PER_RECORD: usize = RECORD_BYTES / WORD_BYTES;
500 if end_jd <= start_jd {
501 return Err(SpkError::Format("excerpt range is empty".into()));
502 }
503 let (start_et, end_et) = (jd_tdb_to_et(start_jd), jd_tdb_to_et(end_jd));
504
505 struct Kept<'a> {
506 segment: &'a Segment,
507 start_et: f64,
508 end_et: f64,
509 first_word: usize,
510 last_word: usize,
511 }
512 let overlapping: Vec<&Segment> = self
513 .segments
514 .iter()
515 .filter(|s| s.start_et <= end_et && s.end_et >= start_et)
516 .collect();
517 let summary_records = overlapping.len().div_ceil(SUMMARIES_PER_RECORD).max(1);
518 let mut word = (1 + 2 * summary_records) * WORDS_PER_RECORD + 1;
520 let mut data: Vec<f64> = Vec::new();
521 let mut kept = Vec::new();
522 for segment in overlapping {
523 self.check_type(segment)?;
524 let last = segment.record_count as i64 - 1;
525 let first_index =
526 (((start_et - segment.init) / segment.interval).floor() as i64).clamp(0, last);
527 let last_index =
528 (((end_et - segment.init) / segment.interval).floor() as i64).clamp(0, last);
529 let (i0, i1) = (first_index as usize, last_index as usize);
530 let count = i1 - i0 + 1;
531 let records = self.read_words(
532 segment.start_word + i0 * segment.record_size,
533 count * segment.record_size,
534 )?;
535 let init = segment.init + i0 as f64 * segment.interval;
536 let first_word = word;
537 data.extend_from_slice(&records);
538 data.extend_from_slice(&[
539 init,
540 segment.interval,
541 segment.record_size as f64,
542 count as f64,
543 ]);
544 word += records.len() + 4;
545 kept.push(Kept {
546 segment,
547 start_et: segment.start_et.max(init),
548 end_et: segment.end_et.min(init + count as f64 * segment.interval),
549 first_word,
550 last_word: word - 1,
551 });
552 }
553
554 let data_records = data.len().div_ceil(WORDS_PER_RECORD);
555 let mut out = vec![0u8; (1 + 2 * summary_records + data_records) * RECORD_BYTES];
556 let put_i32 =
557 |out: &mut [u8], at: usize, v: i32| out[at..at + 4].copy_from_slice(&v.to_le_bytes());
558 let put_f64 =
559 |out: &mut [u8], at: usize, v: f64| out[at..at + 8].copy_from_slice(&v.to_le_bytes());
560
561 out[0..8].copy_from_slice(b"DAF/SPK ");
563 put_i32(&mut out, 8, 2);
564 put_i32(&mut out, 12, 6);
565 let ifname = format!("{:<60}", "astroceleste-engine excerpt");
566 out[16..76].copy_from_slice(&ifname.as_bytes()[..60]);
567 put_i32(&mut out, 76, 2);
568 put_i32(&mut out, 80, (2 + 2 * (summary_records - 1)) as i32);
569 put_i32(&mut out, 84, word as i32);
570 out[88..96].copy_from_slice(b"LTL-IEEE");
571 out[699..727].copy_from_slice(b"FTPSTR:\r:\n:\r\n:\r\x00:\x81:\x10\xce:ENDFTP");
572
573 let chunks: Vec<&[Kept]> = if kept.is_empty() {
575 vec![&[]]
576 } else {
577 kept.chunks(SUMMARIES_PER_RECORD).collect()
578 };
579 for (k, chunk) in chunks.iter().enumerate() {
580 let record = 2 + 2 * k;
581 let base = (record - 1) * RECORD_BYTES;
582 let next = if k + 1 < chunks.len() { record + 2 } else { 0 };
583 let prev = if k > 0 { record - 2 } else { 0 };
584 put_f64(&mut out, base, next as f64);
585 put_f64(&mut out, base + 8, prev as f64);
586 put_f64(&mut out, base + 16, chunk.len() as f64);
587 let names = base + RECORD_BYTES;
588 out[names..names + RECORD_BYTES].fill(b' ');
589 for (i, kept) in chunk.iter().enumerate() {
590 let at = base + 24 + i * 40;
591 put_f64(&mut out, at, kept.start_et);
592 put_f64(&mut out, at + 8, kept.end_et);
593 let s = kept.segment;
594 for (j, v) in [
595 s.target,
596 s.center,
597 s.frame,
598 s.data_type,
599 kept.first_word as i32,
600 kept.last_word as i32,
601 ]
602 .into_iter()
603 .enumerate()
604 {
605 put_i32(&mut out, at + 16 + 4 * j, v);
606 }
607 let name = s.name.as_bytes();
608 let n = name.len().min(40);
609 out[names + i * 40..names + i * 40 + n].copy_from_slice(&name[..n]);
610 }
611 }
612
613 let data_base = (1 + 2 * summary_records) * RECORD_BYTES;
615 for (i, v) in data.iter().enumerate() {
616 put_f64(&mut out, data_base + i * WORD_BYTES, *v);
617 }
618 Ok(out)
619 }
620
621 fn check_words(&self, first_word: usize, count: usize) -> Result<(), SpkError> {
623 if first_word == 0 {
624 return Err(SpkError::Format("word address 0".into()));
625 }
626 if first_word
627 .checked_add(count)
628 .is_none_or(|end| end - 1 > self.words)
629 {
630 return Err(SpkError::Format("read past end of file".into()));
631 }
632 Ok(())
633 }
634
635 fn read_words(&self, first_word: usize, count: usize) -> Result<Vec<f64>, SpkError> {
636 self.check_words(first_word, count)?;
637 let mut buf = vec![0u8; count * WORD_BYTES];
638 self.storage
639 .read_bytes((first_word - 1) * WORD_BYTES, &mut buf)?;
640 Ok(buf
641 .chunks_exact(WORD_BYTES)
642 .map(|b| self.to_f64(b.try_into().unwrap()))
643 .collect())
644 }
645
646 fn address_at(&self, bytes: &[u8], offset: usize) -> Result<usize, SpkError> {
648 let n = self.int_at(bytes, offset);
649 usize::try_from(n).map_err(|_| SpkError::Format(format!("negative address {n}")))
650 }
651
652 fn double_at(&self, bytes: &[u8], offset: usize) -> f64 {
653 self.to_f64(bytes[offset..offset + 8].try_into().unwrap())
654 }
655
656 fn int_at(&self, bytes: &[u8], offset: usize) -> i32 {
657 let raw: [u8; 4] = bytes[offset..offset + 4].try_into().unwrap();
658 if self.little_endian {
659 i32::from_le_bytes(raw)
660 } else {
661 i32::from_be_bytes(raw)
662 }
663 }
664
665 fn to_f64(&self, raw: [u8; 8]) -> f64 {
666 if self.little_endian {
667 f64::from_le_bytes(raw)
668 } else {
669 f64::from_be_bytes(raw)
670 }
671 }
672}
673
674fn chebyshev(coeffs: &[f64], s: f64) -> (f64, f64) {
676 let (mut t_prev, mut t) = (1.0, s);
677 let (mut d_prev, mut d) = (0.0, 1.0);
678 let mut value = coeffs[0];
679 let mut derivative = 0.0;
680 if coeffs.len() > 1 {
681 value += coeffs[1] * s;
682 derivative += coeffs[1];
683 }
684 for &c in &coeffs[2.min(coeffs.len())..] {
685 let t_next = 2.0 * s * t - t_prev;
686 let d_next = 2.0 * t + 2.0 * s * d - d_prev;
687 value += c * t_next;
688 derivative += c * d_next;
689 (t_prev, t) = (t, t_next);
690 (d_prev, d) = (d, d_next);
691 }
692 (value, derivative)
693}
694
695#[cfg(test)]
696mod tests {
697 use super::chebyshev;
698
699 #[test]
700 fn chebyshev_matches_closed_form() {
701 let s: f64 = 0.37;
703 let expected =
704 1.0 + 2.0 * s + 3.0 * (2.0 * s * s - 1.0) + 4.0 * (4.0 * s.powi(3) - 3.0 * s);
705 let expected_d = 2.0 + 12.0 * s + 4.0 * (12.0 * s * s - 3.0);
706 let (v, d) = chebyshev(&[1.0, 2.0, 3.0, 4.0], s);
707 assert!((v - expected).abs() < 1e-12);
708 assert!((d - expected_d).abs() < 1e-12);
709 }
710}