Imported from lox-space/lox (
AGENTS.md). Install upstream withnpx skills add lox-space/lox. Copyright stays with the author.
AGENTS.md
Project Overview
Lox is a safe, ergonomic Rust astrodynamics library with Python bindings (via PyO3/maturin). It provides high-precision time systems, reference frame transformations, orbital mechanics, ground station analysis, and event detection for the modern space industry.
License: MPL-2.0 (REUSE-compliant — all source files must have SPDX headers).
Repository Structure
crates/
lox-core/ # Foundation: coordinates, orbital elements, anomalies
lox-units/ # Physical unit newtypes (Angle, Distance, Velocity, etc.)
lox-time/ # Astronomical time scales (TAI, TT, TDB, TCB, TCG, UT1, UTC)
lox-math/ # Series evaluation, interpolation
lox-bodies/ # Celestial body definitions (SPICE-derived constants)
lox-frames/ # Reference frame transformations (ICRF, J2000, ITRF, TEME, IAU_*)
lox-ephem/ # Ephemeris parsing (SPK/SPICE kernels)
lox-earth/ # Earth-specific: EOP, nutation, precession
lox-io/ # I/O for standard formats (NDM/XML, SPICE, CSV)
lox-orbits/ # Orbit modeling, propagators, visibility, events
lox-space/ # High-level facade API + Python bindings
lox-derive/ # Procedural macros
lox-test-utils/ # ApproxEq trait, benchmarking utilities
tools/
lox-gen/ # Code generation tooling
data/ # Reference data (IERS, SPICE kernels, TLEs, CSV trajectories)
Crate Dependency Graph
lox-core ← lox-units, lox-math, lox-time, lox-bodies
← lox-frames, lox-ephem, lox-earth, lox-io
← lox-orbits ← lox-space (facade + Python bindings)
lox-space is the public-facing crate that re-exports and wraps everything. Python users interact exclusively with lox-space.
Build & Test
Prerequisites: Rust 1.90+ (edition 2024), just, uv (for Python), cargo-nextest.
just test # Run all tests (rstest + doctest + pytest)
just rstest # Rust tests via cargo nextest
just doctest # Rust doc tests
just pytest # Build maturin + run Python tests
just build-pyo3 # Build Python extension module
just lint # clippy + rustfmt + REUSE compliance + README check
just readme # Regenerate README.md from the lox-space crate docs
just coverage # Generate code coverage report
Pyodide (browser) wheel
just pyodide-setup # One-off: cross-build environment + pinned Rust toolchain (~2 GB)
just build-pyodide # Emscripten wheel into dist-pyodide/
just pytest-pyodide # Python tests against the wasm wheel, in Node
The pinned Pyodide release lives in pyodide_version in the justfile; it also
pins the CPython, Emscripten and Rust versions of the build. Pyodide has no
threads, so lox-analysis drops its rayon dependency on wasm targets — see
crates/lox-analysis/src/parallel.rs. Keep new fan-out behind that module
rather than calling rayon directly.
Critical Conventions
Module Convention
This codebase uses the modern Rust module convention (foo.rs + foo/ directory) exclusively. There are no mod.rs files. Do not introduce mod.rs files.
Unit Convention (strictly enforced)
| Context | Position | Velocity | Notes |
|---|---|---|---|
| Internal Rust storage | meters (m) | m/s | All core types store SI units |
| Python API | kilometers (km) | km/s | Conversion at the PyO3 boundary |
| CSV files | kilometers | km/s | Conversion at parse time |
| Ephemeris (SPK) | kilometers | km/s | Conversion where consumed |
Key details:
Cartesian::from_vecs()expects meters.Distancestores meters internally, even when created viakilometers().GravitationalParameterstores m³/s² internally, even when created viakm3_per_s2().Anglestores radians internally.EllipsoidLocationandObservablesaccessors return unitful types (Angle,Distance,Velocity), not rawf64— prefer this for new scalar accessors. Bulk numeric arrays feeding interpolation series (e.g.HorizonMask::new) stayVec<f64>in radians.
Getting this wrong will produce silently incorrect results. Always verify units.
Code Style
- SPDX license headers are mandatory on all source files (checked by
reuse lint). - READMEs: the workspace
README.mdandcrates/lox-space/README.mdshare a body generated from thelox-spacecrate documentation with cargo-rdme. Edit the//!docs incrates/lox-space/src/lib.rsand runjust readme— never edit the section between thecargo-rdmemarkers by hand. Text outside the markers is hand-written and differs between the two files. - Error handling: Use
thiserrorwith domain-specific enum error types (e.g.,TimeError,TrajectoryError). - Type safety: Zero-sized marker traits for time scales (
TimeScale) and reference frames (ReferenceFrame). Prefer compile-time guarantees over runtime checks. - Builder pattern: Used for complex types (
AzElBuilder,TimeBuilder,CartesianBuilder). - Provider injection: EOP and SPK providers are passed explicitly — no global state.
- Feature flags: Crate functionality is feature-gated in
lox-spacefor minimal dependency footprint. - Clippy: Runs with
-D warnings(warnings are errors in CI).
Architecture Patterns
- Generic orbit type:
Orbit<S, O, R>parameterized on state, origin, and frame.OandRdefault to the runtime-determinedOriginandFrame, so plainOrbit<Cartesian>(orCartesianOrbit) is the runtime-polymorphic form; name the zero-sized types —Orbit<Cartesian, Earth, Icrf>— to have the compiler track them. - Where the time scale is tracked: neither the orbit nor the analysis layer parametrises on it — epochs there are runtime-scaled
Time, andlox-analysisdoes not referenceTaiat all. The scale stays in the type system only in the physics core:lox-framesrotations (valid only in a specific scale) andlox-time's offset graph.Sgp4keeps a nativeTime<Tai>inherent API and converts at itsPropagatorboundary. Because the scale is no longer in the type system there, mixing scales is a runtime concern:OrdandSubforTimeboth assert matching scales and panic otherwise. A singleIntervalcannot straddle two scales — it stores an epoch plus aTimeDelta, so the scale lives on the epoch alone, andInterval::new(start, end)panics on a mismatch because it derives the duration by subtraction. Combining two intervals in different scales is still only caught at runtime. UseTime::checked_cmp/checked_subandTimeInterval::try_newwhen the scales come from external input. - Runtime vs. static types: origin and frame each have a trait (
CoordinateOrigin,ReferenceFrame) implemented both by zero-sized marker types and by a closed enum (Origin,Frame) covering the same set. The enums are the defaults, so the fallible accessors (TryPointMass,TryQuasiInertial, …) are the common path. - Cost of the runtime time scale, and where it went: carrying the scale as an enum discriminant makes
Time24 bytes rather than 16 (andTimeInterval40 rather than 32). That cost ~13% on thevisibility_single_pair*benchmarks — cache and memory traffic, not instruction count or dispatch. The lazy event scan more than recovered it (+15% ontest_visibility_benchmark) by no longer materialising aVecper detector and per stage; the same allocation pressure that caused the loss now produces the win. Note localcargo benchwall-time sees almost none of this — only CodSpeed's cache-weighted instrument does, so validate such changes by pushing. AdaptiveSampleris opt-in: analyses default toUniformSampler;with_adaptive_detection()strides by the detect function's own rate bound instead (~113x faster at a 10 s step on the lunar fixture). The default has not been switched.- Time representation:
Time<T: ContinuousTimeScale = TimeScale>with femtosecond precision (i64 seconds + attoseconds). Continuous time scales are the default; leap seconds are handled strictly at the UTC I/O boundary. - Frame transformations: Matrix-based rotation pipelines. Transformation chains: ICRF <-> J2000, and CIO-based (CIRF -> TIRF -> ITRF) or equinox-based (MOD -> TOD -> PEF) paths.
- Dual orbit representations: Cartesian (position/velocity vectors via
glam::DVec3) and Keplerian (classical orbital elements), with conversions between them.
Testing
Development Workflow
Always use red-green TDD when possible: write a failing test first, then implement the minimum code to make it pass, then refactor.
Rust Tests
- Unit tests in
#[cfg(test)]modules within source files. rstestfor parameterized tests;proptestfor property-based testing (e.g., anomaly conversions).lox-test-utilsprovidesApproxEqfor floating-point comparisons.- Benchmarks use
divanand are located incrates/lox-space/benches/. All benchmarks should be added tolox-space, not to individual crates, so they are centralized in one place. Exception:lox-test-utilshas its own benchmarks for theApproxEqinfrastructure.
Python Wrapper Checklist
When modifying the Python API (#[pyclass]/#[pymethods] in lox-space), always update all three of:
- Type stubs —
crates/lox-space/lox_space.pyi - Docs —
crates/lox-space/docs/(e.g.states.mdfor orbits,analysis.mdfor analysis classes) - Tests —
crates/lox-space/tests/
Python Tests
- Located in
crates/lox-space/tests/test_*.py. conftest.pyprovides fixtures:data_dir,provider(EOPProvider),oneweb(TLE constellation),estrack(ground stations).- Markers:
slow,benchmark.
Key Files for Common Tasks
| Task | Files |
|---|---|
| Coordinate types | crates/lox-core/src/coords.rs |
| Unit types | crates/lox-core/src/units.rs |
| Time scales | crates/lox-time/src/ |
| Frame transforms | crates/lox-frames/src/ |
| Orbit propagation | crates/lox-orbits/src/propagators/ |
| Ground analysis | crates/lox-orbits/src/ground.rs, analysis.rs |
| Event detection | crates/lox-orbits/src/events.rs |
| Python bindings | crates/lox-space/src/*/python.rs |
| Python API docs | crates/lox-space/docs/ |
CI/CD
GitHub Actions workflows in .github/workflows/:
- rust.yml: Rust tests, clippy, rustfmt, REUSE compliance, code coverage (Codecov).
- python.yml: Multi-platform wheel builds (Linux/Windows/macOS), pytest, PyPI release on
lox-space-v*tags. - codspeed.yml: Performance benchmarking.
- audit.yml: Dependency security audit.
- release.yml: Release automation.