Frames
In SPICE, almost every vector has an implied coordinate system called a frame.
Frames are referenced by name (strings like "J2000"), and many frames are time-dependent, meaning you need an epoch to transform between them.
What it is
A frame is a named coordinate system.
Common examples:
"J2000": canonical inertial frame (often the default in SPICE workflows)"IAU_EARTH": Earth body-fixed frame (rotates with the Earth)
Two core SPICE primitives:
pxform(from, to, et): returns a 3×3 rotation matrix.sxform(from, to, et): returns a 6×6 state transformation matrix (position + velocity).
Why it matters
A position like [x, y, z] is meaningless unless you know the frame.
Frame mistakes are a top source of subtle bugs:
- mixing inertial and body-fixed frames
- transforming a state (pos+vel) with the wrong kind of matrix
- forgetting that transforms can be time-dependent
Kernels required
It depends on which frames you use:
- None (sometimes): some inertial frames and transforms are built in.
- PCK: body orientation models (for many body-fixed frames)
- FK: frame definitions for mission/instrument frames and custom frames
- CK + SCLK: spacecraft attitude/pointing frames (time-dependent)
If a frame isn’t defined by built-ins or loaded kernels, SPICE will throw.
How it maps to tspice
kit.frameTransform(...) (recommended)
kit.frameTransform(from, to, et) is a wrapper around raw.pxform(...) that returns a Mat3 helper.
// Requires: LSK (for utcToEt).
// Many common frame transforms (including IAU_EARTH) also require a PCK.
const et = await spice.kit.utcToEt("2000 JAN 01 12:00:00");
const R = await spice.kit.frameTransform("J2000", "IAU_EARTH", et);
// Raw SPICE layout (row-major): [m00,m01,m02, m10,m11,m12, m20,m21,m22]
const rowMajor = R.rowMajor;
// Convenience for column-major consumers (WebGL, many math libs)
const colMajor = R.colMajor;
const vFrom = [1, 2, 3] as const;
// Apply row-major 3×3 to a column vector (vTo = R * vFrom)
const vTo = [
rowMajor[0] * vFrom[0] + rowMajor[1] * vFrom[1] + rowMajor[2] * vFrom[2],
rowMajor[3] * vFrom[0] + rowMajor[4] * vFrom[1] + rowMajor[5] * vFrom[2],
rowMajor[6] * vFrom[0] + rowMajor[7] * vFrom[1] + rowMajor[8] * vFrom[2],
] as const;raw.pxform(...) / raw.sxform(...) (parity)
const rm = await spice.raw.pxform("J2000", "IAU_EARTH", et);
// If you need a 6x6 state transform (pos+vel), use sxform.
const xform6 = await spice.raw.sxform("J2000", "IAU_EARTH", et);tspice encodes matrices from the raw layer in row-major order.
If you need to adapt a raw 3×3 row-major array to a column-major array:
const rm = await spice.raw.pxform(from, to, et);
const cm = [rm[0], rm[3], rm[6], rm[1], rm[4], rm[7], rm[2], rm[5], rm[8]];Gotchas
- Direction matters:
pxform(from, to, et)transforms a vector expressed infrominto the same vector expressed into. - Row-major vs column-major: most rendering/math ecosystems default to column-major; use
Mat3.colMajorwhen integrating. - Kernel dependencies are easy to miss: if a frame is kernel-defined, transforms will fail until you load the right FK/PCK/CK/SCLK.
- Time dependence: transforms for rotating frames vary with
et; cache carefully.