Skip to content

Nav pt5: Dynamic Global Map with Loop Closure Voxel Transform#2131

Draft
jeff-hykin wants to merge 201 commits into
mainfrom
jeff/clean/nav4
Draft

Nav pt5: Dynamic Global Map with Loop Closure Voxel Transform#2131
jeff-hykin wants to merge 201 commits into
mainfrom
jeff/clean/nav4

Conversation

@jeff-hykin
Copy link
Copy Markdown
Member

@jeff-hykin jeff-hykin commented May 17, 2026

Problem

We want 1 map thats global and accurate and real time

Solution

Purple Boxes = Important

Screenshot 2026-05-17 at 7 06 39 AM

ApplyClosure (Graph Transformation)

The vid simulates major drift + loop closure.

The GREEN part at the end of the video is the transformation (in slow-motion) being applied to the global point cloud. Here's how its done:

  • Expose the pose graph from PGO
  • Use the delta of a loop closure event as a skeleton movement (video game skeleton)
  • Apply a modified version of linear blending skinning to deform the lidar pointcloud as a chronological mesh
  • Keep track of time using a slightly weird but efficient voxel cloud
loop_closure_transform.mov

Dynamic Point-Clearing

NOTE: I'm scrubbing-through the replay in this video (not real time playback). I'm showing how well you can a human with very minimal artifacting.

How? A slightly modified version of Andrew's Rust Raycast module!

raycast_point_clearing.mov

Complete Wiring
Screenshot 2026-05-17 at 7 32 19 AM

How to Test

# tested on alfred
dimos run alfred-nav

# should also work fine onboard a go2 with livox
dimos run unitree-go2-nav

# test the ApplyClosure (rerun visual)
uv run python -m dimos.navigation.nav_stack.modules.apply_closure.demo_closure_scene --step-ms 200

Contributor License Agreement

  • I have read and approved the CLA.

leshy and others added 30 commits May 6, 2026 14:52
# Conflicts:
#	data/.lfs/go2_hongkong_office.db.tar.gz
#	data/.lfs/go2_short.db.tar.gz
… rrb

Native module (cpp/main.cpp) now publishes two new streams on every
keyframe: GraphNodes3D for keyframe optimized poses, LineSegments3D for
odometry (traversability=1.0) and loop-closure (0.4) edges. Both wire
through SimplePGO::keyPoses() + historyPairs() — no changes needed to
simple_pgo.{h,cpp} since the accessors already exist. Native binary
rebuilt cleanly via nix build .#default --no-write-lock-file.

Python (pgo.py) declares matching pgo_graph_nodes / pgo_graph_edges Out
streams so the rerun bridge auto-discovers and logs them.

nav_stack_rerun_config() now picks _agentic_debug_rerun_blueprint when
agentic_debug=True — an rrb.Horizontal layout with a 3D pane and a
dedicated top-down pane (both Spatial3DView over origin="world", named
"3D" and "top_down" so dimos-viewer persists camera state separately).

demo_better_pgo_viz.py composes the cross-wall sim blueprint with
agentic_debug=True so the new layout + pose graph render together. Used
for manual screenshot validation.
Adds visual_override entries for world/pgo_graph_nodes and
world/pgo_graph_edges that mirror the existing FAR pattern: when
agentic_debug=True, the PGO pose graph renders at z=_AGENTIC_DEBUG_LIFT
(3.0m) instead of the default 1.7m, with slightly larger node radii
(0.15) and edge thickness (0.06) so the green keyframe trajectory
stands out clearly above the terrain cloud in the top-down pane.

