Skip to content

SPICE Kernels

Brahe includes a native reader for NASA JPL NAIF SPICE kernels: binary SPK ephemeris kernels (Chebyshev position/velocity, Types 2 and 3) and binary PCK orientation kernels (Chebyshev Euler angles, Type 2). Kernels are parsed directly by brahe — there is no dependency on the CSPICE toolkit or another SPICE binding at runtime.

Two ways to query loaded kernels are available:

  • Generic queries (spk_position/spk_velocity/spk_state/spk_acceleration, pck_euler_angles/pck_rotation_matrix) take NAIF IDs or frame class IDs directly and resolve against a process-wide kernel registry.
  • Per-body convenience functions (sun_position_spice, moon_state_spice, ...) wrap the same registry for the ten most commonly used bodies.

For downloading and caching the underlying kernel files, see NAIF Ephemeris Kernels.

Loading Kernels

load_spice_kernel accepts either a known kernel name or a filesystem path, and auto-detects SPK vs. binary PCK from the file header:

import brahe as bh
import numpy as np

bh.initialize_eop()

epc = bh.Epoch.from_date(2025, 1, 1, bh.TimeSystem.UTC)

# Loading is idempotent and explicit; spk_* queries also auto-load de440s if
# no kernel has been loaded yet.
bh.load_spice_kernel("de440s")
print(f"Loaded kernels: {bh.loaded_spice_kernels()}")

# Generic queries take NAIF IDs and resolve across all loaded SPK kernels.
r_moon = bh.spk_position(bh.NAIFId.MOON, bh.NAIFId.EARTH, epc)
v_moon = bh.spk_velocity(bh.NAIFId.MOON, bh.NAIFId.EARTH, epc)
x_sun = bh.spk_state(bh.NAIFId.SUN, bh.NAIFId.EARTH, epc)

print(f"\nMoon position rel. Earth (km): {r_moon / 1e3}")
print(f"Moon velocity rel. Earth (m/s): {v_moon}")
print(f"Sun distance rel. Earth (AU): {np.linalg.norm(x_sun[0:3]) / bh.AU:.6f}")

# Kernel-scoped variants query a single named kernel directly, bypassing
# cross-kernel chaining and precedence.
r_moon_de440s = bh.spk_position_from_kernel(
    "de440s", bh.NAIFId.MOON, bh.NAIFId.EARTH, epc
)
print(f"\nMoon position from de440s directly (km): {r_moon_de440s / 1e3}")

# Per-body convenience functions wrap the same queries for the ten most
# commonly used bodies, selecting the kernel via EphemerisSource.
r_mars = bh.mars_position_spice(epc, bh.EphemerisSource.DE440s)
v_mars = bh.mars_velocity_spice(epc, bh.EphemerisSource.DE440s)
x_mars = bh.mars_state_spice(epc, bh.EphemerisSource.DE440s)

print(f"\nMars position rel. Earth (km): {r_mars / 1e3}")
print(f"Mars velocity rel. Earth (m/s): {v_mars}")
print(f"Position/state consistency check: {np.allclose(r_mars, x_mars[0:3])}")
use brahe as bh;
use brahe::spice::{NAIFId, SPICEKernel};

