Skip to content

Latest commit

 

History

History
386 lines (328 loc) · 22 KB

CLAUDE.md

File metadata and controls

386 lines (328 loc) · 22 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

Strapdown-rs is an open-source Rust implementation of strapdown inertial navigation system (INS) algorithms. This repository implements the Free Core version of the product, providing researchers, students, and developers with a lightweight but reproducible simulation tool for GNSS-denied scenarios. The architecture is designed with extensibility in mind to support future commercial professional versions.

Product Philosophy:

  • Free Core (this repo): Open-source academic/research tool focused on reproducible simulations, basic sensor models, and GNSS degradation scenarios
  • Professional/Enterprise (future): Commercial versions with advanced GNSS modeling, synthetic data generation, GUIs, and hardware integration
  • Experimental features: Geophysical navigation capabilities (gravity/magnetic anomaly aiding) remain in this repo as experimental features for research purposes

Architecture

This is a Cargo workspace with three main crates:

1. strapdown-core (/core)

The core library implementing strapdown INS algorithms and simulation framework:

  • lib.rs: Library entry point and 9-state strapdown mechanization in local-level frame (NED). Implements forward propagation equations from Groves textbook (Chapter 5.4-5.5)
  • earth.rs: WGS84 Earth ellipsoid model and geodetic calculations
  • kalman.rs: Kalman-style navigation filters including Unscented Kalman Filter (UKF) for nonlinear state estimation
  • particle.rs: Particle-filter building blocks (the Particle trait, resampling and averaging strategies); not a filter on its own
  • rbpf.rs: The Rao-Blackwellized particle filter, after Canciani & Raquet (2017) -- the one concrete particle filter
  • measurements.rs: Measurement models (GPS position/velocity, barometric altitude, pseudorange, carrier phase) implementing the MeasurementModel trait
  • messages.rs: Event stream handling for GNSS scheduling and fault injection scenarios
  • sim.rs: Simulation utilities, CSV data loading (Sensor Logger format), dead reckoning and closed-loop functions
  • linalg.rs: Linear algebra utilities for matrix operations

Key state representation: 9-state vector [lat, lon, alt, v_n, v_e, v_d, roll, pitch, yaw] in NED frame

Design principles for Free Core:

  • Deterministic simulations with reproducible results (seeded RNG)
  • Configuration-driven (YAML/JSON configs for scenarios)
  • Extensible architecture for future professional features
  • Clean separation between core algorithms and simulation framework

2. strapdown-sim (/sim)

Command-line tool for running INS simulations with GNSS degradation:

  • Modes: dr (dead reckoning), cl (closed loop -- ESKF by default, UKF and EKF by --filter), pf (particle filter), syn (synthetic trajectory). ol exists but is not implemented: it writes no output
  • GNSS fault simulation: dropouts, reduced update rates, measurement corruption, bias injection
  • Input: CSV files with IMU and GNSS measurements (Sensor Logger format)
  • Output: the CLI writes CSV only. Every path through strapdown-sim ends in NavigationResult::to_csv, and OUTPUT_FILE_EXTENSIONS in sim/src/common.rs is ["csv"], so any other extension is rejected rather than filled with CSV. to_hdf5, to_netcdf and to_mcap exist as library writers on NavigationResult, reachable from Rust but not from this binary. There is no Parquet writer at all
  • Configuration: TOML/YAML/JSON scenario files or command-line arguments
  • Built-in logging: Use --log-level and --log-file flags (see LOGGING.md for details)

Free Core scope: Basic GNSS degradation (outages, noise, reduced availability) Future Pro scope: Advanced faults (spoofing, jamming, multipath, terrain masking)

3. strapdown-geonav (/geonav)

Experimental geophysical navigation module (research-grade):

  • Loads NetCDF geophysical maps (gravity/magnetic anomaly grids)
  • Integrates geophysical measurements with INS/GNSS filters
  • Provides alternative PNT in GNSS-denied environments
  • Built-in logging: Use --log-level and --log-file flags (see LOGGING.md for details)
  • Status: Experimental feature for research, may be commercialized in future roadmap

4. analysis (/analysis, Python)

