This Library provides a C++ interface querying, reading and writing Naif SPICE kernels. Built on the Naif Toolkit.
Check out the Astrogeology Software Docs for SpiceQL examples:
- Cassini Tutorial
- pyspiceql Visualization
- REST, Python, C++ API
- WebAssembly Bindings
- Use with Other Libraries
- Use in USGS ISIS and ALE
NAIF Resources - Learn more about the Kernels and SPICE Information that SpiceQL queries:
SpiceQL uses conda to maintain dependencies. If you don't have conda yet, we recommend installing it via Miniforge:
curl -L -O "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh"
bash Miniforge3-$(uname)-$(uname -m).shIf you have ISIS and haven't downloaded kernels, use the downloadIsisData command.
If you don't have ISIS, you can still use the script by downloading the downloadIsisData script and the rclone.conf. You will need to have rclone and python installed to run the script.
# Clone the repo, including submodules. -j8 parellelizes for a faster clone.
git clone --recurse-submodules -j8 https://github.com/DOI-USGS/SpiceQL.git
# To clone submodules later: git submodule update --init --recursive
# Open the newly cloned repo
cd SpiceQL
# Create conda env from environment.yml
# -n <env-name>, any name is fine.
conda env create -f environment.yml -n ssdev
# activate the new env
conda activate ssdev
# make and cd into the build directory.
mkdir build
cd build
# Configure the project, install directory can be anything, here, it's the conda env
cmake .. -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX
# Optional: DB files are installed by default in $CONDA_PREFIX/etc/SpiceQL/db.
# To use files included in the repo, set env var SPICEQL_DEV_DB=True.
export SPICEQL_DEV_DB=True
# Set the environment variable(s) to point to your kernel data.
# The following env vars are used by default in order of priority:
# (1) $SPICEROOT, (2) $ALESPICEROOT, (3) $ISISDATA.
SPICEROOT=/data/kernels/
# ALESPICEROOT=/data/kernels/
# ISISDATA=/data/kernels/
# build and install project
make install
# Optional, Run tests
ctest -j8You can disable different components of the build by setting the CMAKE variables SPICEQL_BUILD_DOCS, SPICEQL_BUILD_TESTS, SPICEQL_BUILD_BINDINGS, or SPICEQL_BUILD_LIB to OFF. For example, the following cmake configuration command will not build the documentation or the tests:
cmake .. -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX -DSPICEQL_BUILD_DOCS=OFF -DSPICEQL_BUILD_TESTS=OFF
The SpiceQL API is available via Python bindings in the module pyspiceql. The bindings are built using SWIG and are on by default. You can disable the bindings in your build by setting SPICEQL_BUILD_BINDINGS to OFF when configuring your build.
See Also: SpiceQL WebAssembly Bindings
# In your terminal in your project directory:
npm install @usgs-astrogeology/spiceql// In your javascript:
import { loadSpiceQL } from '@usgs-astrogeology/spiceql'A Module Bundler, like Vite, is required for this kind of import.
// In your javascript:
import { loadSpiceQL } from 'https://cdn.jsdelivr.net/npm/@usgs-astrogeology/spiceql/dist/spiceql.js';The emscripten dependency in environment.yml provides emcc/emcmake (and Node), so the conda env from Building The Library already has everything needed.
Configure with emcmake (which injects the Emscripten toolchain file) and build:
conda activate ssdev # the env you created above
# A conda env exports host (macOS/Linux) CFLAGS/CXXFLAGS/CPPFLAGS/LDFLAGS that are
# invalid for the Emscripten cross-compile and break wasm-ld. Clear them first.
unset CFLAGS CXXFLAGS CPPFLAGS LDFLAGS
emcmake cmake -S . -B build-wasm \
-DCMAKE_BUILD_TYPE=Release \
-DSPICEQL_WASM=ON \
-DSPICEQL_BUILD_TESTS=OFF \
-DSPICEQL_BUILD_BINDINGS=ON
cmake --build build-wasm -j"$(getconf _NPROCESSORS_ONLN)"If you get a permission denied error, use a lower number of cores in the last command: -j4 or -j1 instead of -j"$(getconf _NPROCESSORS_ONLN)"
| Option | Value | Why |
|---|---|---|
SPICEQL_WASM |
ON |
Selects the WASM build: excludes HDF5/HighFive and libcurl, builds CSPICE from source (patched for wasm-ld), and links everything static. Auto-forced ON under emcmake anyway. |
SPICEQL_BUILD_BINDINGS |
ON |
Builds bindings/wasm (the Embind module + naifspice namespace) instead of the native language bindings. |
SPICEQL_BUILD_TESTS |
OFF |
The C++ gtests are native-only; the WASM surface is exercised by the JS suite (npm test) instead. |
CMAKE_BUILD_TYPE |
Release |
Optimized module. |
This produces the module in build-wasm/bindings/wasm/:
spiceql_wasm.js— the Emscripten loader (ES module)spiceql_wasm.wasm— the compiled WebAssemblyspiceql_wasm.data— preloaded config DB, bundled leap-second kernel, and thenaifspicesignature table
The small hand-written wrappers bindings/wasm/spiceql.js (which you import) and
bindings/wasm/naifspice.js (the raw-CSPICE marshaller it uses) sit on top of those.
Copy the three spiceql_wasm.* artifacts and both wrappers (bindings/wasm/spiceql.js and bindings/wasm/naifspice.js) next to each other (they must be co-located — spiceql.js imports the other two), then import spiceql.js locally:
// example.js — run with: node example.js
import { readFileSync } from 'node:fs';
import { loadSpiceQL } from './spiceql.js';
const spiceql = await loadSpiceQL();
const frameCode = -85;
const sclkTime = 922997380.174174; // We'll use SpiceQL to convert Sclk to ET
// Mount Kernels
const kernelList = [];
const kernelUrls = [
'./data/base/kernels/lsk/naif0012.tls',
'./data/lro/kernels/fk/lro_frames_2014049_v01.tf',
'./data/lro/kernels/sclk/lro_clkcor_2024262_v00.tsc',
]
for (const url of kernelUrls) {
const kernelPath = '/kernels/' + url.split('/').pop(); // Get filename, discard path
spiceql.mountKernel(kernelPath, readFileSync(url)); // Mount as sanitized path
kernelList.push(kernelPath) // Add sanitized path to list
}
// Convert Spacecraft Clock Time (sclk) to Ephemeris Time (et)
const { result: ephTime } = spiceql.doubleSclkToEt(frameCode, sclkTime, {
mission: 'lro',
searchKernels: false,
kernelList,
});
console.info("Ephemeris Time", ephTime);In a browser it works the same way — import spiceql.js in a
<script type="module"> and use fetch() to get kernel bytes for mountKernel.
The five files (spiceql.js, naifspice.js, spiceql_wasm.js,
spiceql_wasm.wasm, spiceql_wasm.data) and your kernels just need to be
served over HTTP from the same folder (any static host works; opening the page from file:// does not,
because the browser blocks fetch()):
<!doctype html>
...
<script type="module">
// Import and Load SpiceQL
import { loadSpiceQL } from 'https://cdn.jsdelivr.net/npm/@usgs-astrogeology/spiceql/dist/spiceql.js';
const spiceqlBasePath = 'https://cdn.jsdelivr.net/npm/@usgs-astrogeology/spiceql/dist/';
const spiceql = await loadSpiceQL({
moduleOverrides: { locateFile: (path) => spiceqlBasePath + path }
});
// Mount Kernels
const kernelList = [];
const kernelUrls = [
'https://asc-isisdata.s3.us-west-2.amazonaws.com/usgs_data/base/kernels/lsk/naif0012.tls',
'https://asc-isisdata.s3.us-west-2.amazonaws.com/usgs_data/lro/kernels/fk/lro_frames_2014049_v01.tf',
'https://asc-isisdata.s3.us-west-2.amazonaws.com/usgs_data/lro/kernels/sclk/lro_clkcor_2024262_v00.tsc',
]
for (const url of kernelUrls) {
const kernelData = await fetch(url); // Fetch
const kernelBuff = new Uint8Array(await kernelData.arrayBuffer()) // Load into Buffer
const kernelPath = '/kernels/' + url.split('/').pop(); // Get filename, discard path
spiceql.mountKernel(kernelPath, kernelBuff); // Mount as sanitized path
kernelList.push(kernelPath) // Add sanitized path to list
}
// Data to query with
const frameCode = -85;
const sclkTime = 922997380.174174;
// Convert Spacecraft Clock Time (sclk) to Ephemeris Time (et)
const { result: ephTime } = spiceql.doubleSclkToEt(frameCode, sclkTime, {
mission: 'lro',
searchKernels: false,
kernelList,
});
console.info("Ephemeris Time", ephTime);
</script>
...# serve the folder over HTTP, then open http://localhost:8000
python -m http.serverEvery CSPICE *_c function can be exposed as a JS function so you can call the
toolkit directly instead of going through the higher-level api.h helpers. These
wrappers are generated at build time by parsing CSPICE's headers.
The namespace is opt-in: loadSpiceQL() does not build it, so the ~650
wrappers are only generated when you actually want the raw toolkit. Import
naifspice.js and call loadNaifspice() with the object loadSpiceQL()
returned (it also attaches the result as spiceql.naifspice). Here is the whole
flow — load, furnish a kernel, and convert a UTC string to ET with str2et:
import { loadSpiceQL } from './spiceql.js';
import { loadNaifspice } from './naifspice.js';
import { readFileSync } from 'node:fs';
const spiceql = await loadSpiceQL();
const nspice = loadNaifspice(spiceql); // build + attach spiceql.naifspice
// Furnish a leap-second kernel, then convert a UTC string to ET.
spiceql.mountKernel('/kernels/naif0012.tls', readFileSync('naif0012.tls'));
nspice.furnsh('/kernels/naif0012.tls');
const et = nspice.str2et('2000 JAN 01 12:00:00'); // 64.18... ET seconds past J2000Most functions get an ergonomic wrapper: pass the CSPICE input arguments in
order as plain JS values (numbers, strings, and arrays for vectors/matrices), and
the outputs are returned. A single output is returned directly; several are
returned as an object keyed by the CSPICE parameter name; a non-void C return
value joins that object under return.
nspice.et2utc(et, 'ISOC', 3, 32); // '2000-01-01T12:00:00.000'
const { radius, longitude, latitude } = ns.reclat([1, 1, 0]); // several outputs -> object
const R = ns.pxform('J2000', 'J2000', et); // matrix output -> nested array (identity)
const { name, found } = ns.bodc2n(399, 32); // { name: 'EARTH', found: true }A signalled SPICE error is thrown as a JS Error carrying the SPICE short and
long messages, and the toolkit is reset so the module stays usable afterward.
A minority of functions (those using CSPICE aggregate types like SpiceCell,
callbacks, or output buffers whose size is only known at runtime) cannot be
marshalled automatically. They are still callable in raw form under their
literal CSPICE name (with the _c suffix) at spiceql.naifspice.raw.<fn>_c,
where every argument is passed straight through as a number (pointers are
addresses). Manage memory yourself with the helpers on the namespace — malloc,
free, getValue, setValue, toCString, fromCString:
const { malloc, free, setValue, getValue } = ns;
const vin = malloc(3 * 8), vout = malloc(3 * 8);
for (let i = 0; i < 3; i++) setValue(vin + i * 8, i + 1, 'double');
ns.raw.vsclg_c(2.0, vin, 3, vout); // scale [1,2,3] by 2 (ndim=3)
const scaled = [0, 1, 2].map((i) => getValue(vout + i * 8, 'double')); // [2, 4, 6]
free(vin); free(vout);The suffix-dropped top-level name of a raw-only function forwards to its raw
form, so ns.bodvrd === ns.raw.bodvrd_c.
Passing kernelList furnishes and unfurnishes those kernels for the duration of a single call. If you want to furnish a set of kernels once and reuse them across many calls, manage the CSPICE pool yourself. Any call made with searchKernels:false and no kernelList uses whatever is already furnished.
spiceql.KernelSet is the RAII helper: constructing it furnishes the kernels, and unload() unfurnishes them. It accepts an array of kernel paths (grouped by type automatically) or a { type: [paths] } object.
import { loadSpiceQL } from './spiceql.js';
import { readFileSync } from 'node:fs';
const spiceql = await loadSpiceQL();
spiceql.mountKernel('/kernels/naif0012.tls', readFileSync('naif0012.tls'));
spiceql.mountKernel('/kernels/lro.tsc', readFileSync('lro_clkcor_2020184_v00.tsc'));
// Furnish a set once...
const ks = new spiceql.KernelSet(['/kernels/naif0012.tls', '/kernels/lro.tsc']);
// ...then make as many calls as you like with searchKernels:false and no
// kernelList; they read the kernels already in the pool.
const opts = { searchKernels: false };
const et = spiceql.strSclkToEt(-85, '1/281199081:48971', opts).result;
const utc = spiceql.etToUtc(et, { ...opts, format: 'ISOC', precision: 3 }).result;
console.log(spiceql.getLoadedKernels()); // ['/kernels/naif0012.tls', '/kernels/lro.tsc']
// Unfurnish when done. KernelSet is a C++ object, so free it explicitly
// (garbage collection will NOT do it for you): unload() then delete().
ks.unload();
ks.delete();
// A subsequent pool-only call now fails because nothing is furnished:
try {
spiceql.strSclkToEt(-85, '1/281199081:48971', opts);
} catch (e) {
console.error(e.message); // SPICE(KERNELVARNOTFOUND) ...
}For finer control there are also free functions that furnish/unfurnish individual kernels: spiceql.load(path), spiceql.unload(path), spiceql.getLoadedKernels(), and spiceql.isLskLoaded().
Notes:
- Kernel search throws in the WASM build (no HDF5 inventory) — always pass an explicit
kernelListwithsearchKernels:false, or furnish kernels yourself. - A
KernelSet(and any Embind object) must be freed with.delete(); it is not garbage-collected.unload()unfurnishes the kernels but keeps the object usable (you canload()more into it). - The remote REST transport (
useWeb:true) is not supported in the WASM build and throws. Awaitingfetch()from inside wasm would require suspending the stack (JSPI), which through Embind forces the entire JS API to become async. If you need the hosted service, call the SpiceQL REST API (https://astrogeology.usgs.gov/apis/spiceql/latest/) directly from JavaScript withfetch()and use the WASM module only for local-kernel work (useWeb:false).
The JS bindings have a test suite that runs with Node's built-in test runner (no extra dependencies). After building:
npm testSee bindings/wasm/tests/README.md for details.
SpiceQL has a simple memoization header only library at Spiceql/include/memo.h. This can cache function results on disk using a binary archive format mapped using a combined hash of a function ID and it's input parameters.
TLDR
#include "memo.h"
int func(int) { ... }
memoization::disk c("cache_path");
// use case 1: wrap function call
// (function ID, the function to wrap and then params
int result1 = c("func_id", func, 3);
// use case 2: wrap function
// (cache object, function ID, function)
auto func_memoed = memoization::make_memoized(c, "func_id", func);
int result2 = func_memoed(3);
assert(result1 == result2);- Create a branch with the new version name (e.g.,
1.0) - Update the version info in following files:
code.json- Append to the metadata with the updated version infoCMakeLists.txt- Update the projectVERSIONvalueCHANGELOG.md- Create a new section with the version number, date, and changes made in the upcoming releasedocs/conf.py- Update the versionrecipe/meta.yaml- Update the package version
- Tag a release candidate from the version branch