Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
0ee5652
the dwa is cooked
Jawad12256 Nov 19, 2025
bc68db2
Merge branch 'main' into dwa-new
wowthecoder Nov 19, 2025
322a8ed
Fast Path PLanning
Nov 24, 2025
417e097
fixing
Nov 24, 2025
6931968
cleaning up
Nov 24, 2025
0f4327b
Merge branch 'main' into dwa-new
wowthecoder Nov 24, 2025
ad2bd95
test: write simple straight line test
wowthecoder Nov 25, 2025
855fb39
test: uncomment straight line test with obstacles
wowthecoder Nov 25, 2025
7be09fa
test: moving robots obstacle course, also added separate motion contr…
wowthecoder Nov 25, 2025
c2a697a
test: implement unit test of 12 robots dashing straight at each other
wowthecoder Nov 26, 2025
c75f02a
test: implement test with 6 robots moving randomly in a half court
wowthecoder Nov 26, 2025
52c55a5
fixing a bug
Nov 26, 2025
7637f5c
increased safety
valentinbruehl Nov 26, 2025
37eeb8e
revert dwa config and planner
wowthecoder Nov 26, 2025
d0b3a6d
rename test file
wowthecoder Nov 26, 2025
7a47ec2
Merge branch 'main' into motion-planning-tests
wowthecoder Nov 26, 2025
388c433
fixing slowing down at sugoals + linting
valentinbruehl Nov 26, 2025
b0a337f
recursion error fix
valentinbruehl Nov 26, 2025
459052e
fix: index out of order bug
valentinbruehl Nov 26, 2025
d1b45b9
updates to fastpathplanning
Dec 1, 2025
495c617
updates
Dec 1, 2025
c9efe28
updating
Dec 2, 2025
71e0ceb
to add new tests
Jan 10, 2026
77baab1
To test new cases
Jan 10, 2026
ee0784b
adding test
Jan 10, 2026
0432c4d
random movement test succeeds with fpp (5 robots)
valentinbruehl Jan 10, 2026
e1138f2
diagonal robot movement test pass with fpp
valentinbruehl Jan 10, 2026
a38b2c7
make number of robots in random_movement_test adjustable
valentinbruehl Jan 10, 2026
9ae1421
use fpp in all tests
valentinbruehl Jan 10, 2026
3912c34
reduce to 3 robots in random_movement_test
valentinbruehl Jan 10, 2026
02329eb
compute distance between two line segments
valentinbruehl Jan 14, 2026
bea2948
g
Jan 14, 2026
826285f
Lined Based obstacles and Config Files Updates
Jan 14, 2026
1311537
Merge branch 'main' into feature/fastpathplanning
valentinbruehl Jan 22, 2026
101110e
fixed issues concerning new functions in math_utils
valentinbruehl Jan 22, 2026
e4c7eaf
updating
Jan 24, 2026
286544e
Removing extra libraries
Jan 24, 2026
8d8041e
Update utama_core/motion_planning/src/fastpathplanning/planner.py
Vortex4627 Jan 24, 2026
ec19004
Adding snake eye convention and removing unwanted code
Jan 24, 2026
ef8bad9
Removed target from collide function
Jan 24, 2026
95785ff
Updating checksegment function
Jan 25, 2026
fb7d793
Including Joel's suggestions
Jan 25, 2026
9121ef7
snake_case and parameter error fix
valentinbruehl Feb 22, 2026
ba83a96
Update utama_core/motion_planning/src/controllers/fastpathplanning.py
Vortex4627 Feb 22, 2026
1da5084
Fix/goalkeeping (#92)
maimaicircle Jan 24, 2026
4ce37fa
chore(release): bump version to v1.5.20
utama-release-manager[bot] Jan 24, 2026
95a3176
Real/extend kick transmission (#93)
energy-in-joles Jan 25, 2026
bde48e7
chore(release): bump version to v1.5.21
utama-release-manager[bot] Jan 25, 2026
e2551f4
Add DeepWiki badge to README
fred-huang122 Feb 3, 2026
ca24507
Feature/rsim noise (#89)
szeyoong-low Feb 10, 2026
cf8cd3b
chore(release): bump version to v1.6.0
utama-release-manager[bot] Feb 10, 2026
7c9a91b
Fix/defence parameter (#103)
NingchuanIC Feb 18, 2026
0431a0c
Feature/kalman (#101)
szeyoong-low Feb 21, 2026
7a84ae8
chore(release): bump version to v1.7.0
utama-release-manager[bot] Feb 21, 2026
d789692
FIR filters (#81)
szeyoong-low Feb 21, 2026
1225fee
chore(release): bump version to v1.8.0
utama-release-manager[bot] Feb 21, 2026
0f6bbf8
reverting mistake
valentinbruehl Feb 22, 2026
df5fc67
planner.py naming convention fixes
valentinbruehl Feb 22, 2026
7aa1fd9
addressed pull request comments
valentinbruehl Feb 22, 2026
25680e3
Updating
Feb 27, 2026
8a158d9
Updating
Feb 27, 2026
baf96f4
Updating
Feb 27, 2026
4685fe9
Updating
Feb 27, 2026
851d748
Updating
Feb 27, 2026
9bd40b9
Added Field Bounds to FastPathPlannig
Mar 5, 2026
5dbf0b7
Added field bounds as an obstacle
Mar 6, 2026
286cfac
Added boundaries are as obstacle, updated how the point of closesnt o…
Mar 6, 2026
16d33f5
Added a project feature, to increase the speed
Mar 11, 2026
c784b27
ignore DS_Store
energy-in-joles Apr 9, 2026
39d9341
Merge branch 'main' into feature/fastpathplanning
energy-in-joles Apr 9, 2026
c644cc3
optimise fastpathplanner
energy-in-joles Apr 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,6 @@ outputs/
wandb/
mappo_*/
*.gif

# macOS
.DS_Store
14 changes: 7 additions & 7 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,14 @@ def pytest_generate_tests(metafunc):


# temporarily excludes the motion planning tests until the motion planning algorithms are working
def pytest_collection_modifyitems(config, items):
if config.getoption("--include-motion-planning"):
return
# def pytest_collection_modifyitems(config, items):
# if config.getoption("--include-motion-planning"):
# return

skip_mp = pytest.mark.skip(reason="motion planning not ready")
for item in items:
if "motion_planning" in item.keywords:
item.add_marker(skip_mp)
# skip_mp = pytest.mark.skip(reason="motion planning not ready")
# for item in items:
# if "motion_planning" in item.keywords:
# item.add_marker(skip_mp)


@pytest.fixture
Expand Down
Binary file added utama_core/.DS_Store
Binary file not shown.
208 changes: 208 additions & 0 deletions utama_core/global_utils/math_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from utama_core.entities.data.vector import Vector2D
from utama_core.entities.game.field import Field, FieldBounds

EPS = 1e-9


def rotate_vector(vx_global: float, vy_global: float, theta: float) -> Tuple[float, float]:
"""Rotates a 2D vector from global coordinates to local coordinates based on a given angle.
Expand Down Expand Up @@ -170,3 +172,209 @@ def assert_contains(outer: FieldBounds, inner: FieldBounds):
raise ValueError(f"Outer right {ox1} does not contain inner right {ix1}")
if oy1 > iy1:
raise ValueError(f"Outer bottom {oy1} does not contain inner bottom {iy1}")


def distance_between_line_segments(
seg1_start: np.ndarray,
seg1_end: np.ndarray,
seg2_start: np.ndarray,
seg2_end: np.ndarray,
) -> float:
"""Calculate the minimum distance between two line segments in 2D space.

Args:
seg1_start (tuple): A tuple representing the start of the first line segment (x1, y1).
seg1_end (tuple): A tuple representing the end of the first line segment (x2, y2).
seg2_start (tuple): A tuple representing the start of the second line segment (x3, y3).
seg2_end (tuple): A tuple representing the end of the second line segment (x4, y4).
Returns:
float: The minimum distance between the two line segments.
"""
if segments_intersect(seg1_start, seg1_end, seg2_start, seg2_end):
return 0.0

return min(
distance_point_to_segment(seg1_start, seg2_start, seg2_end),
distance_point_to_segment(seg1_end, seg2_start, seg2_end),
distance_point_to_segment(seg2_start, seg1_start, seg1_end),
distance_point_to_segment(seg2_end, seg1_start, seg1_end),
)


def distance_point_to_segment(point: np.ndarray, seg_start: np.ndarray, seg_end: np.ndarray) -> float:
"""Calculate the minimum distance from a point to a line segment in 2D space.

Args:
point (tuple): A tuple representing the point (px, py).
seg_start (tuple): A tuple representing the start of the segment (x1, y1).
seg_end (tuple): A tuple representing the end of the segment (x2, y2).
Returns:
float: The minimum distance from the point to the line segment.
"""
point = np.asarray(point)
seg_start = np.asarray(seg_start)
seg_end = np.asarray(seg_end)

seg_vec = seg_end - seg_start
pt_vec = point - seg_start

seg_len_sq = np.dot(seg_vec, seg_vec)

if seg_len_sq < EPS:
return np.linalg.norm(point - seg_start)

t = np.dot(pt_vec, seg_vec) / seg_len_sq

if t < 0:
closest = seg_start
elif t > 1:
closest = seg_end
else:
closest = seg_start + t * seg_vec

return np.linalg.norm(point - closest)


def closest_point_on_segment(point, seg_start, seg_end):
"""Calculate the point on a segment closest to another point.

Args:
point (tuple): A tuple representing the point (px, py).
seg_start (tuple): A tuple representing the start of the segment (x1, y1).
seg_end (tuple): A tuple representing the end of the segment (x2, y2).
Returns:
np.ndarray: An np array representing the closest point on the segment.
"""
point = np.asarray(point)
seg_start = np.asarray(seg_start)
seg_end = np.asarray(seg_end)

seg_vec = seg_end - seg_start
pt_vec = point - seg_start

seg_len_sq = np.dot(seg_vec, seg_vec)

if seg_len_sq < EPS:
return seg_start

t = np.dot(pt_vec, seg_vec) / seg_len_sq

if t < 0:
return seg_start
elif t > 1:
return seg_end
else:
return seg_start + t * seg_vec


def segments_intersect(
seg1_start: np.ndarray,
seg1_end: np.ndarray,
seg2_start: np.ndarray,
seg2_end: np.ndarray,
):
"""Check if two line segments intersect.

Args:
seg1_start (tuple): ((x1, y1), (x2, y2))
seg1_end (tuple): ((x3, y3), (x4, y4))
seg2_start (tuple): ((x5, y5), (x6, y6))
seg2_end (tuple): ((x7, y7), (x8, y8))
Returns:
bool: True if the segments intersect, False otherwise.
"""
p1 = np.asarray(seg1_start)
q1 = np.asarray(seg1_end)
p2 = np.asarray(seg2_start)
q2 = np.asarray(seg2_end)

o1 = point_orientation(p1, q1, p2)
o2 = point_orientation(p1, q1, q2)
o3 = point_orientation(p2, q2, p1)
o4 = point_orientation(p2, q2, q1)

if o1 != o2 and o3 != o4:
return True

if o1 == 0 and on_segment(p1, p2, q1):
return True
if o2 == 0 and on_segment(p1, q2, q1):
return True
if o3 == 0 and on_segment(p2, p1, q2):
return True
if o4 == 0 and on_segment(p2, q1, q2):
return True

return False


def point_orientation(p_1: np.ndarray, p_2: np.ndarray, p_3: np.ndarray) -> int:
"""Calculate the orientation of 3 points (e.g. on a line or in a triangle).

Args:
p_1 (np.ndarray): First point as (x, y).
p_2 (np.ndarray): Second point as (x, y).
p_3 (np.ndarray): Third point as (x, y).

Returns:
int: 0 if collinear, 1 if clockwise, 2 if counterclockwise.
"""
p1 = np.asarray(p_1)
p2 = np.asarray(p_2)
p3 = np.asarray(p_3)

val = (p2[0] - p1[0]) * (p3[1] - p1[1]) - (p2[1] - p1[1]) * (p3[0] - p1[0])

if abs(val) < EPS:
return 0

return 1 if val < 0 else 2


def on_segment(p: np.ndarray, q: np.ndarray, r: np.ndarray) -> bool:
"""Check if point q lies on line segment 'pr'.

Args:
p (np.ndarray): Start point of segment as (x, y).
q (np.ndarray): Point to check as (x, y).
r (np.ndarray): End point of segment as (x, y).

Returns:
bool: True if q lies on segment pr, False otherwise.
"""
p = np.asarray(p)
q = np.asarray(q)
r = np.asarray(r)

return (
min(p[0], r[0]) - EPS <= q[0] <= max(p[0], r[0]) + EPS
and min(p[1], r[1]) - EPS <= q[1] <= max(p[1], r[1]) + EPS
)


def find_intersection(line1, line2):
"""
Find the intersection point of two line segments.

Args:
line1: tuple of two np.arrays (start, end) -> (A, B)
line2: tuple of two np.arrays (start, end) -> (C, D)

Returns:
np.array of intersection point (x, y), or None if no intersection.
"""
A, B = np.asarray(line1[0]), np.asarray(line1[1])
C, D = np.asarray(line2[0]), np.asarray(line2[1])

denom = (B[0] - A[0]) * (D[1] - C[1]) - (B[1] - A[1]) * (D[0] - C[0])

if abs(denom) < EPS:
return None

t = ((C[0] - A[0]) * (D[1] - C[1]) - (C[1] - A[1]) * (D[0] - C[0])) / denom
u = ((C[0] - A[0]) * (B[1] - A[1]) - (C[1] - A[1]) * (B[0] - A[0])) / denom

if -EPS <= t <= 1 + EPS and -EPS <= u <= 1 + EPS:
return A + t * (B - A)

return None
8 changes: 6 additions & 2 deletions utama_core/motion_planning/src/common/control_schemes.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from typing import Type

from utama_core.motion_planning.src.common.motion_controller import MotionController
from utama_core.motion_planning.src.controllers import DWAController, PIDController
from utama_core.motion_planning.src.controllers import (
DWAController,
FastPathPlanningController,
PIDController,
)

CONTROL_SCHEME_MAP = {"pid": PIDController, "dwa": DWAController}
CONTROL_SCHEME_MAP = {"pid": PIDController, "dwa": DWAController, "fpp": FastPathPlanningController}


def get_control_scheme(scheme_name: str) -> Type[MotionController]:
Expand Down
3 changes: 3 additions & 0 deletions utama_core/motion_planning/src/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
from utama_core.motion_planning.src.controllers.dwa_controller import DWAController
from utama_core.motion_planning.src.controllers.fastpathplanning import (
FastPathPlanningController,
)
from utama_core.motion_planning.src.controllers.pid_controller import PIDController
44 changes: 44 additions & 0 deletions utama_core/motion_planning/src/controllers/fastpathplanning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from abc import ABC, abstractmethod

import numpy as np

from utama_core.config.enums import Mode
from utama_core.config.physical_constants import ROBOT_RADIUS
from utama_core.entities.data.vector import Vector2D
from utama_core.entities.game import Game
from utama_core.entities.game.field import Field
from utama_core.motion_planning.src.common.motion_controller import MotionController
from utama_core.motion_planning.src.fastpathplanning.planner import FastPathPlanner
from utama_core.motion_planning.src.pid.pid import get_pids
from utama_core.rsoccer_simulator.src.ssl.envs import SSLStandardEnv


class FastPathPlanningController(MotionController):
Comment thread
valentinbruehl marked this conversation as resolved.
def __init__(self, mode: Mode, rsim_env: SSLStandardEnv | None = None):
self.mode = mode
self.rsim_env: SSLStandardEnv | None = rsim_env
self.pid_oren, self.pid_trans = get_pids(mode)
self.fpp = FastPathPlanner(env=self.rsim_env)
Comment on lines +17 to +21
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 FastPathPlanningController class does not call the parent class init method. Unlike DWAController which calls 'super().init(mode, rsim_env)', this implementation should also call the parent constructor to properly initialize the base MotionController class.

Copilot uses AI. Check for mistakes.

def calculate(
self,
game: Game,
robot_id: int,
target_pos: Vector2D,
target_oren: float,
) -> tuple[Vector2D, float]:
field = game.field

field_bounds = field.field_bounds
robot = game.friendly_robots[robot_id]

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

pos = self.fpp._path_to(game, robot_id, target_pos, field_bounds)
vel = self.pid_trans.calculate(pos, robot.p, robot_id)

return vel, oren

def reset(self, robot_id):
self.pid_oren.reset(robot_id)
Comment thread
Vortex4627 marked this conversation as resolved.
self.pid_trans.reset(robot_id)
18 changes: 18 additions & 0 deletions utama_core/motion_planning/src/fastpathplanning/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from utama_core.config.physical_constants import ROBOT_RADIUS


class fastpathplanningconfig:
Comment thread
valentinbruehl marked this conversation as resolved.
Comment thread
valentinbruehl marked this conversation as resolved.
ROBOT_DIAMETER = 2 * ROBOT_RADIUS

# how fat is the danger zone around obstacles, in multiples of robot diameter
CLEARANCE_MULTIPLIER = 1.5
OBSTACLE_CLEARANCE = ROBOT_DIAMETER * CLEARANCE_MULTIPLIER

# How far outside the danger zone shold the waypoint be
SUBGOAL_MULTIPLIER = 1.2
SUBGOAL_DISTANCE = OBSTACLE_CLEARANCE * SUBGOAL_MULTIPLIER

LOOK_AHEAD_RANGE = 3
MAXRECURSION_LENGTH = 3
PROJECTEDFRAMES = 20
PROJECTION_DISTANCE = 1
Loading
Loading