Skip to content

Lesh/go2 mls rpp#2742

Draft
leshy wants to merge 16 commits into
mainfrom
lesh/go2-mls-rpp
Draft

Lesh/go2 mls rpp#2742
leshy wants to merge 16 commits into
mainfrom
lesh/go2-mls-rpp

Conversation

@leshy

@leshy leshy commented Jul 6, 2026

Copy link
Copy Markdown
Member

dimos run unitree-go2-mls-rpp
dimos run unitree-go2-mls-htc

mustafab0 and others added 16 commits June 25, 2026 17:25
Integrate holonomic path tracking into LocalPlanner with speed profiling,
trajectory types and metrics, JSONL telemetry, run profiles, Go2 blueprint
wiring, hot replan support, and dannav split-stack experiments.
Resolve go2/connection.py and pyproject.toml from main; combine
all_blueprints registry entries from both sides.
- Create DanLocalPlanner: module, placement, wiring
- Create lock_replan commit window
- Add path smoothing and uniform resampling
- Tune configs for testing
- Remove progress markers
- Remove the monolith: delete unitree_go2_dannav.py
Move holonomic_tc under dannav, relocate shared geometry helpers, strip stale
trajectory logging and run-profile code, and tighten controller types, configs,
tests, and unitree_go2_mls_htc blueprint wiring.
…controller' into mustafa/go2-controller-benchmark