fn main() {
    bh::initialize_eop().unwrap();

    let epc = bh::Epoch::from_date(2025, 1, 1, bh::TimeSystem::UTC);

    // Loading is idempotent and explicit; spk_* queries also auto-load de440s
    // if no kernel has been loaded yet.
    bh::spice::load_spice_kernel("de440s").unwrap();
    println!("Loaded kernels: {:?}", bh::spice::loaded_spice_kernels());

    // Generic queries take NAIF IDs and resolve across all loaded SPK kernels.
    let r_moon = bh::spice::spk_position(NAIFId::Moon, NAIFId::Earth, epc).unwrap();
    let v_moon = bh::spice::spk_velocity(NAIFId::Moon, NAIFId::Earth, epc).unwrap();
    let x_sun = bh::spice::spk_state(NAIFId::Sun, NAIFId::Earth, epc).unwrap();

    println!(
        "\nMoon position rel. Earth (km): [{:.3}, {:.3}, {:.3}]",
        r_moon[0] / 1e3,
        r_moon[1] / 1e3,
        r_moon[2] / 1e3
    );
    println!(
        "Moon velocity rel. Earth (m/s): [{:.6}, {:.6}, {:.6}]",
        v_moon[0], v_moon[1], v_moon[2]
    );
    println!(
        "Sun distance rel. Earth (AU): {:.6}",
        x_sun.fixed_rows::<3>(0).norm() / bh::AU
    );

    // Kernel-scoped variants query a single named kernel directly, bypassing
    // cross-kernel chaining and precedence.
    let r_moon_de440s =
        bh::spice::spk_position_from_kernel("de440s", NAIFId::Moon, NAIFId::Earth, epc).unwrap();
    println!(
        "\nMoon position from de440s directly (km): [{:.3}, {:.3}, {:.3}]",
        r_moon_de440s[0] / 1e3,
        r_moon_de440s[1] / 1e3,
        r_moon_de440s[2] / 1e3
    );

    // Per-body convenience functions wrap the same queries for the ten most
    // commonly used bodies, selecting the kernel via SPICEKernel.
    let r_mars = bh::spice::mars_position_spice(epc, SPICEKernel::DE440s).unwrap();
    let v_mars = bh::spice::mars_velocity_spice(epc, SPICEKernel::DE440s).unwrap();
    let x_mars = bh::spice::mars_state_spice(epc, SPICEKernel::DE440s).unwrap();

    println!(
        "\nMars position rel. Earth (km): [{:.3}, {:.3}, {:.3}]",
        r_mars[0] / 1e3,
        r_mars[1] / 1e3,
        r_mars[2] / 1e3
    );
    println!(
        "Mars velocity rel. Earth (m/s): [{:.6}, {:.6}, {:.6}]",
        v_mars[0], v_mars[1], v_mars[2]
    );
    println!(
        "Position/state consistency check: {}",
        (r_mars - x_mars.fixed_rows::<3>(0).into_owned()).norm() < 1e-9
    );
}
Output
Loaded kernels: ['de440s']

Moon position rel. Earth (km): [ 152116.87845619 -307796.3409078  -166865.16270426]
Moon velocity rel. Earth (m/s): [932.54734647 394.5520516  212.86016362]
Sun distance rel. Earth (AU): 0.983353

Moon position from de440s directly (km): [ 152116.87845619 -307796.3409078  -166865.16270426]

Mars position rel. Earth (km): [-51311894.31463633  73955612.24801372  39369562.97881442]
Mars velocity rel. Earth (m/s): [7779.24180983 -397.09894046  284.1025968 ]
Position/state consistency check: True
Loaded kernels: ["de440s"]

Moon position rel. Earth (km): [152116.878, -307796.341, -166865.163]
Moon velocity rel. Earth (m/s): [932.547346, 394.552052, 212.860164]
Sun distance rel. Earth (AU): 0.983353

Moon position from de440s directly (km): [152116.878, -307796.341, -166865.163]

Mars position rel. Earth (km): [-51311894.315, 73955612.248, 39369562.979]
Mars velocity rel. Earth (m/s): [7779.241810, -397.098940, 284.102597]
Position/state consistency check: true

Known kernel names — the eight planetary DE kernels (de430, de432s, de435, de438, de440, de440s, de442, de442s), the seven satellite ephemeris kernels (mar099, mar099s, jup365, sat441, ura184, nep097, plu060), and the binary PCK moon_pa_de440 — are downloaded and cached automatically; any other string is treated as a path to a local .bsp or .bpc file (bring-your-own kernels). The moon_pa_de440 binary PCK is derived from the same DE440 integration as de440.bsp but is distributed by NAIF as a separate file — SPK kernels carry only translational states. See NAIF Ephemeris Kernels for caching details.

Downloadable Kernels

