Skip to content

Conversation

@wowthecoder
Copy link
Contributor

Revamp the DWA algorithm

Collision Detection

  • Changed the collision detection area from circular to rectangular. The collision box has dynamic length at the front depending on how fast the robot is moving. Added optional config show_debug_rectangles to draw the boxes.
  • The intersection area of the collision boxes is used to calculate the obstacle penalty. The penalty is exponential the closer larger the intersection area of the boxes. This is then used to lower the score of that motion segment.

Central loop logic in plan_local():

  • I read the Tigers Mannheim TDP. They are using something similar to DWA, but they sample their trajectories randomly. In contrast our current approach samples from 8 fixed angles and 3 fixed speed scales (8 * 3 = 24 trajectories). I changed it to try with the target-facing angle first, if there's no collision just use this angle. If there is collision, try out 5 other random angles at 3 random speed scales (linearly decreases from 1.0 at 4m distance to 0.05). This optimised the FPS slightly and it's a bit faster (cuz there's less trajectories to evaluate score for).
  • I found that the changed speed scales are what stopped the overshooting, because before this we did not clamp the max scale (1.0) according to distance.

Score function of motion segment

  • Use 3 factors to score a segment: distance to target(+), speed of robot(+), and obstacle presence(-). The final score is the weighted sum of these 3 factors.
  • Bumping the obstacle factor weight from 1.0 to 1.5 did magic for obstacle avoidance. Also fine tuned the other 2 weights so that their scales match, and target proximity contributes the most to the score.

Cleanup

  • Removed segment_overshoots_target(), segment_too_close() and other ineffective functions.

Minor changes to the Motion planning tests:

  • Make the random movement tests have 6 robots to stress test obstacle avoidance
  • Standardise endpoint tolerance (how close the robots have to be to the target position to pass the tests) to 0.1m

Copilot AI review requested due to automatic review settings January 23, 2026 23:33
@wowthecoder
Copy link
Contributor Author

The failing CI test is due to the new tests merged from main, that doesnt concern this PR

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR revamps the Dynamic Window Approach (DWA) algorithm for robot motion planning by introducing several significant improvements to collision detection, trajectory sampling, and scoring.

Changes:

  • Replaced circular collision detection with rectangular bounding boxes that dynamically adjust based on robot velocity
  • Changed from fixed-angle trajectory sampling (24 trajectories) to adaptive random sampling with target-facing angle prioritization
  • Redesigned scoring function with three weighted factors (target progress, speed, obstacle penalty) and tuned weights for better obstacle avoidance
  • Standardized test endpoint tolerance to 0.1m across all motion planning tests and increased stress testing to 6 robots
  • Changed default control scheme from PID to DWA in StrategyRunner

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
utama_core/motion_planning/src/dwa/planner.py Core implementation of new oriented rectangle collision detection, random trajectory sampling, and updated scoring function
utama_core/motion_planning/src/dwa/config.py Updated configuration with new weight parameters and removed obsolete safety radius settings
utama_core/motion_planning/src/dwa/translation_controller.py Added debug visualization for target positions
utama_core/run/strategy_runner.py Changed default control scheme from "pid" to "dwa"
utama_core/tests/motion_planning/random_movement_test.py Increased robot count to 6 and added collision-free initial placement logic
utama_core/tests/motion_planning/strategies/random_movement_strategy.py Fixed circular import with TYPE_CHECKING pattern
utama_core/tests/motion_planning/single_robot_static_obstacle_test.py Standardized endpoint tolerance to 0.1m
utama_core/tests/motion_planning/single_robot_moving_obstacle_test.py Standardized endpoint tolerance to 0.1m
utama_core/tests/motion_planning/multiple_robots_test.py Standardized endpoint tolerance to 0.1m

Comment on lines 234 to 235
obstacles = temporary_obstacles or []
return self._plan_local(game, robot, target, obstacles)
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The temporary_obstacles parameter is documented and passed to _plan_local, but is never actually used in the implementation. The parameter is assigned to a local variable obstacles on line 234 but this variable is not referenced anywhere in the _plan_local method. Either implement support for temporary obstacles or remove this parameter from the API to avoid misleading users of this method.

Copilot uses AI. Check for mistakes.
Comment on lines +168 to +170
self._w_goal = getattr(self._config, "weight_goal")
self._w_obstacle = getattr(self._config, "weight_obstacle")
self._w_speed = getattr(self._config, "weight_speed")
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

Using getattr here is fragile and unnecessary since the config attributes are defined in the dataclass. If an attribute is missing, this will raise an AttributeError at runtime. Access these attributes directly (e.g., self._config.weight_goal) which provides better type safety and clearer error messages if the attribute is missing.

Suggested change
self._w_goal = getattr(self._config, "weight_goal")
self._w_obstacle = getattr(self._config, "weight_obstacle")
self._w_speed = getattr(self._config, "weight_speed")
self._w_goal = self._config.weight_goal
self._w_obstacle = self._config.weight_obstacle
self._w_speed = self._config.weight_speed

Copilot uses AI. Check for mistakes.
break
else:
raise RuntimeError(
"Unable to find non-colliding start positions; " "consider widening bounds or reducing robot count."
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The string concatenation with a space between the two parts creates an unnecessary mid-sentence space. Combine these into a single string or ensure the concatenation is intentional.

Suggested change
"Unable to find non-colliding start positions; " "consider widening bounds or reducing robot count."
"Unable to find non-colliding start positions; consider widening bounds or reducing robot count."

Copilot uses AI. Check for mistakes.
field_bounds: FieldBounds = Field.FULL_FIELD_BOUNDS,
opp_strategy: Optional[AbstractStrategy] = None,
control_scheme: str = "pid", # This is also the default control scheme used in the motion planning tests
control_scheme: str = "dwa", # This is also the default control scheme used in the motion planning tests
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

Changing the default control scheme from "pid" to "dwa" is a significant behavioral change that affects all code using StrategyRunner without explicitly specifying a control_scheme. This could have wide-reaching implications beyond the motion planning tests. Consider whether this should be done in a separate PR with broader testing, or ensure that all affected callers are identified and tested.

Copilot uses AI. Check for mistakes.
Comment on lines +381 to +399
# This should be commented out in real execution to speed up further
self._draw_collision_box(obstacle_safety_rect, "red" if intersection_ratio > 0.5 else "orange", 2)

else:
# Calculate distance between rectangles
dist = our_safety_rect.distance_to(obstacle_safety_rect)

# Apply penalty based on proximity
# Close but not intersecting still gets some penalty
if dist < 0.5: # Within 0.5m
# Exponential decay: closer = higher penalty
# dist=0.0m: penalty ≈ 1.0
# dist=0.2m: penalty ≈ 0.45
# dist=0.5m: penalty ≈ 0.08
penalty = 1.0 * math.exp(-4.0 * dist)
obstacle_factor = max(obstacle_factor, penalty)

# This should be commented out in real execution to speed up further
self._draw_collision_box(obstacle_safety_rect, "green", 1)
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

For better performance, check self._config.show_debug_rectangles before calling _draw_collision_box rather than checking inside the function. This avoids function call overhead when debugging is disabled. Consider wrapping these calls with if self._config.show_debug_rectangles: to short-circuit when the flag is False.

Copilot uses AI. Check for mistakes.
ObstacleRegion,
to_rectangles,
)
from utama_core.motion_planning.src.planning.geometry import point_segment_distance
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The point_segment_distance function is imported but never used in this file. This import can be removed to keep the code clean.

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +15 to +18
weight_goal = 50.0
weight_obstacle = 1.5 # Increased to match new (0-10) obstacle cost scale
weight_speed = 1.0
show_debug_rectangles = False # Enable visualization of bounding boxes for testing
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

These dataclass fields are missing type annotations. Add explicit type annotations for consistency with the other fields in this class. For example: weight_goal: float = 50.0, weight_obstacle: float = 1.5, weight_speed: float = 1.0, and show_debug_rectangles: bool = False.

Suggested change
weight_goal = 50.0
weight_obstacle = 1.5 # Increased to match new (0-10) obstacle cost scale
weight_speed = 1.0
show_debug_rectangles = False # Enable visualization of bounding boxes for testing
weight_goal: float = 50.0
weight_obstacle: float = 1.5 # Increased to match new (0-10) obstacle cost scale
weight_speed: float = 1.0
show_debug_rectangles: bool = False # Enable visualization of bounding boxes for testing

Copilot uses AI. Check for mistakes.
Comment on lines +301 to +305
score = self._evaluate_segment(game, robot, segment_start, segment_end, target)

if score > best_score:
best_score = score
best_move = segment_end
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

In the secondary trajectory sampling loop (when best_collides is True), collision checking is not performed before updating the best move. This is inconsistent with the first loop (lines 269-286) which checks collisions. While the _evaluate_segment function does apply obstacle penalties that should discourage colliding trajectories, the logic should be consistent. Consider either: (1) also calling _segment_collides in this loop and tracking collision state, or (2) removing the explicit collision check from the first loop if the obstacle penalties in scoring are sufficient.

Suggested change
score = self._evaluate_segment(game, robot, segment_start, segment_end, target)
if score > best_score:
best_score = score
best_move = segment_end
score = self._evaluate_segment(game, robot, segment_start, segment_end, target)
collides = self._segment_collides(game, robot, segment_start, segment_end)
if score > best_score:
best_score = score
best_move = segment_end
best_collides = collides

Copilot uses AI. Check for mistakes.
@@ -1,25 +1,157 @@
import copy
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The copy module is imported but never used in this file. This import can be removed to keep the code clean.

Copilot generated this review using guidance from repository custom instructions.
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