Skip to content

Import fix#2783

Closed
jeff-hykin wants to merge 40 commits into
mainfrom
andrew/feat/navigation-dev-2
Closed

Import fix#2783
jeff-hykin wants to merge 40 commits into
mainfrom
andrew/feat/navigation-dev-2

Conversation

@jeff-hykin

Copy link
Copy Markdown
Member

@jeff-hykin jeff-hykin marked this pull request as draft July 7, 2026 21:52
@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR changes the ray-tracing map and 3D navigation stack. The main changes are:

  • Timestamp-matched odometry buffering for ray tracing.
  • Support-filtered local map emission and updated voxel health defaults.
  • New body-frame odometry conversion for Go2 navigation.
  • MLS planner updates for overhead capping, step handling, and path smoothing.
  • Updated native bindings, stubs, tests, and visualization utilities.

Confidence Score: 4/5

The changed mapping and planner paths can produce missing map data or disconnected routes under realistic robot timing and terrain.

  • Lidar clouds can be skipped when odometry stamps are just outside the new fixed tolerance.
  • Local map filtering can hide isolated occupied voxels from the planner.
  • Native planner region capping can remove elevated surfaces when it uses ground-projected z.
  • Step limits can become stricter than the configured metric threshold.

dimos/mapping/ray_tracing/rust/src/main.rs, dimos/navigation/nav_3d/mls_planner/rust/src/main.rs, dimos/navigation/nav_3d/mls_planner/rust/src/mls_planner.rs

Important Files Changed

Filename Overview
dimos/mapping/ray_tracing/rust/src/main.rs Adds timestamped pose matching and support-filtered local map publishing; both change runtime map behavior.
dimos/mapping/ray_tracing/rust/src/voxel_ray_tracer.rs Adds point filtering and support-based emission while removing recency from grazing-ray clearing.
dimos/navigation/nav_3d/mls_planner/rust/src/main.rs Updates native planner ingestion to cap region height using the latest start pose.
dimos/navigation/nav_3d/mls_planner/rust/src/mls_planner.rs Adds overhead configuration and changes metric step thresholds to whole-cell limits.
dimos/navigation/nav_3d/mls_planner/odom_body_frame.py Adds a module that removes fixed sensor mount rotation from PointLio odometry.
dimos/robot/unitree/go2/blueprints/navigation/unitree_go2_nav_3d.py Rewires Go2 navigation around body odometry, updated ray tracing, planner defaults, and recording utilities.

Reviews (1): Last reviewed commit: "fixup import" | Re-trigger Greptile

Comment on lines +64 to 71
let Some((translation, rotation)) = nearest_pose(&self.poses, time_secs(&msg.header.stamp))
else {
warn_throttled!(
Duration::from_secs(1),
"No odometry within tolerance of the cloud stamp, dropped a cloud.",
);
return;
};

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.

P1 Tight Stamp Match Drops Clouds

When lidar and odometry header stamps differ by more than 100ms, this branch returns before update_map runs. At common 10Hz odometry rates that leaves almost no room for skew or batching, so a normal timing offset can make the voxel map stop updating while the robot is moving.

Comment on lines +174 to +175
let points = emit_points(&self.map, voxel_size, Some(cyl), support_min, &live);
let local = points_to_cloud(&points, out_frame_id, stamp);

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.

P1 Local Map Hides Isolated Obstacles

The local map now applies support_min to healthy voxels while the global map does not. With the default support_min=4, a real but isolated occupied voxel, such as a pole or sparse obstacle in the planner cylinder, is omitted from local_map even though it remains in the global map, so local planning can ignore it.

Comment on lines +237 to +251
let z_max = bounds.pose.orientation.z as f32;
let sensor_z = self
.latest_start
.lock()
.expect("start mutex")
.map_or(z_max, |(_, _, z)| z);
let bounds = RegionBounds::capped(
bounds.pose.position.x as f32,
bounds.pose.position.y as f32,
bounds.pose.orientation.x as f32,
bounds.pose.orientation.y as f32,
z_max,
sensor_z,
self.config.max_overhead_m,
);

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.

P1 Ground Z Caps Region Height

This native path feeds the ground-projected start z into RegionBounds::capped, while the Python transformer passes the raw pose z. With max_overhead_m=2.0, a robot at ground level can cap the region near ground_z + 2m and drop valid upper surfaces from the local update, making stairs or elevated traversable areas disappear from the planner graph.


