Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
25b932f
Added extracted vision data with varying degrees of vanishing and noise
szeyoong-low Nov 12, 2025
acda222
Merge remote-tracking branch 'origin' into position_filtering_2, pull…
szeyoong-low Nov 16, 2025
35147f1
Attempt to store past raw game frames; starting work on test cases
szeyoong-low Nov 16, 2025
25875d3
added Andrew's filter
szeyoong-low Nov 18, 2025
058cda2
Integrated filters into position refiner
szeyoong-low Nov 18, 2025
750c925
bad example test
fred-huang122 Nov 18, 2025
361117c
Refined filter weights, added vision data for testing
szeyoong-low Nov 19, 2025
ea88864
Updated test data
szeyoong-low Nov 19, 2025
e7c8170
Noise now added manually, updated test data
szeyoong-low Nov 19, 2025
f5561e5
Amended test cases to address CI failure due to changes to PositionRe…
szeyoong-low Nov 19, 2025
5428838
Refinements to filters based on empirical data
szeyoong-low Nov 25, 2025
756809b
Added some references for building tests
szeyoong-low Nov 29, 2025
b0650d0
First pass unit tests
szeyoong-low Nov 30, 2025
62a633d
Some issues with unit tests. Working on fix
szeyoong-low Nov 30, 2025
e6ee520
Added live testing utilities, conducted analytics for filters, refact…
szeyoong-low Dec 4, 2025
27ee5c1
Finalised analysis of filters, unit tests are working, added more liv…
szeyoong-low Dec 4, 2025
39e1c68
Deleted redundant files
szeyoong-low Dec 4, 2025
6416895
Minor refinements to the live testing utilities
szeyoong-low Dec 4, 2025
81c0c57
fixed formatting issues
szeyoong-low Dec 4, 2025
870f59e
Commented out utilities for testing and exporting data
szeyoong-low Dec 8, 2025
e84df4a
Merge branch 'main' into position_filtering_2 in preparation for pull…
szeyoong-low Dec 8, 2025
8d80265
Completed merge with main
szeyoong-low Dec 8, 2025
3ab1afe
Addressed comments on PR
szeyoong-low Jan 7, 2026
9df7bcd
Removed redundant utility function
szeyoong-low Jan 8, 2026
6be9f1e
cleanup
energy-in-joles Jan 8, 2026
aaf6579
Added back error handling for imports when run from Jupyter
szeyoong-low Jan 8, 2026
171d2bc
Merge branch 'main' into position_filtering_2
energy-in-joles Jan 12, 2026
4280978
Moved unused testing/debugging utilities to a separate text file, tur…
szeyoong-low Jan 14, 2026
e250d76
Merge branch 'position_filtering_2' of github.com:First-Order-RoboCup…
szeyoong-low Jan 14, 2026
98f3add
Merge branch 'main' into position_filtering_2
szeyoong-low Jan 14, 2026
6166a96
Further cleanup
szeyoong-low Jan 18, 2026
020f317
Merge branch 'main' into position_filtering_2
szeyoong-low Jan 18, 2026
43c9f47
Merge branch 'position_filtering_2' of github.com:First-Order-RoboCup…
szeyoong-low Jan 18, 2026
f880544
Removed all testing utilities, will create a testing branch
szeyoong-low Jan 18, 2026
930918e
Cleaned up test suite
szeyoong-low Jan 18, 2026
dca5a22
Removed data analysis
szeyoong-low Jan 18, 2026
f7ae608
Removed data analysis
szeyoong-low Jan 18, 2026
0986968
Removed data analysis
szeyoong-low Jan 18, 2026
4cc647d
Merge branch 'main' into position_filtering_2
szeyoong-low Jan 31, 2026
093ac9d
Final cleanup
szeyoong-low Jan 31, 2026
46fef69
Merge branch 'main' into position_filtering_2
energy-in-joles Feb 21, 2026
4e544c7
revert position_refiner_integration_test
energy-in-joles Feb 21, 2026
bee6a9e
update test
energy-in-joles Feb 21, 2026
9ae93cc
fixes and formatting
energy-in-joles Feb 21, 2026
cd6ab11
remove unncessary comments
energy-in-joles Feb 21, 2026
029bc47
update readme
energy-in-joles Feb 21, 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
21 changes: 11 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,17 @@ First Order Robotics core software stack for [RoboCup SSL](https://ssl.robocup.o
### Folder Structure

1. `strategy`: higher level control from above roles to plays and tactics in decision-tree like abstraction
2. `skills`: lowest level of control for individual robots
3. `motion_planning`: control algorithms for movement and path planning
4. `team_controller`: interfacing with vision (including processing) and robots
5. `run`: The logic for main running loop, including refiners and predictors
6. `global_utils`: store utility functions that can be shared across all folders
7. `entities`: store classes for building field, robot, data entities etc.
8. `rsoccer_simulator`: Lightweight rSoccer simulator for testing
9. `replay`: replay system for storing played games in a .pkl file that can be reconstructed in rsoccer sim
10. `tests`: include all unit tests here
11. `config`: configs for the robots (defaults, settings, roles/tactics enums, etc.)
1. `skills`: lowest level of control for individual robots
1. `motion_planning`: control algorithms for movement and path planning
1. `team_controller`: interfacing with vision (including processing) and robots
1. `run`: The logic for main running loop
1. `data_processing`: processors of vision, robot_info and referee raw data
1. `global_utils`: store utility functions that can be shared across all folders
1. `entities`: store classes for building field, robot, data entities etc.
1. `rsoccer_simulator`: Lightweight rSoccer simulator for testing
1. `replay`: replay system for storing played games in a .pkl file that can be reconstructed in rsoccer sim
1. `tests`: include all unit tests here
1. `config`: configs for the robots (defaults, settings, roles/tactics enums, etc.)

### Code Writing

Expand Down
632 changes: 431 additions & 201 deletions pixi.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ pre-commit = ">=3.8.0"
hatch = ">=1.14.1,<2"
graphviz = ">=13.1.2,<14"
snakeviz = ">=2.2.2,<3"
scipy = ">=1.16.3,<2"
pandas = "==3.0.1"
rich = ">=14.2.0,<15"

[package]
Expand Down
1 change: 1 addition & 0 deletions utama_core/data_processing/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

2 changes: 2 additions & 0 deletions utama_core/data_processing/receivers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from utama_core.data_processing.receivers.referee_receiver import RefereeMessageReceiver
from utama_core.data_processing.receivers.vision_receiver import VisionReceiver
4 changes: 4 additions & 0 deletions utama_core/data_processing/refiners/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from utama_core.data_processing.refiners.position import PositionRefiner
from utama_core.data_processing.refiners.referee import RefereeRefiner
from utama_core.data_processing.refiners.robot_info import RobotInfoRefiner
from utama_core.data_processing.refiners.velocity import VelocityRefiner
119 changes: 119 additions & 0 deletions utama_core/data_processing/refiners/filters/fir.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
from collections import deque
from typing import Union

import numpy as np
from scipy.signal import firwin

from utama_core.entities.data.vision import VisionRobotData


class FIR_filter:
"""
Finite Impulse Response (FIR) filter for 2D position and orientation.
This is essentially a weighted average of the past n data points.

More about the methodology:
https://youtube.com/playlist?list=PLbqhA-NKGP6Afr_KbPUuy_yIBpPR4jzWo&si=7l2BVnsN_jSKq2JL.

Args:
fs (float): Sampling rate (Hz). Defaults to 60.0.
taps (array-like or None): FIR taps. If None, a series of taps of length `window_len` is created.
window_len (int): Number of taps to be created if `taps` is None. Default 20.
cutoff (float): Cutoff frequency of the filter.
"""

def __init__(
self,
fs: float = 60.0,
taps: Union[np.array, None] = None,
window_len: int = 5,
cutoff: Union[float, None] = None,
):
self._fs = float(fs)
self._N = window_len
self._nyquist = 0.4 * self._fs

if cutoff and cutoff < self._nyquist:
self._cutoff = cutoff
else:
# Sets cutoff frequency according to the maximum acceleration and
# velocity of robots, below the limits dictated by Nyquist's theorem.

a_max = 50
v_max = 5
fc = a_max / (2 * np.pi * v_max)

self._cutoff = min(self._nyquist, fc)

if taps is None:
assert window_len >= 1, "window_len must be >= 1"
self._taps = firwin(window_len, self._cutoff, fs=fs)

else:
t = np.asarray(taps, dtype=float).ravel()
assert t.size >= 1, "taps must have at least 1 element"
# Normalize taps to sum to 1 for unity DC gain
self._taps = t / np.sum(t)
self._N = self._taps.size

self._buf_x = deque(maxlen=self._N)
self._buf_y = deque(maxlen=self._N)
self._buf_th = deque(maxlen=self._N)

def step(self, data: tuple[float]):
"""
A single iteration of the filter

Args:
data (tuple[float]): The new vision data received (x and y coordinates
in metres, orientation in radians).

Returns:
tuple[float]: The filtered data
"""

x, y, th = map(float, data)
# theta = normalise_heading(theta)

self._buf_x.append(x)
self._buf_y.append(y)
self._buf_th.append(th)

# Use only the available samples during warm-up
k = len(self._buf_x) # same as len(buf_y) and len(buf_th)
taps_eff = self._taps[-k:]
taps_eff = taps_eff / np.sum(taps_eff) # renormalize

# Position FIR
x_arr = np.asarray(self._buf_x, dtype=float)
y_arr = np.asarray(self._buf_y, dtype=float)
x_f = float(np.dot(taps_eff, x_arr))
y_f = float(np.dot(taps_eff, y_arr))

# Orientation FIR via circular averaging - currently disabled
# th_arr = np.asarray(self._buf_th, dtype=float)
# s = np.dot(taps_eff, np.sin(th_arr))
# c = np.dot(taps_eff, np.cos(th_arr))
# th_f = float(np.arctan2(s, c)) # already wrapped to (-pi, pi]

return x_f, y_f, th

@staticmethod
def filter_robot(filter, data: VisionRobotData) -> VisionRobotData:
"""
Externally callable function for Position Refiner to pass data
to be filtered.

Args:
filter (FIR_filter): The filter associated with a robot
data (VisionRobotData): The new vision data received (x and y
coordinates in metres, orientation in radians).

Returns:
VisionRobotData: The filtered vision data.
"""

# class VisionRobotData: id: int; x: float; y: float; orientation: float
(x_f, y_f, th_f) = filter.step((data.x, data.y, data.orientation))

return VisionRobotData(id=data.id, x=x_f, y=y_f, orientation=th_f)
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
import numpy as np

from utama_core.config.settings import BALL_MERGE_THRESHOLD
from utama_core.data_processing.refiners.base_refiner import BaseRefiner
from utama_core.data_processing.refiners.filters.kalman import (
KalmanFilter,
KalmanFilterBall,
)
from utama_core.entities.data.raw_vision import RawBallData, RawRobotData, RawVisionData
from utama_core.entities.data.vector import Vector2D, Vector3D
from utama_core.entities.data.vision import VisionBallData, VisionData, VisionRobotData
from utama_core.entities.game import Ball, FieldBounds, GameFrame, Robot
from utama_core.global_utils.mapping_utils import map_friendly_enemy_to_colors
from utama_core.run.refiners.base_refiner import BaseRefiner
from utama_core.run.refiners.kalman import KalmanFilter, KalmanFilterBall


class AngleSmoother:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from typing import Optional

from utama_core.data_processing.refiners.base_refiner import BaseRefiner
from utama_core.entities.data.referee import RefereeData
from utama_core.entities.game.team_info import TeamInfo
from utama_core.entities.referee.referee_command import RefereeCommand
from utama_core.entities.referee.stage import Stage
from utama_core.run.refiners.base_refiner import BaseRefiner


class RefereeRefiner(BaseRefiner):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
from dataclasses import replace
from typing import List

from utama_core.data_processing.refiners.base_refiner import BaseRefiner
from utama_core.entities.data.command import RobotResponse
from utama_core.entities.game.game_frame import GameFrame
from utama_core.run.refiners.base_refiner import BaseRefiner

# TODO: current doesn't handle has_ball for enemy robots. In future, implement using vision data

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import numpy as np # Import NumPy

from utama_core.data_processing.refiners.base_refiner import BaseRefiner
from utama_core.entities.data.object import ObjectKey, TeamType
from utama_core.entities.data.vector import Vector2D, Vector3D
from utama_core.entities.game import GameFrame, Robot
Expand All @@ -12,7 +13,6 @@
GameHistory,
get_structured_object_key,
)
from utama_core.run.refiners.base_refiner import BaseRefiner

