fix(navigation): signed momentum term so U-turns cost - #3763
Conversation
…1255) _compute_direction_momentum_score clamped the heading dot product at zero, so a frontier straight behind the robot scored exactly like one off to the side: nothing. Turning around was free, and at a 5% weight the term could not have paid for itself anyway. The clamp floor and the term weight both move into WavefrontConfig: min_momentum_score = -1.0 makes the term signed (+1 straight ahead, -1 a full U-turn), momentum_weight = 0.3 puts direction above the 20% distance term, which is the band the established explorers use (GBPlanner weights direction 4.3x its path-length penalty, Umari 2.0, FUEL w_dir 1.5). Setting min_momentum_score = 0.0 restores the previous behaviour exactly. The frontier scores are only ranked against each other, never against an absolute threshold, so the weights still do not need to sum to 1.
Greptile SummaryThis change makes direction momentum signed and configurable, increasing the preference for frontiers aligned with the most recently selected goal. A timed-out navigation goal does not clear or update that direction, so the next ranking can continue favoring the unreachable heading over an otherwise equivalent reachable frontier behind the robot. T-Rex validation blockedThe deterministic timeout-and-ranking harness could not execute because the repository virtual environment references a missing Python 3.12 interpreter. The harness was prepared to drive the timeout branch and compare forward versus reverse frontier ranking, but the runtime comparison could not be completed. Confidence Score: 4/5Not safe to merge without handling stale exploration direction after a navigation timeout. The timeout branch leaves the direction established for the unreached goal unchanged, while the updated signed momentum term uses that state to alter the next frontier ordering. Runtime execution was unavailable because the configured Python interpreter is missing. Files Needing Attention: dimos/navigation/frontier_exploration/wavefront_frontier_goal_selector.py, particularly the timeout handling in _run_exploration_loop.
What T-Rex did
|
A goal the robot failed to reach kept steering the next ranking: the direction set toward it stayed in place, and with the momentum term now signed that stale heading actively penalized frontiers behind the robot. Zero it in the timeout branch so the selection right after a timeout has no directional preference; it re-establishes on the next chosen goal. Also makes the timeout log message use the configured value (it said 30 seconds while the default is 15).
|
Good catch, and confirmed in the code: Pushed a fix: the timeout branch zeroes |
|
Converting to draft: our sim evidence turned out to be strongly world-dependent (the same arms ping-pong on a raw accumulated map and barely at all on the dataset's own two-pass global map), so the numbers here need real-robot validation before this is worth review time. The mechanism itself (a U-turn currently costs nothing) and the timeout fixes stand. Full data: https://github.com/metrox-eth/vector-dimos/tree/main/benchmarks/pr2830 |
Contribution path
Problem
#1255 describes the robot exploring one metre, walking to the other side of the building for another metre, and coming back. The selector already has the term that should prevent this, but it is disabled by its own clamp:
_compute_direction_momentum_scorereturnsmax(0.0, dot_product), so a frontier straight behind the robot scores exactly the same as one off to the side, which is zero. A U-turn is free. The term is also weighted at 5%, against 20% for distance and 30% each for information gain and distance-from-explored-goals, so even at full alignment it cannot move a ranking that the other terms have decided.Solution
min_momentum_score(default-1.0) replaces the hardcoded clamp floor, making the term signed: +1 straight ahead, 0 sideways, -1 a full U-turn.momentum_weight(default0.3) replaces the hardcoded 5%, putting direction above the 20% distance term, which is the band the established explorers use (GBPlanner weights direction 4.3x its path-length penalty, Umari'srrt_explorationuses a hysteresis gain of 2.0, FUEL usesw_dir = 1.5). Settingmin_momentum_score = 0.0restores the previous behaviour exactly. Frontier scores are only ranked against each other, never against an absolute threshold, so the weights still do not have to sum to 1.Evidence
Offline replay bench on the five HK Go2 recordings shipped with dimOS, robot profile derived from the recordings themselves (0.31 m body; 0.60 m/s measured walking, and a fidelity rerun at the LocalPlanner's 0.55 m/s cap with faithful loop timing, where the robot keeps driving to the old goal during selection). The verdict is stable across both instruments. Full workspace, hypotheses declared before the runs, and raw per-run results: https://github.com/metrox-eth/vector-dimos/tree/main/benchmarks/pr2830
hk_elevator, 12 m lidar, shipped config: real map crossings 12 to 4, round trips 4 to 0, for +3.9% path and +0.5 points of coverage.Two honest caveats. The bench measured this as a re-ranking policy in GBPlanner's multiplicative form (
score * exp(-0.07 * route) * exp(-0.3 * deviation)) layered on the stock scorer, not the additive term in this diff, so the numbers show the direction signal is worth having and not that 0.3 is the optimal weight, which is why both parameters are config-exposed. And on individual starts the policy could stop early: worst casehk_elevator/centre at 4 m lost 40 points of coverage when staying local tripped the selector's own info-gain self-stop sooner. Everything, including that failure, is in the linked workspace. Under the faithful-timing rerun (0.55 m/s, old goal pursued during selection) the shipped-config numbers hold: crossings 28 to 11, round trips 10 to 0, with less total path.How to Test
Three candidates at equal distance, equal cluster size, on an empty map with no explored goals, so direction is the only term that differs: straight ahead scores above sideways scores above straight behind, the gap is exactly
2 * momentum_weight, and withmin_momentum_score = 0.0the straight-behind candidate scores identically to the sideways one, which is the behaviour being fixed.End to end, on the blueprint that carries the explorer (
unitree-go2, replay dbgo2_short); nothing changes except which frontier is picked, and theMomentum:field of the selector's own log line now goes negative when a candidate is behind the robot:Composability with #2830
#2830 replaces the whole weighted sum with
info_gain / (1 + A* path cost)and does not touch_compute_direction_momentum_score, so the sign fix survives either merge order. It also makes #2830 stronger: its heading bonus isinfo_per_cost *= 1.0 + 0.5 * momentum_score, which today is a flat 1.0 for everything behind the robot and becomes a real 0.5x penalty on a U-turn once the term is signed, still bounded in [0.5, 1.5].The
momentum_weighthunk is the only overlap, and it is in a block #2830 deletes. If #2830 lands first I will rebase by dropping that hunk and usingmomentum_weightin place of its hardcoded0.5multiplier. The test lives intest_wavefront_frontier_scoring.pyrather thantest_wavefront_frontier_goal_selector.py, which both #2830 and #3627 add, so there is no test-file conflict either.AI assistance
Claude Code with Claude Fable 5, used throughout: the offline bench, the diff, and this description. I ran the bench, read the results, and understand the change.
Checklist