Skip to content

gsc_pgo: online and offline PGO - #2587

Open
jeff-hykin wants to merge 1248 commits into
mainfrom
jeff/feat/jnav_pgo
Open

jeff-hykin wants to merge 1248 commits into
mainfrom
jeff/feat/jnav_pgo

Conversation

@jeff-hykin

@jeff-hykin jeff-hykin commented Jun 24, 2026 •

Copy link
Copy Markdown
Member
dimos run unitree-go2-mid360-pgo
dimos run unitree-go2-pgo # very conservative

Offline PGO (uses april tags for correction if available)

# china_office.db is pulled from LFS automatically (bare filename -> LFS), so this runs first-try.
python dimos/navigation/jnav/components/loop_closure/gsc_pgo/scripts/post_process.py \
    --db china_office.db \
    --odom pointlio_odometry \
    --lidar pointlio_lidar \
    --tags raw_april_tags \
    --camera color_image \
    --tag-size 0.10 \
    --dict DICT_APRILTAG_36h11 \
    --world-frame world \
    --corrected-odom-frame corrected_odom \
    --corrected-suffix _corrected \
    --suffix "" \
    --closure-spacing 2.0 \
    --lcm-voxel 0.05 \
    --accum-voxel 0.05 \
    --accum-max-range 20.0
# Graceful on other recordings: --odom-tf auto-resolves from the odom stream's own header,
# no camera_info -> AprilTag stage is skipped (ICP + odom only), and a missing base<-optical
# tf edge falls back to the known rig mount geometry. Pass --odom / --lidar to override the
# auto-detected stream names, or --base-optical 'x y z qx qy qz qw' for a non-Go2 rig.
# opt-out toggles (every stage is on by default):
#   --no-odom  --no-lidar  --no-icp  --no-lcm  --no-rrd  --no-accum  --no-tf

Online PGO replay

# eval generates a json and visuals (rrd, plots). china_office.db is pulled from LFS automatically.
uv run python dimos/navigation/jnav/components/loop_closure/eval.py \
    --db-path china_office.db \
    --odom-stream pointlio_odometry \
    --lidar-stream pointlio_lidar \
    --camera-stream color_image \
    --odom-tf odom:mid360_link \
    --tag-frame camera_optical \
    --module-path dimos/navigation/jnav/components/loop_closure/gsc_pgo/module.py \
    --module-name GscPGO \
    --pgo-config-json '{"use_scan_context": true}'
# --odom-tf is 'parent:child' for the odom edge (odom:mid360_link here). On another recording,
# set it to that db's odom parent:child; a missing camera_info stream skips the AprilTag stage.

Note: offline does better than online, but online is so good it doesn't really matter

IMG_20260730_161946_195

Examples

IMG_20260730_170058_261 IMG_20260730_162725_926 IMG_20260730_162831_765 IMG_20260730_162902_133 IMG_20260730_163147_192 IMG_20260730_214048_557

@greptile-apps

greptile-apps Bot commented Jun 24, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces the gsc_pgo online and offline Pose Graph Optimization system: a Rust+GTSAM loop-closure module (Scan Context++ descriptors, ICP closures, GNC), an offline multi-stage post-processing pipeline, a lockstep replay evaluation harness, and a BaselinePGO Python wrapper around the existing C++ PGO state — all wired into the Go2 mid360 and L1 blueprints.

  • Online PGO (gsc_pgo/module.py, Rust binary): GscPGO NativeModule publishes corrected odometry, a pose graph, and TF corrections; the lockstep replay harness feeds recordings through the module and captures the final graph for scoring.
  • Offline PGO (post_process.py): Two-stage AprilTag PGO + ICP loop-closure solve; outputs corrected odom/lidar streams, deformation nodes, and aggregated .pc2.lcm clouds back into the recording db.
  • Evaluation (eval.py, utils.py): Self-consistency scoring (tag-spread + lidar-voxel agreement) with before/after top-down/isometric/rrd visuals; RecordingTF caches the full tf stream with a past-only latch so near-static edges published once at recording start survive later lookups.

