Skip to content

Conversation

@valentinbruehl
Copy link
Member

New motion planning algorithm inspired by the fast-path-planning algorithm described here.

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 introduces a new motion planning algorithm called "fast path planning" inspired by a research paper. The implementation adds a FastPathPlanner class that computes collision-free trajectories by recursively finding subgoals around obstacles, and integrates it with the existing motion control system through a new FastPathPlanningController.

Changes:

  • New FastPathPlanner algorithm with recursive obstacle avoidance
  • FastPathPlanningController integrating the planner with PID control
  • Helper geometry functions for line segment calculations
  • Registration of the new "fpp" control scheme

Reviewed changes

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

Show a summary per file
File Description
planner.py Core implementation of the fast path planning algorithm with obstacle detection and trajectory generation
config.py Configuration constants for the planner (clearances, distances, recursion limits)
fastpathplanning.py Controller class integrating FastPathPlanner with PID control
init.py Exports the new FastPathPlanningController
control_schemes.py Registers "fpp" control scheme in the scheme map
math_utils.py Adds geometry helper functions for line segment distance calculations and intersection detection


oren = self.pid_oren.calculate(target_oren, robot.orientation, robot_id)

pos = self.fpp._path_to(game, robot_id, target_pos)[0][1]
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

The method '_path_to' is being called with 'target_pos' which is a Vector2D, but it's converted to a tuple in line 30 when accessing '[0][1]'. However, the method signature expects a Tuple[float, float] and converts it to np.array internally. The Vector2D object needs to be converted to a tuple (target_pos.x, target_pos.y) before passing it to '_path_to'.

Suggested change
pos = self.fpp._path_to(game, robot_id, target_pos)[0][1]
pos = self.fpp._path_to(game, robot_id, (target_pos.x, target_pos.y))[0][1]

Copilot uses AI. Check for mistakes.

Choose a reason for hiding this comment

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

Not needed, as the _path_to function is converting the target position to an np.array in line 127.

return vel, oren

def reset(self, robot_id):
self.pid_oren.reset(robot_id)
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

The method 'reset' only resets 'pid_oren' but does not reset 'pid_trans'. This is inconsistent with the DWAController implementation which resets both controllers. The translational PID should also be reset to maintain consistent state management.

Suggested change
self.pid_oren.reset(robot_id)
self.pid_oren.reset(robot_id)
self.pid_trans.reset(robot_id)

Copilot uses AI. Check for mistakes.
Comment on lines 18 to 21
def point_to_segment_distance(point: np.ndarray, seg_start: np.ndarray, seg_end: np.ndarray) -> float:
"""Compute the shortest distance between a point and a line segment."""
seg_vec = seg_end - seg_start
t = np.clip(np.dot(point - seg_start, seg_vec) / np.dot(seg_vec, seg_vec), 0, 1)
Copy link
Member

Choose a reason for hiding this comment

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

use

den = np.dot(seg_vec, seg_vec)
if den < 1e-9:
    return np.linalg.norm(point - seg_start)
t = np.clip(np.dot(point - seg_start, seg_vec) / den, 0, 1)

robotpos = np.array([r.p.x, r.p.y])
if (
distance(ourpos, robotpos) < self.LOOK_AHEAD_RANGE
and distance(robotpos, ourpos) > self.OBSTACLE_CLEARANCE
Copy link
Member

Choose a reason for hiding this comment

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

explain why we are doing this. It's not clear why we would filter these out. I know, but people in the future may not. We intend to use DWA when the obstacles overlap our start right?


for o in obstacles:
if point_to_segment_distance(subgoal, o[0], o[1]) < self.OBSTACLE_CLEARANCE:
subgoal = self._find_subgoal(
Copy link
Member

Choose a reason for hiding this comment

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

it's possible for an infinite recursion here

Choose a reason for hiding this comment

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

It is not, because after a point, I believe it will find a subgoal outside the bounds of the field, giving a possible subgoal. This needs to be resolved as we make an edit to add bounds to the planner.

self, segment: Tuple, obstacles, target
): # returns None if no obstacles, else it returns the closest obstacle.
closestobstacle = None
tempdistance = distance(segment[0], segment[1])
Copy link
Member

Choose a reason for hiding this comment

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

why not use tempdistance = float("inf") here?

obstaclelist.append((robotpos, robotpos))
return obstaclelist

def _find_subgoal(self, robotpos, target, closestobstacle, obstacles, recursionfactor, multiple) -> np.array:
Copy link
Member

Choose a reason for hiding this comment

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

the purpose of recursionfactor is really ambiguous. You mean something like left_or_right right? Reconsider the implementation.

Choose a reason for hiding this comment

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

It is a left-right thing. At the moment, the planner is working, but this implementation is not exactly correct.

Once the submission video is completed I want to update it.

Comment on lines 58 to 63
direction = target - robotpos

if recursionfactor % 2 == 1:
perp_dir = rotate_vector(direction[0], direction[1], math.pi / 2)
else:
perp_dir = rotate_vector(direction[0], direction[1], math.pi * 3 / 2)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
direction = target - robotpos
if recursionfactor % 2 == 1:
perp_dir = rotate_vector(direction[0], direction[1], math.pi / 2)
else:
perp_dir = rotate_vector(direction[0], direction[1], math.pi * 3 / 2)
direction = target - robotpos
norm = np.linalg.norm(direction)
if norm < 1e-6: # already at target
return target
direction /= norm
# perpendicular vector
perp = np.array([-direction[1], direction[0]])

Choose a reason for hiding this comment

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

Not updating this at the moment, because it is linked to the previous comment. While _findsubgoal works, its implementation is not exactly correct/the best. So I would like to change it later.

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.

6 participants