Skip to content

Commit db5f9fc

Browse files
Fix/rsim teleport bug (#109)
* fix teleport bug * add teleport position test * clean up movement tests * default filter to false * remove conflict in teleport positions * remove print artefacts from teleport_position_accuracy_test.py * update docstring for strategy_runner.py to reflect filtering false default * move bounds check earlier to prevent mismatch of kalman filter and gameframe mismatch * added comment acknowledge the double initialization of Game in run_test
1 parent 7270ba5 commit db5f9fc

File tree

11 files changed

+339
-183
lines changed

11 files changed

+339
-183
lines changed

utama_core/data_processing/refiners/position.py

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from collections import defaultdict
2-
from dataclasses import replace
2+
from dataclasses import dataclass, replace
33
from functools import partial
44
from typing import Dict, List, Optional, Tuple
55

@@ -31,6 +31,14 @@ def smooth(self, old_angle: float, new_angle: float) -> float:
3131
return smoothed_angle
3232

3333

34+
@dataclass
35+
class VisionBounds:
36+
x_min: float
37+
x_max: float
38+
y_min: float
39+
y_max: float
40+
41+
3442
class PositionRefiner(BaseRefiner):
3543
def __init__(
3644
self,
@@ -40,11 +48,12 @@ def __init__(
4048
):
4149
# alpha=0 means no change in angle (inf smoothing), alpha=1 means no smoothing
4250
self.angle_smoother = AngleSmoother(alpha=1)
43-
self.x_min = field_bounds.top_left[0] - bounds_buffer # expand left
44-
self.x_max = field_bounds.bottom_right[0] + bounds_buffer # expand right
45-
self.y_min = field_bounds.bottom_right[1] - bounds_buffer # expand bottom
46-
self.y_max = field_bounds.top_left[1] + bounds_buffer # expand top
47-
self.BOUNDS_BUFFER = bounds_buffer
51+
self.vision_bounds = VisionBounds(
52+
x_min=field_bounds.top_left[0] - bounds_buffer, # expand left
53+
x_max=field_bounds.bottom_right[0] + bounds_buffer, # expand right
54+
y_min=field_bounds.bottom_right[1] - bounds_buffer, # expand bottom
55+
y_max=field_bounds.top_left[1] + bounds_buffer, # expand top
56+
)
4857

4958
# For Kalman filtering and imputing vanished values.
5059
self.filtering = filtering
@@ -68,7 +77,7 @@ def refine(self, game_frame: GameFrame, data: List[RawVisionData]) -> GameFrame:
6877

6978
# class VisionData: ts: float; yellow_robots: List[VisionRobotData]; blue_robots: List[VisionRobotData]; balls: List[VisionBallData]
7079
# class VisionRobotData: id: int; x: float; y: float; orientation: float
71-
combined_vision_data: VisionData = CameraCombiner().combine_cameras(frames)
80+
combined_vision_data: VisionData = CameraCombiner().combine_cameras(frames, bounds=self.vision_bounds)
7281

7382
time_elapsed = combined_vision_data.ts - game_frame.ts
7483

@@ -87,7 +96,9 @@ def refine(self, game_frame: GameFrame, data: List[RawVisionData]) -> GameFrame:
8796
for y_rbt_id, vision_y_rbt in vision_yellow.items():
8897
if y_rbt_id not in self.kalman_filters_yellow:
8998
self.kalman_filters_yellow[y_rbt_id] = KalmanFilter(id=y_rbt_id)
90-
99+
if y_rbt_id not in yellow_rbt_last_frame:
100+
filtered_yellow_robots.append(vision_y_rbt)
101+
continue
91102
filtered_robot = self.kalman_filters_yellow[y_rbt_id].filter_data(
92103
vision_y_rbt, # new measurement
93104
yellow_rbt_last_frame[y_rbt_id], # last frame
@@ -100,6 +111,9 @@ def refine(self, game_frame: GameFrame, data: List[RawVisionData]) -> GameFrame:
100111
for b_rbt_id, vision_b_rbt in vision_blue.items():
101112
if b_rbt_id not in self.kalman_filters_blue:
102113
self.kalman_filters_blue[b_rbt_id] = KalmanFilter(id=b_rbt_id)
114+
if b_rbt_id not in blue_rbt_last_frame:
115+
filtered_blue_robots.append(vision_b_rbt)
116+
continue
103117

104118
filtered_robot = self.kalman_filters_blue[b_rbt_id].filter_data(
105119
vision_b_rbt, # new measurement
@@ -264,12 +278,6 @@ def _combine_single_team_positions(
264278
friendly: bool,
265279
) -> Dict[int, Robot]:
266280
for robot in vision_robots:
267-
new_x, new_y = robot.x, robot.y
268-
269-
if not (self.x_min <= new_x <= self.x_max and self.y_min <= new_y <= self.y_max):
270-
# Out of bounds so ignore this robot
271-
continue
272-
273281
if robot.id not in new_game_robots:
274282
# At the start of the game, we haven't seen anything yet, so just create a new robot
275283
new_game_robots[robot.id] = PositionRefiner._robot_from_vision(robot, is_friendly=friendly)
@@ -312,9 +320,16 @@ def filter_running(self) -> bool:
312320

313321

314322
class CameraCombiner:
315-
def combine_cameras(self, frames: List[RawVisionData]) -> VisionData:
316-
# Now we have access to the game we can do more sophisticated things
317-
# Such as ignoring outlier cameras etc
323+
def combine_cameras(self, frames: List[RawVisionData], bounds: VisionBounds) -> VisionData:
324+
"""
325+
Combines the vision data from multiple cameras into a single coherent VisionData object.
326+
Also, removes any robot detections that are out of the specified bounds.
327+
Args:
328+
frames (List[RawVisionData]): A list of RawVisionData objects from different cameras.
329+
bounds (VisionBounds): The bounds within which to consider vision data for combination.
330+
Returns:
331+
VisionData: A combined VisionData object containing averaged robot positions and the most confident ball position.
332+
"""
318333

319334
ts = []
320335
# maps robot id to list of frames seen for that robot
@@ -325,13 +340,16 @@ def combine_cameras(self, frames: List[RawVisionData]) -> VisionData:
325340
# Each frame is from a different camera
326341
for frame_ind, frame in enumerate(frames):
327342
for yr in frame.yellow_robots:
328-
yellow_captured[yr.id].append(yr)
343+
if self._bounds_check(yr.x, yr.y, bounds):
344+
yellow_captured[yr.id].append(yr)
329345

330346
for br in frame.blue_robots:
331-
blue_captured[br.id].append(br)
347+
if self._bounds_check(br.x, br.y, bounds):
348+
blue_captured[br.id].append(br)
332349

333350
for b in frame.balls:
334-
balls_captured[frame_ind].append(b)
351+
if self._bounds_check(b.x, b.y, bounds):
352+
balls_captured[frame_ind].append(b)
335353
ts.append(frame.ts)
336354

337355
avg_yellows = list(map(self._avg_robots, yellow_captured.values()))
@@ -380,6 +398,9 @@ def _combine_balls_by_proximity(self, bs: Dict[int, List[RawBallData]]) -> List[
380398
combined_balls.append(b)
381399
return combined_balls
382400

401+
def _bounds_check(self, x: float, y: float, bounds: VisionBounds) -> bool:
402+
return bounds.x_min <= x <= bounds.x_max and bounds.y_min <= y <= bounds.y_max
403+
383404
@staticmethod
384405
def ball_merge_predicate(b1: RawBallData, b2: RawBallData) -> bool:
385406
return abs(b1.x - b2.x) + abs(b1.y - b2.y) < BALL_MERGE_THRESHOLD

utama_core/run/game_gater.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def wait_until_game_valid(
3131
def _add_frame(my_game_frame: GameFrame, opp_game_frame: GameFrame) -> Tuple[GameFrame, Optional[GameFrame]]:
3232
if rsim_env:
3333
vision_frames = [rsim_env._frame_to_observations()[0]]
34+
rsim_env.steps += 1 # Increment the step count to simulate time passing in the environment
3435
else:
3536
vision_frames = [buffer.popleft() if buffer else None for buffer in vision_buffers]
3637
my_game_frame = position_refiner.refine(my_game_frame, vision_frames)

utama_core/run/strategy_runner.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ class StrategyRunner:
111111
Defaults to 0 for each.
112112
rsim_vanishing (float, optional): When running in rsim, cause robots and ball to vanish with the given probability.
113113
Defaults to 0.
114-
filtering (bool, optional): Turn on Kalman filtering. Defaults to true.
114+
filtering (bool, optional): Turn on Kalman filtering. Defaults to false.
115115
"""
116116

117117
def __init__(
@@ -131,7 +131,7 @@ def __init__(
131131
profiler_name: Optional[str] = None,
132132
rsim_noise: RsimGaussianNoise = RsimGaussianNoise(),
133133
rsim_vanishing: float = 0,
134-
filtering: bool = True,
134+
filtering: bool = False,
135135
):
136136
self.logger = logging.getLogger(__name__)
137137

utama_core/tests/common/abstract_test_manager.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,14 @@ def reset_field(self, sim_controller: AbstractSimController, game: Game):
4949
"""
5050
Method is called at start of each test episode in strategyRunner.run_test().
5151
Use this to reset position of robots and ball for the next episode.
52+
5253
Args:
5354
sim_controller (AbstractSimController): The simulation controller to manipulate robot positions.
5455
game (Game): The current game state.
56+
57+
Note:
58+
This causes run_test to initialise Game twice on the first episode.
59+
However, this is necessary to give the test manager access to the initial Game object before the first episode setup.
5560
"""
5661
...
5762

utama_core/tests/motion_planning/multiple_robots_test.py

Lines changed: 5 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -45,50 +45,13 @@ def __init__(self, scenario: MultiRobotScenario):
4545

4646
def reset_field(self, sim_controller: AbstractSimController, game: Game):
4747
"""Reset field with all robots at their starting positions."""
48-
# Teleport friendly robots to starting positions
4948
for i, (x, y) in enumerate(self.scenario.friendly_positions):
50-
if i < MAX_ROBOTS: # Max MAX_ROBOTS robots per team
51-
sim_controller.teleport_robot(
52-
game.my_team_is_yellow,
53-
i,
54-
x,
55-
y,
56-
0.0,
57-
)
58-
59-
# Teleport remaining friendly robots far away
60-
for i in range(len(self.scenario.friendly_positions), MAX_ROBOTS):
61-
sim_controller.teleport_robot(
62-
game.my_team_is_yellow,
63-
i,
64-
-10.0,
65-
-10.0,
66-
0.0,
67-
)
68-
69-
# Teleport enemy robots to starting positions
49+
sim_controller.teleport_robot(game.my_team_is_yellow, i, x, y, 0.0)
50+
7051
for i, (x, y) in enumerate(self.scenario.enemy_positions):
71-
if i < MAX_ROBOTS:
72-
sim_controller.teleport_robot(
73-
not game.my_team_is_yellow,
74-
i,
75-
x,
76-
y,
77-
0.0,
78-
)
79-
80-
# Teleport remaining enemy robots far away
81-
for i in range(len(self.scenario.enemy_positions), MAX_ROBOTS):
82-
sim_controller.teleport_robot(
83-
not game.my_team_is_yellow,
84-
i,
85-
-10.0,
86-
-10.0,
87-
0.0,
88-
)
89-
90-
# Place ball out of the way
91-
sim_controller.teleport_ball(-10.0, -10.0)
52+
sim_controller.teleport_robot(not game.my_team_is_yellow, i, x, y, 0.0)
53+
54+
sim_controller.teleport_ball(0.0, 0.0)
9255

9356
self._reset_metrics()
9457

utama_core/tests/motion_planning/random_movement_test.py

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -52,41 +52,16 @@ def reset_field(self, sim_controller: AbstractSimController, game: Game):
5252
"""Reset field with robots in random starting positions within bounds."""
5353
(min_x, max_x), (min_y, max_y) = self.scenario.field_bounds
5454

55-
# Place robots at random positions within bounds
55+
# Use a fixed seed for reproducibility across test runs
56+
rng = random.Random(42 + self.current_episode_number)
57+
5658
for i in range(self.scenario.n_robots):
57-
x = random.uniform(min_x + 0.5, max_x - 0.5)
58-
y = random.uniform(min_y + 0.5, max_y - 0.5)
59-
sim_controller.teleport_robot(
60-
game.my_team_is_yellow,
61-
i,
62-
x,
63-
y,
64-
0.0,
65-
)
59+
x = rng.uniform(min_x + 0.5, max_x - 0.5)
60+
y = rng.uniform(min_y + 0.5, max_y - 0.5)
61+
sim_controller.teleport_robot(game.my_team_is_yellow, i, x, y, 0.0)
6662
self.targets_reached_count[i] = 0
6763

68-
# Teleport remaining robots far away
69-
for i in range(self.scenario.n_robots, 6):
70-
sim_controller.teleport_robot(
71-
game.my_team_is_yellow,
72-
i,
73-
-10.0,
74-
-10.0,
75-
0.0,
76-
)
77-
78-
# Remove enemy robots
79-
for i in range(6):
80-
sim_controller.teleport_robot(
81-
not game.my_team_is_yellow,
82-
i,
83-
-10.0,
84-
-10.0,
85-
0.0,
86-
)
87-
88-
# Place ball out of the way
89-
sim_controller.teleport_ball(-10.0, -10.0)
64+
sim_controller.teleport_ball(0.0, 0.0)
9065

9166
self._reset_metrics()
9267

utama_core/tests/motion_planning/single_robot_moving_obstacle_test.py

Lines changed: 16 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -57,47 +57,22 @@ def __init__(self, scenario: MovingObstacleScenario, robot_id: int):
5757

5858
def reset_field(self, sim_controller: AbstractSimController, game: Game):
5959
"""Reset field with robot at start position and moving obstacles."""
60-
# Teleport ALL friendly robots off-field
61-
for i in range(6):
62-
if i == self.robot_id:
63-
# Place the test robot at start position
64-
sim_controller.teleport_robot(
65-
game.my_team_is_yellow,
66-
i,
67-
self.scenario.start_position[0],
68-
self.scenario.start_position[1],
69-
0.0,
70-
)
71-
else:
72-
# Move other friendly robots far away
73-
sim_controller.teleport_robot(
74-
game.my_team_is_yellow,
75-
i,
76-
-10.0,
77-
-10.0,
78-
0.0,
79-
)
80-
81-
# Place enemy robots at their center positions (they will start moving via their strategy)
82-
for i in range(6):
83-
if i < len(self.scenario.moving_obstacles):
84-
obstacle_config = self.scenario.moving_obstacles[i]
85-
sim_controller.teleport_robot(
86-
not game.my_team_is_yellow,
87-
i,
88-
obstacle_config.center_position[0],
89-
obstacle_config.center_position[1],
90-
0.0,
91-
)
92-
else:
93-
# Move extra enemy robots far away
94-
sim_controller.teleport_robot(
95-
not game.my_team_is_yellow,
96-
i,
97-
-10.0,
98-
-10.0,
99-
0.0,
100-
)
60+
sim_controller.teleport_robot(
61+
game.my_team_is_yellow,
62+
self.robot_id,
63+
self.scenario.start_position[0],
64+
self.scenario.start_position[1],
65+
0.0,
66+
)
67+
68+
for i, obstacle_config in enumerate(self.scenario.moving_obstacles):
69+
sim_controller.teleport_robot(
70+
not game.my_team_is_yellow,
71+
i,
72+
obstacle_config.center_position[0],
73+
obstacle_config.center_position[1],
74+
0.0,
75+
)
10176

10277
# Place ball at target (for visual reference)
10378
sim_controller.teleport_ball(

0 commit comments

Comments
 (0)