Skip to content

Browser & Pyodide

Lox publishes an Emscripten wheel alongside the native ones, so the full Python API runs in the browser under Pyodide — in a plain web page, in JupyterLite, or in any other Pyodide host.

Installing

Inside a Pyodide runtime:

import micropip

await micropip.install("lox-space")

import lox_space as lox

The wheel is tagged pyemscripten_2026_0_wasm32, which is the ABI of Pyodide 314.x (CPython 3.14). Pyodide releases with a different ABI version need a wheel built against that release.

In a web page:

<script type="module">
  import { loadPyodide } from "https://cdn.jsdelivr.net/pyodide/v314.0.5/full/pyodide.mjs";

  const pyodide = await loadPyodide();
  await pyodide.loadPackage("micropip");
  await pyodide.runPythonAsync(`
    import micropip
    await micropip.install("lox-space")

    import lox_space as lox

    t = lox.Time("TAI", 2026, 8, 24, 12, 0, 0.0)
    state = lox.Cartesian(
        time=t,
        x=6678.0 * lox.km, y=0.0 * lox.km, z=0.0 * lox.km,
        vx=0.0 * lox.km_per_s, vy=7.73 * lox.km_per_s, vz=0.0 * lox.km_per_s,
    )
    print(state.to_keplerian().orbital_period())
  `);
</script>

Differences from the native wheel

The API is the same — nothing is feature-gated out of the browser build — but two properties of the runtime are worth knowing about.

Everything runs on one thread. Pyodide's CPython is built without pthreads, so the fan-out that the native build uses for visibility, power, imaging access and scenario propagation runs sequentially here. Results are identical; large scenarios take proportionally longer.

Files live in a virtual filesystem. Anything that takes a path — SPK, EOPProvider, ItuProvider — reads through Emscripten's in-memory filesystem, so fetch the file and write it there first:

import lox_space as lox
from pyodide.http import pyfetch

response = await pyfetch("https://example.org/kernels/de440s.bsp")
with open("de440s.bsp", "wb") as f:
    f.write(await response.bytes())

ephemeris = lox.SPK("de440s.bsp")

Budget for the download: de440s.bsp is ~31 MB, and the packed ITU-R bundle that ItuProvider expects is ~490 MB — large enough that serving a pre-trimmed subset is usually the better option in a browser.

Building the wheel

The pinned Pyodide release — which in turn pins the CPython, Emscripten and Rust versions of the build — lives in pyodide_version in the justfile. pyodide xbuildenv search lists the releases compatible with a given host Python.

just pyodide-setup     # one-off: cross-build environment + pinned Rust toolchain (~2 GB)
just build-pyodide     # wheel into dist-pyodide/
just pytest-pyodide    # run the Python test suite in Node

just pytest-pyodide skips the tests that compare Lox against packages Pyodide does not ship (astropy, skyfield, spiceypy, spacelink); everything else runs against the wasm wheel.

build-pyodide sets RUSTUP_TOOLCHAIN itself, because pyodide build exports the toolchain it pins without selecting it — left alone, the build would use the default toolchain, which need not have the Emscripten target installed.