logger = logging.getLogger(__name__)

Expand Down
58 changes: 29 additions & 29 deletions utama_core/entities/data/vision.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
from dataclasses import dataclass
from typing import List

# position data: meters
# orientation: radians


@dataclass
class VisionBallData:
x: float
y: float
z: float
confidence: float


@dataclass
class VisionRobotData:
id: int
x: float
y: float
orientation: float


@dataclass
class VisionData:
ts: float
yellow_robots: List[VisionRobotData]
blue_robots: List[VisionRobotData]
balls: List[VisionBallData]
from dataclasses import dataclass
from typing import List
# position data: meters
# orientation: radians
@dataclass
class VisionBallData:
x: float
y: float
z: float
confidence: float
@dataclass
class VisionRobotData:
id: int
x: float
y: float
orientation: float
@dataclass
class VisionData:
ts: float
yellow_robots: List[VisionRobotData]
blue_robots: List[VisionRobotData]
balls: List[VisionBallData]
2 changes: 1 addition & 1 deletion utama_core/run/game_gater.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import time
from typing import Deque, List, Optional, Tuple

from utama_core.data_processing.refiners import PositionRefiner
from utama_core.entities.data.raw_vision import RawVisionData
from utama_core.entities.game.game_frame import GameFrame
from utama_core.rsoccer_simulator.src.ssl.ssl_gym_base import SSLBaseEnv
from utama_core.run.refiners import PositionRefiner