# Conflicts:
#	dimos/control/coordinator.py
Merge fallout: main (via #2697's branch) brought strict mypy over these
files plus the no-__all__ and no-section-marker codebase checks. Add the
missing generic type parameters and annotations, fix the Path
module-vs-class import in velocity_profiler, add K_RETURN/K_BACKSPACE to
the local pygame stub, and drop __all__ lists and section markers.
The unitree-go2-mls-htc perception/planning stack (voxel mapping, MLS
planning, DanLocalPlanner gating) with the tracking stage swapped for
the ControlCoordinator's calibrated RPP follower, for head-to-head IRL
A/B of the two controllers behind the same planner.

PathFollowerTask.set_path now treats a sub-2-pose path as the nav
planner's stop signal and cancels the active follow instead of keeping
the stale path armed — the MLS planner emits an empty Path when nothing
ahead is traversable.
@mintlify

mintlify Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
dimensional 🟢 Ready View Preview Jul 6, 2026, 6:47 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

❌ 3 Tests Failed:

Tests completed Failed Passed Skipped
2710 3 2707 72
View the top 2 failed test(s) by shortest run time
dimos.perception.test_image_embedding.TestImageEmbedding::test_clip_embedding_initialization
Stack Traces | 25.9s 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-248']. Please look at the first test that fails and fix that.

before     = {123726221715136, 123726230107840, 123726246893248, 123726255285952, 123727419516608, 123727427909312, ...}
deadline   = 5822464.163753116
expected_persistent_thread_prefixes = ['Dask-Offload', 'Thread-auto_conversion']
leaked     = [<TMonitor(Thread-248, started daemon 123726204929728)>]
live_new_threads = <function monitor_threads.<locals>.live_new_threads at 0x7087a4fbab60>
request    = <SubRequest 'monitor_threads' for <Function test_clip_embedding_initialization>>
t          = <TMonitor(Thread-248, started daemon 123726204929728)>
test_name  = 'dimos/perception/test_image_embedding.py::TestImageEmbedding::test_clip_embedding_initialization'
thread_names = ['Thread-248']
truly_new  = [<TMonitor(Thread-248, started daemon 123726204929728)>]

dimos/conftest.py:273: Failed
dimos.navigation.cmu_nav.modules.far_planner.test_far_planner_rosbag.TestFarPlannerRosbag::test_waypoint_accuracy
Stack Traces | 73.3s run time
self = <dimos.navigation.cmu_nav.modules.far_planner.test_far_planner_rosbag.TestFarPlannerRosbag object at 0x7087d4480920>

    def test_waypoint_accuracy(self) -> None:
        """Feed identical inputs at original timing and compare waypoint output."""
        if not FAR_PLANNER_BIN.exists():
            pytest.skip(f"FAR planner binary not found: {FAR_PLANNER_BIN}")
    
        window = load_rosbag_window()
        ref_wp = window.way_point
        assert len(ref_wp) > 0, "No reference waypoints in fixture"
    
        lcm = lcmlib.LCM()
        wp_collector = LcmCollector(topic=WAYPOINT_OUT_LCM, msg_type=PointStamped)
        wp_collector.start(lcm)
    
        stop_event = threading.Event()
        handle_thread = threading.Thread(
            target=lcm_handle_loop, args=(lcm, stop_event), daemon=True
        )
        handle_thread.start()
    
        runner = NativeProcessRunner(binary_path=str(FAR_PLANNER_BIN), args=_far_planner_args())
    
        try:
            runner.start()
            assert runner.is_running, "FAR planner binary failed to start"
            time.sleep(_PROCESS_STARTUP_SEC)
    
            # Feed at original timing (1:1 with rosbag)
            feed_at_original_timing(
                lcm,
                window,
                topic_map={
                    "odom": ODOM_LCM,
                    "scan": SCAN_LCM,
                    "terrain": TERRAIN_LCM,
                    "terrain_ext": TERRAIN_EXT_LCM,
                    "goal": GOAL_LCM,
                },
            )
    
            time.sleep(_POST_FEED_DRAIN_SEC)
    
        finally:
            runner.stop()
            stop_event.set()
            handle_thread.join(timeout=DEFAULT_THREAD_JOIN_TIMEOUT)
            wp_collector.stop(lcm)
    
        our_wps = [(msg.x, msg.y) for msg in wp_collector.messages]
    
        # Compute deviation score
        score = _compute_waypoint_deviation(our_wps, ref_wp)
    
        # Log score for visibility
        logger.info(f"\n{'=' * 60}")
        logger.info("FAR PLANNER DEVIATION SCORE")
        logger.info(f"  Our waypoints:     {len(our_wps)}")
        logger.info(f"  Reference:         {len(ref_wp)}")
        logger.info(f"  Count ratio:       {score['count_ratio']:.3f}")
        logger.info(f"  Mean error:        {score['mean_error_m']:.3f} m")
        logger.info(f"  Max error:         {score['max_error_m']:.3f} m")
        logger.info(f"  Mean X diff:       {score['mean_x_diff']:.3f} m")
        logger.info(f"  Mean Y diff:       {score['mean_y_diff']:.3f} m")
        logger.info(f"{'=' * 60}\n")
    
        # Assertions — generous thresholds, the point is to measure
        assert len(our_wps) > 0, "FAR planner produced no waypoints"
>       assert score["mean_error_m"] < 5.0, (
            f"Mean waypoint error {score['mean_error_m']:.2f}m exceeds 5m threshold"
        )
E       AssertionError: Mean waypoint error 5.17m exceeds 5m threshold
E       assert 5.168279292669851 < 5.0

handle_thread = <Thread(Thread-156 (lcm_handle_loop), stopped daemon 123727740139200)>
lcm        = <LCM object at 0x7087a42c2d90>
our_wps    = [(6.282645225524902, -3.307107925415039), (6.294029712677002, -3.225991725921631), (6.3008599281311035, -3.22527170181...1311035, -3.225271701812744), (6.3008599281311035, -3.2252719402313232), (6.3008599281311035, -3.225271701812744), ...]
ref_wp     = array([[     5.5327,        -0.3,         2.5,        0.75],
       [     5.7342,        -0.3,         2.5,        0.7...,      7.8589,      1.4869,    0.095978],
       [     65.337,      7.8589,      1.4869,    0.095978]], shape=(300, 4))
runner     = NativeProcessRunner(binary_path='.../result/bin/far_planner_...hred', '4', '--new_point_counter', '5', '--obs_inflate_size', '1', '--visualize_ratio', '0.4', '--wp_churn_dist', '0'])
score      = {'count_ratio': 0.5833333333333334, 'max_error_m': 8.72389189155798, 'mean_error_m': 5.168279292669851, 'mean_x_diff': 3.6648347092810125, ...}
self       = <dimos.navigation.cmu_nav.modules.far_planner.test_far_planner_rosbag.TestFarPlannerRosbag object at 0x7087d4480920>
stop_event = <threading.Event at 0x7087ccd4f200: set>
window     = RosbagWindow(odom=array([[     5.3674,           0,           0, ...,           0,           0,           1],
       [...,           1],
       [     0.2975,           0,           0,           0,           0,           0,           1]]))])