Confidence Score: 4/5

  • The PR is mostly safe to merge; the one new defect found is isolated to the BaselinePGO pass-through path for placeholder Go2 poses and does not affect the primary GscPGO module.
  • The Rust PGO core and the offline pipeline are large and novel, and the Rust crate has no CI coverage (builds only in the Nix flake dev shell). The BaselinePGO._publish_corrected function calls _transform_to_pose3 with a zero-quaternion odom on the Go2's startup placeholder poses, which produces an invalid GTSAM Pose3 and corrupts the published corrected odometry for those early frames. Everything else reviewed — GscPGO, RecordingTF, the eval harness, and the blueprints — looks correct for the intended use cases.
  • baseline_pgo/module.py (_publish_corrected zero-rotation path); gsc_pgo/rust/ (no CI — must be validated in the Nix dev shell before any Rust changes)

Important Files Changed

Filename Overview
dimos/navigation/jnav/components/loop_closure/baseline_pgo/module.py New BaselinePGO module wrapping the existing C++ PGO state; contains a logic bug where a zero-rotation placeholder odom (from Go2 startup) is passed to _transform_to_pose3 inside _publish_corrected, producing a NaN GTSAM Pose3. Also accesses several private _PGOState fields throughout.
dimos/navigation/jnav/components/loop_closure/gsc_pgo/module.py New GscPGO NativeModule wiring the Rust PGO binary; port types match the binary's outputs. The _on_correction_for_tf subscription pattern correctly republishes the Rust-emitted TFMessage to the tf port. Config is detailed and well-documented.
dimos/navigation/jnav/components/loop_closure/gsc_pgo/utils/replay.py Lockstep replay harness and run_module_graph; temp-file paths keyed only on the parent directory name can collide in parallel batch runs. counts_store not protected by try/finally (also noted in previous review). drift_t0=0.0 default makes drift injection effectively unusable without an explicit start timestamp (also in previous review).
dimos/navigation/jnav/components/loop_closure/eval.py Evaluation driver for loop-closure modules against a recording. SqliteStore lifecycle is now managed with a with statement, addressing the prior open-handle concern. The empty odom stream guard is present. Several edge cases around empty streams and drift injection were previously flagged and appear partially addressed in this version.
dimos/navigation/jnav/components/loop_closure/gsc_pgo/scripts/post_process.py Offline PGO pipeline (AprilTag PGO + ICP loop closures). SqliteStore is now managed via with. Empty odom guard present. Several issues previously flagged (empty lidar stream crash, LCM write crash on zero scans, corrected_store_tf=None accumulation) are partially addressed in the current version, though some edge cases remain.
dimos/navigation/jnav/components/loop_closure/gsc_pgo/utils/artifacts.py Write helpers for corrected odom, lidar, deformation nodes, pose graph, and raycast-accumulated maps. write_aggregated_lcm crashes on an empty scan list (previously flagged). raycast_accumulate signature no longer accepts None for store_tf, addressing the silent-empty-cloud path from previous review.
dimos/navigation/jnav/utils/recording_tf.py New RecordingTF / PastOnlyTBuffer that caches the full tf stream and overrides the odom edge. The lazy-load + past-only latch design correctly handles near-static frames published once at recording start. Edge override drops all time-varying recorded edges before injecting the fed trajectory.
.github/workflows/ci.yml Excludes the gsc_pgo/rust crate from CI Cargo/Clippy (no gtsam on plain Ubuntu runner); acknowledged in comments as intentional — build and tests must be run locally in the Nix flake dev shell. No other structural CI changes.

Reviews (52): Last reviewed commit: "gsc_pgo: relock rust deps after merging ..." | Re-trigger Greptile

Comment thread dimos/memory2/db_tf.py Outdated
Comment thread dimos/memory2/db_tf.py Outdated
Comment thread dimos/navigation/jnav/utils/recording_db.py Outdated
Comment thread dimos/memory2/db_tf.py Outdated
Comment thread dimos/navigation/jnav/components/loop_closure/unrefined_pgo/module.py Outdated
@codecov

