spicekit: a small Rust SPICE reader
What we wrote, what it covers, and how it benchmarks against CSpice
We’ve been chipping away at a small Rust library called
spicekit, and it’s at a point where it’s worth showing off.
It’s a pure-Rust reader for the SPICE kernel formats — the SPK
ephemeris files, binary PCK rotation files, and supported text-kernel
content that anyone doing solar-system dynamics ends up parsing.
Originally it was just the guts of adam-core’s SPICE access path,
lifted out into its own crate so we (and anyone else) can use it on
its own.
It’s not a port of CSpice. The base crate doesn’t link
CSpice and there’s no translated CSpice source in it — every
executable line is written from the published NAIF file-format specs.
Parity with CSpice is checked in a sibling crate, spicekit-bench,
which links cspice-sys behind a feature flag and compares every
numeric path and each supported text-kernel semantic against CSpice at
machine-epsilon-scale tolerances. The only CSpice-derived artifact we
ship is the body-name table (data, not code), and it’s reproduced under
NAIF’s distribution terms.
What it covers
| Kernel | Read | Write |
|---|---|---|
| DAF (shared SPK/PCK container) | yes | — |
| SPK Type 2 (Chebyshev, position-only) | yes | — |
| SPK Type 3 (Chebyshev, position + velocity) | yes | yes |
| SPK Type 9 (Lagrange, unequal time steps) | yes | yes |
| SPK Type 13 (Hermite, unequal time steps) | yes | — |
Binary PCK (pxform, sxform, batch variants) | yes | — |
Text kernels — body bindings, LSK DELTET/*, Earth→ITRF93 FK | yes | — |
Built-in NAIF body table (bodn2c / bodc2n) | 692 entries | — |
That’s deliberately less than CSpice — we covered the operations
adam-core actually issues, not the full surface. The text-kernel
coverage is now a little broader than the first cut: custom
NAIF_BODY_NAME / NAIF_BODY_CODE bindings, leapseconds-kernel
DELTET/* values, and the narrow OBJECT_EARTH_FRAME = 'ITRF93'
frame-kernel association used with binary Earth PCKs. Other text-kernel
assignments are skipped by the parser on a best-effort basis, and
furnsh-style loaders should reject otherwise-empty unsupported text
kernels instead of silently dropping them. No async I/O, no CLI.
Using it from Python
pip install spicekit
import numpy as np
import spicekit
spk = spicekit.NaifSpk("/path/to/de440.bsp")
# Single state: Earth (399) wrt SSB (0) at J2000.
x, y, z, vx, vy, vz = spk.state(target=399, center=0, et=0.0)
# Batch over a week.
ets = np.linspace(0.0, 7 * 86_400.0, 1_000, dtype=np.float64)
states = spk.state_batch(target=399, center=0, ets=ets) # (N, 6)
# PCK rotations.
pck = spicekit.NaifPck("/path/to/earth_latest_high_prec.bpc")
M = pck.sxform("J2000", "ITRF93", et=0.0) # 6x6
# Body names.
spicekit.naif_bodn2c("EARTH") # 399
spicekit.naif_bodc2n(399) # "EARTH"
The shapes mirror the CSpice operations they replace, so swapping in
for a spiceypy call site is mostly an import change.
Using it from Rust
use spicekit::{NaifFrame, SpkFile};
let spk = SpkFile::open("de440.bsp")?;
let state = spk.state(399, 0, 0.0, NaifFrame::J2000)?;
println!("{:?}", state); // [x, y, z, vx, vy, vz]
Memory-mapped, no FFI. There’s no kernel pool / furnsh aggregator
in spicekit yet — each SpkFile is just an object you hold, and if
you want CSpice-style “load these N files and dispatch by coverage”
you build that on top. adam-core does exactly that in a small
sibling crate; we’ll likely lift it down into spicekit once the API
settles.
Benchmarks
The parity harness also measures latency on the same matched inputs.
Speedup is cspice / spicekit. The latest table is substantially
better than our early measurements; here is a representative slice
from the current (0.2.2) BENCHMARKS.md:
| op | case | n | cspice p50 (µs) | spicekit p50 (µs) | speedup |
|---|---|---|---|---|---|
spkez_batch | sun wrt SSB, J2000 | 100 | 71.76 | 6.38 | 11.24x |
spkez_batch | earth wrt sun, ecliptic | 100 | 772.15 | 28.32 | 27.26x |
spkez_batch | moon wrt earth, ITRF93 | 100 | 1802.21 | 61.42 | 29.34x |
sxform_batch | ITRF93 → ECLIPJ2000 | 100 | 709.05 | 22.95 | 30.89x |
spkez_batch | sun wrt SSB, J2000 | 10 000 | 7109.29 | 638.08 | 11.14x |
spkez_batch | moon wrt earth, ITRF93 | 10 000 | 43 614.27 | 6123.54 | 7.12x |
sxform_batch | ITRF93 → J2000 | 10 000 | 21 942.29 | 2323.51 | 9.44x |
lsk_dtpool | DELTET/* metadata | 50 000 | 12 417.64 | 15.46 | 803.31x |
lsk_gdpool | DELTET/* values | 50 000 | 25 715.63 | 2288.72 | 11.24x |
namfrm | ITRF93 | 10 000 | 2817.19 | 275.01 | 10.24x |
The broad pattern now is: 20–30x wins at moderate batch sizes on state and frame transforms, roughly 7–11x at 10k-element batches, and still ~2x on body-name lookup. The text-kernel numbers are especially large for metadata because supported LSK content is parsed into structured Rust data once, instead of repeatedly round-tripping through CSpice’s global kernel pool. These are single-threaded benchmark loops on the CI Linux x86_64 runner; the gains are batching, data layout, and FFI avoidance rather than hidden parallelism.
Things we like about it
- Per-file readers. A
NaifSpkis just an object you hold. CSpice’s full pool semantics —furnsh, last-loaded-wins,unload, dispatch across many files — aren’t in the base spicekit API yet. Supported text-kernel content is parsed, but the aggregate loader still lives in a small adam-core-side crate that wraps aVec<spicekit reader>and does the dispatch. Once that API stabilizes we’ll likely move it into spicekit so callers don’t have to rebuild it. - Threaded reads work. Memory-mapped, no locks; Ray workers and Tokio tasks can share a reader without ceremony.
- Smaller native footprint. No CSpice in the wheel.
- The parity oracle is reusable.
spicekit-benchis a fairly general “do these two SPICE implementations agree?” harness, which is handy any time we touch the numerics or supported text-kernel semantics.
Source, issues, and the full benchmark table: github.com/B612-Asteroid-Institute/spicekit. The crate is on crates.io and the Python package is on PyPI.