wp_collector = LcmCollector(topic='/rbfp_wp#geometry_msgs.PointStamped', msg_type=<class 'dimos.msgs.geometry_msgs.PointStamped.Point...003, 5821805.641974057, 5821805.842952432, 5821806.041943681, 5821806.241650374, 5821806.446772929, 5821806.640474071])

.../modules/far_planner/test_far_planner_rosbag.py:276: AssertionError
View the full list of 1 ❄️ flaky test(s)
dimos.e2e_tests.test_dimsim_spatial_memory::test_go_to_the_bed

Flake rate in main: 20.39% (Passed 82 times, Failed 21 times)

Stack Traces | 559s run time
lcm_spy = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x7d308f01e3c0>
start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x7d308edfb600>
human_input = <function human_input.<locals>.send_human_input at 0x7d308edfb6a0>
dim_sim = <dimos.e2e_tests.dim_sim_client.DimSimClient object at 0x7d308f0233b0>
explore_house = <function explore_house.<locals>.explore at 0x7d308edfbba0>

    @pytest.mark.self_hosted_large
    def test_go_to_the_bed(lcm_spy, start_blueprint, human_input, dim_sim, explore_house) -> None:
        start_blueprint(
            "run",
            "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)
    
        explore_house()
    
        human_input("go to the bed")
    
>       lcm_spy.wait_until_odom_position(-3.567, -1.332, threshold=2, timeout=180)

dim_sim    = <dimos.e2e_tests.dim_sim_client.DimSimClient object at 0x7d308f0233b0>
explore_house = <function explore_house.<locals>.explore at 0x7d308edfbba0>
human_input = <function human_input.<locals>.send_human_input at 0x7d308edfb6a0>
lcm_spy    = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x7d308f01e3c0>
start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x7d308edfb600>

dimos/e2e_tests/test_dimsim_spatial_memory.py:32: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
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 0x7d308edfbd80>
        self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x7d308f01e3c0>
        threshold  = 2
        timeout    = 180
        x          = -3.567
        y          = -1.332
dimos/e2e_tests/lcm_spy.py:153: in wait_for_message_result
    wait_until(
        event      = <threading.Event at 0x7d308e5b07a0: unset>
        fail_message = 'Failed to get to position x=-3.567, y=-1.332'
        listener   = <function LcmSpy.wait_for_message_result.<locals>.listener at 0x7d308edfbc40>
        predicate  = <function LcmSpy.wait_until_odom_position.<locals>.predicate at 0x7d308edfbd80>
        self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x7d308f01e3c0>
        timeout    = 180
        topic      = '/odom#geometry_msgs.PoseStamped'
        type       = <class 'dimos.msgs.geometry_msgs.PoseStamped.PoseStamped'>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

predicate = <bound method Event.is_set of <threading.Event at 0x7d308e5b07a0: 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=-3.567, y=-1.332

deadline   = 3464561.449060349
interval   = 0.1
message    = 'Failed to get to position x=-3.567, y=-1.332'
predicate  = <bound method Event.is_set of <threading.Event at 0x7d308e5b07a0: unset>>
timeout    = 180

.../utils/testing/waiting.py:35: TimeoutError

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

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.

2 participants