spicekit: a small Rust SPICE reader

What we wrote, what it covers, and how it benchmarks against CSpice

Alec Alec · · 03 Mins read
Asteroid Institute preview image

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

KernelReadWrite
DAF (shared SPK/PCK container)yes
SPK Type 2 (Chebyshev, position-only)yes
SPK Type 3 (Chebyshev, position + velocity)yesyes
SPK Type 9 (Lagrange, unequal time steps)yesyes
SPK Type 13 (Hermite, unequal time steps)yes
Binary PCK (pxform, sxform, batch variants)yes
Text kernels — body bindings, LSK DELTET/*, Earth→ITRF93 FKyes
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:

opcasencspice p50 (µs)spicekit p50 (µs)speedup
spkez_batchsun wrt SSB, J200010071.766.3811.24x
spkez_batchearth wrt sun, ecliptic100772.1528.3227.26x
spkez_batchmoon wrt earth, ITRF931001802.2161.4229.34x
sxform_batchITRF93 → ECLIPJ2000100709.0522.9530.89x
spkez_batchsun wrt SSB, J200010 0007109.29638.0811.14x
spkez_batchmoon wrt earth, ITRF9310 00043 614.276123.547.12x
sxform_batchITRF93 → J200010 00021 942.292323.519.44x
lsk_dtpoolDELTET/* metadata50 00012 417.6415.46803.31x
lsk_gdpoolDELTET/* values50 00025 715.632288.7211.24x
namfrmITRF9310 0002817.19275.0110.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 NaifSpk is 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 a Vec<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-bench is 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.