codecov Bot commented Jun 24, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 37.74235% with 1445 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
dimos/navigation/jnav/utils/apriltags.py 27.30% 367 Missing and 3 partials ⚠️
...s/navigation/jnav/components/loop_closure/utils.py 13.22% 269 Missing ⚠️
dimos/navigation/jnav/utils/trajectory_metrics.py 15.50% 158 Missing ⚠️
...omponents/loop_closure/gsc_pgo/scripts/make_rrd.py 19.55% 143 Missing and 1 partial ⚠️
...components/loop_closure/gsc_pgo/utils/artifacts.py 21.71% 137 Missing ⚠️
...mponents/loop_closure/gsc_pgo/utils/offline_pgo.py 42.22% 103 Missing and 1 partial ⚠️
dimos/navigation/jnav/msgs/Graph3D.py 35.92% 66 Missing ⚠️
dimos/navigation/jnav/utils/recording_tf.py 31.52% 63 Missing ⚠️
dimos/navigation/jnav/msgs/GraphDelta3D.py 34.17% 52 Missing ⚠️
dimos/navigation/jnav/msgs/DeformationNode.py 47.61% 22 Missing ⚠️
... and 8 more
@@            Coverage Diff             @@
##             main    #2587      +/-   ##
==========================================
- Coverage   77.83%   77.18%   -0.65%     
==========================================
  Files        1329     1377      +48     
  Lines      126868   131092    +4224     
  Branches    11035    12775    +1740     
==========================================
+ Hits        98743   101188    +2445     
- Misses      24974    26704    +1730     
- Partials     3151     3200      +49     
Flag Coverage Δ
OS-ubuntu-24.04-arm 73.80% <37.74%> (-0.75%) ⬇️
OS-ubuntu-latest 74.32% <37.74%> (-0.76%) ⬇️
Py-3.10 74.32% <37.74%> (-0.76%) ⬇️
Py-3.11 74.31% <37.74%> (-0.76%) ⬇️
Py-3.12 74.32% <37.74%> (-0.76%) ⬇️
Py-3.13 74.32% <37.74%> (-0.76%) ⬇️
Py-3.14 74.32% <37.74%> (-0.76%) ⬇️
Py-3.14t 74.32% <37.74%> (-0.76%) ⬇️
SelfHosted-Large 30.53% <22.23%> (+0.33%) ⬆️
SelfHosted-Linux 35.02% <22.23%> (+0.37%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
.../loop_closure/gsc_pgo/scripts/test_post_process.py 100.00% <100.00%> (ø)
...os/navigation/jnav/msgs/test_LocationConstraint.py 100.00% <100.00%> (ø)
dimos/navigation/jnav/utils/test_apriltags.py 100.00% <100.00%> (ø)
dimos/robot/all_blueprints.py 100.00% <ø> (ø)
...ree/go2/blueprints/smart/unitree_go2_mid360_pgo.py 90.90% <90.90%> (ø)
...ot/unitree/go2/blueprints/smart/unitree_go2_pgo.py 91.66% <91.66%> (ø)
.../robot/unitree/go2/go2_mid360_static_transforms.py 84.21% <33.33%> (-9.54%) ⬇️
...ion/jnav/components/loop_closure/gsc_pgo/module.py 94.50% <94.50%> (ø)
dimos/navigation/jnav/msgs/LocationConstraint.py 88.34% <88.34%> (ø)
dimos/navigation/jnav/utils/voxel_map.py 60.00% <60.00%> (ø)
... and 12 more

... and 80 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jeff-hykin jeff-hykin changed the title jnav: port PGO/loop-closure + tf-tree for memory2 stores gsc_pgo: online and offline PGO Jun 24, 2026
@jeff-hykin
jeff-hykin enabled auto-merge (squash) June 24, 2026 08:35
@github-actions github-actions Bot added ready-to-merge Required CI checks have passed on this PR and removed ready-to-merge Required CI checks have passed on this PR labels Jun 24, 2026
- drop the comparison PGO modules, keep gsc_pgo only
- guard SQL table names, lock the tf lazy-load, close cached stores
- port the apriltag gate helpers add_april needs; mypy strict fixes
- move the gsc_pgo CLI scripts into gsc_pgo/scripts/ and use descriptive names
- MultiTBuffer.lookup (no warning) for DbTf; regenerate all_blueprints.py
- includes the pose-setter fix from jeff/fix/pose_setter_for
A LocationConstraint carries ready-made BetweenFactor args: the body->location
pose, a full 6x6 covariance, to_id, frame_id and a constraint_instance_id that
lets a source revise or remove an earlier constraint.
…e graph)