Post-processing and experiment tooling, exposed as the analyze CLI:

  • preprocess.py: rebuilds data/input from the Sensor Logger exports in data/raw -- resampling, splitting recordings at IMU dropouts, and downloading the _gravity.nc / _magnetic.nc maps beside each trajectory. just preprocess runs it at 10 Hz, which gives 10 Hz inertial propagation against the ~1 Hz the GNSS was actually recorded at (the GNSS columns stay NaN in nine rows out of ten; do not interpolate them up). data/input is the one directory every consumer reads -- all 21 conf/*.toml, geo-stats, postprocess and geoperf-* -- so change the rate, not the path. just preprocess-1hz is the 1 Hz variant, which is the rate every result before the geophysical fixes used
  • geostats.py: characterises the geophysical measurements against those maps -- per-field bias, measurement noise and signal-to-noise, plus the de-correlation length that sets geo_interval_s. Its anomaly models mirror core/src/earth.rs and geonav/src/lib.rs term for term, and self_check() asserts they still agree; a statistic computed from a differently computed anomaly would describe a quantity the filter never sees. --apply-to conf (just geo-adopt) writes the measured bias, noise and bias prior into all 18 geophysical configs, line by line so the comments survive, rewriting only keys already present so a *_grav.toml stays gravity-only. geo_frequency_s is held back behind --apply-interval: the noise figures are measurements, but moving the interval from 1 s to one measurement per de-correlation length changes what the experiment asks of the aid. core/tests/example_configs.rs asserts all 18 carry the same values, so a partial adoption fails the build rather than producing a comparison across two different R
  • compare.py / plotting.py: error statistics, LaTeX tables and map figures

Run it with uv run analyze <subcommand> from the repository root.

Common Commands

Build & Test

# Build entire workspace in release mode
cargo build --workspace --release

# Run all tests
cargo test --workspace

# Run tests for specific crate
cargo test --package strapdown-core
cargo test --package strapdown-sim

# Run specific test
cargo test --package strapdown-core test_name

# Run with code coverage (needs `cargo install cargo-tarpaulin` first)
cargo coverage
# Or: cargo tarpaulin --workspace --timeout 600

Lint & Format

# Lint the wide configuration (warnings are errors)
cargo lint

# ...and the narrow one. CI lints BOTH, and `cargo lint` alone is not CI parity: a call site
# that exists only under --all-features can orphan an import that --no-default-features then
# reports as dead. Run both before pushing.
cargo lint-min
cargo test-min

# Apply the clippy fixes it can
cargo lint-fix

# Format code
cargo fmt --all

# Check formatting without writing (CI parity)
cargo fmt-check

Running Simulations

# Dead reckoning. The subcommand comes first: `-i` before it is rejected.
# `dr` is the dead-reckoning command. `ol` is a separate, **unimplemented** subcommand:
# `run_open_loop` validates its paths, writes nothing and prints "Open-loop mode is not yet
# fully implemented". Do not reach for `ol` expecting a result.
./target/release/strapdown-sim dr -i input.csv -o output.csv

# Closed-loop with a duty-cycled GNSS outage and AR(1) degradation.
# `--sched` is passthrough|fixed|duty and `--fault` is none|degraded|slowbias|hijack.
./target/release/strapdown-sim cl -i input.csv -o output.csv \
  --seed 42 \
  --sched duty --on-s 100.0 --off-s 50.0 \
  --fault degraded --sigma-pos-m 10.0

# Geophysical navigation (the geonav-sim binary was folded into strapdown-sim;
# build with `cargo build --release -p strapdown-sim --features geonav`)
./target/release/strapdown-sim cl -i data/input/input.csv -o output.csv \
  --geo --gravity-resolution one-minute

Datasets

Sample datasets are no longer vendored in this repository. Test fixtures live in core/tests/; generate your own trajectories with the syn subcommand:

./target/release/strapdown-sim syn -o synthetic.csv --duration-s 600 --seed 42

Free Core Features (Definition of Done)

The Free Core implementation must achieve the following capabilities:

  1. Reproducible Simulations:

    • Researchers can configure scenarios (input data, filter config, GNSS settings) and get identical results with same random seed
    • Particle filter results are statistically consistent across runs
    • Deterministic behavior for scientific reproducibility
  2. Dataset Support:

    • Import third-party datasets (KITTI, nuScenes, Carla, MEMS-Nav, etc.) via converter tools
    • Pre-processed hosted datasets that work out-of-the-box
    • CLI/interactive tools for format conversion
  3. GNSS Degradation Modeling (Basic):

    • Complete outages (dropouts with configurable start/duration)
    • Increased noise levels
    • Reduced satellite availability
    • Measurement corruption and bias injection
  4. Configuration-Driven:

    • TOML/YAML/JSON scenario files describing: trajectory/data path, sensor parameters, noise models, degradation events, filter configuration, output format
    • Command-line interface with config file support and argument overrides
  5. Output Formats:

    • CSV, HDF5, NetCDF and MCAP export for analysis in Python/MATLAB/R. These are NavigationResult methods, not CLI output modes: strapdown-sim writes CSV and rejects every other extension, so reaching the other three means calling to_hdf5, to_netcdf or to_mcap from Rust
    • Parquet is deliberately not supported at either layer: sim/src/common.rs refuses .parquet rather than naming a file Parquet and filling it with CSV
    • Navigation solution time series with position, velocity, attitude estimates
  6. Python Integration:

    • Python bindings for core functionality (future work)
    • Enables integration with ML/AI workflows and analysis pipelines

Development Notes

Code Style and Conventions

  • Naming conventions:
    • Variables and functions: snake_case
    • Types (structs, enums, traits): CamelCase
    • Constants: SCREAMING_SNAKE_CASE
    • Names should be descriptive (avoid abbreviations and mathematical symbols)
  • Function design:
    • Keep functions focused on a single task
    • Break up long functions (>25 statements) into smaller private helper functions
    • Each function should have a clear purpose reflected in its name
  • Documentation:
    • Use Rust doc comments extensively (/// and //!)
    • Include examples in documentation
    • Reference Groves textbook equations by section/equation number where applicable
  • Testing:
    • Write unit tests for each module
    • Include integration tests for full system behavior
    • Test edge cases and error handling
    • Use assert_approx_eq for floating-point comparisons

Reference Material

  • Primary reference: "Principles of GNSS, Inertial, and Multisensor Integrated Navigation Systems, 2nd Edition" by Paul D. Groves
  • Equations reference Groves by section/equation number
  • Variables named by quantity (not mathematical symbols) for clarity

Coordinate Conventions

  • Navigation frame: local-level frame (latitude, longitude, altitude)
    • Default convention is North-East-Down (NED), matching Groves; ENU is an explicit opt-in
    • Users control via is_enu boolean flags and sign conventions; StrapdownState::to_ned and to_enu convert an existing state between the two
    • Vertical velocity: positive down in NED, positive up in ENU
    • altitude is height above the ellipsoid -- positive up -- in both frames, valid over [-11,000m, 30,000m]. It is not a "down" coordinate in NED
    • TestDataRecord carries no frame tag, so every entry point that loads one takes the frame from its caller: sim::dead_reckoning(records, is_enu) and is_enu on UkfConfig/EkfConfig/EskfConfig, all defaulting to NED. On the CLI that is --enu (and is_enu in a config file); Sensor Logger exports need it, syn output does not
    • Declaring the wrong frame is rejected, not guessed at: sim::check_declared_frame rotates the leading records' specific force into the navigation frame and requires its sign to match (+g on ENU up, -g on NED down), returning StrapdownError::InvalidConfiguration naming the flag to pass. Mechanizing NED records as ENU adds the gravity model instead of cancelling it and falls at 2 g
  • Attitude representation: Direction cosine matrices (DCM), Euler angles (XYZ rotation)
  • Position: WGS84 geodetic (lat/lon in degrees, altitude in meters)
  • Velocities: Local-level frame (m/s)
  • IMU data: Specific force (m/s²) and angular rate (rad/s) in body frame, NOT preprocessed
    • Raw IMU output includes gravitational acceleration
    • Strapdown equations handle gravity removal during propagation

Filter Architecture

  • Common interface: All filters implement the NavigationFilter trait defined in kalman.rs
    • Required methods: predict() and update()
    • Unified state representation via StrapdownState
  • Forward propagation: Uses StrapdownState::propagate() with strapdown equations (Chapter 5.4-5.5)
  • Measurement models: Implement the MeasurementModel trait for update step
    • Trait provides predict_measurement() and innovation_covariance() methods
    • Implemented models: GPS position, GPS velocity, barometric altitude, pseudorange, carrier phase
  • UKF implementation:
    • Uses unscented transform with sigma points for nonlinear state estimation
    • Handles full 9-state navigation solution
  • Particle filter implementation (rbpf.rs): Canciani & Raquet's marginalized particle filter (IEEE TAES 53(1), 2017), error-state and closed-loop
    • Particles sample horizontal position error only (δlat, δlon); the paper's linear states -- altitude, velocity, nav-frame tilt, the barometer-aiding error δh_a, the barometer loop's vertical-acceleration error δâ, and each map bias as V (Gauss-Markov) + c (constant) -- are one Kalman filter whose covariance all particles share. No IMU bias states: adding them made the RBPF diverge on degraded-GNSS runs
    • Barometer aiding is a third-order loop in the mechanization; barometer readings feed the loop and are not measurement updates
    • Time update once per measurement epoch (transition and noise accumulate between); one measurement update for every other sensor (C = H T, weights under C P Cᵀ + R)
    • horizontal_process_noise_std_m: RbpfConfig (the library) defaults to the paper's zero (eq. 19), which diverges with GNSS-rate fixes on MEMS data. strapdown-sim and its [particle_filter] section default to 1 m/√s (sim::DEFAULT_PF_HORIZONTAL_PROCESS_NOISE_STD_M), as the conf/ recipes set
    • Reports the Kalman filters' layout, [9 nav, b_a, b_g, map biases], with zero bias rows; core/src/rbpf.rs's module docs list the departures from the paper
  • Process noise: sim::DEFAULT_PROCESS_NOISE_DENSITY is a spectral density -- a variance per second. Each filter forms $Q_k = q,\Delta t$; it was a per-step variance with no dt anywhere until #374, which made effective Q a function of sample rate (a 50x spread across the 1 Hz, 10 Hz and 50 Hz data this repo uses)
  • Design for extensibility: Core algorithms separate from simulation framework to enable future professional features

Data Format

Input CSV must contain timestamped sensor measurements:

  • Timestamps: ISO UTC format with timezone
  • IMU: acc_x/y/z (m/s²), gyro_x/y/z (rad/s), mag_x/y/z (µT)
  • GNSS: latitude, longitude, altitude, speed, bearing
  • Orientation: roll, pitch, yaw (degrees) or quaternions

Current dataset format: Sensor Logger app format

Future dataset support (via converters): KITTI, nuScenes, Carla, MEMS-Nav

Testing

  • Extensive unit tests across core modules (use cargo test --workspace to run all)
  • Tests use assert_approx_eq macro for floating-point comparisons
  • Integration tests in core/tests/integration_tests.rs verify full simulation pipeline
  • Deterministic tests verify reproducibility with seeded RNG
  • Coverage reports generated with cargo tarpaulin

Environment Setup

Plain cargo, no environment manager. Pixi was removed in #335; git clone && cargo build is the whole setup.

  • rust-toolchain.toml pins the toolchain (1.91) and fetches it on the first cargo command. .cargo/config.toml carries the env vars and the lint/lint-fix/fmt-check/docs/ coverage aliases that pixi.toml's [tasks] used to
  • Beyond Rust: a C/C++ compiler and cmake >= 3.26, needed only for features that touch a C library. libhdf5, libnetcdf, zlib and freetype are compiled from vendored crates.io sources, so no system library is searched for. Ubuntu 22.04 (cmake 3.22) and Debian 12 (3.25) are too old. cargo build -p strapdown-core alone needs no C toolchain at all
  • HDF5_DIR must stay unset -- hdf5-metno-sys prefers a system library whenever it is set, even with static on, and the vendored netCDF build then fails against those headers
  • libfontconfig is a runtime dependency of strapdown-sim's plotting feature (dlopened), not a build-time one
  • The repository is a two-language monorepo: three Rust crates in a Cargo workspace, and the Python analysis package in a uv workspace declared by the root pyproject.toml. The two share the directory and nothing else -- analysis/ is not a Cargo member and the crates are not uv members. analysis/ was untracked between c5f72c6 and the monorepo change; the notebooks under examples/ are still untracked
  • Python setup is uv sync from the repository root. That builds the analysis member and puts its analyze CLI on uv run, which is why the justfile recipes say uv run analyze ... and not uv run --project analysis ...
  • pygmt is imported inside the functions that use it, never at module scope. It dlopens the GMT C library on import, so a module-scope import makes importing analysis at all fail wherever GMT is absent -- which took down every subcommand, including the ones that touch no maps, and would take down CI. .github/workflows/python.yml deliberately installs no GMT, so it is what keeps this true

Lint Policy

Python. uv run ruff check, uv run ruff format --check and uv run pytest -q from the repository root, which is what .github/workflows/python.yml runs and what just check-python wraps. Configuration is [tool.ruff] in the root pyproject.toml; target-version there and requires-python in analysis/pyproject.toml must agree.

Rust. Lints are enforced at deny, workspace-wide, not warn-level. cargo lint and one of the blocking CI jobs run cargo clippy --workspace --all-targets --all-features -- -D warnings.

That is only half the gate. A second blocking job runs cargo clippy -p strapdown-core --all-targets --no-default-features -- -D warnings, and the two configurations disagree: an item used only behind a feature gate is live in one and dead in the other. cargo lint-min is that second configuration, and cargo test-min its test counterpart; run both alongside cargo lint before pushing.

  • clippy::pedantic, clippy::nursery, missing_docs, missing_debug_implementations, unreachable_pub, rust_2018_idioms
  • Zero-panic policy in library code: unwrap_used, expect_used, panic are denied. Return a StrapdownError (core/src/error.rs). Tests are exempt via clippy.toml
  • Every pub item -- including struct fields and enum variants -- needs a doc comment, and a fallible function needs an # Errors section
  • Relax a lint once in [workspace.lints.clippy] with a reason, never with a scattered call-site #[allow]
  • The 1.91 pin lives in five places that must agree: rust-toolchain.toml, rust-version in Cargo.toml, msrv in clippy.toml, and the dtolnay/rust-toolchain@1.91 pins in rust.yml and publish.yml -- because pedantic/nursery gain lints every release

See AGENTS.md for the full policy and the reasoning behind each existing exception.

Extensibility for Professional Versions

When developing Free Core features, consider extensibility for future professional products:

Pro features (out of scope for this repo):

  • Synthetic data generation (vehicle dynamics, waypoint following, realistic sensor models)
  • Advanced GNSS modeling (satellites, pseudoranges, DOP, spoofing, jamming, multipath, terrain masking)
  • Batch runner for parameter sweeps
  • GUI (Bevy-based scenario editor, 3D visualization)
  • Professional reports (PDF/HTML with plots and metrics)
  • Advanced data adapters (ROS bag, RINEX, standard nav formats)

Design principles for extensibility:

  • Trait-based abstractions for sensors, filters, and measurement models
  • Configuration schema that can be extended without breaking changes
  • Clean separation between algorithms (strapdown-core) and application logic (strapdown-sim)
  • Plugin architecture for adding new sensors/measurement models in pro versions

Workflow Notes

  • Main branch for PRs: main
  • Create feature branches for development
  • Current focus: Implementing Free Core specification features for reproducible research simulations
  • When asked to execute on an issue or new high-level feature request, ask the user if this should be completed on the current branch or to create a new branch
  • gh must be 2.71 or newer. Older clients request the projectCards field GitHub removed with Projects (classic), so gh pr view/gh issue view exit 1 and gh pr edit exits 1 while leaving the body unchanged. gh api and the --json forms are unaffected. No Debian or Ubuntu package is new enough -- install from cli.github.com. See #365