Skip to content

fix(navigation): signed momentum term so U-turns cost - #3763

Draft
metrox-eth wants to merge 2 commits into
dimensionalOS:mainfrom
metrox-eth:fix/signed-momentum
Draft

fix(navigation): signed momentum term so U-turns cost#3763
metrox-eth wants to merge 2 commits into
dimensionalOS:mainfrom
metrox-eth:fix/signed-momentum

Conversation

@metrox-eth

@metrox-eth metrox-eth commented Aug 29, 2026

Copy link
Copy Markdown

Note (2026-08-30): the benchmarked M4.3 and this diff use different direction state (EMA of actual displacement kept through timeouts, vs bearing to the chosen goal zeroed on timeout); the numbers below do not validate this implementation. Also, min_momentum_score=0 restores the clamp but not the original 0.05 weight. Kept as draft pending a real-robot A/B.

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_score returns max(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 (default 0.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's rrt_exploration uses a hysteresis gain of 2.0, FUEL uses w_dir = 1.5). Setting min_momentum_score = 0.0 restores 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

  • Over 64 paired starts, every one of the 24 class-A crossings under stock (a nearby frontier existed and lost the ranking) disappears (24 to 0); all fixable classes together go 30 to 5. These are the decisions where the robot walked a median 23.9 m past an available candidate a median 4.2 m away, on a score ratio of 1.32.
  • 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.
  • What is left is not a ranking problem: at the 33 residual crossings the nearest other available candidate sits at a median 27.7 m of route length (against 6.7 m before the change), and 85% of them are locally forced: no available alternative within a 6 m vicinity at that moment (that share depends on the vicinity radius; full sensitivity table in the workspace).

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 case hk_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

uv run pytest dimos/navigation/frontier_exploration/test_wavefront_frontier_scoring.py -v

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 with min_momentum_score = 0.0 the 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 db go2_short); nothing changes except which frontier is picked, and the Momentum: field of the selector's own log line now goes negative when a candidate is behind the robot:

dimos --replay run unitree-go2

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 is info_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_weight hunk is the only overlap, and it is in a block #2830 deletes. If #2830 lands first I will rebase by dropping that hunk and using momentum_weight in place of its hardcoded 0.5 multiplier. The test lives in test_wavefront_frontier_scoring.py rather than test_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

  • I have read and approved the CLA.

…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.
@github-actions github-actions Bot added the first-time-contributor PR opened by an author who had not previously committed to this repository label Aug 29, 2026
@greptile-apps

greptile-apps Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This 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 blocked

The 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/5

Not 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.

T-Rex T-Rex Logs

What T-Rex did

  • The deterministic Python harness could not start because the Python 3.12 interpreter was missing, so no runtime result was observed.
  • I reviewed the signed momentum configuration changes added to the frontier goal selector, including where negative reverse momentum is retained and where it is applied at 0.3, and noted that the harness expects forward-first scoring with a manual-reset condition but cannot run because Python 3.12 is unavailable.
  • The cleanup in the timeout branch is missing, as it only logs and does not reset the direction set during goal selection, leaving that aspect of validation untested.
  • Artifacts were prepared to support review, including a Python code artifact describing the momentum changes and two logs that document the blocked run due to the missing interpreter.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (1)

  1. dimos/navigation/frontier_exploration/wavefront_frontier_goal_selector.py, line 834-835 (link)

    P1 Stale direction skews ranking

    When a navigation goal times out, exploration_direction remains aimed at the unreached goal. The stronger signed momentum term then applies a 0.6 relative swing between otherwise equal forward and reverse candidates during the next ranking, causing reachable alternatives behind that stale direction to lose and exploration to keep selecting toward the failed direction.

Reviews (1): Last reviewed commit: "fix(navigation): signed momentum term so..." | Re-trigger Greptile

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).
@metrox-eth

Copy link
Copy Markdown
Author

Good catch, and confirmed in the code: exploration_direction is only ever set toward the chosen goal at selection time, and the timeout branch never clears it, so with the term now signed a stale heading toward an unreached goal would penalize frontiers behind the robot on the next ranking.

Pushed a fix: the timeout branch zeroes exploration_direction, so the selection right after a timeout carries no directional preference (the term already returns 0 for a zero direction); it re-establishes on the next chosen goal. Added a test for the neutrality, and the timeout log message now uses the configured value (it said 30 seconds while the default is 15).

@metrox-eth
metrox-eth marked this pull request as draft August 30, 2026 07:59
@metrox-eth

Copy link
Copy Markdown
Author

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

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

Labels

first-time-contributor PR opened by an author who had not previously committed to this repository

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant