From c8eba379ae177d10f3bb2ad762c2a167b4aef755 Mon Sep 17 00:00:00 2001 From: saurav Date: Fri, 13 Feb 2026 20:10:24 -0800 Subject: [PATCH 1/8] added trackers to track pass results --- .../thunderscope/log/stats/pass_results.py | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/software/thunderscope/log/stats/pass_results.py b/src/software/thunderscope/log/stats/pass_results.py index f5e86fb552..1bb55254e9 100644 --- a/src/software/thunderscope/log/stats/pass_results.py +++ b/src/software/thunderscope/log/stats/pass_results.py @@ -1,13 +1,32 @@ +from software.thunderscope.log.trackers import ( + PossessionTracker, + PassTracker, + TrackerBuilder, + RefereeTracker, + GoalieTracker, +) +from software.thunderscope.proto_unix_io import ProtoUnixIO + class PassResultsTracker: """Class to track the results of any passes taken i.e looking at if our position in the game got better or worse after certain time intervals """ - def __init__(self, friendly_colour_yellow: bool, buffer_size: int = 5): + def __init__(self, + proto_unix_io: ProtoUnixIO, friendly_colour_yellow: bool, buffer_size: int = 5): self.friendly_colour_yellow = friendly_colour_yellow - pass + self.tracker = ( + TrackerBuilder(proto_unix_io=proto_unix_io) + .add_tracker(PassTracker, callback=self._update_shot_count) + .add_tracker(PossessionTracker, callback=self._update_posession) + .add_tracker( + RefereeTracker, + callback=self._update_referee_info_friendly, + friendly_color_yellow=self.friendly_colour_yellow, + ) + ) def refresh(self) -> None: """Refreshes the kick tracker so we stay up to date on new passes""" From 1e6ba116da987cbdef58c50fe3ded40fd207a8c0 Mon Sep 17 00:00:00 2001 From: saurav Date: Sat, 14 Feb 2026 19:28:37 -0800 Subject: [PATCH 2/8] finshed pass results logger for intervals --- src/software/thunderscope/constants.py | 9 ++ .../thunderscope/log/stats/pass_results.py | 150 ++++++++++++++++-- src/software/thunderscope/log/stats/stats.py | 7 + 3 files changed, 157 insertions(+), 9 deletions(-) diff --git a/src/software/thunderscope/constants.py b/src/software/thunderscope/constants.py index 217d87a302..41f8845836 100644 --- a/src/software/thunderscope/constants.py +++ b/src/software/thunderscope/constants.py @@ -413,3 +413,12 @@ class RuntimeManagerConstants: RELEASES_URL = "https://api.github.com/repos/UBC-Thunderbots/Software/releases" DOWNLOAD_URL = "https://github.com/UBC-Thunderbots/Software/releases/download/" MAX_RELEASES_FETCHED = 5 + + +class PassResultsConstants: + PASS_RESULTS_DIRECTORY_PATH = "/tmp/tbots/ml" + PASS_RESULTS_FILE_NAME_TEMPLATE = "pass_results_$interval.csv" + + PASS_RESULTS_TEMPLATE = ( + "$pass_start_x,$pass_start_y" "$pass_end_x,$pass_end_y" "$speed,$score\n" + ) diff --git a/src/software/thunderscope/log/stats/pass_results.py b/src/software/thunderscope/log/stats/pass_results.py index 1bb55254e9..254a1d716e 100644 --- a/src/software/thunderscope/log/stats/pass_results.py +++ b/src/software/thunderscope/log/stats/pass_results.py @@ -3,9 +3,21 @@ PassTracker, TrackerBuilder, RefereeTracker, - GoalieTracker, ) from software.thunderscope.proto_unix_io import ProtoUnixIO +from datetime import datetime +from dataclasses import dataclass +from software.thunderscope.constants import PassResultsConstants +import os + + +@dataclass +class PassLog: + pass_: Pass + timestamp: datetime + friendly_score: int + enemy_score: int + class PassResultsTracker: """Class to track the results of any passes taken @@ -13,24 +25,144 @@ class PassResultsTracker: after certain time intervals """ - def __init__(self, - proto_unix_io: ProtoUnixIO, friendly_colour_yellow: bool, buffer_size: int = 5): + FRIENDLY_GOAL_SCORE = 10 + ENEMY_GOAL_SCORE = -FRIENDLY_GOAL_SCORE + FRIENDLY_POSSESSION_SCORE = 2 + ENEMY_POSSESSION_SCORE = -FRIENDLY_POSSESSION_SCORE + NEUTRAL_SCORE = 0 + + # the time intervals to log results for after each pass + # so after a pass, wait X seconds and then log game state + INTERVALS = [1, 5, 10] + + def __init__( + self, + proto_unix_io: ProtoUnixIO, + friendly_colour_yellow: bool, + buffer_size: int = 5, + ): self.friendly_colour_yellow = friendly_colour_yellow self.tracker = ( TrackerBuilder(proto_unix_io=proto_unix_io) - .add_tracker(PassTracker, callback=self._update_shot_count) - .add_tracker(PossessionTracker, callback=self._update_posession) + .add_tracker(PassTracker, callback=self._add_pass_timestamp) + .add_tracker(PossessionTracker, callback=self._update_friendly_possession) .add_tracker( RefereeTracker, - callback=self._update_referee_info_friendly, + callback=self._update_scores_friendly, friendly_color_yellow=self.friendly_colour_yellow, ) + .add_tracker( + RefereeTracker, + callback=self._update_scores_friendly, + friendly_color_yellow=(not self.friendly_colour_yellow), + ) + ) + + self.pass_times_map: dict[int, list[PassLog]] = { + interval: [] for interval in self.INTERVALS + } + + self.is_friendly_possession: bool | None = False + self.friendly_score = 0 + self.enemy_score = 0 + + self.pass_results_file_map = {} + + def setup(self): + pass_results_dir = PassResultsConstants.PASS_RESULTS_DIRECTORY_PATH + + # create all directories in path if they doesn't exist + os.makedirs(os.path.dirname(pass_results_dir), exist_ok=True) + + for interval in self.INTERVALS: + self.pass_results_file_map[interval] = open( + os.path.join( + pass_results_dir, + PassResultsConstants.PASS_RESULTS_FILE_NAME_TEMPLATE.format( + interval=interval + ), + ) + ) + + def cleanup(self): + for interval in self.INTERVALS: + if self.pass_results_file_map[interval]: + self.pass_results_file_map[interval].close() + + def _update_friendly_possession(self, is_friendly_possession: bool | None) -> None: + self.is_friendly_possession = is_friendly_possession + + def _update_scores_friendly(self, friendly_score: int, *_) -> None: + self.friendly_score = friendly_score + + def _update_scores_enemy(self, enemy_score: int, *_) -> None: + self.enemy_score = enemy_score + + def _add_pass_timestamp(self, pass_: Pass) -> None: + self.pass_times_map[self.INTERVALS[0]].append( + PassLog( + pass_=pass_, + timestamp=datetime.now(), + friendly_score=self.friendly_score, + enemy_score=self.enemy_score, + ) ) def refresh(self) -> None: """Refreshes the kick tracker so we stay up to date on new passes""" - pass + self.tracker.refresh() + + def _log_pass_result(self, logged_pass: PassLog, interval: int) -> None: + pass_score = self._get_pass_score(logged_pass) + + self._log_pass_result_to_file( + file=self.pass_results_file_map[interval], + pass_=logged_pass.pass_, + score=pass_score, + ) + + def _get_pass_score(self, logged_pass: PassLog) -> int: + if self.friendly_score > logged_pass.friendly_score: + return self.FRIENDLY_GOAL_SCORE + + if self.enemy_score > logged_pass.enemy_score: + return self.ENEMY_GOAL_SCORE + + if self.is_friendly_possession: + return self.FRIENDLY_POSSESSION_SCORE + elif self.is_friendly_possession is False: + return self.ENEMY_POSSESSION_SCORE + + return self.NEUTRAL_SCORE + + def _log_pass_result_to_file(self, file, pass_: Pass, score: int) -> None: + pass_result_string = PassResultsConstants.PASS_RESULTS_TEMPLATE.format( + pass_start_x=pass_.passer_point.x_meters, + pass_start_y=pass_.passer_point.y_meters, + pass_end_x=pass_.receiver_point.x_meters, + pass_end_y=pass_.receiver_point.y_meters, + speed=pass_.pass_speed_m_per_s, + score=score, + ) + + file.write(pass_result_string) + + def _update_pass_timestamps(self): + for idx, interval in enumerate(self.INTERVALS): + pass_timestamps = self.pass_times_map[idx] + + time_now = datetime.now() + + while ( + pass_timestamps + and (time_now - pass_timestamps[0].timestamp).total_seconds() > interval + ): + pass_with_timestamp = pass_timestamps.pop(0) + + self._log_pass_result(pass_with_timestamp, interval) - def record_pass_taken(self, pass_taken: Pass): - pass + if idx < len(self.INTERVALS) - 1: + self.pass_times_map[self.INTERVALS[idx + 1]].append( + pass_with_timestamp + ) diff --git a/src/software/thunderscope/log/stats/stats.py b/src/software/thunderscope/log/stats/stats.py index ecc110ac76..1c93e73c79 100644 --- a/src/software/thunderscope/log/stats/stats.py +++ b/src/software/thunderscope/log/stats/stats.py @@ -1,4 +1,5 @@ from software.thunderscope.log.stats.fullsystem_stats import FullSystemStats +from software.thunderscope.log.stats.pass_results import PassResultsTracker from software.thunderscope.proto_unix_io import ProtoUnixIO from proto.import_all_protos import * @@ -22,11 +23,17 @@ def __init__( record_enemy_stats=record_enemy_stats, ) + self.pass_results = PassResultsTracker( + proto_unix_io=proto_unix_io, friendly_colour_yellow=friendly_color_yellow + ) + def refresh(self): self.fs_stats.refresh() def __enter__(self): self.fs_stats.setup() + self.pass_results.setup() def __exit__(self, exc_type, exc_value, traceback): self.fs_stats.cleanup() + self.pass_results.cleanup() From a65e99d9789f8b0aaca12273f2096f9185c44494 Mon Sep 17 00:00:00 2001 From: Thunderbots Date: Mon, 16 Feb 2026 01:42:58 -0800 Subject: [PATCH 3/8] format --- src/software/thunderscope/constants.py | 2 +- src/software/thunderscope/log/stats/BUILD | 9 +++ .../thunderscope/log/stats/pass_results.py | 66 +++++++++++++++---- 3 files changed, 64 insertions(+), 13 deletions(-) diff --git a/src/software/thunderscope/constants.py b/src/software/thunderscope/constants.py index 41f8845836..6eed6bf18d 100644 --- a/src/software/thunderscope/constants.py +++ b/src/software/thunderscope/constants.py @@ -417,7 +417,7 @@ class RuntimeManagerConstants: class PassResultsConstants: PASS_RESULTS_DIRECTORY_PATH = "/tmp/tbots/ml" - PASS_RESULTS_FILE_NAME_TEMPLATE = "pass_results_$interval.csv" + PASS_RESULTS_FILE_NAME_TEMPLATE = "pass_results_{interval}.csv" PASS_RESULTS_TEMPLATE = ( "$pass_start_x,$pass_start_y" "$pass_end_x,$pass_end_y" "$speed,$score\n" diff --git a/src/software/thunderscope/log/stats/BUILD b/src/software/thunderscope/log/stats/BUILD index d0197aa479..132ee133eb 100644 --- a/src/software/thunderscope/log/stats/BUILD +++ b/src/software/thunderscope/log/stats/BUILD @@ -10,11 +10,20 @@ py_library( ], ) +py_library( + name = "pass_results", + srcs = ["pass_results.py"], + deps = [ + "//software/thunderscope/log/trackers:tracker_module", + ], +) + py_library( name = "stats", srcs = ["stats.py"], deps = [ ":fullsystem_stats", + ":pass_results", "//software/thunderscope:thread_safe_buffer", ], ) diff --git a/src/software/thunderscope/log/stats/pass_results.py b/src/software/thunderscope/log/stats/pass_results.py index 254a1d716e..d0cc4c98a4 100644 --- a/src/software/thunderscope/log/stats/pass_results.py +++ b/src/software/thunderscope/log/stats/pass_results.py @@ -9,6 +9,7 @@ from dataclasses import dataclass from software.thunderscope.constants import PassResultsConstants import os +from proto.import_all_protos import * @dataclass @@ -33,7 +34,7 @@ class PassResultsTracker: # the time intervals to log results for after each pass # so after a pass, wait X seconds and then log game state - INTERVALS = [1, 5, 10] + INTERVALS_S = [1, 5, 10] def __init__( self, @@ -41,6 +42,12 @@ def __init__( friendly_colour_yellow: bool, buffer_size: int = 5, ): + """Initializes the pass results tracker + + :param proto_unix_io: the proto unix io to use + :param friendly_colour_yellow: if the friendly color is yellow or not + :param buffer_size: buffer size to use + """ self.friendly_colour_yellow = friendly_colour_yellow self.tracker = ( @@ -60,7 +67,7 @@ def __init__( ) self.pass_times_map: dict[int, list[PassLog]] = { - interval: [] for interval in self.INTERVALS + interval: [] for interval in self.INTERVALS_S } self.is_friendly_possession: bool | None = False @@ -70,24 +77,30 @@ def __init__( self.pass_results_file_map = {} def setup(self): + """Creates the relevant directories and a csv file for each of the + intervals in INTERVALS + """ pass_results_dir = PassResultsConstants.PASS_RESULTS_DIRECTORY_PATH # create all directories in path if they doesn't exist os.makedirs(os.path.dirname(pass_results_dir), exist_ok=True) - for interval in self.INTERVALS: + for interval in self.INTERVALS_S: self.pass_results_file_map[interval] = open( os.path.join( pass_results_dir, PassResultsConstants.PASS_RESULTS_FILE_NAME_TEMPLATE.format( interval=interval ), - ) + ), + "a", ) def cleanup(self): - for interval in self.INTERVALS: + """Flushes content and closes all the files for all intervals""" + for interval in self.INTERVALS_S: if self.pass_results_file_map[interval]: + self.pass_results_file_map[interval].flush() self.pass_results_file_map[interval].close() def _update_friendly_possession(self, is_friendly_possession: bool | None) -> None: @@ -100,7 +113,10 @@ def _update_scores_enemy(self, enemy_score: int, *_) -> None: self.enemy_score = enemy_score def _add_pass_timestamp(self, pass_: Pass) -> None: - self.pass_times_map[self.INTERVALS[0]].append( + """Adds the given pass, the current timestamp, and the current scores to the lowest interval's list + :param pass_: the pass to add + """ + self.pass_times_map[self.INTERVALS_S[0]].append( PassLog( pass_=pass_, timestamp=datetime.now(), @@ -110,19 +126,35 @@ def _add_pass_timestamp(self, pass_: Pass) -> None: ) def refresh(self) -> None: - """Refreshes the kick tracker so we stay up to date on new passes""" + """Refreshes the tracker so we stay up to date on new passes + and checks to see if any passes are older than their interval + """ self.tracker.refresh() - def _log_pass_result(self, logged_pass: PassLog, interval: int) -> None: + def _log_pass_result(self, logged_pass: PassLog, interval_s: int) -> None: + """For an already recorded pass, calculates and logs its score for the given interval + i.e after seconds following the pass + + :param logged_pass: a pass that already occurred that we want to find the score for + :param interval_s: how long (in seconds) it has been after the pass + """ pass_score = self._get_pass_score(logged_pass) self._log_pass_result_to_file( - file=self.pass_results_file_map[interval], + file=self.pass_results_file_map[interval_s], pass_=logged_pass.pass_, score=pass_score, ) def _get_pass_score(self, logged_pass: PassLog) -> int: + """For the given logged pass, get the score based on the current game state + If the friendly / enemy scores at the time of pass are different + Or if possession has changed, return the corresponding score + Else, return the neutral score + + :param logged_pass: the pass to score + :return: a single integer score for the pass + """ if self.friendly_score > logged_pass.friendly_score: return self.FRIENDLY_GOAL_SCORE @@ -137,6 +169,12 @@ def _get_pass_score(self, logged_pass: PassLog) -> int: return self.NEUTRAL_SCORE def _log_pass_result_to_file(self, file, pass_: Pass, score: int) -> None: + """Logs a single pass's result to the given file handle + + :param file: the file handle to write to + :param pass_: the pass to log + :param score: the score for the given pass + """ pass_result_string = PassResultsConstants.PASS_RESULTS_TEMPLATE.format( pass_start_x=pass_.passer_point.x_meters, pass_start_y=pass_.passer_point.y_meters, @@ -149,7 +187,11 @@ def _log_pass_result_to_file(self, file, pass_: Pass, score: int) -> None: file.write(pass_result_string) def _update_pass_timestamps(self): - for idx, interval in enumerate(self.INTERVALS): + """For all currently logged passes, check if the interval they belong to has passed + If so, log their score to the corresponding file + And move them to the next interval if exists + """ + for idx, interval in enumerate(self.INTERVALS_S): pass_timestamps = self.pass_times_map[idx] time_now = datetime.now() @@ -162,7 +204,7 @@ def _update_pass_timestamps(self): self._log_pass_result(pass_with_timestamp, interval) - if idx < len(self.INTERVALS) - 1: - self.pass_times_map[self.INTERVALS[idx + 1]].append( + if idx < len(self.INTERVALS_S) - 1: + self.pass_times_map[self.INTERVALS_S[idx + 1]].append( pass_with_timestamp ) From 522db074cb46f2ae732fddc245d0b2b4ceebfadf Mon Sep 17 00:00:00 2001 From: Thunderbots Date: Mon, 16 Feb 2026 01:49:02 -0800 Subject: [PATCH 4/8] add pass results to stata refresh --- src/software/thunderscope/log/stats/stats.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/software/thunderscope/log/stats/stats.py b/src/software/thunderscope/log/stats/stats.py index 1dd94dd23b..3dbac1077a 100644 --- a/src/software/thunderscope/log/stats/stats.py +++ b/src/software/thunderscope/log/stats/stats.py @@ -29,6 +29,7 @@ def __init__( def refresh(self): self.fs_stats.refresh() + self.pass_results.refresh() def __enter__(self): self.fs_stats.setup() From bddb40923caafae0a0afa497febc540205542f5b Mon Sep 17 00:00:00 2001 From: Thunderbots Date: Mon, 16 Feb 2026 02:32:18 -0800 Subject: [PATCH 5/8] added headers for files --- src/software/thunderscope/constants.py | 12 ++- .../thunderscope/log/stats/pass_results.py | 99 +++++++++++++------ 2 files changed, 78 insertions(+), 33 deletions(-) diff --git a/src/software/thunderscope/constants.py b/src/software/thunderscope/constants.py index 6eed6bf18d..1ba4d0e4a5 100644 --- a/src/software/thunderscope/constants.py +++ b/src/software/thunderscope/constants.py @@ -419,6 +419,12 @@ class PassResultsConstants: PASS_RESULTS_DIRECTORY_PATH = "/tmp/tbots/ml" PASS_RESULTS_FILE_NAME_TEMPLATE = "pass_results_{interval}.csv" - PASS_RESULTS_TEMPLATE = ( - "$pass_start_x,$pass_start_y" "$pass_end_x,$pass_end_y" "$speed,$score\n" - ) + FRIENDLY_GOAL_SCORE = 10 + ENEMY_GOAL_SCORE = -FRIENDLY_GOAL_SCORE + FRIENDLY_POSSESSION_SCORE = 2 + ENEMY_POSSESSION_SCORE = -FRIENDLY_POSSESSION_SCORE + NEUTRAL_SCORE = 0 + + # the time intervals to log results for after each pass + # so after a pass, wait X seconds and then log game state + INTERVALS_S = [1, 5, 10] diff --git a/src/software/thunderscope/log/stats/pass_results.py b/src/software/thunderscope/log/stats/pass_results.py index d0cc4c98a4..b2eea5d10f 100644 --- a/src/software/thunderscope/log/stats/pass_results.py +++ b/src/software/thunderscope/log/stats/pass_results.py @@ -10,6 +10,8 @@ from software.thunderscope.constants import PassResultsConstants import os from proto.import_all_protos import * +from software.thunderscope.thread_safe_buffer import ThreadSafeBuffer +import software.python_bindings as tbots_cpp @dataclass @@ -26,15 +28,26 @@ class PassResultsTracker: after certain time intervals """ - FRIENDLY_GOAL_SCORE = 10 - ENEMY_GOAL_SCORE = -FRIENDLY_GOAL_SCORE - FRIENDLY_POSSESSION_SCORE = 2 - ENEMY_POSSESSION_SCORE = -FRIENDLY_POSSESSION_SCORE - NEUTRAL_SCORE = 0 - - # the time intervals to log results for after each pass - # so after a pass, wait X seconds and then log game state - INTERVALS_S = [1, 5, 10] + NUM_ROBOTS = 6 + FRIENDLY_POS_TEMPLATE = "{{friendly_{index}_x}},{{friendly_{index}_y}}" + ENEMY_POS_TEMPLATE = "{{enemy_{index}_x}},{{enemy_{index}_y}}" + + PASS_FEATURES_TEMPLATE = ( + "{pass_start_x},{pass_start_y}" + "{pass_end_x},{pass_end_y}" + "{speed}," + "{ball_x},{ball_y}" + f"{",".join(FRIENDLY_POS_TEMPLATE.format(index=index) for index in range(NUM_ROBOTS))}," + f"{",".join(FRIENDLY_POS_TEMPLATE.format(index=index) for index in range(NUM_ROBOTS))}," + "{score}\n" + ) + + PASS_RESULTS_TEMPLATE = ( + "{pass_start_x},{pass_start_y}" + "{pass_end_x},{pass_end_y}" + "{speed}," + "{score}\n" + ) def __init__( self, @@ -50,6 +63,9 @@ def __init__( """ self.friendly_colour_yellow = friendly_colour_yellow + self.world_buffer = ThreadSafeBuffer(buffer_size, World) + proto_unix_io.register_observer(World, self.world_buffer) + self.tracker = ( TrackerBuilder(proto_unix_io=proto_unix_io) .add_tracker(PassTracker, callback=self._add_pass_timestamp) @@ -67,7 +83,7 @@ def __init__( ) self.pass_times_map: dict[int, list[PassLog]] = { - interval: [] for interval in self.INTERVALS_S + interval: [] for interval in PassResultsConstants.INTERVALS_S } self.is_friendly_possession: bool | None = False @@ -76,6 +92,8 @@ def __init__( self.pass_results_file_map = {} + self.world = None + def setup(self): """Creates the relevant directories and a csv file for each of the intervals in INTERVALS @@ -85,20 +103,30 @@ def setup(self): # create all directories in path if they doesn't exist os.makedirs(os.path.dirname(pass_results_dir), exist_ok=True) - for interval in self.INTERVALS_S: - self.pass_results_file_map[interval] = open( - os.path.join( - pass_results_dir, - PassResultsConstants.PASS_RESULTS_FILE_NAME_TEMPLATE.format( - interval=interval - ), + for interval in PassResultsConstants.INTERVALS_S: + file_path = os.path.join( + pass_results_dir, + PassResultsConstants.PASS_RESULTS_FILE_NAME_TEMPLATE.format( + interval=interval ), + ) + + is_new_file = not os.path.exists(file_path) + + self.pass_results_file_map[interval] = open( + file_path, "a", ) + # write the headers first if the file doesn't already exist + if is_new_file: + self.pass_results_file_map[interval].write( + self._get_pass_result_headers() + ) + def cleanup(self): """Flushes content and closes all the files for all intervals""" - for interval in self.INTERVALS_S: + for interval in PassResultsConstants.INTERVALS_S: if self.pass_results_file_map[interval]: self.pass_results_file_map[interval].flush() self.pass_results_file_map[interval].close() @@ -116,7 +144,7 @@ def _add_pass_timestamp(self, pass_: Pass) -> None: """Adds the given pass, the current timestamp, and the current scores to the lowest interval's list :param pass_: the pass to add """ - self.pass_times_map[self.INTERVALS_S[0]].append( + self.pass_times_map[PassResultsConstants.INTERVALS_S[0]].append( PassLog( pass_=pass_, timestamp=datetime.now(), @@ -129,6 +157,11 @@ def refresh(self) -> None: """Refreshes the tracker so we stay up to date on new passes and checks to see if any passes are older than their interval """ + world_msg = self.world_buffer.get(block=False, return_cached=True) + + if world_msg: + self.world = tbots_cpp.World(world_msg) + self.tracker.refresh() def _log_pass_result(self, logged_pass: PassLog, interval_s: int) -> None: @@ -156,17 +189,23 @@ def _get_pass_score(self, logged_pass: PassLog) -> int: :return: a single integer score for the pass """ if self.friendly_score > logged_pass.friendly_score: - return self.FRIENDLY_GOAL_SCORE + return PassResultsConstants.FRIENDLY_GOAL_SCORE if self.enemy_score > logged_pass.enemy_score: - return self.ENEMY_GOAL_SCORE + return PassResultsConstants.ENEMY_GOAL_SCORE if self.is_friendly_possession: - return self.FRIENDLY_POSSESSION_SCORE + return PassResultsConstants.FRIENDLY_POSSESSION_SCORE elif self.is_friendly_possession is False: - return self.ENEMY_POSSESSION_SCORE + return PassResultsConstants.ENEMY_POSSESSION_SCORE + + return PassResultsConstants.NEUTRAL_SCORE + + def _get_pass_features_headers(self): + return self.PASS_FEATURES_TEMPLATE.replace("{", "").replace("}", "") - return self.NEUTRAL_SCORE + def _get_pass_result_headers(self): + return self.PASS_RESULTS_TEMPLATE.replace("{", "").replace("}", "") def _log_pass_result_to_file(self, file, pass_: Pass, score: int) -> None: """Logs a single pass's result to the given file handle @@ -175,7 +214,7 @@ def _log_pass_result_to_file(self, file, pass_: Pass, score: int) -> None: :param pass_: the pass to log :param score: the score for the given pass """ - pass_result_string = PassResultsConstants.PASS_RESULTS_TEMPLATE.format( + pass_result_string = self.PASS_RESULTS_TEMPLATE.format( pass_start_x=pass_.passer_point.x_meters, pass_start_y=pass_.passer_point.y_meters, pass_end_x=pass_.receiver_point.x_meters, @@ -191,7 +230,7 @@ def _update_pass_timestamps(self): If so, log their score to the corresponding file And move them to the next interval if exists """ - for idx, interval in enumerate(self.INTERVALS_S): + for idx, interval in enumerate(PassResultsConstants.INTERVALS_S): pass_timestamps = self.pass_times_map[idx] time_now = datetime.now() @@ -204,7 +243,7 @@ def _update_pass_timestamps(self): self._log_pass_result(pass_with_timestamp, interval) - if idx < len(self.INTERVALS_S) - 1: - self.pass_times_map[self.INTERVALS_S[idx + 1]].append( - pass_with_timestamp - ) + if idx < len(PassResultsConstants.INTERVALS_S) - 1: + self.pass_times_map[ + PassResultsConstants.INTERVALS_S[idx + 1] + ].append(pass_with_timestamp) From 4b92f31f447e89bd1d11d224722536c8dee20dc9 Mon Sep 17 00:00:00 2001 From: Thunderbots Date: Mon, 16 Feb 2026 02:32:47 -0800 Subject: [PATCH 6/8] removed headers for features --- .../thunderscope/log/stats/pass_results.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/software/thunderscope/log/stats/pass_results.py b/src/software/thunderscope/log/stats/pass_results.py index b2eea5d10f..5a9e48d711 100644 --- a/src/software/thunderscope/log/stats/pass_results.py +++ b/src/software/thunderscope/log/stats/pass_results.py @@ -28,20 +28,6 @@ class PassResultsTracker: after certain time intervals """ - NUM_ROBOTS = 6 - FRIENDLY_POS_TEMPLATE = "{{friendly_{index}_x}},{{friendly_{index}_y}}" - ENEMY_POS_TEMPLATE = "{{enemy_{index}_x}},{{enemy_{index}_y}}" - - PASS_FEATURES_TEMPLATE = ( - "{pass_start_x},{pass_start_y}" - "{pass_end_x},{pass_end_y}" - "{speed}," - "{ball_x},{ball_y}" - f"{",".join(FRIENDLY_POS_TEMPLATE.format(index=index) for index in range(NUM_ROBOTS))}," - f"{",".join(FRIENDLY_POS_TEMPLATE.format(index=index) for index in range(NUM_ROBOTS))}," - "{score}\n" - ) - PASS_RESULTS_TEMPLATE = ( "{pass_start_x},{pass_start_y}" "{pass_end_x},{pass_end_y}" @@ -201,9 +187,6 @@ def _get_pass_score(self, logged_pass: PassLog) -> int: return PassResultsConstants.NEUTRAL_SCORE - def _get_pass_features_headers(self): - return self.PASS_FEATURES_TEMPLATE.replace("{", "").replace("}", "") - def _get_pass_result_headers(self): return self.PASS_RESULTS_TEMPLATE.replace("{", "").replace("}", "") From 9f8bbcc13b84c0020f07cecf0f3e324d95da5270 Mon Sep 17 00:00:00 2001 From: Thunderbots Date: Mon, 16 Feb 2026 02:47:38 -0800 Subject: [PATCH 7/8] fixed file format + csv logging works now --- .../thunderscope/log/stats/pass_results.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/software/thunderscope/log/stats/pass_results.py b/src/software/thunderscope/log/stats/pass_results.py index 5a9e48d711..f8b9a8042b 100644 --- a/src/software/thunderscope/log/stats/pass_results.py +++ b/src/software/thunderscope/log/stats/pass_results.py @@ -29,8 +29,8 @@ class PassResultsTracker: """ PASS_RESULTS_TEMPLATE = ( - "{pass_start_x},{pass_start_y}" - "{pass_end_x},{pass_end_y}" + "{pass_start_x},{pass_start_y}," + "{pass_end_x},{pass_end_y}," "{speed}," "{score}\n" ) @@ -41,7 +41,7 @@ def __init__( friendly_colour_yellow: bool, buffer_size: int = 5, ): - """Initializes the pass results tracker + """Initializes the pass resuidxlts tracker :param proto_unix_io: the proto unix io to use :param friendly_colour_yellow: if the friendly color is yellow or not @@ -109,6 +109,7 @@ def setup(self): self.pass_results_file_map[interval].write( self._get_pass_result_headers() ) + self.pass_results_file_map[interval].flush() def cleanup(self): """Flushes content and closes all the files for all intervals""" @@ -130,6 +131,7 @@ def _add_pass_timestamp(self, pass_: Pass) -> None: """Adds the given pass, the current timestamp, and the current scores to the lowest interval's list :param pass_: the pass to add """ + # TODO: use world timestamp time instead of datetime time self.pass_times_map[PassResultsConstants.INTERVALS_S[0]].append( PassLog( pass_=pass_, @@ -150,6 +152,8 @@ def refresh(self) -> None: self.tracker.refresh() + self._update_pass_timestamps() + def _log_pass_result(self, logged_pass: PassLog, interval_s: int) -> None: """For an already recorded pass, calculates and logs its score for the given interval i.e after seconds following the pass @@ -207,6 +211,7 @@ def _log_pass_result_to_file(self, file, pass_: Pass, score: int) -> None: ) file.write(pass_result_string) + file.flush() def _update_pass_timestamps(self): """For all currently logged passes, check if the interval they belong to has passed @@ -214,8 +219,9 @@ def _update_pass_timestamps(self): And move them to the next interval if exists """ for idx, interval in enumerate(PassResultsConstants.INTERVALS_S): - pass_timestamps = self.pass_times_map[idx] + pass_timestamps = self.pass_times_map[interval] + # TODO: use world timestamp time instead of datetime time time_now = datetime.now() while ( @@ -223,6 +229,9 @@ def _update_pass_timestamps(self): and (time_now - pass_timestamps[0].timestamp).total_seconds() > interval ): pass_with_timestamp = pass_timestamps.pop(0) + print( + f"Pass {pass_with_timestamp.pass_} is older than interval {interval}" + ) self._log_pass_result(pass_with_timestamp, interval) From decac945708b5edcc390fc07aa8a7809c7ced9b8 Mon Sep 17 00:00:00 2001 From: Thunderbots Date: Sat, 21 Feb 2026 15:54:50 -0800 Subject: [PATCH 8/8] removed world buffer --- src/software/thunderscope/log/stats/pass_results.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/software/thunderscope/log/stats/pass_results.py b/src/software/thunderscope/log/stats/pass_results.py index f8b9a8042b..88457eb3cb 100644 --- a/src/software/thunderscope/log/stats/pass_results.py +++ b/src/software/thunderscope/log/stats/pass_results.py @@ -10,8 +10,6 @@ from software.thunderscope.constants import PassResultsConstants import os from proto.import_all_protos import * -from software.thunderscope.thread_safe_buffer import ThreadSafeBuffer -import software.python_bindings as tbots_cpp @dataclass @@ -49,9 +47,6 @@ def __init__( """ self.friendly_colour_yellow = friendly_colour_yellow - self.world_buffer = ThreadSafeBuffer(buffer_size, World) - proto_unix_io.register_observer(World, self.world_buffer) - self.tracker = ( TrackerBuilder(proto_unix_io=proto_unix_io) .add_tracker(PassTracker, callback=self._add_pass_timestamp) @@ -145,11 +140,6 @@ def refresh(self) -> None: """Refreshes the tracker so we stay up to date on new passes and checks to see if any passes are older than their interval """ - world_msg = self.world_buffer.get(block=False, return_cached=True) - - if world_msg: - self.world = tbots_cpp.World(world_msg) - self.tracker.refresh() self._update_pass_timestamps()