What is this?
A research workspace for GNSS positioning in dense cities, where buildings
block and reflect signals. CUDA/C++ kernels and Python tools run GPU particle
filters, double-difference carrier tracking, ray tracing against 3D city
meshes and factor-graph optimization, scored against RTKLIB and EKF baselines
on public data (UrbanNav, PLATEAU, PPC, GSDC2023).
Rebuild the v0.4 reproducibility archive (including the historical v0.3 audit evidence):
python tools/build_release_bundle.py --output dist/reproducibility --archive dist/gnss_gpu-v0.4.0-reproducibility.zip
Why you might care
- 🛰️ Fills RTK gaps. On UrbanNav Odaiba, RTK has the better median
(libgnss++ 0.07 m, RTKLIB demo5 0.34 m) but misses 3–22% of epochs. Filling
libgnss++ gaps with the GPU PF (1.50 m median, 98% coverage) raises epochs
within 5 m from 74.5% to 87.8% (RTKLIB 86.6%; Shinjuku 76.4% →
84.2%, RTKLIB 73.5%).
- ⚡ Fast. A 1,000,000-particle predict → weight → resample step takes
32 ms on a 6 GB Turing GPU; a 10,000-epoch batch WLS solve ~3 ms
(benchmarks).
- 🏙️ City-aware NLOS. LOS/NLOS from PLATEAU 3D meshes, 57.8× BVH speedup.
- 📈 Reproducible scoring. Same-input, same-metric comparisons; the
live snapshot is built from
committed CSVs.
RB-FGO-PF: integer ambiguity without premature collapse
Urban RTK usually commits to carrier integers after a ratio test, and a
wrong fix then stays until something notices it. RB-FGO-PF keeps every
carrier ambiguity float in one GNSS/IMU factor graph (GTSAM) and lets
particles carry the integer hypotheses (“basins”), so a wrong fix keeps
competing with the right one. Results are on the six PPC 2024 runs (Tokyo
1–3, Nagoya 1–3) in real time: each epoch uses only past and current
observations, as the challenge required.
| PPC OFFICIAL, real time (% of distance within 0.5 m) |
Tokyo 1 |
Tokyo 2 |
Tokyo 3 |
Nagoya 1 |
Nagoya 2 |
Nagoya 3 |
Mean |
| One filter (3-seed mean) |
85.2 |
93.7 |
92.3 |
77.2 |
57.5 |
83.7 |
81.6 |
| Ensemble of 4 filters |
86.9 |
95.8 |
93.7 |
80.2 |
61.1 |
90.4 |
84.7 |
| Ensemble + 3D city map (7 filters) |
92.2 |
96.2 |
94.4 |
81.6 |
62.1 |
91.0 |
86.3 |
The PPC2024 winner (TURING) scored 85.6% on the hidden half it was ranked on
and 78.7% on the public half.
| Correct FIX / epochs (wrong FIX / FIX) |
Tokyo 1 |
Tokyo 2 |
Tokyo 3 |
Nagoya 1 |
Nagoya 2 |
Nagoya 3 |
| inuex35 TC-FGO (local reproduction) |
46.7% (5.72%) |
59.7% (1.78%) |
58.2% (2.06%) |
– |
– |
– |
| libgnss++ GTSAM FGO |
37.7% (29.9%) |
77.3% (1.57%) |
68.2% (1.56%) |
– |
– |
– |
libgnss++ KF-RTK (ppc-demo, low-cost preset) |
68.0% (8.76%) |
76.7% (1.80%) |
71.6% (3.01%) |
64.9% (14.8%) |
48.6% (2.15%) |
29.8% (29.4%) |
| RB-FGO-PF, real time |
62.0% (0.00%) |
82.3% (0.00%) |
77.8% (0.00%) |
70.0% (0.00%) |
52.0% (0.06%) |
31.4% (0.00%) |
A FIX is correct when its 3D error is under 0.5 m; one script scores every
system.
flowchart TD
subgraph F["Each of 4 to 7 filters"]
G["Shared float factor graph (GTSAM)<br/>IMU, DD code and carrier"] --> K["Top-K integer candidates"]
K --> P["64 particles, each an integer set (basin),<br/>weighted by the carrier fit and resampled"]
P -.->|"agreed integers"| G
P --> L["FIX label"]
P --> C["Position candidates"]
end
Obs["Rover + base GNSS, IMU"] --> G
Map["PLATEAU 3D city model<br/>(map-aided filters only)"] -.->|"blocked satellites"| G
C --> E["Per-epoch choice:<br/>classifier + height bias"]
L --> E
E --> Out["Position + FIX / FLOAT"]
- Filter. Each particle holds one set of double-differenced integers and
is weighted by how well it fits this epoch’s carrier and code; resampling
drops sets that stop fitting. FIX is reported when the particles agree on
the integers and the fix stays within 0.5 m of the graph’s float position;
an accepted integer set is kept while it still fits the carrier.
- Ensemble. Filters with different seeds, and one that also uses
one-band carriers, run side by side. Each epoch a classifier, trained on
the other five runs, picks one of their positions. Unfixed positions are
lowered by 0.10–0.15 m, because reflected signals lift the float solution
in street canyons. The FIX label comes from the four map-free filters.
- 3D city map (optional). Three more filters trace a ray from the
predicted antenna position to each satellite through the PLATEAU building
models and drop or down-weight blocked satellites
(
plateau_los.py, CPU).
Not like-for-like with PPC2024: teams were ranked on a hidden half of these
runs, our classifier and height bias were trained on them (leaving out the
run being scored), and the 3D map is extra data. Nagoya 2’s 375 s street
canyon stays meters off for every filter. Ensemble scores:
rt_ensemble.py. Details, including
offline outputs (15 s lag, backward pass):
technical report ·
benchmark record ·
design ·
code.
Particle-filter localization on OpenStreetMap
The particle cloud is localized on the real street network (UrbanNav Odaiba).
Results at a glance
| Method |
Dataset |
P50 |
RMS 2D |
| PF 100K (DD + smoother + stop-detect) |
UrbanNav Tokyo Odaiba |
1.50 m |
4.06 m |
| RTKLIB demo5 (rtklibexplorer, 2026-09) |
UrbanNav Tokyo Odaiba |
0.34 m |
40.89 m |
libgnss++ RTK, low-cost preset (78.5% coverage) |
UrbanNav Tokyo Odaiba |
0.07 m |
1.97 m |
| libgnss++ RTK + PF gap fill (98.2% coverage) |
UrbanNav Tokyo Odaiba |
0.20 m |
3.55 m |
PF anchored to libgnss++ RTK FIX (preset urbannav_rtk_anchored, 98.2% coverage) |
UrbanNav Tokyo Odaiba |
0.10 m |
0.77 m |
| PF + RobustClear-10K (external mainline) |
UrbanNav, 5 seq / 2 cities |
— |
66.6 m |
| EKF baseline |
UrbanNav, 5 seq / 2 cities |
— |
93.25 m |
Odaiba rows: 2026-10-04, public subset, one shared base calibration
(calibration,
RTK record,
PF record).
P50/RMS are over each method’s own output epochs. PF 100K is preset
odaiba_stop_detect, 5 seeds. The anchored PF redraws the cloud at every RTK
FIX and is within 3 m on 97% of reference epochs (RTKLIB demo5: 69%)
(record); it also
improves all three PPC Tokyo routes
(PPC record). Without a wheel
odometer, use preset rtk_anchored_doppler (6 PPC routes: 90.5% within 5 m vs
78.6% for RTK alone). Anchored row: gnssplusplus 9d89a58a; other rows
304798e7.
PF-only RTK stretch campaign
Full epoch denominator, no truth or runtime FGO, declared false FIX ≤ 1%.
libgnss++ is an external reference only.
| 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 |
WP173 declares FIX once an MLAMBDA ratio ≥ 3.0 holds for 5 epochs with 6+
satellites and the WP172 consensus gate: Tokyo 10.87%, Nagoya 18.07% of
epochs, zero audited false FIX. Candidate supply costs 42.7 ms/epoch, under
the 100 ms budget.
The external-validation RMS is high because it averages the hardest
deep-urban sequences; what matters is the gap to EKF and RTKLIB on the same
epochs. See the results snapshot.
Open the Odaiba particle-cloud video
Zero-data terminal demo (Odaiba comparison, PPC held-out check, PLATEAU mask
replay gain):
PYTHONPATH=python:. python3 examples/demo_pf_localization_improvement.py
Ray-traced NLOS diffraction on real city data
Models why urban pseudoranges are biased — knife-edge (ITU-R P.526) and
UTD (Kouyoumjian–Pathak) diffraction plus reflection over PLATEAU
meshes — and scores it against real UrbanNav residuals.
Open the full LOS/NLOS deck.gl sweep
Correcting each satellite to transmission time (with Sagnac) gives a clean
NLOS reference (LOS median 1.0 m, AUC 0.92). On it, UTD matches the
measured bias distribution better than knife-edge, as in Zhang & Hsu (2021).
| Diffraction model |
Wasserstein-1 ↓ |
KS ↓ |
| knife-edge (ITU-R P.526) |
1.84 |
0.46 |
| UTD (Kouyoumjian–Pathak) |
1.70 |
0.29 |
UrbanNav Odaiba, 60 epochs, 249k-triangle PLATEAU mesh. Reproduce with
PYTHONPATH=examples python examples/plot_nlos_diffraction_figure.py Odaiba 60.
Simulate GNSS anywhere in a city — and predict how well it will work
gnss_gpu.scenario simulates what a receiver sees at a place or along a route
(visible satellites, LOS/NLOS, pseudorange errors, C/N0, Doppler) and exports
RINEX 3.04 OBS (--rinex-out). gnss_gpu.coverage_map batches all grid
cells × satellites into single CUDA launches to map visibility, availability,
DOP and expected error over a 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)
Use cases on 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% while expected HPE spikes past 300 m on near-singular geometry. |
| RTK base-station placement |
demo_rtk_base_placement.py |
Candidates tie on LOS count and HDOP; the common-view satellite count with the rover route ranks them. |
| Site multipath assessment |
demo_urban_multipath.py |
Sky map of LOS/NLOS tracks with multipath excess and C/N0 (UTD): 28.6% NLOS, 18.9 dB-Hz LOS-vs-NLOS C/N0 gap. |
Every model is scored against measured residuals, and the pipeline is open
from CityGML parsing to the CUDA kernels.
Quick start
GPU-first start
Needs 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-acquisition
doctor checks the driver, GPU, compiler, CMake, bindings and a CUDA
round-trip. On Windows, use python in a Developer PowerShell; set the
architecture with gnss-gpu build --architecture 89.
GPU experiment loop
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-candidate
Each run writes a runs/ directory with a v1 manifest.json (git SHA, GPU,
input hashes, parameters, metrics, artifacts); compare writes
comparison.md (--json PATH for JSON). Local data (rover/base RINEX, nav,
reference.csv, optional imu.csv) is never downloaded implicitly. Fetch the
Tokyo subset with:
python experiments/fetch_urbannav_subset.py --run Odaiba --output-dir data/urbannav/Tokyo
--allow-cpu-fallback is for CPU smoke tests only.
Browser/CPU reference
For parallel GPS L1 C/A IQ generation without CUDA, see
CPU signal simulation, including C/N0-calibrated
noise, navigation-bit transitions and continuous waveform blocks.
The unknown-carrier acquisition example
adds CPU carrier/code-delay search and independent whole-search noise calibration.
The blockwise tracking example
adds frequency refinement, carrier-aided code following, outage and reacquisition diagnostics.
The tracker now defaults to two-block acquisition confirmation,
with paired examples recording false captures and the added acquisition delay.
The continuous multipath tracking experiment
compares direct-only and reflected signals with paired noise and a finite wall edge.
An experimental two-path fitter
records resolution limits, high-SNR improvements and noisy failure cases; it is not yet used for tracking correction.
The rain and wet-wall experiment propagates rain attenuation
and independently prescribed surface films through CPU IQ acquisition to position/clock estimates.
The wet-troposphere residual study adds a separate
signed delay control and measures its effect on height and receiver clock.
The wet antenna/radome replay tests prescribed
loss, code bias and phase through continuous wetting/drying, including outages and recovery.
The position-publication policy tests redundant
observations, DOP, residuals and temporal consistency on separate noise seeds, including the availability cost.
The position-reference recovery study
tests median references and sustained candidate clusters against the previous gate on fresh noise.
The range-uncertainty study tests causal
range scatter, weighted position estimates and nominal covariance coverage before changing publication rules.
The literature-backed propagation model
records published GNSS simulation references and the implemented polarization extension.
Zero install:
— or locally:
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 plotly
Run the demo (no GPU, no data, ~1 second)
Naive WLS vs robust SPP on a simulated urban canyon:
PYTHONPATH=python python3 examples/demo_urban_canyon_sim.py
method 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
Use the robust SPP solver from Python
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,
)
Invalid shapes, non-finite values, negative weights or options raise
ValueError. NLOS simulation and the PLATEAU mask replay suite:
PYTHONPATH=python python3 examples/demo_nlos_simulation.py
PYTHONPATH=python:. python3 experiments/run_plateau_nlos_demo_suite.py
The suite replays SPP/PF/FGO with the mask (tow,epoch_idx,prn,is_los) and
writes JSON/Markdown/CSV summaries.
| 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% |
Tests and kernels
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 doctor
GPU demos need a prior build. On CPU-only installs, native calls raise
gnss_gpu.NativeBackendUnavailableError; build --dry-run shows the build
command.
ROS 2 node
ros2/gnss_gpu_ros gates multipath/NLOS spikes in
sensor_msgs/NavSatFix (Hampel + CV Kalman) and publishes an RViz path.
ros2 run gnss_gpu_ros robust_navsat_filter --ros-args -r fix:=/your_gnss_driver/fix
Repository layout
python/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"]
Where to look next
A note on scope
Experiment-first: stable code lives in python/gnss_gpu/ and src/; runs,
sweeps and reports in experiments/ and internal_docs/. Trust a generated
CSV/HTML only if experiments/results/README.md
lists it and internal_docs/plan.md records its
build command.
Development policy
- Stable code goes in
python/gnss_gpu/ or src/; variant-heavy logic stays
in experiments/ until it survives fixed evaluation.
- Promote on same-input, same-metric comparisons, not one pilot split.
- Record durable decisions in
internal_docs/decisions.md.
- Never vendor, link or derive code/config from GPL-3.0 sources such as
gici-open.
License
Apache-2.0
CPU simulation research checkpoint
The CPU research checkpoint summarizes moving
and accelerating reflections, receiver uncertainty, and half-window carrier
initialization. It includes the limits of the synthetic results and links to
curated reports and figures. These research estimators remain opt-in.
评论