Filter vision detections to roster defined by vision→cmd mapping - #127
Conversation
PositionRefiner now accepts allowed_yellow_ids and allowed_blue_ids. Any robot ID reported by SSL-Vision that is not in the allowlist is dropped before it enters the game state, preventing stray detections from permanently accumulating in friendly_robots/enemy_robots. StrategyRunner derives the allowlists from yellow/blue vision_to_cmd_mapping keys so the roster is implicitly defined by what is configured in gefr.py (or any other runner) — no extra config needed.
There was a problem hiding this comment.
Pull request overview
This PR introduces optional per-team allowlists to prevent stray SSL-Vision robot detections (IDs not in the roster) from entering and accumulating in the GameFrame robot dictionaries, by deriving those allowlists from the configured vision→command ID mappings and filtering detections in PositionRefiner.
Changes:
- Plumbs
allowed_yellow_ids/allowed_blue_idsfromStrategyRunnerinto the refiner initialization pipeline. - Extends
PositionRefinerto filter vision robot detections (and prior-frame carryover) to the allowlisted IDs when provided. - Derives allowlists from the vision→cmd mapping keys (intended for real mode) to avoid additional configuration.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
utama_core/run/strategy_runner.py |
Derives and passes per-color allowlists into side/refiner setup so roster filtering can be enabled automatically from mappings. |
utama_core/data_processing/refiners/position.py |
Applies allowlist filtering to vision detections (and last-frame robot sets) to prevent non-roster IDs from entering/persisting in game state. |
| # Derive per-color roster allowlists from the vision→cmd mappings (real mode only). | ||
| # Any robot ID seen by vision that is not in the allowlist is silently dropped so that | ||
| # stray detections from robots not in play never pollute the game state. | ||
| _allowed_yellow = ( | ||
| frozenset(yellow_vision_to_cmd_mapping.keys()) if yellow_vision_to_cmd_mapping is not None else None | ||
| ) | ||
| _allowed_blue = frozenset(blue_vision_to_cmd_mapping.keys()) if blue_vision_to_cmd_mapping is not None else None | ||
|
|
There was a problem hiding this comment.
Fixed in commit 9f215d1: allowlists are now derived from self.yellow_vision_to_cmd_mapping / self.blue_vision_to_cmd_mapping — i.e. after _validate_vision_to_cmd_mapping has already run and normalised them. Ignored opponent mappings return {}, so frozenset({}) or None → None → no allowlist applied, consistent with the ignore semantics.
| if self.allowed_yellow_ids is not None: | ||
| yellow_vision_robots = [r for r in yellow_vision_robots if r.id in self.allowed_yellow_ids] | ||
| old_yellow_robots = {k: v for k, v in old_yellow_robots.items() if k in self.allowed_yellow_ids} | ||
| if self.allowed_blue_ids is not None: | ||
| blue_vision_robots = [r for r in blue_vision_robots if r.id in self.allowed_blue_ids] |
There was a problem hiding this comment.
Fixed in commit f85d874: added test_allowlist_filters_stray_yellow_robot and test_no_allowlist_passes_all_robots to position_unit_test.py covering exactly this case.
There was a problem hiding this comment.
Fixed in commit f85d874: added test_allowlist_filters_stray_yellow_robot and test_no_allowlist_passes_all_robots to position_unit_test.py covering exactly this case.
- Friendly vision_to_cmd_mapping is now required in real mode (single-team and PVP); previously only PVP enforced this. - Count check (mapping entries == exp_friendly/exp_enemy) now applies in single-team real mode, not just PVP. - Post-game-frame coverage check (_validate_mapping_covers_game_frame) now runs for single-team real mode, not just PVP. - Update tests: fix test that incorrectly assumed None mapping was valid for the friendly team; add 4 new tests for single-team validation paths.
Previously the allowlists passed to PositionRefiner were derived from raw (unvalidated) mapping params before _validate_vision_to_cmd_mapping ran, meaning a bad mapping could silently construct an incorrect refiner before raising. Also, rsim mode could temporarily get a non-None allowlist if a mapping was mistakenly provided. Fix: validate both mappings first, then derive frozenset allowlists from the validated self.*_vision_to_cmd_mapping dicts. Empty dict (rsim) → falsy frozenset → None (no filtering). Use opp_strategy as a sentinel for self.opp truthiness check during validation (overwritten immediately after by _setup_sides_data).
_validate_mapping_covers_game_frame now lives in GameGater as a static method and is called from wait_until_game_valid, after the first valid game frame is available. StrategyRunner passes the real-mode mappings in; non-real modes pass None so no check runs. This keeps all "is the game state valid?" logic in the one place that owns waiting for a valid initial frame, rather than split between GameGater (robot/ball count) and StrategyRunner._load_game (ID coverage).
…st tests When GameGater is waiting for a valid frame and an allowlist is active, the periodic status print now shows both the observed vision IDs and the expected IDs from the mapping so a misconfigured mapping is immediately visible in the terminal output rather than causing a silent hang. Also adds two PositionRefiner unit tests covering the allowlist path: one that verifies stray IDs are dropped when an allowlist is set, and one that verifies all IDs pass through when no allowlist is configured.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
utama_core/run/strategy_runner.py:352
- This comment still references
_validate_mapping_covers_game_frame()on StrategyRunner, but that helper was moved to GameGater. Updating the reference will prevent confusion when debugging mapping validation failures.
# At init time we only know how many robots to expect, not their
# actual vision IDs (those are non-contiguous in some deployments).
# Check count here; key-coverage against observed IDs happens in
# _validate_mapping_covers_game_frame() after _load_game().
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
utama_core/run/strategy_runner.py:352
- This comment still refers to
_validate_mapping_covers_game_frame()running “after _load_game()”, but that method was removed from StrategyRunner and the coverage check now happens insideGameGater.wait_until_game_valid()(viaGameGater._validate_mapping_covers_game_frame). The stale reference is misleading for future maintenance/debugging.
# At init time we only know how many robots to expect, not their
# actual vision IDs (those are non-contiguous in some deployments).
# Check count here; key-coverage against observed IDs happens in
# _validate_mapping_covers_game_frame() after _load_game().
| # if we are not running an opp strat, but the opponent-color mapping provided, warn it will be ignored | ||
| if self.opp is None and self.my_team_is_yellow ^ is_yellow: | ||
| warnings.warn( | ||
| "vision_to_cmd_mapping is provided but will be ignored since the opponent team is not being controlled." | ||
| ) | ||
|
|
Summary
Stricter real-mode validation:
Single-team real mode behaviour:
Test plan
New unit tests (42 → 77 passing)
🤖 Generated with Claude Code