A passive test rig: Point-LIO reads the Mid-360, the PGO builds the loop-closed
pose graph and Rerun draws keyframes and edges. Drive the dog separately.
@TomCC7
TomCC7 disabled auto-merge June 26, 2026 03:27
@github-actions github-actions Bot removed the ready-to-merge Required CI checks have passed on this PR label Jun 26, 2026
TomCC7
TomCC7 previously approved these changes Jun 26, 2026

@TomCC7 TomCC7 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test

@TomCC7
TomCC7 dismissed their stale review June 26, 2026 03:28

test

tf lookups no longer load the whole tf history into RAM. A `tf_graph` stream
records each topology change; rows are tagged with their child frame and looked
up with one index-served query, with the graph cached in RAM when it is small.
Includes reparent, churn and 54k-row deep-tree tests.
The raw baseline is now the recording's world->base_link tf sampled at each odom
time, with a startup tolerance window. Recordings without tf are rejected rather
than silently falling back to odom poses.
- remove unused helpers, hardcode the tf_graph stream name
- order graph snapshots by (ts, id) so same-timestamp changes resolve correctly
- drop the hardcoded /tf_static LCM topic; an optional tf_static In port is
  folded into the tf stream and flagged static
Adds a DeformationNode message and a tf_deformation_nodes stream (recorded by
default, a no-op when unconnected). gsc_pgo publishes it, and DbTf blends the
per-node corrections into lookups along matching tf edges.
Comment thread dimos/memory2/db_tf_sql.py Outdated
Comment thread dimos/memory2/db_tf_sql.py Outdated
Comment thread dimos/navigation/jnav/components/loop_closure/gsc_pgo/scripts/post_process.py Outdated
@jeff-hykin jeff-hykin added the backport:skip Skip creating a backport to any release branches label Jul 2, 2026
@github-actions github-actions Bot added the ready-to-merge Required CI checks have passed on this PR label Jul 2, 2026
…cess

- DbTfLive (in-RAM) and DbTfSql (indexed queries + deformation correction)
- post_process detects AprilTags when no tag stream exists and persists the
  deformation nodes and pose graph as typed streams
- post_process honors each scan's frame_id instead of a hardcoded lidar frame
- eval: tf-based raw pose lookup; skip DbTfSql tests when sqlite-vec can't load
paul-nechifor and others added 30 commits October 12, 2026 19:57
Co-authored-by: yimo <yimo@dimensional.org>
Co-authored-by: leshy <lesh@sysphere.org>
Co-authored-by: bogwi <bogdan@dimensional.org>
Co-authored-by: Paul Nechifor <paul@nechifor.net>
Co-authored-by: Moshi Wei <wmswms938@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Sam Bull <git@sambull.org>
gsc_pgo is pinned to the public gsc_pgo repo and wrapped as a native module.
Alongside it: the jnav PGO message types (Graph3D, GraphDelta3D, Landmark,
Marker), jnav utils (apriltags, recording_db, trajectory_metrics, kitti,
voxel_map, module_loading), comparison PGO modules, loop-closure eval scripts,
tf tree support in the memory2 store, the offline map post-processing pipeline
(post_process, add_april, detect_tags, make_rrd) and docs for both.
…ePGO's import

Unused apriltag, trajectory-metric and drift-injection helpers, the unused LoopClosure spec and base_link_from_camera_optical are gone; BaselinePGO imports Ivan's PGO core from dimos/navigation/go2/loop_closure.

This branch was previously deployed

1 inactive (outdated) deployment
cachix — 1273adc5 Deployed Sep 4, 2026 by jeff-hykin via cachix-build #8749
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:skip Skip creating a backport to any release branches

Projects

None yet

Development

Successfully merging this pull request may close these issues.