lidarslam_ros2 v0.6.0
The deterministic mapping release. Same bag + same config now provably produces the same map: byte-identical loop edges, trajectories and submap streams across runs, enforced by the release gate. The change is architectural — the backend's decision logic moved out of the wall-clock ROS callbacks into pure, unit-tested cores — and it surfaced and fixed several real bugs on the way.
Highlights
Deterministic offline mapping (the headline)
- Backend:
graph_slam_offline_runnerreplays a recorded odometry bag through the same submap creation, event-drivenBackendCoreloop search and pose-graph optimization the live node uses — no executor, no wall clock. Three runs are byte-identical on both gate substrates: - Livox MID-360 (1079 m, 640 submaps): loop edges + optimized trajectory md5-identical, APE vs reference 7.262851264566504 to the last digit.
- NTU VIRAL tnp_01: 9 loop edges, Leica-GT APE 0.8460511516520233 to the
last digit.
Before v0.6 the same byte-identical input produced different loop edges
every run (pure scheduling artifacts; see
docs/research/determinism-variance-attribution.md). - Frontend:
scan_matcher_offline_runnerdrives the realScanMatcherComponentin lockstep over a raw sensor bag (intra-process pub/sub, drained single-threaded executor, synchronous map update). Three runs over the full NTU tnp_01 bag (5795 clouds) are byte-identical (5795 poses, 133 submaps) — the first determinism coverage for the scanmatcher (NDT) frontend. - Gate-enforced:
run_release_readiness_checks.shgains opt-in--offline-determinism-bagand--frontend-determinism-bagstages that fail the release gate on any byte-level mismatch.
Event-driven loop search by default
The backend searches loops on submap arrival instead of a wall-clock timer.
The v0.4 deterministic_loop_scheduling experiment is retired — its negative
result (a deterministic schedule does not make outcomes deterministic)
motivated this architecture. Live evidence for the flip: NTU corrected APE
0.965 (within the ≤ 1.00 profile), MID-360 live replay APE 6.28–6.49 vs the
legacy timer's 7.17–7.23. The legacy path remains available behind
event_driven_loop_search: false for one release and will be removed in
v0.7. Live mode is improved but not byte-deterministic — that contract
belongs to the offline runners.
Real bugs fixed along the way
- A silent ~6% input-frame drop under load (cloud subscription was best-effort QoS with a shallow queue while loop search blocked the executor) — inputs are now synchronized with a deep queue.
- IMU "rotation" edges weighted the translation block of the g2o error and GNSS "position" anchors weighted the rotation block — GNSS georeferencing had never actually pulled. Both fixed; with GNSS enabled, maps are now georeferenced in the projector's local ENU frame via 2D yaw alignment with gauge release.
- Missing tie-breaks in five loop-candidate rankings (
unordered_mapiteration order leaked into results).
Architecture: pure cores, thin shells
graph_based_slam_component.cpp3421 → 2265 lines; the loop-closure pipeline lives in 19 tested pure headers assembled by a ROS-freeBackendCore.scanmatcher_component.cpp2140 → 1694 lines; pose prediction, pose acceptance (including the Tracking/Suspect/Recovery state machine), IMU processing and the map-update policy are pure headers with characterization tests that pin exact log bytes and float arithmetic.- Package test count grew past 1100, including determinism contract tests ("same ordered input twice ⇒ bitwise-identical state/output").
Accuracy (unchanged, re-verified throughout)
The RTK-SLAM total-station ground-truth gates from v0.5 stayed green through all 26 PRs of this release; the stadtgarten_seq2 raw APE 0.835 m reproduced bit-identically 19 consecutive times — the strongest behavior-preservation evidence we have ever attached to a refactor.
| Sequence | Env | APE RMSE | Gate |
|---|---|---|---|
| Construction Hall 2 | indoor | 0.154 m | blocking, pass ≤ 0.30 |
| Construction Hall 1 | indoor (hard) | 0.403 m | blocking, pass ≤ 0.55 |
| Stadtgarten 2 | outdoor park | 0.835 m | report-only |
| Stadtgarten 1 | outdoor park, 1 km | 1.666 m | report-only |
Upgrade notes
deterministic_loop_schedulingno longer exists; remove it from custom param files.event_driven_loop_searchdefaults totrue.- The legacy wall-clock loop-search timer (
event_driven_loop_search: false) is deprecated and scheduled for removal in v0.7.
Full details: docs/roadmap/v0.6.md (all phases and gates),
docs/research/determinism-variance-attribution.md (Phase 0 attribution),
CHANGELOG.md.