class GameGater:
Expand Down
2 changes: 0 additions & 2 deletions utama_core/run/receivers/__init__.py

This file was deleted.

4 changes: 0 additions & 4 deletions utama_core/run/refiners/__init__.py

This file was deleted.

8 changes: 6 additions & 2 deletions utama_core/run/strategy_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
MAX_GAME_HISTORY,
TIMESTEP,
)
from utama_core.data_processing.receivers import VisionReceiver
from utama_core.data_processing.refiners import (
PositionRefiner,
RobotInfoRefiner,
VelocityRefiner,
)
from utama_core.entities.data.command import RobotCommand
from utama_core.entities.data.raw_vision import RawVisionData
from utama_core.entities.game import Game, GameHistory
Expand All @@ -33,8 +39,6 @@
from utama_core.rsoccer_simulator.src.ssl.envs import SSLStandardEnv
from utama_core.rsoccer_simulator.src.Utils.gaussian_noise import RsimGaussianNoise
from utama_core.run import GameGater
from utama_core.run.receivers import VisionReceiver
from utama_core.run.refiners import PositionRefiner, RobotInfoRefiner, VelocityRefiner
from utama_core.strategy.common.abstract_strategy import AbstractStrategy
from utama_core.team_controller.src.controllers import (
AbstractSimController,
Expand Down
31 changes: 22 additions & 9 deletions utama_core/skills/src/defend_parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@

import numpy as np

from utama_core.data_processing.predictors.position import predict_ball_pos_at_x
from utama_core.entities.data.vector import Vector2D
from utama_core.entities.game import Game
from utama_core.motion_planning.src.common.motion_controller import MotionController
from utama_core.rsoccer_simulator.src.ssl.envs.standard_ssl import SSLStandardEnv
from utama_core.run.predictors.position import predict_ball_pos_at_x
from utama_core.skills.src.go_to_point import go_to_point


Expand All @@ -22,10 +22,20 @@ def defend_parameter(
if len(game.friendly_robots) > 2:
if game.ball.p.y >= -0.5 and robot_id == 1:
target_pos = [3.0 if game.my_team_is_right else -3.0, -1.2]
return go_to_point(game, motion_controller, robot_id, Vector2D(target_pos[0], target_pos[1]))
return go_to_point(
game,
motion_controller,
robot_id,
Vector2D(target_pos[0], target_pos[1]),
)
elif game.ball.p.y < -0.5 and robot_id == 2:
target_pos = [3.0 if game.my_team_is_right else -3.0, 1.2]
return go_to_point(game, motion_controller, robot_id, Vector2D(target_pos[0], target_pos[1]))
return go_to_point(
game,
motion_controller,
robot_id,
Vector2D(target_pos[0], target_pos[1]),
)
if robot_id == 1:
goal_frame = 0.5
else:
Expand Down Expand Up @@ -65,12 +75,9 @@ def distance(x1, y1, x2, y2):
if (
vel[0] ** 2 + vel[1] ** 2 > 0.05
and (ball_y_at_baseline is not None and ball_y_at_baseline[1] < 0.5 and ball_y_at_baseline[1] > -0.5)
and (
ball_y_at_robo is not None
and abs(ball_y_at_robo[1] - defenseing_friendly.p.y) > 0.1
)
and (ball_y_at_robo is not None and abs(ball_y_at_robo[1] - defenseing_friendly.p.y) > 0.1)
):
x2, y2 = 4.5 if game.my_team_is_right else -4.5, goal_frame + 0.2 if robot_id == 1 else goal_frame - 0.2
x2, y2 = 4.5 if game.my_team_is_right else -4.5, (goal_frame + 0.2 if robot_id == 1 else goal_frame - 0.2)
x3, y3, x4, y4 = positions_to_defend_parameter(x2, y2)
target_pos = np.array([x4, y4])

Expand All @@ -92,4 +99,10 @@ def distance(x1, y1, x2, y2):
vec_dir = np.array([0.0, 0.0])
target_pos = np.array([x4, y4]) - vec_dir * robot_rad

return go_to_point(game, motion_controller, robot_id, Vector2D(target_pos[0], target_pos[1]), dribbling=True)
return go_to_point(
game,
motion_controller,
robot_id,
Vector2D(target_pos[0], target_pos[1]),
dribbling=True,
)
Loading
Loading