Verified visually via demo_better_pgo_viz with the cross-wall sim —
green keyframe nodes + edges are now plainly identifiable above
terrain in both the 3D and top_down rerun panels.
rerun's Spatial3DView doesn't have a top-down camera API, so the
"top_down" pane introduced in a7a9be9 was just a duplicate 3D view.
Drop _agentic_debug_rerun_blueprint and use _default_rerun_blueprint
unconditionally — the agentic_debug lift on visual_override is what
actually makes the pose graph and nav markers readable from any angle.
C++ side (main.cpp): when searchForLoopPairs sets m_cache_pairs (i.e.
this keyframe will be incorporated into iSAM2 with a loop factor),
snapshot the current global poses before smoothAndUpdate. After the
update, build a nav_msgs::Path-encoded LoopClosureDeltas message:
position = post.t - r_delta * pre.t, orientation = quaternion(post.R *
pre.R^T). Publish on the new pgo_loop_closure topic. Stderr logs the
event count for live observability.

Python side (pgo.py): declare pgo_loop_closure: Out[NavPath] so the
new topic is registered alongside corrected_odometry/pgo_tf/etc.

Slow test (test_pgo_loop_closure.py): replays og_nav_60s through the
native binary with permissive thresholds (loop_time_thresh=5s,
min_loop_detect_duration=1s, loop_search_radius=2m,
loop_score_thresh=0.5) so the recording reliably triggers loop
closures. Subscribes to pgo_loop_closure, logs each event the moment
it arrives (event #, poses_length, frame_id, first delta), and after
the run validates each event has >0 poses, finite translations
(<100m), and unit-norm quaternions (drift <0.05). Stdout from a run
shows 19 events, sizes 10..35, max |t|=0.0013m, max |q|-1|=1e-6 —
exactly the small-nudge profile expected from a self-consistent
recording.
Replaces the kdtree-on-keyframe-positions loop search with a Scan
Context (Kim & Kim 2018) descriptor-based pipeline:

  1. addKeyPose now also caches a polar-binned (20 rings × 60 sectors)
     max-z descriptor + the per-row mean "ring key" for each keyframe.
     The descriptor is appearance-based and pose-independent, so it
     keeps working even when odometry has drifted enough that the new
     keyframe is no longer "near" its old neighbours in pose-space.

  2. searchForLoopPairs first asks Scan Context for a candidate:
     ring-key L2 distance ranks all past keyframes, top-K are scored
     by column-shifted cosine distance on the full descriptor, the
     best below the threshold (default 0.4) is the candidate. The
     winning column shift is also converted to a yaw rotation and used
     to seed ICP, which dramatically improves convergence on revisits
     that arrive at a different heading from the original pass.

  3. Position-based search is retained as a fallback when SC is
     disabled or finds nothing, so existing behaviour is preserved.

Replaces ~50 lines of position-search with ~30 lines of SC retrieval
in searchForLoopPairs; new scan_context.{h,cpp} (~150 lines, MIT
attribution to upstream irapkaist/scancontext concepts but no source
copied) implements the descriptor + distance.

Side-effect: this makes on-start relocalization a small follow-up
addition — descriptors + ring-keys + poses are now per-keyframe state
that can be serialised, and the SC search path already does
"appearance-based pose recovery without an initial pose guess."

Verified via test_pgo_loop_closure.py: 17 loop-closure events fired
across the og_nav_60s rosbag (was 19 with naive position search; SC
is more selective and rejects two borderline-position matches that
weren't actually visual revisits). All events have valid shape + tiny
quaternion/translation deltas as expected for a self-consistent bag.
…n search misses

Adds CLI args to expose Scan Context config on the native binary
(--use_scan_context, --sc_n_rings, --sc_n_sectors, --sc_max_range_m,
--sc_top_k, --sc_match_threshold).

New slow test test_pgo_synthetic_drift.py:
- Synthesises a 4-wall point-cloud room with two distinctive interior
  columns (so the scene isn't rotationally symmetric).
- Generates an out-and-back trajectory: drives east 8m then returns
  to the origin, heading unchanged.
- Injects DRIFT_AT_REVISIT_M = 5m of additive y-drift into the
  reported odometry, ramped linearly with travelled distance. The
  body-frame scan stays byte-identical between first and second visit
  (same true sensor view of the same scene); the odom pose at revisit
  is 5m offset.
- Runs the native PGO binary twice over the same input:
  * use_scan_context=true  → expect ≥1 loop event
  * use_scan_context=false → expect 0 loop events (drift >> 1m radius)
- Dumps PGO stderr after each run for diagnostics.

Result: SC fires 10 loop closure events on the synthetic trajectory;
position-based search fires 0 — exactly the demonstration of why we
swapped to appearance-based place recognition. Both assertions pass.

Verifies the core SC value prop: appearance-based place recognition
doesn't depend on the (drifted) pose, so it keeps working when the
odometry has wandered far enough that the kdtree-on-positions search
no longer finds neighbours.
Test files now use setup_logger() / logger.info(...) per the
fix_nits rule "no print() calls in tests; use logging if diagnostics
are genuinely needed." Matches the existing test_pgo_rosbag.py
convention. Also drops the now-unused sys import.

Also clears a stale docstring on demo_better_pgo_viz.py: it claimed
the demo enabled a "horizontal 3D + top-down panes" layout, which was
reverted in 1801759 — rerun's Spatial3DView didn't support an
initial camera angle (rrb.EyeControls3D existed at the time but
wasn't used). The remaining value of agentic_debug=True is the visual
override lift, which the new docstring describes accurately.

No behavioural change. Tests still pass.
Sweep over names introduced by the better_pgo work that hit fix_nits
"expand mod -> module" rule:

- scan_context: cfg -> config (param + 12 call-sites); d (return val) ->
  descriptor in make_descriptor/make_ring_key/make_sector_key; pt -> point
  in the descriptor build loop; zf -> point_z (float cast); q_col/c_col
  -> query_column/candidate_column; q_norm/c_norm -> query_norm/
  candidate_norm; cj -> shifted_j; d (in best_distance return loop) ->
  distance with min_distance for the running best.

- simple_pgo: desc -> descriptor on the per-keyframe cache; k ->
  top_k_count for the partial-sort bound; structured-binding `auto [d,
  shift]` -> `auto [distance, shift]`.

- main.cpp: kp -> keyframe; ps -> pose_stamped (build_graph_nodes and
  build_loop_closure_deltas); a/b -> start/end and p1/p2 ->
  start_pose/end_pose in append_segment; n -> count for the loop bound;
  lc_msg -> loop_closure_msg at the publish site.

- tests: ps -> pose in the validate loop (test_pgo_loop_closure);
  c,s -> cos_yaw,sin_yaw in _yaw_rotation (test_pgo_synthetic_drift).

Names that intentionally stay short are the math-convention ones:
r/t for SE(3) rotation+translation, q for quaternion, i/j as loop
indices, idx as keyframe index, ts as timestamp, dt for time delta,
tx/ty/tz/qx/qy/qz/qw for component decomposition. The fix_nits rule
calls out mod/lc as the target pattern; expanding the math-notation
names would make the code less readable, not more.

Also drops one section-label comment ("# Log each event the moment it
arrives.") whose adjacent function name already conveys the same and
one in-loop "# node_type 1 = odom/robot" that repeats info already
stated in the function-level docstring.

Native binary rebuilt + slow test still passes (17 events, all valid).
Drops in the wiring for evaluating the PGO native module on KITTI-360.
Cannot run end-to-end yet — the dataset is gated behind a registered
login at cvlibs.net so the data download is a manual user step.

What's here:
- kitti360_loader.py: parses the KITTI-360 directory layout (data_3d_raw
  + data_poses + calibration); composes per-frame lidar→world pose by
  chaining cam0_to_world ⊕ inv(velo_to_cam). Exposes a frame iterator
  + scan_xyz(frame_id).
- loop_groundtruth.py: LCDNet/KITTI-convention groundtruth (≥50 frame
  gap, ≤4m radius), order-agnostic scoring of detected pairs.
- run_kitti360_benchmark.py: argparse CLI, spawns the native binary on
  private LCM topics, plays (registered_scan, odometry) from disk,
  subscribes to pgo_graph_edges to extract loop-closure pairs (via
  traversability ≈ 0.4 segments) and pgo_loop_closure for delta event
  counts. Writes JSON.
- README.md: download instructions for the official "Test SLAM 3D"
  12 GB package, published SOTA reference numbers from LCDNet + ISC
  papers (LCDNet 0.91-0.93 AP, Scan Context 0.62-0.78 AP), expected
  ballpark for our minimal SC port.
jeff-hykin added 12 commits May 17, 2026 05:10
PGO's pose_graph was rendering as nodes-only outside agentic_debug
mode: with no visual_override registered, the bridge fell back to
Graph3D.to_rerun() which intentionally returns just rr.Points3D of
the nodes (to_rerun_multi is the path that emits edges too).

Bridge fix: in final_convert, prefer to_rerun_multi(base_path=...)
when the message exposes it. The bridge already knows the
entity_path it would log to, so pass it as base_path. Graph3D
consumers now get nodes + edges by default, no per-blueprint
visual_override required. Existing visual_overrides still win
because they run before final_convert in the pipe.

Only Graph3D currently defines to_rerun_multi, so this is
effectively a Graph3D-rendering fix; other RerunConvertible types
(TFMessage, etc.) fall through to to_rerun() unchanged.
- per-module (registered_scan→lidar) remaps now live inside create_nav_stack,
  so per-robot blueprints (alfred, g1, mobile.py) stop repeating
  (FastLio2,"lidar","registered_scan")
- ApplyClosure publishes on map_override; auto-connects into RayTracingVoxelMap
  without an explicit remap
- UnityBridgeModule renames registered_scan→lidar to match the new convention
- local_planner: switch Bool import to dimos_lcm.std_msgs.Bool
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 17, 2026

Greptile Summary

This PR adds a Dynamic Global Map with Loop Closure Voxel Transform to the navigation stack. It introduces a Rust-based RayTracingVoxelMap module for real-time voxel map maintenance (with DDA-based raycast clearing), a new ApplyClosure module that applies iSAM2 pose-graph corrections to the accumulated voxel map via temporal Linear Blend Skinning, and replaces the GraphNodes3D/GraphEdges3D message types with a unified Graph3D + GraphDelta3D protocol.

  • New RayTracingVoxelMap (Rust) maintains a voxel hash-set with per-voxel health scoring and configurable slow-clock sequencing; publishes DynamicCloud, a compact binary message that carries both occupancy and a sparse timestamped event log mirrored in Python and Rust.
  • ApplyClosure latches the latest global map, and on every PGO loop_closure_event, warps voxel positions using per-voxel slow-clock timestamps to blend SE(3) deltas from each pose-graph node (slerp on rotation, lerp on translation), then publishes the result as a map_override back into RayTracingVoxelMap.
  • SimplePlanner is ported from PointCloud2/terrain_map_ext to DynamicCloud/global_map; NativeModule gains a [DIMOS_NATIVE_READY] readiness protocol and a separate build() lifecycle phase.

Confidence Score: 5/5

The PR is safe to merge. The core LBS warp math, DDA raycast clearing, DynamicCloud wire-format round-trip, and the closure-feedback loop are all correct.

The apply-closure math correctly applies R*p + t in both the single-node and multi-node code paths. The DynamicCloud binary format is pinned by cross-language tests. Health-value restoration from map overrides correctly roundtrips for all published voxels. The previously flagged corrected_global_map port-name mismatch is resolved. Only minor documentation and observability improvements remain.

No files require special attention beyond the inline suggestions.

Important Files Changed

Filename Overview
dimos/navigation/nav_stack/modules/apply_closure/apply_closure.py New module: LBS-based voxel map warping on loop closure events. Core math (slerp/lerp blend of SE(3) deltas, merge_duplicate_voxels) is sound. Port declarations and publish call match. One comment inaccurately claims DynamicCloud always copies its input array.
dimos/mapping/ray_tracing/rust/src/main.rs New Rust voxel-map module with DDA raycast clearing, slow-clock sequencing, and map-override path. Health/timestamp logic is well-tested. Known DDA iteration cap (FIXME 4096) is already noted.
dimos/msgs/nav_msgs/DynamicCloud.py New message type with binary wire format mirrored in Rust. Encode/decode round-trips correctly; strict tail-size check catches corruption.
dimos/msgs/nav_msgs/GraphDelta3D.py New message for PGO loop-closure deltas. Wire format is big-endian, consistent with Graph3D. ts!=0 convention is self-consistent.
dimos/msgs/nav_msgs/Graph3D.py New graph message replacing GraphNodes3D. ts!=0 convention matches GraphDelta3D. to_rerun_multi returns list-of-tuples for separate node/edge entity paths.
dimos/navigation/nav_stack/main.py Blueprint wiring updated with remappings for RayTracingVoxelMap, ApplyClosure, PGO, and frame-name propagation. Closure feedback loop is intentional.
dimos/navigation/nav_stack/modules/pgo/pgo.py PGO module now exposes pose_graph and loop_closure_event outputs and inherits LoopClosure protocol. Legacy pgo_tf/TF publishing removed.
dimos/core/native_module.py Adds DIMOS_NATIVE_READY readiness protocol, separates build from start via new @rpc build(), and improves _native_ignore_fields() logic. Ready-marker line is silently consumed without logging.
dimos/mapping/ray_tracing/rust/src/dynamic_cloud.rs Rust mirror of DynamicCloud.py. Encode/decode are byte-for-byte compatible with the Python side; both sides pin a shared known-bytes fixture in tests.
dimos/navigation/nav_stack/modules/simple_planner/simple_planner.py Ported from PointCloud2/terrain_map_ext to DynamicCloud/global_map. world_positions() is a compatible drop-in for as_numpy() in _classify_points.

Sequence Diagram

sequenceDiagram
    participant FastLio2 as FastLio2
    participant RT as RayTracingVoxelMap
    participant AC as ApplyClosure
    participant PGO as PGO
    participant SP as SimplePlanner

    FastLio2->>RT: lidar PointCloud2
    FastLio2->>RT: odometry
    Note over RT: DDA raycast clearing + health scoring
    RT->>AC: global_map DynamicCloud
    RT->>SP: global_map DynamicCloud
    AC->>AC: latch latest map

    FastLio2->>PGO: registered_scan + odometry
    Note over PGO: iSAM2 keyframe + ScanContext loop search
    PGO->>AC: loop_closure_event GraphDelta3D
    Note over AC: LBS warp via slerp/lerp per voxel timestamp
    AC->>RT: map_override DynamicCloud warped
    Note over RT: Clear + restore from override
    RT->>AC: global_map corrected on next lidar scan
    RT->>SP: global_map corrected on next lidar scan
    Note over SP: Rebuild costmap from world_positions
Loading

Reviews (2): Last reviewed commit: "-" | Re-trigger Greptile

Comment thread dimos/navigation/nav_stack/modules/apply_closure/apply_closure.py Outdated
Comment on lines +451 to +460

struct ExtractError(&'static str);
impl std::fmt::Display for ExtractError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0)
}
}

fn extract_xyz(msg: &PointCloud2) -> Result<Vec<(f32, f32, f32)>, ExtractError> {
let mut x_off: Option<usize> = None;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Hardcoded DDA iteration cap may silently truncate rays

max_iter = 4096 is a safety guard against infinite loops, but if a ray must traverse more than 4096 voxels (e.g., max_range = 30 m at voxel_size = 0.1 m gives up to ~520 cells for an axial ray, but a near-axis-aligned diagonal can reach ~900 cells), the loop exits early without marking the remaining voxels as misses. This leaves stale occupied voxels along the far end of long rays. The existing FIXME notes the concern; consider deriving max_iter from ceil(max_range / voxel_size * sqrt(3)) or removing the cap entirely since DDA is guaranteed to terminate.

Comment on lines +180 to +182
quantity_size = num_points * 4
if len(data) < offset + voxels_size + quantity_size + _U32_SIZE:
raise ValueError("DynamicCloud: data too short for voxels + quantity + num_events")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 ts=0.0 silently encodes as zero nanoseconds

timestamp_nanos = int(self.ts * 1_000_000_000) if self.ts else 0 treats ts=0.0 as "no timestamp" via Python truthiness. The lcm_decode path has a matching asymmetry. This is self-consistent but a brief comment noting the 0 = unset convention would prevent future confusion.

@jeff-hykin jeff-hykin marked this pull request as draft May 17, 2026 14:20
@codecov
Copy link
Copy Markdown

codecov Bot commented May 17, 2026

❌ 3 Tests Failed:

Tests completed Failed Passed Skipped
1840 3 1837 28
View the top 3 failed test(s) by shortest run time
dimos.core.test_core::test_classmethods
Stack Traces | 0.003s run time
def test_classmethods() -> None:
        # Test class property access
        class_rpcs = Navigation.rpcs
        print("Class rpcs:", class_rpcs)
        # Test instance property access
        nav = Navigation()
        instance_rpcs = nav.rpcs
        print("Instance rpcs:", instance_rpcs)
    
        # Assertions
        assert isinstance(class_rpcs, dict), "Class rpcs should be a dictionary"
        assert isinstance(instance_rpcs, dict), "Instance rpcs should be a dictionary"
        assert class_rpcs == instance_rpcs, "Class and instance rpcs should be identical"
    
        # Check that we have the expected RPC methods
        assert "navigate_to" in class_rpcs, "navigate_to should be in rpcs"
        assert "start" in class_rpcs, "start should be in rpcs"
>       assert len(class_rpcs) == 8
E       AssertionError: assert 7 == 8
E        +  where 7 = len({'build': <function ModuleBase.build at 0x7fb4a2e087c0>, 'get_skills': <function ModuleBase.get_skills at 0x7fb4a2e09440>, 'navigate_to': <function Navigation.navigate_to at 0x7fb2d061af20>, 'set_module_ref': <function ModuleBase.set_module_ref at 0x7fb4a2e09300>, ...})

class_rpcs = {'build': <function ModuleBase.build at 0x7fb4a2e087c0>, 'get_skills': <function ModuleBase.get_skills at 0x7fb4a2e094...vigation.navigate_to at 0x7fb2d061af20>, 'set_module_ref': <function ModuleBase.set_module_ref at 0x7fb4a2e09300>, ...}
instance_rpcs = {'build': <function ModuleBase.build at 0x7fb4a2e087c0>, 'get_skills': <function ModuleBase.get_skills at 0x7fb4a2e094...vigation.navigate_to at 0x7fb2d061af20>, 'set_module_ref': <function ModuleBase.set_module_ref at 0x7fb4a2e09300>, ...}
nav        = <dimos.core.test_core.Navigation object at 0x7fb2653ec710>

dimos/core/test_core.py:80: AssertionError
dimos.robot.test_all_blueprints::test_blueprint_is_valid[unitree-go2-nav]
Stack Traces | 0.009s run time
blueprint_name = 'unitree-go2-nav'

    @pytest.mark.parametrize("blueprint_name", UBUNTU_BLUEPRINTS)
    def test_blueprint_is_valid(blueprint_name: str) -> None:
        """Validate blueprints that should import on the ubuntu-latest runner."""
>       _check_blueprint(blueprint_name)

blueprint_name = 'unitree-go2-nav'

dimos/robot/test_all_blueprints.py:99: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
dimos/robot/test_all_blueprints.py:75: in _check_blueprint
    blueprint = get_blueprint_by_name(blueprint_name)
        blueprint_name = 'unitree-go2-nav'
        message    = 'Missing required tools: git-lfs.\n\nGit LFS installation instructions: https://git-lfs.github.io/'
dimos/robot/get_all_blueprints.py:47: in get_blueprint_by_name
    module = __import__(module_path, fromlist=[attr])
        attr       = 'unitree_go2_nav'
        module_path = 'dimos.robot.unitree.go2.blueprints.navigation.unitree_go2_nav'
        name       = 'unitree-go2-nav'
.../blueprints/navigation/unitree_go2_nav.py:40: in <module>
    "paths_dir": str(GO2_LOCAL_PLANNER_PRECOMPUTED_PATHS),
        Any        = typing.Any
        ClockSyncConfigurator = <class 'dimos.protocol.service.system_configurator.clock_sync.ClockSyncConfigurator'>
        FastLio2   = <class 'dimos.hardware.sensors.lidar.fastlio2.module.FastLio2'>
        GO2        = RobotConfig(name='unitree_go2', model_path=PosixPath('.../unitree/go2/go2.urdf')...}, auto_convert_meshes=True, tf_extra_links=[], task_type='trajectory', task_priority=10, collision_exclusion_pairs=[])
        GO2Connection = <class 'dimos.robot.unitree.go2.connection.GO2Connection'>
        GO2_LOCAL_PLANNER_PRECOMPUTED_PATHS = <[RuntimeError('Missing required tools: git-lfs.\n\nGit LFS installation instructions: https://git-lfs.github.io/') raised in repr()] LfsPath object at 0x7fb26c1749d0>
        MovementManager = <class 'dimos.navigation.movement_manager.movement_manager.MovementManager'>
        __annotations__ = {}
        __builtins__ = <builtins>
        __cached__ = '.../navigation/__pycache__/unitree_go2_nav.cpython-312.pyc'
        __doc__    = None
        __file__   = '/home/runner/work/dimos/dimos/.../blueprints/navigation/unitree_go2_nav.py'
        __loader__ = <_frozen_importlib_external.SourceFileLoader object at 0x7fb26c0fcf80>
        __name__   = 'dimos.robot.unitree.go2.blueprints.navigation.unitree_go2_nav'
        __package__ = 'dimos.robot.unitree.go2.blueprints.navigation'
        __spec__   = ModuleSpec(name='dimos.robot.unitree.go2.blueprints.navigation.unitree_go2_nav', loader=<_frozen_importlib_external.So...7fb26c0fcf80>, origin='/home/runner/work/dimos/dimos/.../blueprints/navigation/unitree_go2_nav.py')
        annotations = _Feature((3, 7, 0, 'beta', 1), None, 16777216)
        autoconnect = <function autoconnect at 0x7fb384c06f20>
        create_nav_stack = <function create_nav_stack at 0x7fb26c10cd60>
        global_config = GlobalConfig(robot_ip=None, robot_ips=None, xarm7_ip=None, xarm6_ip=None, can_port=None, simulation='', replay=False, ...e, obstacle_avoidance=True, detection_model='moondream', listen_host='127.0.0.1', dimsim_scene='apt', dimsim_port=8090)
        nav_stack_rerun_config = <function nav_stack_rerun_config at 0x7fb26c10ce00>
        os         = <module 'os' (frozen)>
        vis_module = <function vis_module at 0x7fb270d52020>
dimos/utils/data.py:324: in __str__
    return str(self._ensure_downloaded())
        self       = <[RuntimeError('Missing required tools: git-lfs.\n\nGit LFS installation instructions: https://git-lfs.github.io/') raised in repr()] LfsPath object at 0x7fb26c1749d0>
dimos/utils/data.py:302: in _ensure_downloaded
    cache = get_data(filename)
        cache      = None
        filename   = 'unitree_g1_local_planner_precomputed_paths'
        self       = <[RuntimeError('Missing required tools: git-lfs.\n\nGit LFS installation instructions: https://git-lfs.github.io/') raised in repr()] LfsPath object at 0x7fb26c1749d0>
dimos/utils/data.py:259: in get_data
    archive_path = _decompress_archive(_pull_lfs_archive(archive_name))
        archive_name = 'unitree_g1_local_planner_precomputed_paths'
        data_dir   = PosixPath('.../dimos/dimos/data')
        file_path  = PosixPath('.../dimos/dimos/data/unitree_g1_local_planner_precomputed_paths')
        name       = 'unitree_g1_local_planner_precomputed_paths'
        nested_path = None
        path_parts = ('unitree_g1_local_planner_precomputed_paths',)
dimos/utils/data.py:186: in _pull_lfs_archive
    _check_git_lfs_available()
        filename   = 'unitree_g1_local_planner_precomputed_paths'
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

    def _check_git_lfs_available() -> bool:
        missing = []
    
        # Check if git is available
        try:
            subprocess.run(["git", "--version"], capture_output=True, check=True, text=True)
        except (subprocess.CalledProcessError, FileNotFoundError):
            missing.append("git")
    
        # Check if git-lfs is available
        try:
            subprocess.run(["git-lfs", "version"], capture_output=True, check=True, text=True)
        except (subprocess.CalledProcessError, FileNotFoundError):
            missing.append("git-lfs")
    
        if missing:
>           raise RuntimeError(
                f"Missing required tools: {', '.join(missing)}.\n\n"
                "Git LFS installation instructions: https://git-lfs.github.io/"
            )
E           RuntimeError: Missing required tools: git-lfs.
E           
E           Git LFS installation instructions: https://git-lfs.github.io/

missing    = ['git-lfs']

dimos/utils/data.py:135: RuntimeError
dimos.mapping.ray_tracing.test_clearing::test_ray_tracing_clearing
Stack Traces | 25.4s run time
@pytest.mark.slow
    def test_ray_tracing_clearing():
        loss = run(num_frames=20, frame_dt=0.1, use_rerun=False)
        # Observed on a clean run: forget_box ≈ 15, ghost_person ≈ 78 over
        # 19 matched frames. Thresholds are 3-4× the observed values — meant
        # to flag outright regressions (ray tracer eats the box, never clears,
        # etc.) without being flaky on timing jitter.
>       assert loss["matched_frames"] >= 15, f"too few matched frames: {loss}"
E       AssertionError: too few matched frames: {'score': 0.0, 'forget_box': 0, 'ghost_person': 0, 'matched_frames': 0, 'expected_frames': 20, 'received_frames': 0, 'box_voxel_count': 100}
E       assert 0 >= 15

loss       = {'box_voxel_count': 100, 'expected_frames': 20, 'forget_box': 0, 'ghost_person': 0, ...}

.../mapping/ray_tracing/test_clearing.py:419: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@jeff-hykin jeff-hykin marked this pull request as ready for review May 17, 2026 14:43
@jeff-hykin jeff-hykin enabled auto-merge (squash) May 17, 2026 14:43
@jeff-hykin jeff-hykin marked this pull request as draft May 17, 2026 14:45
auto-merge was automatically disabled May 17, 2026 14:45

Pull request was converted to draft

@jeff-hykin jeff-hykin changed the title Dynamic Global Map with Loop Closure Voxel Transform Nav pt5: Dynamic Global Map with Loop Closure Voxel Transform May 17, 2026
Comment thread dimos/memory2/module.py
ts = getattr(msg, "ts", None) or time.time()
frame_id = getattr(msg, "frame_id", None) or default_frame_id
# For msgs that carry a parent→child transform (Odometry,
# TransformStamped), child_frame_id is the body whose pose we
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

(will clean this up)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants