GPU-accelerated GNSS positioning for the urban canyon — particle filters, ray-traced NLOS, and factor-graph experiments in real cities.
v0.3 release audit · Live results snapshot · Technical report · Benchmarks · Examples · Input shapes · GSDC2023 solution · Experiment log · Decisions · How it's built
gnss_gpu is a research workspace for pushing smartphone- and survey-grade GNSS
positioning in dense cities, where buildings block and reflect satellite signals and
classic EKF/RTK pipelines fall apart. It pairs CUDA/C++ kernels with Python tooling to
run GPU particle filters, double-difference carrier tracking, ray-traced line-of-sight
checks against 3D city meshes, and factor-graph optimization — then scores them
honestly against RTKLIB and EKF baselines on real public datasets (UrbanNav, PLATEAU,
and the GSDC2023 Kaggle smartphone-decimeter challenge).
The v0.3 reproducibility archive can be rebuilt and verified with one command:
python tools/build_release_bundle.py --output dist/reproducibility --archive dist/gnss_gpu-v0.3.0-reproducibility.zip- 🛰️ It beats the classic baseline where it hurts most. On UrbanNav Tokyo Odaiba,
the
PF 100K (DD + smoother + stop-detect)filter reaches 1.36 m P50 / 4.11 m RMS versus RTKLIB demo5 at 2.67 m / 13.08 m over 12,228 aligned epochs — a 49% better median and 69% better RMS. - ⚡ It's genuinely fast. A full 1,000,000-particle filter step
(predict → weight → resample → estimate) runs in 81 ms (≈12 Hz) on a consumer Ada
GPU; a 10,000-epoch batch WLS solve takes ~1 ms. See
benchmarks/RESULTS.md. - 🏙️ City-aware NLOS handling. Ray tracing against PLATEAU 3D building meshes does line-of-sight / non-line-of-sight classification with a 57.8× BVH speedup, so urban multipath can be rejected instead of trusted.
- 📈 Honest, reproducible scoring. Every headline number comes from a fixed same-input/same-metric comparison, and the live snapshot is regenerated straight from the committed result CSVs.
The milestone-2 estimator treats integer ambiguities as persistent discrete
basins and Rao-Blackwellizes the continuous fixed-lag GNSS/IMU graph. On the
shared Tokyo PPC benchmark, the shipped WP18 configuration beats the published
inuex35 <50cm_full% result on all three runs using the stricter matched-3D
score (missing rover epochs count as failures).
<50cm_full% |
Tokyo run1 | Tokyo run2 | Tokyo run3 |
|---|---|---|---|
| inuex35 README | 56.7% | 69.9% | 67.9% |
| libgnss++ GTSAM | 56.8% | 80.5% | 72.8% |
| RB-FGO-PF (ours, 3D) | 59.6% | 78.7% | 78.1% |
| Shipped RB-FGO-PF quality | run1 | run2 | run3 |
|---|---|---|---|
| FixRMS ↓ | 0.104 m | 0.121 m | 0.150 m |
| Fix rate | 43.1% | 75.1% | 73.7% |
| Median fixed error | 3.3 cm | 2.6 cm | 3.2 cm |
| False fixes / shipped fixes ↓ | 0.59% | 0.37% | 3.09% |
| PPC OFFICIAL | 57.80% | 80.02% | 82.19% |
The result is deliberately reported with its limits: run3's 3.09% false-fix
rate is above the approximately 2% integrity target, and run1 AllRMS is dominated
by a tunnel float tail. A later basin-memory ablation improves full-run run3 to
83.32% <50cm_full% and 0.67% false fixes, but regresses run2 purity; it is
preserved as a negative result and is not the shipped configuration. See the
benchmark record,
RB-FGO-PF design, and
WP15 CUDA batch-LAMBDA report.
The README headline is not just a table: the sampled particle cloud is localized on the real street network, with the posterior contracting around the driven UrbanNav route while the full-view trail is drawn from the continuous trajectory.
| Method | Dataset | P50 | RMS 2D |
|---|---|---|---|
| PF 100K (DD + smoother + stop-detect) | UrbanNav Tokyo Odaiba | 1.36 m | 4.11 m |
| RTKLIB demo5 | UrbanNav Tokyo Odaiba | 2.67 m | 13.08 m |
| PF + RobustClear-10K (external mainline) | UrbanNav, 5 seq / 2 cities | — | 66.6 m |
| EKF baseline | UrbanNav, 5 seq / 2 cities | — | 93.25 m |
The current development campaign keeps the full epoch denominator, uses no
reference truth or runtime FGO, and requires declared false FIX at or below 1%.
Latest libgnss++ selected results are shown only as an external reference;
every imported RTK idea must still pass the PF posterior and promotion gates.
| Full-epoch matched 3D | PF-only locked current | latest libgnss++ reference | v0.3 promotion floor |
|---|---|---|---|
| Tokyo run1 | 46.51% (5,546 / 11,924) | 80.02% | 45.00% |
| Nagoya run1 | 75.37% (5,715 / 7,583) | 85.84% | 69.55% non-degradation |
The full WP172 dual-RTK candidate supply is measured at a conservative 42.653 ms/epoch when executed sequentially (21.826 ms/epoch concurrently), under the 100 ms production budget. WP173 then declares FIX only after a standard MLAMBDA ratio of at least 3.0 survives five contiguous epochs with at least six satellites and the complete WP172 consensus gate. Tokyo declares 1,296/11,924 FIX epochs (10.87%) and Nagoya declares 1,370/7,583 (18.07%), with zero audited false FIX in both runs.
The external-validation RMS is high in absolute terms because it averages the hardest deep-urban sequences (including failure stretches). The point is the relative gap: the GPU PF stack consistently wins against EKF and RTKLIB on the same epochs. Full tables, figures, and limitations live on the results snapshot.
Open the Odaiba particle-cloud video
For the zero-data terminal demo behind this visual:
PYTHONPATH=python:. python3 examples/demo_pf_localization_improvement.pyIt reads checked-in artifacts and prints the UrbanNav Odaiba PF-vs-RTKLIB improvement plus the PLATEAU LOS/NLOS mask replay gain for PF.
Beyond rejecting blocked satellites, the package models why an urban pseudorange is biased — knife-edge (ITU-R P.526) and UTD (Kouyoumjian–Pathak) diffraction plus specular reflection over PLATEAU 3D building meshes — and scores the physics against real UrbanNav residuals.
Open the full LOS/NLOS deck.gl sweep
A subtle but decisive step is correcting each satellite to signal-transmission time (with the Sagnac rotation). Without it a per-satellite tens-of-metres range error swamps the multipath signal; with it the residual becomes a clean NLOS ground truth (LOS median 1.0 m, AUC 0.92). On that clean reference, UTD reproduces the measured multipath-bias distribution better than knife-edge — reproducing the literature (Zhang & Hsu, 2021) on properly corrected real data.
| Diffraction model | Wasserstein-1 ↓ | KS ↓ |
|---|---|---|
| knife-edge (ITU-R P.526) | 1.84 | 0.46 |
| UTD (Kouyoumjian–Pathak) | 1.70 | 0.29 |
UrbanNav Tokyo Odaiba, 60 epochs over a 249k-triangle PLATEAU mesh. Reproduce with
PYTHONPATH=examples python examples/plot_nlos_diffraction_figure.py Odaiba 60(uses the installed package's CUDA ray-tracing for line-of-sight checks).
The same city-aware physics also runs forward: given a place (or a route), a time
window, the constellations, and a PLATEAU mesh, the scenario engine
(gnss_gpu.scenario) simulates the observables a receiver would see there — visible
satellites, LOS/NLOS, pseudorange with clock/ionosphere/troposphere/multipath errors,
C/N0 and Doppler — and can export them as RINEX 3.04 OBS (--rinex-out) to feed
RTKLIB or any receiver-evaluation pipeline. On top of it, GPU area sweeps
(gnss_gpu.coverage_map) batch the line-of-sight rays for all grid cells × all
satellites into single CUDA launches and map predicted GNSS quality — visible/LOS
counts, availability, DOP, expected horizontal error — over a whole district in
seconds.
PYTHONPATH=python python examples/demo_scenario_engine.py # observables at a point
PYTHONPATH=python python examples/demo_coverage_map.py # per-cell quality map (PNG + deck.gl)Three worked use cases on the same real Odaiba data (UrbanNav route + PLATEAU mesh):
| Use case | Demo | What it surfaces |
|---|---|---|
| Test-course / route evaluation | demo_route_accuracy.py |
Availability (≥4 LOS) stays 100%, yet expected HPE spikes past 300 m where the surviving 4-satellite geometry goes near-singular — a quality cliff a satellite-count threshold would miss. |
| RTK base-station placement | demo_rtk_base_placement.py |
Open-sky candidate cells tie on LOS count and HDOP; the common-view satellite count against the rover route is what actually ranks them. |
| Site multipath assessment | demo_urban_multipath.py |
Fixed-site sky map of LOS/NLOS tracks with per-satellite multipath excess and C/N0 (UTD diffraction): 28.6% NLOS, 18.9 dB-Hz LOS-vs-NLOS C/N0 gap at a canyon point. |
Unlike closed propagation tools, every model here is scored against real measured residuals (the UTD-vs-knife-edge benchmark above), and the whole pipeline is open — from CityGML parsing to the CUDA kernels.
Assumes an NVIDIA GPU, a current driver, and a CUDA Toolkit with nvcc.
git clone --recurse-submodules https://github.com/rsasaki0109/gnss_gpu.git
cd gnss_gpu
python3 -m venv .venv
# Linux/macOS: source .venv/bin/activate
# Windows PowerShell: .venv\Scripts\Activate.ps1
python3 -m pip install --upgrade pip
python3 python/gnss_gpu/cli.py doctor
python3 python/gnss_gpu/cli.py build
gnss-gpu doctor
gnss-gpu run --preset signal-acquisitiondoctor checks the driver, GPU, CUDA compiler, CMake, native bindings, and a
signal-simulation → acquisition CUDA round-trip (READY TO BUILD without
bindings, READY TO RUN with). On Windows use python in a Developer
PowerShell; target an architecture with gnss-gpu build --architecture 89.
gnss-gpu run --preset plateau-nlos # PLATEAU mask/replay suite
gnss-gpu data inspect data/urbannav/Tokyo/Odaiba
gnss-gpu run --preset urbannav-pf --input data/urbannav/Tokyo/Odaiba
gnss-gpu compare runs/urbannav-pf-baseline runs/urbannav-pf-candidateBoth presets write a timestamped runs/ directory with a common v1
manifest.json (schema, version, git_sha, backend, gpu,
input_hashes, parameters, metrics, hashed artifacts), per-estimator
summaries, and reports. compare prints precision/runtime deltas and writes
comparison.md; use --json PATH for machine-readable output.
The local-data path never downloads anything: it expects rover/base RINEX OBS, a
broadcast navigation file, and reference.csv (optional imu.csv). Fetch the
Tokyo subset explicitly with:
python experiments/fetch_urbannav_subset.py --run Odaiba --output-dir data/urbannav/Tokyo--allow-cpu-fallback is CPU smoke-test only and is not the GPU benchmark path.
git clone --recurse-submodules https://github.com/rsasaki0109/gnss_gpu.git
cd gnss_gpu
python3 -m venv .venv && source .venv/bin/activate
python3 -m pip install --upgrade pip
python3 -m pip install -r requirements.txt
python3 -m pip install pytest pandas scipy requests matplotlib plotlyUrban-canyon NLOS simulation solving each epoch with naive WLS vs. robust SPP:
PYTHONPATH=python python3 examples/demo_urban_canyon_sim.pymethod P50 err RMS err
--------------------------------------------------
naive WLS (L2) 10.30 m 10.21 m
robust SPP (Cauchy) 2.00 m 2.39 m
--------------------------------------------------
robust vs naive: 81% better P50, 77% better RMS
Robust down-weighting of NLOS-biased measurements is the same idea the GPU particle-filter stack scales up to beat RTKLIB demo5 on real UrbanNav data.
import numpy as np
from gnss_gpu import robust_spp
sat_ecef = np.asarray(...) # shape: (n_sat, 3), metres
pseudoranges = np.asarray(...) # shape: (n_sat,), metres
weights = np.ones(len(pseudoranges))
coarse_ecef = np.asarray(...) # shape: (3,), metres
position_ecef = robust_spp(
sat_ecef, pseudoranges, weights=weights,
init_pos=coarse_ecef, weight_func="cauchy", threshold=15.0,
)Bad input shapes, non-finite values, negative weights, and invalid solver options
raise ValueError. For a measurement-level NLOS simulator with ray-cast
blockage, C/N0 attenuation, excess delay, and geometry-aware SPP comparison:
PYTHONPATH=python python3 examples/demo_nlos_simulation.py
PYTHONPATH=python:. python3 experiments/run_plateau_nlos_demo_suite.pyThe suite exports the mask, replays SPP/PF/FGO, and writes combined
JSON/Markdown/CSV summaries; the mask CSV contract is tow,epoch_idx,prn,is_los.
| Replay consumer | Baseline RMS | Mask-soft RMS | RMS gain |
|---|---|---|---|
| SPP | 11.85 m | 4.07 m | 65.6% |
| PF | 11.18 m | 1.40 m | 87.4% |
| local-FGO | 8.10 m | 0.38 m | 95.4% |
PYTHONPATH=python pytest tests/test_*_wrapper.py -q # CPU-only smoke test
PYTHONPATH=python python3 -m pytest tests/ -q # full suite (CUDA tests skipped)
python3 python/gnss_gpu/cli.py build # build native kernels
gnss-gpu doctorGPU demos in examples/ import native modules and need a prior
build. On CPU-only installs, native-only calls raise
gnss_gpu.NativeBackendUnavailableError; broken extensions report their
original import error. Use build --dry-run to inspect the build command.
For outdoor robots, ros2/gnss_gpu_ros packages the
trajectory-filtering ideas validated on GSDC2023 as a ROS 2 node: it gates
multipath/NLOS spikes in sensor_msgs/NavSatFix streams (Hampel + CV Kalman)
before they reach your fusion stack, and publishes an RViz-friendly path.
ros2 run gnss_gpu_ros robust_navsat_filter --ros-args -r fix:=/your_gnss_driver/fixpython/gnss_gpu/ Reusable Python package code
src/ CUDA/C++ kernels and native bindings
examples/ Runnable demos (start here)
benchmarks/ GPU throughput benchmarks (+ RESULTS.md)
experiments/ Experiment runners, sweeps, reports, one-off probes
experiments/results/ Generated CSV/HTML/plot outputs
docs/ Generated visual snapshot site (the live demo)
ros2/gnss_gpu_ros/ ROS 2 robust NavSatFix filter node
internal_docs/ Working notes, decisions, handoffs, current state
third_party/gnssplusplus/ C++ GNSS/RTK/PPP/CLAS solver subproject
tests/ Python tests for stable helpers and experiment logic
flowchart LR
Data["PPC / UrbanNav / GSDC data"] --> Lib["libgnss++\nSPP/RTK/diagnostics"]
Lib --> Floor[".pos / diagnostics\nhybrid floor and candidates"]
Data --> GPU["gnss_gpu\nPF/RBPF/DD/FGO experiments"]
Floor --> GPU
GPU --> Score["honest scoring\nCSV/HTML reports\nKaggle/PPC artifacts"]
| Goal | First place to look |
|---|---|
| See the live, regenerated results | Results snapshot site |
| Run a demo | examples/ |
| Check GPU throughput | benchmarks/RESULTS.md |
| Continue current GSDC2023 Kaggle work | internal_docs/plan.md |
| Understand current PPC production state | internal_docs/ppc_current_status.md |
| Find durable decisions and negative results | internal_docs/decisions.md |
| Work on reusable Python code | python/gnss_gpu/ |
| Work on native CUDA/C++ code | src/ |
| Work on the C++ GNSS solver baseline | third_party/gnssplusplus/README.md |
This is not a single polished application — it is intentionally experiment-first.
Stable code lives in the library/native directories (python/gnss_gpu/, src/), while
fast-moving runs, sweeps, generated reports, and Kaggle/PPC handoffs live in
experiments/ and internal_docs/. Many CSV/HTML files are generated or local-only;
before trusting one, check that it is listed in
experiments/results/README.md and that its build
command is recorded in internal_docs/plan.md.
- Keep stable reusable code in
python/gnss_gpu/orsrc/; keep variant-heavy logic inexperiments/until it survives fixed evaluation. - Do not promote a method because it wins one pilot split. Prefer same-input, same-metric comparisons over new abstractions.
- Record durable decisions in
internal_docs/decisions.md. - Do not vendor, link, or derive production code/config from GPL-3.0 reference sources
such as
gici-open.