/// Max traversable vertical step in cells.
pub fn step_cells(&self) -> i32 {
(self.step_threshold_m / self.voxel_size).floor() as i32

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.

P1 Step Threshold Rounds Down

step_cells() now floors the metric threshold, so non-integral ratios become stricter than the configured step_threshold_m. For example, 0.15m with 0.08m voxels allows only one cell of climb, causing routes over steps below the configured metric limit to be treated as disconnected.

Comment on lines 305 to 310

/// Spare a clearing miss only when a grazing ray skims a recently hit planar
/// surface. Stale voxels or those with no normal are left to the health checks.
fn should_spare(
c: &Voxel,
ray_unit: Vector3<f32>,
graze_cos: f32,
frame: u32,
recency_window: u32,
) -> bool {
/// Spare a clearing miss when a grazing ray skims a planar surface.
fn should_spare(c: &Voxel, ray_unit: Vector3<f32>, graze_cos: f32) -> bool {
match c.normal {
Some(n) => {
frame.saturating_sub(c.recency) <= recency_window && ray_unit.dot(&n).abs() < graze_cos
}
Some(n) => ray_unit.dot(&n).abs() < graze_cos,
None => false,

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 Stale Normals Spare Clearing

Removing the recency check lets any voxel with an old fitted normal be spared from grazing-ray clearing forever. A stale planar obstacle that is later seen only at grazing angles can keep its occupied health much longer than before, leaving old geometry in the map.

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
2688 2 2686 71
View the full list of 2 ❄️ flaky test(s)
dimos.e2e_tests.test_dimsim_walk_forward::test_walk_forward

Flake rate in main: 23.85% (Passed 83 times, Failed 26 times)

Stack Traces | 204s run time
lcm_spy = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x76ea3820e180>
start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x76ea3735f060>
human_input = <function human_input.<locals>.send_human_input at 0x76ea3735e980>
dim_sim = <dimos.e2e_tests.dim_sim_client.DimSimClient object at 0x76ea36b543b0>

    @pytest.mark.self_hosted_large
    def test_walk_forward(lcm_spy, start_blueprint, human_input, dim_sim) -> None:
        start_blueprint(
            "run",
            "--disable",
            "spatial-memory",
            "--disable",
            "security-module",
            "unitree-go2-agentic",
            simulator="dimsim",
        )
        lcm_spy.save_topic(".../McpClient/on_system_modules/res")
        lcm_spy.wait_for_saved_topic(".../McpClient/on_system_modules/res", timeout=1200.0)
    
        origin_x, origin_y = 1, 2
        dim_sim.set_agent_position(origin_x, origin_y)
    
        human_input("move forward 3 meter")
    
>       lcm_spy.wait_until_odom_position(origin_x + 3, origin_y, threshold=0.4, timeout=120)

dim_sim    = <dimos.e2e_tests.dim_sim_client.DimSimClient object at 0x76ea36b543b0>
human_input = <function human_input.<locals>.send_human_input at 0x76ea3735e980>
lcm_spy    = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x76ea3820e180>
origin_x   = 1
origin_y   = 2
start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x76ea3735f060>

dimos/e2e_tests/test_dimsim_walk_forward.py:37: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
dimos/e2e_tests/lcm_spy.py:167: in wait_until_odom_position
    self.wait_for_message_result(
        predicate  = <function LcmSpy.wait_until_odom_position.<locals>.predicate at 0x76ea3735cfe0>
        self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x76ea3820e180>
        threshold  = 0.4
        timeout    = 120
        x          = 4
        y          = 2
dimos/e2e_tests/lcm_spy.py:153: in wait_for_message_result
    wait_until(
        event      = <threading.Event at 0x76ea36b54c50: unset>
        fail_message = 'Failed to get to position x=4, y=2'
        listener   = <function LcmSpy.wait_for_message_result.<locals>.listener at 0x76ea3735f6a0>
        predicate  = <function LcmSpy.wait_until_odom_position.<locals>.predicate at 0x76ea3735cfe0>
        self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x76ea3820e180>
        timeout    = 120
        topic      = '/odom#geometry_msgs.PoseStamped'
        type       = <class 'dimos.msgs.geometry_msgs.PoseStamped.PoseStamped'>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

predicate = <bound method Event.is_set of <threading.Event at 0x76ea36b54c50: unset>>

    def wait_until(
        predicate: Callable[[], bool],
        *,
        timeout: float,
        interval: float = 0.1,
        message: str | None = None,
    ) -> None:
        """Poll ``predicate`` until it returns truthy or ``timeout`` elapses."""
        deadline = time.monotonic() + timeout
        while time.monotonic() < deadline:
            if predicate():
                return
            time.sleep(interval)
>       raise TimeoutError(message or f"Timed out after {timeout}s waiting for condition")
E       TimeoutError: Failed to get to position x=4, y=2

deadline   = 3558037.235201337
interval   = 0.1
message    = 'Failed to get to position x=4, y=2'
predicate  = <bound method Event.is_set of <threading.Event at 0x76ea36b54c50: unset>>
timeout    = 120

.../utils/testing/waiting.py:35: TimeoutError
dimos.perception.test_image_embedding.TestImageEmbedding::test_clip_embedding_initialization

Flake rate in main: 30.00% (Passed 7 times, Failed 3 times)

Stack Traces | 27.3s run time
request = <SubRequest 'monitor_threads' for <Function test_clip_embedding_initialization>>

    @pytest.fixture(autouse=True)
    def monitor_threads(request):
        # Capture threads before test runs
        test_name = request.node.nodeid
        with _seen_threads_lock:
            _before_test_threads[test_name] = {
                t.ident for t in threading.enumerate() if t.ident is not None
            }
    
        yield
    
        with _seen_threads_lock:
            before = _before_test_threads.get(test_name, set())
    
        # Threads intentionally left running for the whole process and cleaned up on
        # exit, so they don't count as per-test leaks.
        expected_persistent_thread_prefixes = [
            "Dask-Offload",
            # HuggingFace safetensors conversion thread - no user cleanup API
            # https://github..../transformers/issues/29513
            "Thread-auto_conversion",
        ]
    
        def live_new_threads():
            # Threads created during this test that are still running. A thread that
            # has already stopped is not a leak -- it's done, just not yet reaped
            # from threading's registry -- so we key on is_alive(), not presence.
            result = []
            for t in threading.enumerate():
                if t.ident is None or t.ident in before or t.name == "MainThread":
                    continue
                if any(t.name.startswith(prefix) for prefix in expected_persistent_thread_prefixes):
                    continue
                if t.is_alive():
                    result.append(t)
            return result
    
        # Some C extensions tear their callback threads down asynchronously, so a
        # thread can stay alive briefly after the test cleaned up its owner (notably
        # zenoh's pyo3-closure threads, freed shortly after the session is closed).
        # A single snapshot races that teardown and flags a thread that is about to
        # exit. Give new threads a grace period to drain; only ones that stay alive
        # are real leaks (a genuinely leaked thread never exits).
        deadline = time.monotonic() + 5.0
        leaked = live_new_threads()
        while leaked and time.monotonic() < deadline:
            time.sleep(0.02)
            leaked = live_new_threads()
    
        if not leaked:
            return
    
        with _seen_threads_lock:
            # Report each leaked thread only once across the session.
            truly_new = [t for t in leaked if t.ident not in _seen_threads]
            for t in leaked:
                _seen_threads.add(t.ident)
    
        if not truly_new:
            return
    
        thread_names = [t.name for t in truly_new]
    
>       pytest.fail(
            f"Non-closed threads created during this test. Thread names: {thread_names}. "
            "Please look at the first test that fails and fix that."
        )
E       Failed: Non-closed threads created during this test. Thread names: ['Thread-249']. Please look at the first test that fails and fix that.

before     = {136248583657152, 136248592049856, 136248608835264, 136248617227968, 136248634013376, 136248642406080, ...}
deadline   = 4548545.617423176
expected_persistent_thread_prefixes = ['Dask-Offload', 'Thread-auto_conversion']
leaked     = [<TMonitor(Thread-249, started daemon 136248465680064)>]
live_new_threads = <function monitor_threads.<locals>.live_new_threads at 0x7beb41f0a7a0>
request    = <SubRequest 'monitor_threads' for <Function test_clip_embedding_initialization>>
t          = <TMonitor(Thread-249, started daemon 136248465680064)>
test_name  = 'dimos/perception/test_image_embedding.py::TestImageEmbedding::test_clip_embedding_initialization'
thread_names = ['Thread-249']
truly_new  = [<TMonitor(Thread-249, started daemon 136248465680064)>]

dimos/conftest.py:273: Failed

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 closed this Jul 8, 2026
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.

3 participants