Name File Size Coverage Contents
de440s de440s.bsp ~33 MB 1849–2150 Sun, Moon, all planets and planetary-system barycenters (default for spk_*/*_spice auto-init)
de440 de440.bsp ~120 MB 1550–2650 Same content as de440s, wider time span
de430 de430.bsp ~120 MB Standard precision, extended time span
de432s de432s.bsp ~11 MB Small variant tuned for New Horizons Pluto targeting
de435 de435.bsp ~120 MB Higher accuracy for inner planets
de438 de438.bsp ~120 MB Standard precision
de442 de442.bsp ~120 MB Intended for the MESSENGER mission to Mercury
de442s de442s.bsp ~33 MB Small variant of de442
mar099s mar099s.bsp ~68 MB 1995–2050 Mars, Phobos, Deimos (default Mars satellite kernel for body-center auto-download)
mar099 mar099.bsp ~1.1 GB 1600–2600 Mars, Phobos, Deimos, wider time span
jup365 jup365.bsp ~1.1 GB 1600–2200 Jupiter, Io, Europa, Ganymede, Callisto
sat441 sat441.bsp ~662 MB 1750–2250 Saturn, Titan and the mid-size moons
ura184 ura184_part-3.bsp ~387 MB 1600–2399 Uranus, Miranda, Ariel, Umbriel, Titania, Oberon
nep097 nep097.bsp ~105 MB 1600–2400 Neptune, Triton
plu060 plu060.bsp ~135 MB 1800–2200 Pluto, Charon
moon_pa_de440 moon_pa_de440_200625.bpc ~13 MB matches de440/de440s Lunar principal-axis orientation (binary PCK, not an SPK)

Any string that does not match a name in this table is treated as a filesystem path to a local .bsp or .bpc file.

Registry behavior:

  • Idempotent: calling load_spice_kernel with a name/path that is already loaded is a no-op.
  • Persistent: kernels stay resident until unload_spice_kernel or clear_spice_kernels is called.
  • Precedence: spk_* queries resolve across every loaded SPK kernel; for body pairs covered by more than one kernel, the most recently loaded kernel wins (matching SPICE's own "last loaded wins" convention). pck_* queries search loaded PCK kernels newest-first for a frame with coverage at the requested epoch.
  • Auto-initialization: spk_position/spk_velocity/spk_state/spk_acceleration load de440s automatically if no SPK kernel has been loaded yet. Binary PCKs are never auto-loaded through this generic interface — load_spice_kernel("moon_pa_de440") (or an explicit path) must be called first. The Moon's LFPA/LFME frame functions (see Lunar Reference Frames) are a narrow exception: they auto-load moon_pa_de440 on first use.

Loading Multiple Kernels at Once

load_common_spice_kernels and load_all_spice_kernels pre-load a curated set of kernels in one call, so a session does not pay per-kernel download latency the first time each body is queried:

Function Loads Download size
load_common_spice_kernels() de440s, moon_pa_de440 ~46 MB
load_all_spice_kernels() de440s, moon_pa_de440, mar099s, jup365, sat441, ura184, nep097, plu060 ~2.5 GB

Each kernel load within these calls is idempotent, so calling either alongside individual load_spice_kernel calls is safe. Prefer load_common_spice_kernels unless outer-planet body centers, their moons, or Pluto are needed — load_all_spice_kernels downloads over 2 GB on first use.

Querying Ephemeris Data

Generic NAIF-ID Queries

spk_position, spk_velocity, spk_state, and spk_acceleration take a target NAIF ID, a center NAIF ID, and an Epoch, and resolve across all loaded SPK kernels:

Function Returns Units
spk_position(target, center, epc) Position of target rel. center m
spk_velocity(target, center, epc) Velocity of target rel. center m/s
spk_state(target, center, epc) [x, y, z, vx, vy, vz] of target rel. center m, m/s
spk_acceleration(target, center, epc) Acceleration of target rel. center m/s²

Common NAIF IDs are exposed through the NAIFId enum (Python: IntEnum; Rust: enum NAIFId with an Id(i32) catch-all variant), covering the ten planetary-system barycenters, the Sun, the eight planet body centers, and the major natural satellites used elsewhere in brahe (Phobos, Deimos, the four Galilean moons, Titan, the five major Uranian moons, Triton, Charon). NAIFId values compare and pass equal to the equivalent raw integer, so either form works everywhere a NAIF ID is expected:

1
2
3
4
5
import brahe as bh

epc = bh.Epoch.from_date(2025, 1, 1, bh.TimeSystem.UTC)
r1 = bh.spk_position(bh.NAIFId.MOON, bh.NAIFId.EARTH, epc)
r2 = bh.spk_position(301, 399, epc)  # equivalent raw NAIF IDs
1
2
3
4
use brahe::spice::{spk_position, NAIFId};

let r1 = spk_position(NAIFId::Moon, NAIFId::Earth, epc).unwrap();
let r2 = spk_position(301, 399, epc).unwrap(); // equivalent raw NAIF IDs

Any NAIF ID present in a loaded kernel but not named by the enum (e.g. a minor body or spacecraft) is passed as a raw integer (Python) or NAIFId::Id(...) (Rust). Full ID listing: NAIF Integer ID Codes.

Kernel-Scoped Queries

spk_position_from_kernel, spk_velocity_from_kernel, spk_state_from_kernel, and spk_acceleration_from_kernel take an additional kernel_name argument and query that kernel only — no cross-kernel chaining is performed and the registry's precedence rules do not apply. The named kernel is auto-loaded if not already resident. Use these when a query must come from a specific kernel regardless of what else is loaded.

Per-Body Functions

sun_position_spice, moon_position_spice, mercury_position_spice, venus_position_spice, mars_position_spice, jupiter_position_spice, saturn_position_spice, uranus_position_spice, neptune_position_spice, and solar_system_barycenter_position_spice (alias: ssb_position_spice) each have _velocity_spice and _state_spice counterparts, all queried relative to Earth (NAIFId.EARTH / NAIFId::Earth, ID 399). They take an Epoch and an ephemeris source selecting which DE kernel to query (EphemerisSource.DE440s / EphemerisSource.DE440 in Python, SPICEKernel::DE440s / SPICEKernel::DE440 in Rust). Computing the state shares a single record lookup between position and velocity, so prefer *_state_spice over separate position/velocity calls when both are needed.

The Mars/Jupiter/Saturn/Uranus/Neptune functions return the planet body center. The DE kernel only carries the planetary-system barycenter for these outer planets, so the body center is computed as a two-leg sum: the barycenter relative to Earth from the DE kernel, plus the body center relative to the barycenter from the planet's satellite ephemeris kernel. That satellite kernel is auto-downloaded and loaded on first use (mar099s ~68 MB, jup365 ~1.1 GB, sat441 ~662 MB, ura184 ~387 MB, nep097 ~105 MB).

Each of the five outer planets also has mars_barycenter_position_spice, jupiter_barycenter_position_spice, and so on (with _velocity_spice / _state_spice counterparts) that return the planetary-system barycenter using only the DE kernel — no satellite-kernel download. The barycenter and body center differ by up to a few hundred km due to large moons (e.g. ~290 km for Saturn from Titan, ~230 km for Jupiter from the Galilean moons; only ~0.2 m for Mars). Prefer the _barycenter_ variants for third-body force modeling, which uses the system barycenter with the system GM. Sun, Moon, Mercury, and Venus have no significant satellites, so their body-center and barycenter positions coincide and no barycenter variant is provided.

PCK Orientation

PCK queries take a frame class ID — either a raw NAIF frame class ID or a FrameId enum value. The lunar principal-axis frame from the moon_pa_de440 kernel is registered under frame class ID 31008 (FrameId.MOON_PA_DE440 / FrameId::MoonPaDe440).

Function Returns
pck_euler_angles(frame_id, epc) (angles, rates) as raw arrays: [phi, delta, w] (rad) and their rates (rad/s)
pck_euler_angle(frame_id, epc) EulerAngle (order ZXZ, radians)
pck_euler_rates(frame_id, epc) [phi_dot, delta_dot, w_dot] (rad/s) as a raw array
pck_euler_angle_and_rates(frame_id, epc) (EulerAngle, rates) from a single shared segment lookup
pck_quaternion(frame_id, epc) Quaternion (unit quaternion, ICRF to body-fixed)
pck_rotation_matrix(frame_id, epc) RotationMatrix (ICRF to body-fixed)

pck_euler_angles (the original, tuple-of-arrays form) is unchanged and still available alongside the typed functions. pck_rotation_matrix returns a RotationMatrix object rather than a raw array — index it directly (r[(0, 0)]) or call .to_matrix() for a numpy array in Python; in Rust call .to_matrix() to get an SMatrix3<f64> indexable as r.to_matrix()[(0, 0)]. See the EulerAngle, Quaternion, and RotationMatrix references for the full type APIs.

The example below uses the original pck_euler_angles/pck_rotation_matrix pair:

import brahe as bh
import numpy as np

bh.initialize_eop()

# PCKs are never auto-initialized; they must be loaded explicitly.
bh.load_spice_kernel("moon_pa_de440")

epc = bh.Epoch.from_date(2025, 1, 1, bh.TimeSystem.UTC)

angles, rates = bh.pck_euler_angles(bh.FrameId.MOON_PA_DE440, epc)
print(f"Euler angles [phi, delta, w] (rad): {angles}")
print(f"Euler angle rates (rad/s): {rates}")

R = bh.pck_rotation_matrix(bh.FrameId.MOON_PA_DE440, epc).to_matrix()
print(f"\nICRF -> Moon principal-axis rotation matrix:\n{R}")
print(f"Orthogonality check |R @ R^T - I|: {np.linalg.norm(R @ R.T - np.eye(3)):.3e}")
use brahe as bh;
use brahe::spice::FrameId;

fn main() {
    bh::initialize_eop().unwrap();

    // PCKs are never auto-initialized; they must be loaded explicitly.
    bh::spice::load_spice_kernel("moon_pa_de440").unwrap();

    let epc = bh::Epoch::from_date(2025, 1, 1, bh::TimeSystem::UTC);

    let (angles, rates) = bh::spice::pck_euler_angles(FrameId::MoonPaDe440, epc).unwrap();
    println!(
        "Euler angles [phi, delta, w] (rad): [{:.6}, {:.6}, {:.6}]",
        angles[0], angles[1], angles[2]
    );
    println!(
        "Euler angle rates (rad/s): [{:.3e}, {:.3e}, {:.3e}]",
        rates[0], rates[1], rates[2]
    );

    let r = bh::spice::pck_rotation_matrix(FrameId::MoonPaDe440, epc)
        .unwrap()
        .to_matrix();
    println!("\nICRF -> Moon principal-axis rotation matrix:");
    for row in 0..3 {
        println!(
            "  [{:.6}, {:.6}, {:.6}]",
            r[(row, 0)],
            r[(row, 1)],
            r[(row, 2)]
        );
    }
    let identity_error = (r * r.transpose() - nalgebra::Matrix3::identity()).norm();
    println!("Orthogonality check |R * R^T - I|: {:.3e}", identity_error);
}
Output
1
2
3
4
5
6
7
8
Euler angles [phi, delta, w] (rad): [-3.16931030e-03  3.81694347e-01  4.66419052e+03]
Euler angle rates (rad/s): [-2.13863841e-09  1.68051671e-09  2.66374634e-06]

ICRF -> Moon principal-axis rotation matrix:
[[-0.47351708  0.81760746  0.32756621]
 [-0.88078385 -0.43905364 -0.17734633]
 [-0.00118055 -0.37249155  0.92803483]]
Orthogonality check |R @ R^T - I|: 3.940e-16
1
2
3
4
5
6
7
8
Euler angles [phi, delta, w] (rad): [-0.003169, 0.381694, 4664.190515]
Euler angle rates (rad/s): [-2.139e-9, 1.681e-9, 2.664e-6]

ICRF -> Moon principal-axis rotation matrix:
  [-0.473517, 0.817607, 0.327566]
  [-0.880784, -0.439054, -0.177346]
  [-0.001181, -0.372492, 0.928035]
Orthogonality check |R * R^T - I|: 5.134e-16

Reference Frame

SPK and PCK outputs are expressed in the kernel's inertial reference frame. For DE4xx-era kernels (DE430 and later) that frame is ICRF, even though NAIF labels it "J2000" in kernel metadata and documentation — see the NAIF Frames Required Reading for the frame-ID definitions and the historical "J2000" naming. Brahe applies no additional bias rotation between the kernel output and GCRF: values from spk_*/*_spice queries are GCRF-compatible directly.

Performance

SPK and PCK kernels are parsed once, at load_spice_kernel time, into an in-memory segment table. Queries take a short-lived read lock on the shared registry only to look up the relevant segment chain; the Chebyshev evaluation itself runs outside any lock. Repeated queries for the same target/center pair reuse a cached chain, so sequential access patterns (e.g. a propagator stepping through epochs) avoid re-resolving kernel coverage on every call.

See Also