diff --git a/.gitignore b/.gitignore index 076a0b3477..6748ed8402 100644 --- a/.gitignore +++ b/.gitignore @@ -58,7 +58,7 @@ src/external/external # Misc. Log Files *.log -*.csv + ### Node ### @@ -162,3 +162,4 @@ src/external # macOS folder info .DS_Store +*_venv/ diff --git a/scripts/compile_pip_requirements.sh b/scripts/compile_pip_requirements.sh index baa1946e74..2e9cd417a2 100755 --- a/scripts/compile_pip_requirements.sh +++ b/scripts/compile_pip_requirements.sh @@ -20,3 +20,4 @@ bazel run //software/embedded/ansible:requirements.update bazel run //software/simulated_tests:requirements.update bazel run //software/embedded/robot_diagnostics_cli:requirements.update bazel run //starlark/nanopb:requirements.update +bazel run //software/ml:requirements.update diff --git a/src/MODULE.bazel b/src/MODULE.bazel index d793364852..6ee8979337 100644 --- a/src/MODULE.bazel +++ b/src/MODULE.bazel @@ -70,6 +70,13 @@ pip.parse( requirements_lock = "//starlark/nanopb:requirements_lock.txt", ) use_repo(pip, "nanopb_deps") +pip.parse( + hub_name = "ml_deps", + python_interpreter = "/opt/tbotspython/bin/python", + python_version = "3.12", + requirements_lock = "//software/ml:requirements_lock.txt", +) +use_repo(pip, "ml_deps") ############################################## # Configure Dependencies diff --git a/src/software/evaluation/loggers/BUILD b/src/software/evaluation/loggers/BUILD new file mode 100644 index 0000000000..d6d6c4edd3 --- /dev/null +++ b/src/software/evaluation/loggers/BUILD @@ -0,0 +1,17 @@ +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "stats_logger", + srcs = ["stats_logger.py"], + deps = [ + "//software/evaluation/trackers:tracker", + ], +) + +py_library( + name = "pass_logger", + srcs = ["pass_logger.py"], + deps = [ + "//software/evaluation/trackers:tracker", + ], +) diff --git a/src/software/evaluation/loggers/pass_logger.py b/src/software/evaluation/loggers/pass_logger.py new file mode 100644 index 0000000000..1148367b76 --- /dev/null +++ b/src/software/evaluation/loggers/pass_logger.py @@ -0,0 +1,123 @@ +from software.thunderscope.proto_unix_io import ProtoUnixIO +from software.thunderscope.constants import PassResultsConstants +import os +from proto.import_all_protos import * +from software.evaluation.logs.event_log import ( + Team as TeamEnum, +) +from software.evaluation.trackers.pass_log_tracker import ( + PassLogTracker, +) +from software.evaluation.logs.pass_log import ( + PassLog, +) +import queue + + +class PassLogger: + """Class to track passes. + + i.e When a pass happens, we want to log the game state and the pass itself + As well as the game state at certain intervals after the pass + to see the outcomes of the pass + """ + + EVENT_BUFFER_SIZE = 100 + + def __init__( + self, + proto_unix_io: ProtoUnixIO, + friendly_colour_yellow: bool, + out_file_name: str | None = None, + 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 out_file_name: name of file to write pass results to. + If None, uses the value from constants + :param buffer_size: buffer size to use + """ + self.friendly_colour_yellow = friendly_colour_yellow + + # track pass results to this queue + self.pass_result_queue = queue.Queue(self.EVENT_BUFFER_SIZE) + + self.pass_tracker = PassLogTracker( + proto_unix_io=proto_unix_io, + from_team=( + TeamEnum.YELLOW if self.friendly_colour_yellow else TeamEnum.BLUE + ), + event_queue=self.pass_result_queue, + buffer_size=buffer_size, + ) + + self.events_file_path = os.path.join( + PassResultsConstants.PASS_RESULTS_DIRECTORY_PATH, + PassResultsConstants.PASS_RESULTS_FILE_NAME + if out_file_name is None + else out_file_name, + ) + self.events_file_handle = None + + def _get_team(self, is_friendly: bool) -> TeamEnum: + """Gets the correct Team enum value for either the current friendly or enemy team + + :param is_friendly: whether to return the team for friendly or enemy + :return: the corresponding Team Enum value + """ + return ( + TeamEnum.YELLOW + if (self.friendly_colour_yellow == is_friendly) + else TeamEnum.BLUE + ) + + def __enter__(self): + """Creates any missing directories and opens an append file handle""" + # create temp stats directory if it doesn't exist + os.makedirs(os.path.dirname(self.events_file_path), exist_ok=True) + + self.events_file_handle = open(self.events_file_path, "a") + + return self + + def __exit__(self, exc_type, exc_value, traceback): + """Flushes content and closes the log file""" + if self.events_file_handle: + self.events_file_handle.flush() + self.events_file_handle.close() + + 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 + """ + self.pass_tracker.refresh() + + if not self.events_file_handle: + return + + while not self.pass_result_queue.empty(): + try: + # Get item without blocking + pass_log = self.pass_result_queue.get_nowait() + + self._log_pass(pass_log) + except queue.Empty: + return + + def _log_pass(self, pass_log: PassLog): + """Logs a single pass to file + + :param result: the result to log + """ + if not self.events_file_handle: + return + + try: + csv_row = pass_log.to_csv_row() + self.events_file_handle.write(csv_row + "\n") + self.events_file_handle.flush() + + except (IOError, FileNotFoundError, PermissionError): + pass diff --git a/src/software/evaluation/loggers/stats_logger.py b/src/software/evaluation/loggers/stats_logger.py new file mode 100644 index 0000000000..5067a7ca19 --- /dev/null +++ b/src/software/evaluation/loggers/stats_logger.py @@ -0,0 +1,166 @@ +import os + +from software.evaluation.trackers import ( + PossessionTracker, + ShotTracker, + PassTracker, + TrackerBuilder, + RefereeTracker, + GoalieTracker, +) +from dataclasses import dataclass +from software.thunderscope.proto_unix_io import ProtoUnixIO +from software.thunderscope.constants import RuntimeManagerConstants +from software.evaluation.logs.event_log import Team as TeamEnum, EventLog +import logging +from proto.import_all_protos import * +import queue + + +@dataclass +class FSStats: + """Stats for how well a FullSystem is performing""" + + num_yellow_cards: int = 0 + num_red_cards: int = 0 + num_scores: int = 0 + + num_shots_on_net: int = 0 + num_enemy_shots_blocked: int = 0 + + +class StatsLogger: + # From GoalieTacticConfig + INCOMING_SHOT_MIN_VELOCITY = 0.2 + + EVENT_BUFFER_SIZE = 100 + + def __init__( + self, + proto_unix_io: ProtoUnixIO, + friendly_colour_yellow: bool, + out_file_name: str | None = None, + buffer_size: int = 5, + record_enemy_stats: bool = False, + ): + """Initializes the FullSystem Stats Tracker + + :param friendly_colour_yellow: if the friendly colour is yellow + :param out_file_name: name of file to write stats to. + If None, uses the value from constants + :param buffer_size: the buffer size for protocol buffers + :param record_enemy_stats: if this should record both friendly and enemy stats or just friendly + """ + self.friendly_colour_yellow = friendly_colour_yellow + + self.events_file_path = os.path.join( + RuntimeManagerConstants.RUNTIME_EVENTS_DIRECTORY_PATH, + RuntimeManagerConstants.RUNTIME_EVENTS_FILE + if out_file_name is None + else out_file_name, + ) + # initialized in setup() + self.events_file_handle = None + + self.event_queue = queue.Queue(self.EVENT_BUFFER_SIZE) + + # flag to turn off logging stats if needed + self.logging_enabled = True + + self.tracker = ( + TrackerBuilder( + proto_unix_io=proto_unix_io, + from_team=( + TeamEnum.YELLOW if self.friendly_colour_yellow else TeamEnum.BLUE + ), + event_queue=self.event_queue, + buffer_size=buffer_size, + ) + .add_tracker(PassTracker) + .add_tracker(ShotTracker) + .add_tracker(PossessionTracker) + .add_tracker( + RefereeTracker, + friendly_color_yellow=self.friendly_colour_yellow, + toggle_logging=self._toggle_logging, + ) + .add_tracker(GoalieTracker, for_friendly=True) + ) + + self.record_enemy_stats = record_enemy_stats + if self.record_enemy_stats: + self.enemy_tracker = ( + TrackerBuilder( + proto_unix_io=proto_unix_io, + from_team=( + TeamEnum.YELLOW + if self.friendly_colour_yellow + else TeamEnum.BLUE + ), + for_team=( + TeamEnum.BLUE + if self.friendly_colour_yellow + else TeamEnum.YELLOW + ), + event_queue=self.event_queue, + buffer_size=buffer_size, + ) + .add_tracker( + RefereeTracker, + friendly_color_yellow=(not self.friendly_colour_yellow), + toggle_logging=self._toggle_logging, + ) + .add_tracker(GoalieTracker, for_friendly=False) + ) + + def refresh(self) -> None: + """Refreshes the events for the game so far""" + self.tracker.refresh() + + if not self.events_file_handle: + return + + while not self.event_queue.empty(): + try: + # Get item without blocking + event = self.event_queue.get_nowait() + + self._write_event_to_file(event) + except queue.Empty: + return + + def __enter__(self): + """Sets up the file resources for logging + Creates any missing directories and stores the file handle + """ + # create temp stats directory if it doesn't exist + os.makedirs(os.path.dirname(self.events_file_path), exist_ok=True) + + self.events_file_handle = open(self.events_file_path, "a") + + return self + + def __exit__(self, exc_type, exc_value, traceback): + """Writes all logs back to file, and cleans up any created file resources after logging""" + if self.events_file_handle: + self.events_file_handle.flush() + self.events_file_handle.close() + + def _toggle_logging(self, should_log: bool) -> None: + self.logging_enabled = should_log + + def _write_event_to_file(self, event: EventLog) -> None: + """Write the given stats to the given file + + :param event: the event to write + """ + if not self.events_file_handle: + return + + try: + csv_row = event.to_csv_row() + self.events_file_handle.write(csv_row + "\n") + self.events_file_handle.flush() + + except (IOError, FileNotFoundError, PermissionError): + logging.warning("Failed to write event to file") diff --git a/src/software/evaluation/logs/BUILD b/src/software/evaluation/logs/BUILD new file mode 100644 index 0000000000..a3c88d88a6 --- /dev/null +++ b/src/software/evaluation/logs/BUILD @@ -0,0 +1,49 @@ +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "log_interface", + srcs = ["log_interface.py"], + deps = [ + "//proto:import_all_protos", + "//software/thunderscope:time_provider", + ], +) + +py_library( + name = "world_state_log", + srcs = ["world_state_log.py"], + data = [ + "//software:py_constants.so", + ], + deps = [ + ":log_interface", + ], +) + +py_library( + name = "event_log", + srcs = ["event_log.py"], + deps = [ + ":log_interface", + ":world_state_log", + ], +) + +py_library( + name = "pass_log", + srcs = ["pass_log.py"], + deps = [ + ":event_log", + ":log_interface", + ], +) + +py_library( + name = "logs", + deps = [ + ":event_log", + ":log_interface", + ":pass_log", + ":world_state_log", + ], +) diff --git a/src/software/evaluation/logs/event_log.py b/src/software/evaluation/logs/event_log.py new file mode 100644 index 0000000000..56ccfb370b --- /dev/null +++ b/src/software/evaluation/logs/event_log.py @@ -0,0 +1,108 @@ +from __future__ import annotations +from dataclasses import dataclass +from enum import StrEnum, auto +from proto.import_all_protos import * +from typing import Any, override +from software.evaluation.logs.log_interface import TimestampedEvalLog +from software.evaluation.logs.world_state_log import WorldStateLog + + +class EventType(StrEnum): + """Enum for the different types of events we want to track""" + + PASS = auto() + SHOT_ON_GOAL = auto() + ENEMY_SHOT_ON_GOAL = auto() + SHOT_BLOCKED = auto() + FRIENDLY_POSSESSION_START = auto() + FRIENDLY_POSSESSION_END = auto() + ENEMY_POSSESSION_START = auto() + ENEMY_POSSESSION_END = auto() + GAME_START = auto() + GAME_END = auto() + GOAL_SCORED = auto() + YELLOW_CARD = auto() + RED_CARD = auto() + + +class Team(StrEnum): + """The teams present in the game""" + + BLUE = auto() + YELLOW = auto() + + +@dataclass(kw_only=True) +class EventLog(TimestampedEvalLog): + """Represents a single event being tracked, where and for whom the event is, and the game state at the time of the event""" + + event_type: EventType + from_team: Team + for_team: Team + world_state_log: WorldStateLog + + num_cols = TimestampedEvalLog.get_num_cols() + 3 + WorldStateLog.get_num_cols() + + @staticmethod + def from_world( + world_msg: World, event_type: EventType, from_team: Team, for_team: Team + ) -> EventLog: + """Creates an EventLog from a world protobuf message + + :param world_msg: the world object containing the state of the game + :param event_type: the type of event being recorded + :param from_team: the team that the event is coming from + :param for_team: the team that the event is for + :return: a fully populated EventLog including world state + """ + world_state_log = WorldStateLog.from_world(world_msg=world_msg) + + return EventLog( + event_type=event_type, + from_team=from_team, + for_team=for_team, + world_state_log=world_state_log, + ) + + @override + @classmethod + def get_num_cols(cls) -> int: + return EventLog.num_cols + + @override + def to_array(self) -> list[Any]: + return ( + super().to_array() + + [ + self.event_type.value, + self.from_team.value, + self.for_team.value, + ] + + self.world_state_log.to_array() + ) + + @staticmethod + @override + def from_csv_row(row_iter: Iterator[str]) -> EventLog | None: + """Parses a full CSV row into an EventLog.""" + # 1. Handle Timestamp (inherited from TimestampedEvalLog) + timestamp = float(next(row_iter)) + + # 2. Parse Enums/Metadata + event_type = EventType(next(row_iter)) + from_team = Team(next(row_iter)) + for_team = Team(next(row_iter)) + + # 3. Delegate the remaining iterator to WorldStateLog + world_state = WorldStateLog.from_csv_row(row_iter) + + if not world_state: + return None + + return EventLog( + timestamp=timestamp, + event_type=event_type, + from_team=from_team, + for_team=for_team, + world_state_log=world_state, + ) diff --git a/src/software/evaluation/logs/log_interface.py b/src/software/evaluation/logs/log_interface.py new file mode 100644 index 0000000000..728af35adb --- /dev/null +++ b/src/software/evaluation/logs/log_interface.py @@ -0,0 +1,85 @@ +from __future__ import annotations +from abc import abstractmethod, ABC +from dataclasses import dataclass, field +from proto.import_all_protos import * +from typing import Iterator, Any, override +from google.protobuf.descriptor import Descriptor, FieldDescriptor +from software.thunderscope.time_provider import time_provider_instance + + +def count_primitive_fields(descriptor: Descriptor): + """Recursively counts the number of primitive fields in a Protobuf message + using its descriptor. + + :param message: the message descriptor to count all leaf-level primitive fields for + :return: the count of primitive fields + """ + count = 0 + + for field in descriptor.fields: + # Check if the field is a nested message + if field.type == FieldDescriptor.TYPE_MESSAGE: + # Get the nested message class to recurse into its descriptor + nested_message = field.message_type + # Recurse using the nested message's descriptor + count += count_primitive_fields(nested_message) + else: + # It's a primitive type (double, float, int, bool, string, etc.) + count += 1 + return count + + +class IEvalLog(ABC): + @classmethod + @abstractmethod + def get_num_cols(cls) -> int: + """Gets the number of columns present in this log""" + raise NotImplementedError("Please use the appropriate subclass of log!") + + @abstractmethod + def to_array(self) -> list[Any]: + """Converts this log to an array of elements""" + raise NotImplementedError("Please use the appropriate subclass of log!") + + def to_csv_row(self): + """Converts this log into a Comma Separated Values string + + :return: a string of values separated by commas + """ + row_array = self.to_array() + assert len(row_array) == type(self).get_num_cols() + + return ",".join([str(elem) for elem in row_array]) + + @staticmethod + @abstractmethod + def from_csv_row(row_iter: Iterator[str], **kwargs) -> IEvalLog | None: + """Converts a CSV row into an instance of this log + + :param row_iter: an iterator representing a csv row, which returns elements one by one + :param **kwargs: any extra arguments needed for this log not present in the csv row + """ + raise NotImplementedError("Please use the appropriate subclass of log!") + + +@dataclass +class TimestampedEvalLog(IEvalLog): + timestamp: float = field(default_factory=time_provider_instance.elapsed_time_ns) + + def get_timestamp(self) -> float: + """Get this log's timestamp""" + return self.timestamp + + @classmethod + @override + def get_num_cols(cls) -> int: + return 1 + + @override + def to_array(self) -> list[Any]: + return [self.timestamp] + + @staticmethod + @override + def from_csv_row(row_iter: Iterator[str], **kwargs) -> TimestampedEvalLog | None: + return TimestampedEvalLog(timestamp=float(next(row_iter))) diff --git a/src/software/evaluation/logs/pass_log.py b/src/software/evaluation/logs/pass_log.py new file mode 100644 index 0000000000..1d5ea865f1 --- /dev/null +++ b/src/software/evaluation/logs/pass_log.py @@ -0,0 +1,135 @@ +from __future__ import annotations +from software.evaluation.logs.log_interface import ( + TimestampedEvalLog, + count_primitive_fields, +) +from software.evaluation.logs.event_log import EventLog, EventType +from dataclasses import dataclass +from enum import IntEnum +from proto.import_all_protos import * +import uuid +from typing import override + + +class PassLogType(IntEnum): + """Enum for the different types of pass results we want to log + + Each value consists of a label and an interval in seconds + """ + + RESULT_1S = 1 + RESULT_5S = 5 + RESULT_10S = 10 + RESULT_20S = 20 + RESULT_30S = 30 + RESULT_0S = 0 + + +@dataclass(kw_only=True) +class PassLog(TimestampedEvalLog): + """Class representing a Pass we are tracking + Contains the pass event itself within the nested EventLog + along with information about the pass itself + """ + + # since the same pass is logged multiple times, + # we assign an id to make it easier to correlate + pass_id: uuid.UUID + + pass_event: EventLog + pass_log_type: PassLogType + pass_: Pass + + num_cols: int = ( + EventLog.get_num_cols() + 2 + count_primitive_fields(Pass.DESCRIPTOR) + ) + + def get_pass_start_point(self) -> list[float]: + """Returns the current pass start position as a [float, float] array + represnting x, y coordinates + """ + return [self.pass_.passer_point.x_meters, self.pass_.passer_point.y_meters] + + def get_pass_end_point(self) -> list[float]: + """Returns the current pass end position as a [float, float] array + represnting x, y coordinates + """ + return [self.pass_.receiver_point.x_meters, self.pass_.receiver_point.y_meters] + + def get_pass_speed(self) -> float: + return self.pass_.pass_speed_m_per_s + + @classmethod + @override + def get_num_cols(cls) -> int: + return PassLog.num_cols + + @staticmethod + def from_world_and_pass( + pass_id: uuid.UUID, + pass_: Pass, + pass_result_type: PassLogType, + world_msg: World, + team: Team, + ) -> PassLog: + pass_event = EventLog.from_world( + world_msg=world_msg, + event_type=EventType.PASS, + from_team=team, + for_team=team, + ) + + return PassLog( + timestamp=pass_event.get_timestamp(), + pass_id=pass_id, + pass_event=pass_event, + pass_=pass_, + pass_log_type=pass_result_type, + ) + + @override + def to_array(self): + pass_event_array = self.pass_event.to_array() + + pass_array = ( + [self.pass_id, self.pass_log_type] + + self.get_pass_start_point() + + self.get_pass_end_point() + + [self.pass_.pass_speed_m_per_s] + ) + + return pass_event_array + pass_array + + @staticmethod + @override + def from_csv_row(row_iter: Iterator[str]) -> PassLog | None: + # 1. Delegate to EventLog to parse the event and world state + pass_event = EventLog.from_csv_row(row_iter) + + if not pass_event: + return None + + # 2. Parse Pass ID + pass_id = uuid.UUID(next(row_iter)) + + # 3. Parse PassLogType (Enum) + pass_result_type = PassLogType(int(next(row_iter))) + + # 4. Parse the Pass message fields + pass_msg = Pass( + passer_point=Point( + x_meters=float(next(row_iter)), y_meters=float(next(row_iter)) + ), + receiver_point=Point( + x_meters=float(next(row_iter)), y_meters=float(next(row_iter)) + ), + pass_speed_m_per_s=float(next(row_iter)), + ) + + return PassLog( + pass_id=pass_id, + timestamp=pass_event.get_timestamp(), + pass_event=pass_event, + pass_log_type=pass_result_type, + pass_=pass_msg, + ) diff --git a/src/software/evaluation/logs/world_state_log.py b/src/software/evaluation/logs/world_state_log.py new file mode 100644 index 0000000000..b921e3ec61 --- /dev/null +++ b/src/software/evaluation/logs/world_state_log.py @@ -0,0 +1,209 @@ +from __future__ import annotations +from dataclasses import dataclass +from proto.import_all_protos import * +from software.evaluation.logs.log_interface import IEvalLog, count_primitive_fields +from software.py_constants import DIV_B_NUM_ROBOTS +from typing import Any, Iterator, override + + +@dataclass +class RobotLog(IEvalLog): + """Represents a single robot on the field, with ID and current state.""" + + id: int + state: RobotState + + num_cols: int = count_primitive_fields(RobotState.DESCRIPTOR) + + @classmethod + @override + def get_num_cols(cls) -> int: + return RobotLog.num_cols + + def get_position(self) -> list[float]: + """Returns the current ball position as a [float, float] array + represnting x, y coordinates + """ + return [ + self.state.global_position.x_meters, + self.state.global_position.y_meters, + ] + + @override + def to_array(self) -> list[Any]: + return self.get_position() + [ + self.state.global_orientation.radians, + self.state.global_velocity.x_component_meters, + self.state.global_velocity.y_component_meters, + self.state.global_angular_velocity.radians_per_second, + ] + + @staticmethod + @override + def from_csv_row(row_iter: Iterator[str], id: int = 0) -> RobotLog | None: + data = [next(row_iter) for _ in range(RobotLog.num_cols)] + + # Check if this was a placeholder (all 'None' strings) + if all(val == "None" for val in data): + return None + + state = RobotState( + global_position=Point(x_meters=float(data[0]), y_meters=float(data[1])), + global_orientation=Angle(radians=float(data[2])), + global_velocity=Vector( + x_component_meters=float(data[3]), y_component_meters=float(data[4]) + ), + global_angular_velocity=AngularVelocity(radians_per_second=float(data[5])), + ) + + # Since we don't store ID in the CSV row per your to_csv_row, + # the ID is usually inferred by the caller based on index + return RobotLog(id=id, state=state) + + +@dataclass +class BallLog(IEvalLog): + """Represents a single ball on the field.""" + + state: BallState + + num_cols: int = count_primitive_fields(BallState.DESCRIPTOR) - 1 + + def get_position(self) -> list[float]: + """Returns the current ball position as a [float, float] array + represnting x, y coordinates + """ + return [ + self.state.global_position.x_meters, + self.state.global_position.y_meters, + ] + + @classmethod + @override + def get_num_cols(cls) -> int: + return BallLog.num_cols + + @override + def to_array(self) -> list[Any]: + return self.get_position() + [ + self.state.global_velocity.x_component_meters, + self.state.global_velocity.y_component_meters, + ] + + @staticmethod + @override + def from_csv_row(row_iter: Iterator[str]) -> BallLog | None: + """Consumes columns from the iterator to reconstruct the Ball.""" + data = [next(row_iter) for _ in range(BallLog.num_cols)] + + state = BallState( + global_position=Point(x_meters=float(data[0]), y_meters=float(data[1])), + global_velocity=Vector( + x_component_meters=float(data[2]), y_component_meters=float(data[3]) + ), + distance_from_ground=0.0, + ) + + return BallLog(state=state) + + +@dataclass +class WorldStateLog(IEvalLog): + """Represents the current state of the world""" + + ball_state: BallLog + friendly_robots: list[RobotLog] + enemy_robots: list[RobotLog] + + num_cols: int = ( + DIV_B_NUM_ROBOTS * RobotLog.get_num_cols() * 2 + BallLog.get_num_cols() + ) + + @staticmethod + def from_world(world_msg: World) -> WorldStateLog: + """Creates a WorldStateLog from a world protobuf message + + :param world_msg: the world object containing the state of the game + :return: a fully populated WorldStateLog including ball and robot states + """ + ball_state = BallLog(state=world_msg.ball.current_state) + + friendly_robots = [ + RobotLog(id=robot.id, state=robot.current_state) + for robot in world_msg.friendly_team.team_robots + ] + enemy_robots = [ + RobotLog(id=robot.id, state=robot.current_state) + for robot in world_msg.enemy_team.team_robots + ] + + return WorldStateLog( + ball_state=ball_state, + friendly_robots=friendly_robots, + enemy_robots=enemy_robots, + ) + + @classmethod + @override + def get_num_cols(cls) -> int: + return WorldStateLog.num_cols + + def robots_to_array(self, robots: list[RobotLog]) -> list[Any]: + """Serializes robots into flattened columns within a list + + :param robot_states: the list of RobotState objects to flatten + :return: a list of values representing the states for all robots + """ + num_cols_per_robot = RobotLog.get_num_cols() + + # robot state columns will be added based on robot id + robot_state_map = {robot.id: robot for robot in robots} + + robots_array = [] + + # Add friendly robots: [r1_data, r2_data...] based on id + for idx in range(DIV_B_NUM_ROBOTS): + new_state = [] + + if idx not in robot_state_map: + new_state = [None for _ in range(num_cols_per_robot)] + else: + robot_state = robot_state_map[idx] + new_state = robot_state.to_array() + + robots_array.extend(new_state) + + return robots_array + + @override + def to_array(self) -> list[Any]: + return ( + self.ball_state.to_array() + + self.robots_to_array(self.friendly_robots) + + self.robots_to_array(self.enemy_robots) + ) + + @staticmethod + def from_csv_row(row_iter: Iterator[str]) -> WorldStateLog | None: + # 1. Parse Ball + ball_state = BallLog.from_csv_row(row_iter) + + # 2. Parse Friendly Robots + friendly_robots = [] + for id in range(DIV_B_NUM_ROBOTS): + robot = RobotLog.from_csv_row(row_iter, id=id) + if robot: + friendly_robots.append(robot) + + # 3. Parse Enemy Robots + enemy_robots = [] + for id in range(DIV_B_NUM_ROBOTS): + robot = RobotLog.from_csv_row(row_iter, id=id) + if robot: + enemy_robots.append(robot) + + return WorldStateLog( + ball_state=ball_state, + friendly_robots=friendly_robots, + enemy_robots=enemy_robots, + ) diff --git a/src/software/thunderscope/log/trackers/BUILD b/src/software/evaluation/trackers/BUILD similarity index 89% rename from src/software/thunderscope/log/trackers/BUILD rename to src/software/evaluation/trackers/BUILD index 79a4365113..0583aa9e53 100644 --- a/src/software/thunderscope/log/trackers/BUILD +++ b/src/software/evaluation/trackers/BUILD @@ -8,9 +8,9 @@ py_library( "__init__.py", "goalie_tracker.py", "kick_tracker.py", + "pass_log_tracker.py", "possession_tracker.py", "referee_tracker.py", - "tracked_event.py", "tracker.py", "tracker_builder.py", ], @@ -20,6 +20,7 @@ py_library( ], visibility = ["//visibility:public"], deps = [ + "//software/evaluation/logs", "//software/thunderscope:time_provider", ], ) diff --git a/src/software/evaluation/trackers/__init__.py b/src/software/evaluation/trackers/__init__.py new file mode 100644 index 0000000000..cc758c3d6b --- /dev/null +++ b/src/software/evaluation/trackers/__init__.py @@ -0,0 +1,16 @@ +from software.evaluation.trackers.kick_tracker import ShotTracker, PassTracker +from software.evaluation.trackers.possession_tracker import PossessionTracker +from software.evaluation.trackers.tracker_builder import TrackerBuilder +from software.evaluation.trackers.referee_tracker import RefereeTracker +from software.evaluation.trackers.goalie_tracker import GoalieTracker +from software.evaluation.trackers.pass_log_tracker import PassLogTracker + +__all__ = [ + "PossessionTracker", + "ShotTracker", + "PassTracker", + "TrackerBuilder", + "RefereeTracker", + "GoalieTracker", + "PassLogTracker", +] diff --git a/src/software/thunderscope/log/trackers/goalie_tracker.py b/src/software/evaluation/trackers/goalie_tracker.py similarity index 68% rename from src/software/thunderscope/log/trackers/goalie_tracker.py rename to src/software/evaluation/trackers/goalie_tracker.py index 36ee9af432..0eaf1c990c 100644 --- a/src/software/thunderscope/log/trackers/goalie_tracker.py +++ b/src/software/evaluation/trackers/goalie_tracker.py @@ -1,10 +1,11 @@ -from software.thunderscope.log.trackers.tracker import Tracker -from typing import Callable, override +from software.evaluation.trackers.tracker import Tracker +from typing import override from proto.import_all_protos import * -from software.thunderscope.thread_safe_buffer import ThreadSafeBuffer from software.thunderscope.proto_unix_io import ProtoUnixIO import software.python_bindings as tbots_cpp from software.py_constants import ROBOT_MAX_RADIUS_METERS +from software.evaluation.logs.event_log import EventType, Team +import queue class GoalieTracker(Tracker): @@ -17,56 +18,64 @@ class GoalieTracker(Tracker): def __init__( self, + proto_unix_io: ProtoUnixIO, + from_team: Team, + for_team: Team, + event_queue: queue.Queue, for_friendly: bool, - callback: Callable[[bool, bool], None], - buffer_size: int = 5, + **kwargs, ): """Initializes the Goalie tracker :param for_friendly: if we should track shots on goal for the friendly or enemy team - :param callback: function to call when there is a new shot on goal - called with 2 booleans: - - If there is a shot on goal right now - - If there was a shot on goal before - lets us track new shots on goal + when shots are blocked - :param buffer_size: buffer size for the tracker's io + :param proto_unix_io: the proto unix io to get the game state from + :param from_team: the team that this tracker is tracking from (events are from this team) + :param for_team: the team that this tracker is tracking for (events are for this team) + default is same as the from_team, but can be different + :param event_queue: the queue to write events to """ - super().__init__(callback=callback, buffer_size=buffer_size) - - self.world_buffer = ThreadSafeBuffer(buffer_size, World) + super().__init__( + proto_unix_io=proto_unix_io, + from_team=from_team, + for_team=for_team, + event_queue=event_queue, + **kwargs, + ) self.for_friendly = for_friendly self.is_shot_incoming = False @override - def set_proto_unix_io(self, proto_unix_io: ProtoUnixIO) -> None: - super().set_proto_unix_io( - proto_unix_io, - [ - (World, self.world_buffer), - ], - ) - - @override - def refresh(self): - """Refresh and update the callback with the latest shot on goal information""" - world_msg = self.world_buffer.get(block=False, return_cached=True) - - if not world_msg: + def refresh_tracker(self) -> None: + """Refresh and log any new shots on goal""" + if self.cached_world is None: return - world = tbots_cpp.World(world_msg) - latest_is_shot_incoming = self._is_goal_shot_incoming( - world.ball(), world.field(), for_friendly=self.for_friendly + self.cached_world.ball(), + self.cached_world.field(), + for_friendly=self.for_friendly, ) - if self.callback: - self.callback(latest_is_shot_incoming, self.is_shot_incoming) + self._log_incoming_shot(latest_is_shot_incoming) self.is_shot_incoming = latest_is_shot_incoming + def _log_incoming_shot(self, new_shot_incoming): + event_type = None + + if not new_shot_incoming and self.is_shot_incoming: + event_type = EventType.SHOT_BLOCKED + + if new_shot_incoming and not self.is_shot_incoming: + event_type = EventType.ENEMY_SHOT_ON_GOAL + + if not event_type: + return + + self.write_event(event_type=event_type) + def _get_goal_shot_region( self, field: tbots_cpp.Field, for_friendly: bool ) -> tbots_cpp.Rectangle: diff --git a/src/software/thunderscope/log/trackers/kick_tracker.py b/src/software/evaluation/trackers/kick_tracker.py similarity index 66% rename from src/software/thunderscope/log/trackers/kick_tracker.py rename to src/software/evaluation/trackers/kick_tracker.py index 0d7c6d0153..3c2d683aec 100644 --- a/src/software/thunderscope/log/trackers/kick_tracker.py +++ b/src/software/evaluation/trackers/kick_tracker.py @@ -3,10 +3,12 @@ import software.python_bindings as tbots_cpp from proto.visualization_pb2 import AttackerVisualization from proto.import_all_protos import * -from typing import Callable, Any, override +from typing import override from software.thunderscope.thread_safe_buffer import ThreadSafeBuffer -from software.thunderscope.log.trackers.tracker import Tracker +from software.evaluation.trackers.tracker import Tracker from software.thunderscope.proto_unix_io import ProtoUnixIO +from software.evaluation.logs.event_log import EventType, Team +import queue class KickTracker(Tracker): @@ -37,31 +39,40 @@ class KickTracker(Tracker): def __init__( self, - callback: Optional[Callable[[Any], None]] = None, - buffer_size: int = 5, + proto_unix_io: ProtoUnixIO, + from_team: Team, + for_team: Team, + event_queue: queue.Queue, + **kwargs, ): - """Initialize the kick tracker - :param callback: an optional callback to call when there's a kick + """Initializes the KickTracker + + :param proto_unix_io: the proto unix io to get the game state from + :param from_team: the team that this tracker is tracking from (events are from this team) + :param for_team: the team that this tracker is tracking for (events are for this team) + default is same as the from_team, but can be different + :param event_queue: the queue to write events to """ - super().__init__(callback=callback, buffer_size=buffer_size) + super().__init__( + proto_unix_io=proto_unix_io, + from_team=from_team, + for_team=for_team, + event_queue=event_queue, + **kwargs, + ) + self.latest_kick_angle: tbots_cpp.Angle = tbots_cpp.Angle() self.kick_taken = False self.attacker_vis_buffer = ThreadSafeBuffer( self.buffer_size, AttackerVisualization ) - self.world_buffer = ThreadSafeBuffer(self.buffer_size, World) - - @override - def set_proto_unix_io(self, proto_unix_io: ProtoUnixIO) -> None: - super().set_proto_unix_io( - proto_unix_io, - [ - (AttackerVisualization, self.attacker_vis_buffer), - (World, self.world_buffer), - ], + self.proto_unix_io.register_observer( + AttackerVisualization, self.attacker_vis_buffer ) + self.curr_pass = None + def _get_new_kick_angle( self, origin: Point, target: Point, latest_angle: tbots_cpp.Angle ) -> Optional[tbots_cpp.Angle]: @@ -94,22 +105,20 @@ def _get_new_kick_angle( return None - def refresh(self) -> None: + @override + def refresh_tracker(self) -> None: """Refreshes the tracker by getting the current state of the world and the latest attacker visualization """ - attacker_vis_msg = self.attacker_vis_buffer.get(block=False) - - if not attacker_vis_msg: + if self.cached_world is None: return - world_msg = self.world_buffer.get(block=False, return_cached=True) + attacker_vis_msg = self.attacker_vis_buffer.get(block=False) - if world_msg is None: + if not attacker_vis_msg: return - world = tbots_cpp.World(world_msg) - self._refresh_kicks(attacker_vis_msg, world) + self._refresh_kicks(attacker_vis_msg, self.cached_world) def _refresh_kicks( self, attacker_vis_msg: AttackerVisualization, world: tbots_cpp.World @@ -120,18 +129,38 @@ def _refresh_kicks( class PassTracker(KickTracker): """Tracker for tracking the attacker's passes""" - def __init__(self, callback: Callable[[Pass], None] = None, buffer_size: int = 5): - """Initialize the pass tracker - :param callback: an optional callback to call when there's a pass + def __init__( + self, + proto_unix_io: ProtoUnixIO, + from_team: Team, + for_team: Team, + event_queue: queue.Queue, + **kwargs, + ): + """Initializes the PassTracker + + :param proto_unix_io: the proto unix io to get the game state from + :param from_team: the team that this tracker is tracking from (events are from this team) + :param for_team: the team that this tracker is tracking for (events are for this team) + default is same as the from_team, but can be different + :param event_queue: the queue to write events to """ - super().__init__(callback=callback, buffer_size=buffer_size) + super().__init__( + proto_unix_io=proto_unix_io, + from_team=from_team, + for_team=for_team, + event_queue=event_queue, + **kwargs, + ) @override def _refresh_kicks( self, attacker_vis_msg: AttackerVisualization, world: tbots_cpp.World ) -> None: """Refreshes the pass tracker with the new attacker visualization - and the latest state of the world + and the latest state of the world. + + Logs any new pass events. :param attacker_vis_msg: the latest attacker visualization message :param world: the current world state @@ -151,6 +180,7 @@ def _refresh_kicks( if new_pass_angle is not None: self.latest_kick_angle = new_pass_angle self.kick_taken = False + self.curr_pass = attacker_vis_msg.pass_ ball = world.ball() @@ -160,27 +190,46 @@ def _refresh_kicks( self.MIN_SHOT_SPEED, self.MAX_KICK_ANGLE_DIFFERENCE, ): + self.write_event(event_type=EventType.PASS) self.kick_taken = True - - if self.callback: - self.callback(attacker_vis_msg.pass_) + self.curr_pass = None class ShotTracker(KickTracker): """Tracker for tracking the attacker's shots on goal""" - def __init__(self, callback: Callable[[Shot], None] = None, buffer_size: int = 5): - """Initialize the shot tracker - :param callback: an optional callback to call when there's a shot + def __init__( + self, + proto_unix_io: ProtoUnixIO, + from_team: Team, + for_team: Team, + event_queue: queue.Queue, + **kwargs, + ): + """Initializes the ShotTracker + + :param proto_unix_io: the proto unix io to get the game state from + :param from_team: the team that this tracker is tracking from (events are from this team) + :param for_team: the team that this tracker is tracking for (events are for this team) + default is same as the from_team, but can be different + :param event_queue: the queue to write events to """ - super().__init__(callback=callback, buffer_size=buffer_size) + super().__init__( + proto_unix_io=proto_unix_io, + from_team=from_team, + for_team=for_team, + event_queue=event_queue, + **kwargs, + ) @override def _refresh_kicks( self, attacker_vis_msg: AttackerVisualization, world: tbots_cpp.World ) -> None: """Refreshes the shot tracker with the new attacker visualization - and the latest state of the world + and the latest state of the world. + + Logs any new shot events. :param attacker_vis_msg: the latest attacker visualization message :param world: the current world state @@ -213,5 +262,4 @@ def _refresh_kicks( ): self.kick_taken = True - if self.callback: - self.callback(attacker_vis_msg.shot) + self.write_event(event_type=EventType.SHOT_ON_GOAL) diff --git a/src/software/evaluation/trackers/pass_log_tracker.py b/src/software/evaluation/trackers/pass_log_tracker.py new file mode 100644 index 0000000000..f4127deb10 --- /dev/null +++ b/src/software/evaluation/trackers/pass_log_tracker.py @@ -0,0 +1,159 @@ +from software.evaluation.trackers.kick_tracker import PassTracker +from software.evaluation.logs.pass_log import ( + PassLogType, + PassLog, +) +from software.evaluation.logs.event_log import EventType, Team +from typing import override +from proto.import_all_protos import * +from software.thunderscope.proto_unix_io import ProtoUnixIO +import queue +from software.thunderscope.time_provider import time_provider_instance +import uuid +from software.py_constants import NANOSECONDS_PER_SECOND + + +class PassLogTracker(PassTracker): + """Tracker to keep track of all new passes + Logs the pass itself + world state at the time of pass + Plus the world state at specific intervals after that pass + """ + + def __init__( + self, + proto_unix_io: ProtoUnixIO, + from_team: Team, + event_queue: queue.Queue, + for_team: Team | None = None, + **kwargs, + ): + """Initializes the PassLogTracker + + :param proto_unix_io: the proto unix io to get the game state from + :param from_team: the team that this tracker is tracking from (events are from this team) + :param for_team: the team that this tracker is tracking for (events are for this team) + default is same as the from_team, but can be different + :param event_queue: the queue to write events to + """ + super().__init__( + proto_unix_io=proto_unix_io, + from_team=from_team, + for_team=for_team if for_team is not None else from_team, + event_queue=event_queue, + **kwargs, + ) + + self.score = 0 + + self.intervals = [item.value for item in PassLogType] + self.interval_labels = [item for item in PassLogType] + + self.logged_passes_map: dict[int, list[PassLog]] = { + interval: [] for interval in self.intervals + } + + @override + def refresh_tracker(self) -> None: + """Refreshes the logged passes to log interval states""" + # IMPORTANT: to refresh and keep track of new passes from parent + super().refresh_tracker() + + self._update_pass_timestamps() + + @override + def write_event(self, event_type: EventType) -> None: + """This method is called by the super-class when there is a new pass + Adds the pass to the first list of logged passes, at the smallest interval + + :param event_type: (unused) + """ + if not self.curr_pass: + return + + pass_result = self._log_pass( + pass_id=uuid.uuid4(), + pass_result_type=PassLogType.RESULT_0S, + pass_=self.curr_pass, + ) + + if not pass_result: + return + + self.logged_passes_map[self.intervals[0]].append(pass_result) + + def set_score(self, score: float) -> None: + """Sets the current game score to the given value + + :param score: the new score + """ + self.score = score + + def _log_pass( + self, pass_id: uuid.UUID, pass_result_type: PassLogType, pass_: Pass + ) -> PassLog | None: + """Logs a single pass, with the given pass and event type + + :param pass_result_type: the result type corresponding to the interval after which + we are logging the result + :param pass_: the pass whose results are being logged + :param score: the score for this pass's results compared to when the pass started + """ + if not self.cached_world: + return None + + pass_result = PassLog.from_world_and_pass( + pass_id=pass_id, + pass_result_type=pass_result_type, + pass_=pass_, + world_msg=self.cached_world_msg, + team=self.from_team, + ) + + self.event_queue.put(pass_result) + + return pass_result + + def _log_if_over_interval( + self, tracked_pass: PassLog, interval: float, pass_result_type: PassLogType + ) -> bool: + """Checks if the given pass we logged is older than the given interval + If so, logs a result with the given type with the correct score + + :param logged_pass: the pass we previously logged, with the timestamp and score at the time + :param interval: the interval that the pass may be older than + :param pass_result_type: the result type corresponding to this interval + :return: True if the pass is older than interval (and therefore result has been logged) + False if not + """ + if ( + time_provider_instance.elapsed_time_ns() - tracked_pass.get_timestamp() + > interval * NANOSECONDS_PER_SECOND + ): + self._log_pass( + pass_id=tracked_pass.pass_id, + pass_result_type=pass_result_type, + pass_=tracked_pass.pass_, + ) + + return True + + return False + + def _update_pass_timestamps(self): + """For all currently logged passes, check if the interval they belong to has passed + If so, log that pass result + And move them to the next interval if exists + """ + for idx, interval in enumerate(self.intervals): + logged_passes = self.logged_passes_map[interval] + + pass_result_type = self.interval_labels[idx] + + # passes are in the list in chronological order + while logged_passes and self._log_if_over_interval( + logged_passes[0], interval, pass_result_type + ): + logged_pass = logged_passes.pop(0) + + if idx < len(self.intervals) - 1: + self.logged_passes_map[self.intervals[idx + 1]].append(logged_pass) diff --git a/src/software/evaluation/trackers/possession_tracker.py b/src/software/evaluation/trackers/possession_tracker.py new file mode 100644 index 0000000000..c7dcd3ede2 --- /dev/null +++ b/src/software/evaluation/trackers/possession_tracker.py @@ -0,0 +1,114 @@ +from software.evaluation.trackers.tracker import Tracker +from typing import override +from software.thunderscope.proto_unix_io import ProtoUnixIO +from proto.import_all_protos import * +import software.python_bindings as tbots_cpp +from software.evaluation.logs.event_log import EventType, Team +import queue +from software.py_constants import BALL_TO_FRONT_OF_ROBOT_DISTANCE_WHEN_DRIBBLING + + +class PossessionTracker(Tracker): + """Tracker to track and log when ball possession changes""" + + def __init__( + self, + proto_unix_io: ProtoUnixIO, + from_team: Team, + for_team: Team, + event_queue: queue.Queue, + **kwargs, + ): + """Initializes the PossessionTracker + + :param proto_unix_io: the proto unix io to get the game state from + :param from_team: the team that this tracker is tracking from (events are from this team) + :param for_team: the team that this tracker is tracking for (events are for this team) + default is same as the from_team, but can be different + :param event_queue: the queue to write events to + """ + super().__init__( + proto_unix_io=proto_unix_io, + from_team=from_team, + for_team=for_team, + event_queue=event_queue, + **kwargs, + ) + + # start with no team having possession + self.curr_possession = None + + @override + def refresh_tracker(self) -> None: + """Refresh and logs any changes in ball possession""" + if self.cached_world is None: + return + + self._log_posession_for_friendly( + self.cached_world.friendlyTeam(), + self.cached_world.enemyTeam(), + self.cached_world.ball().position(), + ) + + def _log_posession_for_friendly( + self, + friendly_team: tbots_cpp.Team, + enemy_team: tbots_cpp.Team, + ball_position: tbots_cpp.Point, + ) -> None: + """Detects possession changes and logs the corresponding start/end events + + Checks the current world state against the last known possession. + True if friendly possession, False is enemy possession, None if neither + + If a transition occurs (e.g., Friendly -> None, None -> Enemy), it triggers + a write_event with the appropriate EventType and updates the internal + possession state. + + :param friendly_team: the friendly team object + :param enemy_team: the enemy team object + :param ball_position: the current position of the ball + :return: None + """ + new_possession = None + + if self._check_posession_for_team(friendly_team, ball_position): + new_possession = True + elif self._check_posession_for_team(enemy_team, ball_position): + new_possession = False + + # if possession didn't change, no need to log + if new_possession == self.curr_possession: + return + + # mark the end of the last possession since it has changed + if self.curr_possession: + self.write_event(event_type=EventType.FRIENDLY_POSSESSION_END) + elif self.curr_possession == False: + self.write_event(event_type=EventType.ENEMY_POSSESSION_END) + + # log the start of the new, changed possession + if new_possession: + self.write_event(event_type=EventType.FRIENDLY_POSSESSION_START) + elif new_possession == False: + self.write_event(event_type=EventType.ENEMY_POSSESSION_START) + + self.curr_possession = new_possession + + def _check_posession_for_team( + self, team: tbots_cpp.Team, ball_position: tbots_cpp.Point + ) -> bool: + """Check if the given team has possession of the ball + + :param team: the team to check + :param ball_position: the current ball position + :return: True if the team has possession, False otherwise + """ + for robot in team.getAllRobots(): + # higher tolerance to make possession a bit stickier + if robot.isNearDribbler( + ball_position, BALL_TO_FRONT_OF_ROBOT_DISTANCE_WHEN_DRIBBLING * 2 + ): + return True + + return False diff --git a/src/software/evaluation/trackers/referee_tracker.py b/src/software/evaluation/trackers/referee_tracker.py new file mode 100644 index 0000000000..53f8fc0b6a --- /dev/null +++ b/src/software/evaluation/trackers/referee_tracker.py @@ -0,0 +1,131 @@ +from typing import override, Callable +from software.evaluation.trackers.tracker import Tracker +from proto.import_all_protos import * +from software.thunderscope.thread_safe_buffer import ThreadSafeBuffer +from software.thunderscope.proto_unix_io import ProtoUnixIO +from software.evaluation.logs.event_log import EventType, Team +import queue + + +class RefereeTracker(Tracker): + """Tracks Referee events, like goals and yellow / red cards for the friendly team only""" + + # we want to ignore all breaks, times before the game actually starts + # and all penalty related stages + STAGES_TO_IGNORE = [ + Referee.Stage.PENALTY_SHOOTOUT_BREAK, + Referee.Stage.PENALTY_SHOOTOUT, + Referee.Stage.NORMAL_FIRST_HALF_PRE, + Referee.Stage.NORMAL_SECOND_HALF_PRE, + Referee.Stage.EXTRA_TIME_BREAK, + Referee.Stage.EXTRA_FIRST_HALF_PRE, + Referee.Stage.EXTRA_SECOND_HALF_PRE, + ] + + def __init__( + self, + proto_unix_io: ProtoUnixIO, + from_team: Team, + for_team: Team, + event_queue: queue.Queue, + friendly_color_yellow: bool, + toggle_logging: Callable[[bool], None] | None = None, + **kwargs, + ): + """Initializes the RefereeTracker + + :param proto_unix_io: the proto unix io to get the game state from + :param from_team: the team that this tracker is tracking from (events are from this team) + :param for_team: the team that this tracker is tracking for (events are for this team) + default is same as the from_team, but can be different + :param event_queue: the queue to write events to + :param friendly_color_yellow: if the friendly color is yellow or blue + """ + super().__init__( + proto_unix_io=proto_unix_io, + from_team=from_team, + for_team=for_team, + event_queue=event_queue, + **kwargs, + ) + + self.referee_buffer = ThreadSafeBuffer(self.buffer_size, Referee) + self.proto_unix_io.register_observer(Referee, self.referee_buffer) + + self.friendly_color_yellow = friendly_color_yellow + + # we can use this callback to turn on / off logging + # during stages we don't care about + self.toggle_logging = toggle_logging + + self.num_yellow_cards = 0 + self.num_red_cards = 0 + self.num_goals = 0 + + self.curr_stage = None + + @override + def refresh_tracker(self) -> None: + """Refresh and log the latest referee information""" + referee_msg = self.referee_buffer.get(block=False, return_cached=True) + + if not referee_msg: + return + + game_stage = referee_msg.stage + + if game_stage in self.STAGES_TO_IGNORE: + if self.toggle_logging: + self.toggle_logging(False) + return + + if self.toggle_logging: + self.toggle_logging(True) + + # if game has just started, log a game start event once + if game_stage == Referee.Stage.NORMAL_FIRST_HALF: + self.curr_stage = self._log_event_if_change( + new_value=game_stage, + old_value=self.curr_stage, + event_type=EventType.GAME_START, + ) + return + + # if the game has just ended, log a game end event once + if game_stage == Referee.Stage.POST_GAME: + self.curr_stage = self._log_event_if_change( + new_value=game_stage, + old_value=self.curr_stage, + event_type=EventType.GAME_END, + ) + return + + self.curr_stage = game_stage + + if referee_msg.HasField("yellow" if self.friendly_color_yellow else "blue"): + team_info = ( + referee_msg.yellow if self.friendly_color_yellow else referee_msg.blue + ) + + if team_info.HasField("score"): + self.num_goals = self._log_event_if_change( + team_info.score, self.num_goals, EventType.GOAL_SCORED + ) + + if team_info.HasField("yellow_cards"): + self.num_yellow_cards = self._log_event_if_change( + team_info.yellow_cards, self.num_yellow_cards, EventType.YELLOW_CARD + ) + + if team_info.HasField("red_cards"): + self.num_red_cards = self._log_event_if_change( + team_info.red_cards, self.num_red_cards, EventType.RED_CARD + ) + + def _log_event_if_change( + self, new_value: int, old_value: int, event_type: EventType + ) -> int: + if new_value != old_value: + self.write_event(event_type=event_type) + + return new_value diff --git a/src/software/evaluation/trackers/tracker.py b/src/software/evaluation/trackers/tracker.py new file mode 100644 index 0000000000..4d69c41d4e --- /dev/null +++ b/src/software/evaluation/trackers/tracker.py @@ -0,0 +1,74 @@ +from software.thunderscope.proto_unix_io import ProtoUnixIO +from typing import Callable +from software.thunderscope.thread_safe_buffer import ThreadSafeBuffer +from proto.import_all_protos import * +import software.python_bindings as tbots_cpp +import queue +from software.evaluation.logs.event_log import EventType, Team, EventLog + + +class Tracker: + """Generic tracker base class. Just tracks the world state.""" + + def __init__( + self, + proto_unix_io: ProtoUnixIO, + from_team: Team, + event_queue: queue.Queue, + for_team: Team, + callback: Callable[[EventType], None] = None, + buffer_size: int = 5, + ): + """Initializes the tracker with the given callback and buffer size + + :param proto_unix_io: the proto unix io to get the game state from + :param from_team: the team that this tracker is tracking from (events are from this team) + :param for_team: the team that this tracker is tracking for (events are for this team) + default is same as the from_team, but can be different + :param event_queue: the queue to write events to + :param callback: optional callback to call when there's an event + :param buffer_size: buffer size for the tracker's io + """ + self.event_queue = event_queue + self.buffer_size = buffer_size + self.from_team = from_team + self.for_team = for_team + self.callback = callback + + self.proto_unix_io = proto_unix_io + self.world_buffer = ThreadSafeBuffer(self.buffer_size, World) + self.proto_unix_io.register_observer(World, self.world_buffer) + + self.cached_world = None + + def refresh(self) -> None: + """Refreshes the tracker to get the latest world message""" + world_msg = self.world_buffer.get(block=False, return_cached=True) + + if world_msg is None: + return + + self.cached_world_msg = world_msg + self.cached_world = tbots_cpp.World(world_msg) + + self.refresh_tracker() + + def refresh_tracker(self) -> None: + pass + + def write_event(self, event_type: EventType) -> None: + """Writes a single event to the event queue of the given type + + :param event_type: the type of event to log + """ + if not self.cached_world: + return + + event = EventLog.from_world( + world_msg=self.cached_world_msg, + event_type=event_type, + from_team=self.from_team, + for_team=self.for_team, + ) + + self.event_queue.put(event) diff --git a/src/software/evaluation/trackers/tracker_builder.py b/src/software/evaluation/trackers/tracker_builder.py new file mode 100644 index 0000000000..eaba7b16c2 --- /dev/null +++ b/src/software/evaluation/trackers/tracker_builder.py @@ -0,0 +1,60 @@ +from software.thunderscope.proto_unix_io import ProtoUnixIO +from software.evaluation.trackers.tracker import Tracker +from typing import Type, Self +from software.evaluation.logs.event_log import Team +import queue + + +class TrackerBuilder: + """Builder class to combine different trackers and update them together""" + + def __init__( + self, + proto_unix_io: ProtoUnixIO, + from_team: Team, + event_queue: queue.Queue, + for_team: Team | None = None, + buffer_size: int = 5, + ) -> None: + """Initializes the builder + + :param proto_unix_io: the unix io that the trackers should listen on + :param from_team: the team that this tracker is tracking from (events are from this team) + :param for_team: the team that this tracker is tracking for (events are for this team) + default is same as the from_team, but can be different + :param event_queue: the queue to write events to + """ + self.proto_unix_io = proto_unix_io + self.from_team = from_team + self.for_team = from_team if for_team is None else for_team + self.buffer_size = buffer_size + + self.event_queue = event_queue + + self.trackers = [] + + def add_tracker( + self, + tracker_cls: Type[Tracker], + **kwargs, + ) -> Self: + """Adds a single tracker to the list + + :param tracker_cls: The class of the tracker to instantiate + :param **kwargs: tracker-specific arguments + """ + tracker = tracker_cls( + proto_unix_io=self.proto_unix_io, + event_queue=self.event_queue, + from_team=self.from_team, + for_team=self.for_team, + buffer_size=self.buffer_size, + **kwargs, + ) + self.trackers.append(tracker) + return self + + def refresh(self) -> None: + """Refreshes all the trackers""" + for tracker in self.trackers: + tracker.refresh() diff --git a/src/software/ml/BUILD b/src/software/ml/BUILD new file mode 100644 index 0000000000..b8311033a0 --- /dev/null +++ b/src/software/ml/BUILD @@ -0,0 +1,30 @@ +load("@ml_deps//:requirements.bzl", "requirement") +load("@rules_python//python:pip.bzl", "compile_pip_requirements") + +package(default_visibility = ["//visibility:public"]) + +compile_pip_requirements( + name = "requirements", + src = ":requirements.in", + requirements_txt = ":requirements_lock.txt", +) + +py_binary( + name = "train_passing", + srcs = ["passing/train_passing.py"], + deps = [ + requirement("torch"), + requirement("pandas"), + requirement("numpy"), + requirement("torch_geometric"), + requirement("scikit-learn"), + requirement("onnxscript"), + "//software/evaluation/logs:event_log", + "//software/evaluation/logs:pass_log", + "//software/ml/models/hetero_gnn", + "//software/ml/models/hetero_gnn:config", + "//software/ml/passing/data:build_graph", + "//software/ml/passing/data:labelled_passes", + "//software/ml/passing/data:types", + ], +) diff --git a/src/software/ml/data_cleanup/BUILD b/src/software/ml/data_cleanup/BUILD new file mode 100644 index 0000000000..bd90d6b489 --- /dev/null +++ b/src/software/ml/data_cleanup/BUILD @@ -0,0 +1,36 @@ +load("@simulated_tests_deps//:requirements.bzl", "requirement") + +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "stats_result", + srcs = ["stats_result.py"], + data = [ + "//software:python_bindings.so", + ], + deps = [ + ":result_interface", + "//proto:import_all_protos", + "//software/evaluation/logs:event_log", + "//software/evaluation/logs:log_interface", + ], +) + +py_test( + name = "stats_result_test", + srcs = [ + "stats_result_test.py", + ], + deps = [ + requirement("pytest"), + ":stats_result", + ], +) + +py_library( + name = "result_interface", + srcs = ["result_interface.py"], + deps = [ + "//software/evaluation/logs:log_interface", + ], +) diff --git a/src/software/ml/data_cleanup/result_interface.py b/src/software/ml/data_cleanup/result_interface.py new file mode 100644 index 0000000000..b5b1c75812 --- /dev/null +++ b/src/software/ml/data_cleanup/result_interface.py @@ -0,0 +1,8 @@ +from abc import abstractmethod +from software.evaluation.logs.log_interface import TimestampedEvalLog + + +class IResult: + @abstractmethod + def update_result(self, eval_log: TimestampedEvalLog) -> None: + raise NotImplementedError("Please use the appropriate subclass!") diff --git a/src/software/ml/data_cleanup/stats_result.py b/src/software/ml/data_cleanup/stats_result.py new file mode 100644 index 0000000000..4123e2663b --- /dev/null +++ b/src/software/ml/data_cleanup/stats_result.py @@ -0,0 +1,130 @@ +from __future__ import annotations +from dataclasses import dataclass, field +from software.evaluation.logs.log_interface import IEvalLog +from software.ml.data_cleanup.result_interface import IResult +from software.evaluation.logs.event_log import EventLog, Team, EventType +from typing import cast, Any, List, override, Iterator +import software.python_bindings as tbots_cpp +from proto.import_all_protos import * + + +@dataclass +class StatsResult(IResult, IEvalLog): + friendly_team: Team + score: int = 0 + enemy_score: int = 0 + yellow_cards: int = 0 + red_cards: int = 0 + has_possession: bool | None = None + shots_on_net: int = 0 + ball_in_enemy_half: bool = False + passes: int = 0 + blocked_enemy_shots: int = 0 + + _field: tbots_cpp.Field = field(init=False, repr=False, default=None) + + @classmethod + @override + def get_num_cols(cls) -> int: + """Returns the number of elements in the to_array output (9 fields)""" + return 9 + + @override + def to_array(self) -> List[Any]: + return [ + self.score, + self.enemy_score, + self.yellow_cards, + self.red_cards, + self.has_possession, + self.shots_on_net, + self.ball_in_enemy_half, + self.passes, + self.blocked_enemy_shots, + ] + + def _is_in_enemy_half(self, point_to_check: List[float]): + if self._field is None: + self._field = tbots_cpp.Field.createSSLDivisionBField() + + point = tbots_cpp.Point(point_to_check[0], point_to_check[1]) + + return tbots_cpp.contains(self._field.enemyHalf(), point) + + @override + def update_result(self, eval_log: IEvalLog) -> None: + tracked_event = cast(EventLog, eval_log) + + for_friendly = tracked_event.for_team == self.friendly_team + + self.ball_in_enemy_half = self._is_in_enemy_half( + tracked_event.world_state_log.ball_state.get_position() + ) + + match tracked_event.event_type: + case EventType.PASS if for_friendly: + self.passes += 1 + + case EventType.SHOT_ON_GOAL if for_friendly: + self.shots_on_net += 1 + + case EventType.GOAL_SCORED: + if for_friendly: + self.score += 1 + else: + self.enemy_score += 1 + + case EventType.SHOT_BLOCKED if for_friendly: + self.blocked_enemy_shots += 1 + + case EventType.YELLOW_CARD if for_friendly: + self.yellow_cards += 1 + + case EventType.RED_CARD if for_friendly: + self.red_cards += 1 + + case EventType.FRIENDLY_POSSESSION_START: + self.has_possession = True + case EventType.FRIENDLY_POSSESSION_END: + self.has_possession = None + + case EventType.ENEMY_POSSESSION_START: + self.has_possession = False + case EventType.ENEMY_POSSESSION_END: + self.has_possession = None + + case EventType.GAME_END | EventType.GAME_START: + self.has_possession = None + self.ball_in_enemy_half = False + self.passes = 0 + self.score = 0 + self.enemy_score = 0 + self.yellow_cards = 0 + self.red_cards = 0 + self.shots_on_net = 0 + self.blocked_enemy_shots = 0 + + @staticmethod + def _parse_bool(val: str) -> bool | None: + val = val.strip().lower() + if val in ("none", ""): + return None + return val == "true" + + @staticmethod + @override + def from_csv_row(row_iter: Iterator[str], friendly_team: Team) -> StatsResult: + """Converts a CSV row back into a StatsResult instance.""" + return StatsResult( + friendly_team=friendly_team, + score=int(next(row_iter)), + enemy_score=int(next(row_iter)), + yellow_cards=int(next(row_iter)), + red_cards=int(next(row_iter)), + # Handling 'None' string or empty values for Optional bool + has_possession=StatsResult._parse_bool(next(row_iter)), + shots_on_net=int(next(row_iter)), + ball_in_enemy_half=next(row_iter).lower() == "true", + passes=int(next(row_iter)), + blocked_enemy_shots=int(next(row_iter)), + ) diff --git a/src/software/ml/data_cleanup/stats_result_test.py b/src/software/ml/data_cleanup/stats_result_test.py new file mode 100644 index 0000000000..077b177fc5 --- /dev/null +++ b/src/software/ml/data_cleanup/stats_result_test.py @@ -0,0 +1,91 @@ +import pytest +from unittest.mock import MagicMock, patch +from software.ml.data_cleanup.stats_result import StatsResult +from software.evaluation.logs.event_log import Team, EventType + + +class TestStatsResult: + FRIENDLY_TEAM = Team.BLUE + ENEMY_TEAM = Team.YELLOW + + @pytest.fixture + def stats(self, friendly_team): + # Initialize the dataclass + return StatsResult(friendly_team=self.FRIENDLY_TEAM) + + def create_mock_event(self, event_type, for_team, ball_pos=(0, 0)): + mock_event = MagicMock() + mock_event.event_type = event_type + mock_event.for_team = for_team + mock_event.world_state_log.ball_state.get_position.return_value = ball_pos + return mock_event + + def test_increment_passes(self, stats, friendly_team): + event = self.create_mock_event(EventType.PASS, for_team=self.FRIENDLY_TEAM) + + stats.update_result(event) + + assert stats.passes == 1 + + enemy_event = self.create_mock_event(EventType.PASS, for_team=self.ENEMY_TEAM) + stats.update_result(enemy_event) + assert stats.passes == 1 # Still 1 + + def test_goal_scoring(self, stats, friendly_team): + friendly_goal = self.create_mock_event( + EventType.GOAL_SCORED, for_team=self.FRIENDLY_TEAM + ) + enemy_goal = self.create_mock_event( + EventType.GOAL_SCORED, for_team=self.ENEMY_TEAM + ) + + stats.update_result(friendly_goal) + assert stats.score == 1 + assert stats.enemy_score == 0 + + stats.update_result(enemy_goal) + assert stats.score == 1 + assert stats.enemy_score == 1 + + def test_possession(self, stats): + stats.update_result( + self.create_mock_event(EventType.FRIENDLY_POSSESSION_START, None) + ) + assert stats.has_possession is True + + stats.update_result( + self.create_mock_event(EventType.FRIENDLY_POSSESSION_END, None) + ) + assert stats.has_possession is None + + stats.update_result( + self.create_mock_event(EventType.ENEMY_POSSESSION_START, None) + ) + assert stats.has_possession is False + + @patch.object(StatsResult, "_is_in_enemy_half") + def test_ball_position_update(self, mock_is_in_half, stats): + mock_is_in_half.return_value = True + event = self.create_mock_event(EventType.PASS, self.FRIENDLY_TEAM) + + stats.update_result(event) + + assert stats.ball_in_enemy_half is True + mock_is_in_half.assert_called_once() + + def test_game_reset(self, stats, friendly_team): + stats.score = 5 + stats.passes = 10 + stats.has_possession = True + + reset_event = self.create_mock_event(EventType.GAME_START, friendly_team) + stats.update_result(reset_event) + + assert stats.score == 0 + assert stats.passes == 0 + assert stats.has_possession is None + assert stats.ball_in_enemy_half is False + + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/src/software/ml/models/hetero_gnn/BUILD b/src/software/ml/models/hetero_gnn/BUILD new file mode 100644 index 0000000000..b9385bd5dc --- /dev/null +++ b/src/software/ml/models/hetero_gnn/BUILD @@ -0,0 +1,14 @@ +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "config", + srcs = ["config.py"], +) + +py_library( + name = "hetero_gnn", + srcs = ["hetero_gnn.py"], + deps = [ + ":config", + ], +) diff --git a/src/software/ml/models/hetero_gnn/config.py b/src/software/ml/models/hetero_gnn/config.py new file mode 100644 index 0000000000..ef29aa5594 --- /dev/null +++ b/src/software/ml/models/hetero_gnn/config.py @@ -0,0 +1,35 @@ +from dataclasses import dataclass +from typing import Dict, Tuple, Any + + +@dataclass +class HeteroGNNConfig: + ## Supports 2 options: + # Mapping between (src, rel, dst) -> (GNN_Class, kwargs_for_that_layer) + # Example: {('A', 'to', 'B'): (SAGEConv, {'aggr': 'max'})} + # OR + # A single convolution for all edges, eg: (SAGEConv, {'aggr': 'max'}) + edge_specs: ( + Tuple[Any, Dict[str, Any]] + | Dict[Tuple[str, str, str], Tuple[Any, Dict[str, Any]]] + ) + + # Global architectural parameters + + # Number of features in the hidden layers + hidden_channels: int + + # number of features for the final output layer + out_channels: int + + # number of layers in total + num_layers: int + + # Number of features in the input data + # Can be -1 for lazy initialization + in_channels: int = -1 + + # How to merge different edge types at a target node (e.g., 'sum', 'mean', 'cat') + global_aggr: str = "sum" + + return_node: str | None = None diff --git a/src/software/ml/models/hetero_gnn/hetero_gnn.py b/src/software/ml/models/hetero_gnn/hetero_gnn.py new file mode 100644 index 0000000000..2ed03355db --- /dev/null +++ b/src/software/ml/models/hetero_gnn/hetero_gnn.py @@ -0,0 +1,71 @@ +import torch +import torch.nn.functional as F +from torch_geometric.nn import HeteroConv +from software.ml.models.hetero_gnn.config import HeteroGNNConfig +from typing import override + + +class GenericHeteroGNN(torch.nn.Module): + def __init__(self, config: HeteroGNNConfig, metadata: tuple): + super().__init__() + self.config = config + self.convs = torch.nn.ModuleList() + + # Extract node_types and edge_types from metadata + _, edge_types = metadata + + for i in range(config.num_layers): + # Define input and output dims for this layer + out_dim = ( + config.out_channels + if i == config.num_layers - 1 + else config.hidden_channels + ) + + # Build the dictionary of convolutions for HeteroConv + conv_dict = {} + + if isinstance(config.edge_specs, dict): + # Specific mapping per edge type + for edge_type, (layer_class, layer_kwargs) in config.edge_specs.items(): + # We use (-1, -1) for in_channels to handle heterogeneous node feature sizes + conv_dict[edge_type] = layer_class( + (-1, -1), out_dim, **layer_kwargs + ) + else: + # Single convolution for ALL edge types + layer_class, layer_kwargs = config.edge_specs + for edge_type in edge_types: + conv_dict[edge_type] = layer_class( + (-1, -1), out_dim, **layer_kwargs + ) + + self.convs.append(HeteroConv(conv_dict, aggr=config.global_aggr)) + + self.head_1s = torch.nn.Linear(config.hidden_channels, config.out_channels) + self.head_5s = torch.nn.Linear(config.hidden_channels, config.out_channels) + self.head_10s = torch.nn.Linear(config.hidden_channels, config.out_channels) + + @override + def forward(self, x_dict, edge_index_dict): + for i, conv in enumerate(self.convs): + new_x_dict = conv(x_dict, edge_index_dict) + + for node_type, x in x_dict.items(): + if node_type not in new_x_dict: + new_x_dict[node_type] = x + + x_dict = new_x_dict + + # Apply activation/dropout except for the final layer + if i < len(self.convs) - 1: + x_dict = {key: F.relu(x) for key, x in x_dict.items()} + x_dict = { + key: F.dropout(x, p=0.3, training=self.training) + for key, x in x_dict.items() + } + + if self.config.return_node: + return x_dict[self.config.return_node] + else: + return x_dict diff --git a/src/software/ml/passing/data/BUILD b/src/software/ml/passing/data/BUILD new file mode 100644 index 0000000000..370959e913 --- /dev/null +++ b/src/software/ml/passing/data/BUILD @@ -0,0 +1,50 @@ +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "pass_result", + srcs = ["pass_result.py"], + deps = [ + "//software/evaluation/logs:event_log", + "//software/evaluation/logs:pass_log", + "//software/ml/data_cleanup:result_interface", + "//software/ml/data_cleanup:stats_result", + ], +) + +py_library( + name = "labelled_passes", + srcs = ["labelled_passes.py"], + deps = [ + ":pass_result", + "//software/evaluation/logs:pass_log", + "//software/ml/data_cleanup:stats_result", + ], +) + +py_library( + name = "types", + srcs = ["types.py"], + deps = [ + ":labelled_passes", + ], +) + +py_library( + name = "build_graph", + srcs = ["build_graph.py"], + deps = [ + ":labelled_passes", + ":types", + ], +) + +py_binary( + name = "generate_labelled_passes", + srcs = ["generate_labelled_passes.py"], + deps = [ + ":labelled_passes", + ":pass_result", + "//software/evaluation/logs:event_log", + "//software/evaluation/logs:pass_log", + ], +) diff --git a/src/software/ml/passing/data/build_graph.py b/src/software/ml/passing/data/build_graph.py new file mode 100644 index 0000000000..d07dd0b0f6 --- /dev/null +++ b/src/software/ml/passing/data/build_graph.py @@ -0,0 +1,149 @@ +import torch +import numpy as np +from torch_geometric.data import HeteroData +from typing import List +from dataclasses import fields +from software.evaluation.logs.pass_log import PassLogType +from software.ml.passing.data.labelled_passes import LabelledPass, Label +from software.ml.passing.data.types import ( + NodeType, + AttackerBallEdge, + BallPassDestinationEdge, + ReceiverPassDestinationEdge, + EnemyPassDestinationEdge, + GlobalToAnyEdge, + add_edge_to_data, +) + + +def get_dist(pos_1: List[float], pos_2: List[float]): + return np.linalg.norm(np.array(pos_1) - np.array(pos_2)) + + +def create_pyg_data(labelled_pass: LabelledPass) -> HeteroData: + data = HeteroData() + world_state = labelled_pass.pass_log.pass_event.world_state_log + ball_state = world_state.ball_state + + ball_pos = np.array(ball_state.get_position()) + + ## Building all Nodes + + # ball node + data[NodeType.BALL.value].x = torch.tensor( + [ball_state.to_array()], dtype=torch.float + ) + + # Attacker = Friendly robot closest to ball + attacker_idx = min( + range(len(world_state.friendly_robots)), + key=lambda i: get_dist(world_state.friendly_robots[i].get_position(), ball_pos), + ) + attacker = world_state.friendly_robots[attacker_idx] + data[NodeType.ATTACKER.value].x = torch.tensor( + [attacker.to_array()], dtype=torch.float + ) + + # Receivers - all other friendly robots + receivers = [ + robot + for i, robot in enumerate(world_state.friendly_robots) + if i != attacker_idx + ] + data[NodeType.RECEIVER.value].x = torch.tensor( + [receiver.to_array() for receiver in receivers], dtype=torch.float + ) + + # enemy robots as new node type + enemies = world_state.enemy_robots + data[NodeType.ENEMY.value].x = torch.tensor( + [enemy_robot.to_array() for enemy_robot in enemies], dtype=torch.float + ) + + # pass destination as a new node + pass_destination = labelled_pass.pass_log.get_pass_end_point() + data[NodeType.PASS_DESTINATION.value].x = torch.tensor( + [pass_destination], dtype=torch.float + ) + + # global stats as a node + global_features = [ + x if x is not None else 0.0 for x in labelled_pass.result.to_array() + ] + data[NodeType.GLOBAL.value].x = torch.tensor([global_features], dtype=torch.float) + + attacker_ball_edge = AttackerBallEdge( + distance=get_dist(attacker.get_position(), ball_pos), from_index=0, to_index=0 + ) + add_edge_to_data(attacker_ball_edge, data) + + ball_destination_edge = BallPassDestinationEdge( + pass_speed=labelled_pass.pass_log.get_pass_speed(), + is_shot_on_goal=False, + from_index=0, + to_index=0, + ) + add_edge_to_data(ball_destination_edge, data) + + for index, receiver in enumerate(receivers): + distance = get_dist(receiver.get_position(), pass_destination) + edge = ReceiverPassDestinationEdge( + distance=distance, from_index=index, to_index=0 + ) + add_edge_to_data(edge, data) + + for index, enemy in enumerate(enemies): + distance = get_dist(enemy.get_position(), pass_destination) + edge = EnemyPassDestinationEdge( + distance=distance, + intercept_dist=0, + time_delta=0, + from_index=index, + to_index=0, + ) + add_edge_to_data(edge, data) + + # add an edge from the global node to every other node + for node_type in data.node_types: + if node_type == NodeType.GLOBAL.value: + continue + + num_target_nodes = data[node_type].num_nodes + + for idx in range(num_target_nodes): + edge = GlobalToAnyEdge( + from_index=0, to_index=idx, destination_name=NodeType(node_type) + ) + add_edge_to_data(edge, data) + + ## Building Labels for data + # Convert Label dataclass to one-hot vector + label_array = [] + + for label_type in PassLogType: + if label_type == PassLogType.RESULT_0S: + continue + + if label_type in labelled_pass.labels: + label_dict = labelled_pass.labels[label_type].__dict__ + label_array.extend([int(v) for v in label_dict.values()]) + else: + label_array.extend([0.0 for _ in range(len(fields(Label)))]) + + label_vector = np.array(label_array, dtype=np.float32) + + data.y = torch.tensor(label_vector, dtype=torch.float) + + return data, label_vector + + +def process_all_passes(labelled_passes: List[LabelledPass]): + graphs = [] + labels = [] + + for labelled_pass in labelled_passes: + graph, label = create_pyg_data(labelled_pass) + graphs.append(graph) + labels.append(label) + + return graphs, labels diff --git a/src/software/ml/passing/data/generate_labelled_passes.py b/src/software/ml/passing/data/generate_labelled_passes.py new file mode 100644 index 0000000000..869595eb4e --- /dev/null +++ b/src/software/ml/passing/data/generate_labelled_passes.py @@ -0,0 +1,76 @@ +import os +import csv +import sys + +from typing import List + +from software.evaluation.logs.event_log import EventLog, Team +from software.evaluation.logs.pass_log import PassLog +from software.ml.passing.data.labelled_passes import LabelledPass, label_passes +from software.ml.passing.data.pass_result import generate_pass_results + +dir_path = os.path.dirname(os.path.realpath(__file__)) +ml_dir_path = os.path.dirname(os.path.dirname(dir_path)) +datasets_path = os.path.join(ml_dir_path, "datasets") + + +def load_and_label_passes( + pass_csv_file, event_csv_file, friendly_team +) -> List[LabelledPass]: + pass_logs = [] + + with open( + os.path.join(datasets_path, pass_csv_file), mode="r", encoding="utf-8" + ) as pass_csv: + reader = csv.reader(pass_csv) + + for row in reader: + pass_logs.append(PassLog.from_csv_row(iter(row))) + + print("Passes loaded!") + + event_logs = [] + with open( + os.path.join(datasets_path, event_csv_file), mode="r", encoding="utf-8" + ) as event_csv: + reader = csv.reader(event_csv) + + for row in reader: + event_logs.append(EventLog.from_csv_row(iter(row))) + + print("Events loaded!") + + pass_results = generate_pass_results( + event_logs=event_logs[0:10000], pass_logs=pass_logs, friendly_team=friendly_team + ) + + print("Pass Results Generated!") + + labelled_passes = label_passes(pass_results) + + print("Passes labelled!") + + return labelled_passes + + +def save_labelled_passes(labelled_passes: List[LabelledPass], out_file_name: str): + """Saves a list of LabelledPass objects to a CSV file in the datasets directory.""" + output_path = os.path.join(datasets_path, out_file_name) + + with open(output_path, mode="w", encoding="utf-8", newline="") as file: + for labelled_pass in labelled_passes: + file.write(labelled_pass.to_csv_row() + "\n") + + print(f"Successfully saved {len(labelled_passes)} labelled passes to {output_path}") + + +if __name__ == "__main__": + if len(sys.argv) != 4: + print( + "Usage: python generate_labelled_passes.py " + ) + sys.exit(1) + + labelled_passes = load_and_label_passes(sys.argv[1], sys.argv[2], Team.BLUE) + + save_labelled_passes(labelled_passes=labelled_passes, out_file_name=sys.argv[3]) diff --git a/src/software/ml/passing/data/labelled_passes.py b/src/software/ml/passing/data/labelled_passes.py new file mode 100644 index 0000000000..85dcbf7833 --- /dev/null +++ b/src/software/ml/passing/data/labelled_passes.py @@ -0,0 +1,185 @@ +from __future__ import annotations +from dataclasses import dataclass +from software.evaluation.logs.pass_log import PassLog, PassLogType +from software.evaluation.logs.log_interface import IEvalLog +from software.evaluation.logs.event_log import Team +from software.ml.passing.data.pass_result import PassResult +from software.ml.data_cleanup.stats_result import StatsResult +import uuid +from typing import List, Any, Iterator, override + + +@dataclass +class Label(IEvalLog): + has_score_changed: bool = False + has_enemy_score_changed: bool = False + have_yellow_cards_changed: bool = False + have_red_cards_changed: bool = False + has_possession_changed: bool = False + have_shots_on_net_changed: bool = False + is_enemy_possession: bool = False + has_ball_in_half_changed: bool = False + is_ball_in_enemy_half: bool = False + + @classmethod + @override + def get_num_cols(cls) -> int: + return 9 + + @override + def to_array(self) -> List[Any]: + return [ + self.has_score_changed, + self.has_enemy_score_changed, + self.have_yellow_cards_changed, + self.have_red_cards_changed, + self.has_possession_changed, + self.have_shots_on_net_changed, + self.is_enemy_possession, + self.has_ball_in_half_changed, + self.is_ball_in_enemy_half, + ] + + @staticmethod + @override + def from_csv_row(row_iter: Iterator[str]) -> Label: + def _parse(val: str) -> bool: + return val.strip().lower() == "true" + + return Label( + has_score_changed=_parse(next(row_iter)), + has_enemy_score_changed=_parse(next(row_iter)), + have_yellow_cards_changed=_parse(next(row_iter)), + have_red_cards_changed=_parse(next(row_iter)), + has_possession_changed=_parse(next(row_iter)), + have_shots_on_net_changed=_parse(next(row_iter)), + is_enemy_possession=_parse(next(row_iter)), + has_ball_in_half_changed=_parse(next(row_iter)), + is_ball_in_enemy_half=_parse(next(row_iter)), + ) + + +@dataclass +class LabelledPass(IEvalLog): + pass_log: PassLog + result: StatsResult + labels: dict[PassLogType, Label] + + @staticmethod + def get_num_labels() -> int: + # exclude the 0s interval label + return len(PassLogType) - 1 + + @classmethod + @override + def get_num_cols(cls) -> int: + labels_count = LabelledPass.get_num_labels() * Label.get_num_cols() + return PassLog.get_num_cols() + StatsResult.get_num_cols() + labels_count + + @override + def to_array(self) -> List[Any]: + arr = [] + arr.extend(self.pass_log.to_array()) + arr.extend(self.result.to_array()) + + for log_type in PassLogType: + if log_type == PassLogType.RESULT_0S: + continue + + label = self.labels.get( + log_type, Label() + ) # Default to empty label if missing + arr.extend(label.to_array()) + return arr + + @staticmethod + def from_csv_row( + row_iter: Iterator[str], friendly_team: Team + ) -> LabelledPass | None: + pass_log = PassLog.from_csv_row(row_iter) + + if pass_log is None: + return None + + result = StatsResult.from_csv_row(row_iter, friendly_team=friendly_team) + + labels = {} + for log_type in PassLogType: + if log_type == PassLogType.RESULT_0S: + continue + + labels[log_type] = Label.from_csv_row(row_iter) + + return LabelledPass(pass_log=pass_log, result=result, labels=labels) + + +def label_passes(pass_results: list[PassResult]) -> list[LabelledPass]: + # Map Pass IDs to their 0s result (game performance at the moment of the pass) for quick lookup + # {pass_id: StatsResult at t0} + t0_baselines: dict[uuid.UUID, PassResult] = {} + for result in pass_results: + if result.pass_log.pass_log_type == PassLogType.RESULT_0S: + t0_baselines[result.pass_log.pass_id] = result + + pass_labels: dict[uuid.UUID, dict[PassLogType, Label]] = {} + + # start with the t0 baselines as the previous interval state + baselines = t0_baselines.copy() + + # assign labels to all of the other pass results from other intervals + # comparing them to the previous interval's state + for result in pass_results: + pass_id = result.pass_log.pass_id + log_type = result.pass_log.pass_log_type + + if log_type != PassLogType.RESULT_0S: + # Look up the baseline for this specific pass + baseline = baselines.get(pass_id) + + if not baseline: + # Handle edge case where the previous interval log might be missing for an ID + continue + + pass_result = result.result + baseline_result = baseline.result + + # Calculate deltas relative to the 0s mark + label = Label( + has_score_changed=pass_result.score > baseline_result.score, + has_enemy_score_changed=pass_result.enemy_score + > baseline_result.enemy_score, + have_yellow_cards_changed=pass_result.yellow_cards + > baseline_result.yellow_cards, + have_red_cards_changed=pass_result.red_cards + > baseline_result.red_cards, + has_possession_changed=pass_result.has_possession + != baseline_result.has_possession, + have_shots_on_net_changed=pass_result.shots_on_net + > baseline_result.shots_on_net, + # State at current interval + is_enemy_possession=pass_result.has_possession is False, + has_ball_in_half_changed=pass_result.ball_in_enemy_half + != baseline_result.ball_in_enemy_half, + is_ball_in_enemy_half=pass_result.ball_in_enemy_half, + ) + + if result.pass_log.pass_id not in pass_labels: + pass_labels[result.pass_log.pass_id] = {} + + pass_labels[result.pass_log.pass_id][result.pass_log.pass_log_type] = label + + # update the baseline to the current interval's result for future intervals + baselines[result.pass_log.pass_id] = result + + labelled_passes = [] + + for pass_result in t0_baselines.values(): + labelled_passes.append( + LabelledPass( + pass_log=pass_result.pass_log, + result=pass_result.result, + labels=pass_labels[pass_result.pass_log.pass_id], + ) + ) + + return labelled_passes diff --git a/src/software/ml/passing/data/pass_result.py b/src/software/ml/passing/data/pass_result.py new file mode 100644 index 0000000000..00a7051aeb --- /dev/null +++ b/src/software/ml/passing/data/pass_result.py @@ -0,0 +1,45 @@ +from dataclasses import dataclass, replace +from software.ml.data_cleanup.result_interface import IResult +from software.ml.data_cleanup.stats_result import StatsResult +from software.evaluation.logs.pass_log import PassLog +from software.evaluation.logs.event_log import EventLog, Team +from typing import List + + +@dataclass +class PassResult(IResult): + pass_log: PassLog + result: StatsResult + + +def generate_pass_results( + event_logs: List[EventLog], pass_logs: List[PassLog], friendly_team: Team +) -> List[PassResult]: + """Processes a mixed list of logs chronologically to map the + game state (StatsResult) to specific PassLogs. + """ + all_logs = event_logs + pass_logs + + # 1. Sort everything by timestamp. + all_logs.sort(key=lambda x: x.get_timestamp()) + + # 2. Initialize our running state + current_stats = StatsResult(friendly_team=friendly_team) + pass_results: List[PassResult] = [] + + # 3. Iterate through the sorted logs + for log in all_logs: + if isinstance(log, EventLog): + # Update the cumulative stats based on the EventLog + prev_score = current_stats.score + current_stats.update_result(log) + if prev_score != current_stats.score: + print(current_stats) + + elif isinstance(log, PassLog): + # Create a snapshot of the stats AT THIS MOMENT. + snapshot = replace(current_stats) + + pass_results.append(PassResult(pass_log=log, result=snapshot)) + + return pass_results diff --git a/src/software/ml/passing/data/types.py b/src/software/ml/passing/data/types.py new file mode 100644 index 0000000000..87e3994e45 --- /dev/null +++ b/src/software/ml/passing/data/types.py @@ -0,0 +1,112 @@ +from dataclasses import dataclass, field +from typing import List, Tuple, Any, override +from enum import Enum +import torch +from torch_geometric.data import HeteroData + + +class NodeType(Enum): + ATTACKER = "attacker" + RECEIVER = "receiver" + ENEMY = "enemy" + BALL = "ball" + PASS_DESTINATION = "pass_destination" + GLOBAL = "global_context" + + +@dataclass +class BaseEdge: + name: Tuple[str, str, str] = field(init=False) + from_index: int + to_index: int + + def get_edge_features(self) -> list[Any]: + return [] + + +@dataclass +class GlobalToAnyEdge(BaseEdge): + destination_name: NodeType + + def __post_init__(self): + self.name = (NodeType.GLOBAL.value, "to", self.destination_name.value) + + +@dataclass +class AttackerBallEdge(BaseEdge): + distance: float + + def __post_init__(self): + self.name = (NodeType.ATTACKER.value, "to", NodeType.BALL.value) + + @override + def get_edge_features(self) -> List[Any]: + return [self.distance] + + +@dataclass +class BallPassDestinationEdge(BaseEdge): + is_shot_on_goal: bool + pass_speed: float + + def __post_init__(self): + self.name = (NodeType.BALL.value, "to", NodeType.PASS_DESTINATION.value) + + @override + def get_edge_features(self) -> List[Any]: + return [self.is_shot_on_goal] + + +@dataclass +class ReceiverPassDestinationEdge(BaseEdge): + distance: float + + def __post_init__(self): + self.name = (NodeType.RECEIVER.value, "to", NodeType.PASS_DESTINATION.value) + + @override + def get_edge_features(self) -> List[Any]: + return [self.distance] + + +@dataclass +class EnemyPassDestinationEdge(BaseEdge): + distance: float + intercept_dist: float + time_delta: float + + def __post_init__(self): + self.name = (NodeType.ENEMY.value, "to", NodeType.PASS_DESTINATION.value) + + @override + def get_edge_features(self) -> List[Any]: + return [self.distance, self.intercept_dist, self.time_delta] + + +def add_edge_to_data(edge: BaseEdge, data: HeteroData) -> HeteroData: + # 1. Create the new edge index [2, 1] + new_index = torch.tensor([[edge.from_index], [edge.to_index]], dtype=torch.long) + + # 2. Create the new features [1, num_features] + edge_features = edge.get_edge_features() + new_attr = torch.tensor([edge_features], dtype=torch.float) + + # 3. Check if this edge type already exists in data + if edge.name in data.edge_types: + # Concatenate along the edge dimension (dim=1 for index, dim=0 for attr) + data[edge.name].edge_index = torch.cat( + [data[edge.name].edge_index, new_index], dim=1 + ) + + if len(edge_features) > 0: + data[edge.name].edge_attr = torch.cat( + [data[edge.name].edge_attr, new_attr], dim=0 + ) + else: + # First time seeing this edge type + data[edge.name].edge_index = new_index + + if len(edge_features) > 0: + data[edge.name].edge_attr = new_attr + + return data diff --git a/src/software/ml/passing/train_passing.py b/src/software/ml/passing/train_passing.py new file mode 100644 index 0000000000..1229edce65 --- /dev/null +++ b/src/software/ml/passing/train_passing.py @@ -0,0 +1,411 @@ +import random +import sys +import torch +import os +from sklearn.metrics import f1_score +from torch_geometric.data import HeteroData +from torch_geometric.nn import SAGEConv, GATConv +from torch_geometric.loader import DataLoader +import torch.onnx +import numpy as np +from typing import List, Any +from dataclasses import fields +from software.ml.models.hetero_gnn.hetero_gnn import GenericHeteroGNN +from software.ml.models.hetero_gnn.config import HeteroGNNConfig +from software.ml.passing.data.types import NodeType +from software.ml.passing.data.build_graph import process_all_passes +from software.ml.passing.data.labelled_passes import LabelledPass, Label +from software.evaluation.logs.event_log import Team +from software.evaluation.logs.pass_log import PassLogType +import csv + +dir_path = os.path.dirname(os.path.realpath(__file__)) +ml_dir_path = os.path.dirname(dir_path) +datasets_path = os.path.join(ml_dir_path, "datasets") +onnx_path = os.path.join(dir_path, "onnx") + +NUM_INTERVALS = len(PassLogType) - 1 +LABEL_FIELD_NAMES = [f.name for f in fields(Label)] +NUM_LABELS_PER_INTERVAL = len(LABEL_FIELD_NAMES) + + +def load_labelled_passes( + labelled_pass_file: str, friendly_team: Team +) -> List[LabelledPass]: + input_path = os.path.join(datasets_path, labelled_pass_file) + labelled_passes = [] + + if not os.path.exists(input_path): + raise Exception(f"Error: File {input_path} does not exist.") + + with open(input_path, mode="r", encoding="utf-8") as f: + reader = csv.reader(f) + for row in reader: + try: + labelled_pass = LabelledPass.from_csv_row( + iter(row), friendly_team=friendly_team + ) + labelled_passes.append(labelled_pass) + except Exception as e: + print(f"Skipping row due to error: {e}") + + print( + f"Successfully loaded {len(labelled_passes)} labelled passes from {input_path}" + ) + return labelled_passes + + +def calculate_label_weights(labelled_passes: list[LabelledPass]): + """Calculates the positive weights for BCEWithLogitsLoss for each label type + based on the actual frequency of events in the dataset. + """ + # 1. Flatten all labels into a single large 2D array [num_samples, num_bits] + all_label_tensors = [] + + for labelled_pass in labelled_passes: + # Concatenate labels for all intervals (1s, 5s, 10s, etc.) + # into one long vector for this pass + pass_bits = [] + + # iterate through intervals in order + for interval in sorted(labelled_pass.labels.keys(), key=lambda x: x.value): + label = labelled_pass.labels[interval] + pass_bits.extend( + [ + label.has_score_changed, + label.has_enemy_score_changed, + label.have_yellow_cards_changed, + label.have_red_cards_changed, + label.has_possession_changed, + label.have_shots_on_net_changed, + label.is_enemy_possession, + label.has_ball_in_half_changed, + label.is_ball_in_enemy_half, + ] + ) + all_label_tensors.append(pass_bits) + + # Convert to numpy for easy counting + labels_np = np.array(all_label_tensors, dtype=np.float32) + + # 2. Calculate weights for each bit + # pos_weight = (num_negative_samples) / (num_positive_samples) + num_positives = np.sum(labels_np, axis=0) + num_negatives = labels_np.shape[0] - num_positives + + # Avoid division by zero if an event never happened in the dataset + # We use 1.0 as a default weight for those cases + label_weights = np.divide( + num_negatives, + num_positives, + out=np.ones_like(num_positives), + where=num_positives != 0, + ) + + print(f"Weights are: {label_weights}") + + # 3. Convert to Torch Tensor + return torch.tensor(label_weights, dtype=torch.float32) + + +PRIORITY_EVENTS = [ + "has_score_changed", + "has_enemy_score_changed", + "have_yellow_cards_changed", + "have_red_cards_changed", + "have_shots_on_net_changed", +] + +PRIORITY_EVENT_SCALE = 5.0 + + +def scale_priority_labels( + label_weights: torch.Tensor, + multiplier: float = PRIORITY_EVENT_SCALE, +) -> torch.Tensor: + """Scales the weights for specific fields across all time intervals.""" + # 1. Get the list of field names from the Label dataclass in order + + + # Clone to avoid modifying the original tensor in-place if desired + scaled_weights = label_weights.clone() + + # 2. Find the local indices of the fields we want to boost + priority_event_indices = [ + i for i, name in enumerate(LABEL_FIELD_NAMES) if name in PRIORITY_EVENTS + ] + + # 3. Apply the multiplier to those indices across every interval block + for idx in range(NUM_INTERVALS): + offset = idx * NUM_LABELS_PER_INTERVAL + for priority_idx in priority_event_indices: + global_idx = offset + priority_idx + scaled_weights[global_idx] *= multiplier + + return scaled_weights + +PRIORITY_INTERVALS = { + PassLogType.RESULT_5S: 5.0, + PassLogType.RESULT_10S: 2.0, + PassLogType.RESULT_20S: 2.0 +} + +def scale_priority_intervals( + label_weights: torch.Tensor +): + scaled_weights = label_weights.clone() + + for idx, interval_type in enumerate(PassLogType): + if interval_type in PRIORITY_INTERVALS: + start_idx = idx * NUM_LABELS_PER_INTERVAL + end_idx = start_idx + NUM_LABELS_PER_INTERVAL + + scaled_weights[start_idx:end_idx] *= PRIORITY_INTERVALS[interval_type] + + return scaled_weights + + +def undersample_passes(all_passes: list[LabelledPass], boring_keep_ratio: float = 0.1): + """Filters the dataset to keep all 'interesting' passes and a + fraction of the 'boring' ones. + """ + interesting_passes = [] + boring_passes = [] + + for labelled_pass in all_passes: + # Check if ANY label in ANY interval is True + # Note: We exclude 'is_enemy_possession' and 'is_ball_in_enemy_half' + # from the 'interesting' check because they are static states, not events. + is_interesting = False + for interval_label in labelled_pass.labels.values(): + # We check specific event-based flags + if any( + [ + interval_label.has_score_changed, + interval_label.has_enemy_score_changed, + interval_label.have_yellow_cards_changed, + interval_label.have_red_cards_changed, + interval_label.has_possession_changed, + interval_label.have_shots_on_net_changed, + interval_label.has_ball_in_half_changed, + ] + ): + is_interesting = True + break + + if is_interesting: + interesting_passes.append(labelled_pass) + else: + boring_passes.append(labelled_pass) + + # Sample a percentage of the boring passes + num_boring_to_keep = int(len(boring_passes) * boring_keep_ratio) + sampled_boring = random.sample(boring_passes, num_boring_to_keep) + + combined = interesting_passes + sampled_boring + random.shuffle(combined) # Shuffle so the model doesn't see all goals at once + + print( + f"Original: {len(all_passes)} | Interesting: {len(interesting_passes)} | Boring Kept: {len(sampled_boring)}" + ) + return combined + +NUM_EPOCHS = 200 +LEARNING_RATE = 0.001 + +def train_single_model( + loader: DataLoader, + model: GenericHeteroGNN, + label_weights: torch.Tensor, + epochs=NUM_EPOCHS, + learning_rate=LEARNING_RATE, +) -> GenericHeteroGNN: + # 1. Setup the "Judge" (Loss Function) and the "Optimizer" (Weight Updater) + # BCEWithLogitsLoss combines a Sigmoid layer and the BCELoss in one single class. + # It's more numerically stable than using a plain Sigmoid followed by BCELoss. + criterion = torch.nn.BCEWithLogitsLoss(pos_weight=label_weights) + optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate) + + # dynamic learning rate, adapts by looking at how loss changes over `patience` epochs, + # and adjusts learning rate down if loss is not changing (i.e we are missing the min) + scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.75, patience=10) + + model.train() + + for epoch in range(epochs): + total_loss = 0 + for data in loader: + optimizer.zero_grad() # Clear old gradients from previous step + + # 3. Forward Pass: Get raw logits from the 'destination' node + # logits shape: [batch_size, num_types, num_labels] + logits = model(data.x_dict, data.edge_index_dict) + + # 4. Align the target labels + # data.y was built from your Label dataclass + target = data.y.view(logits.size()) # Ensure shapes match exactly + + # 5. Compute Loss + loss = criterion(logits, target) + + # 6. Backward Pass: Calculate gradients + loss.backward() + + # optimization: to prevent the scaled gradient update from being too large + # torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=10.0) + + # 7. Optimization: Nudge weights to reduce loss using the scheduler + # scheduler.step(loss) + optimizer.step() + + total_loss += loss.item() + + print(f"Epoch {epoch+1}/{epochs} | Loss: {total_loss/len(loader):.4f}") + + return model + + +LAYER_OPTIONS = [2, 4] +CONVOLUTION_OPTIONS = [(SAGEConv, {"aggr": "mean"}), (GATConv, {"heads": 8, "add_self_loops": False})] +HIDDEN_DIMENSION_OPTIONS = [64, 128] + +MODEL_NAME_TEMPLATE = "hetero_gnn_L{num_layers}_C{conv_type}_H{num_hidden_dims}" + +TRAIN_DATA_CROSS_RATIO = 0.8 + + +def evaluate_model(loader: DataLoader, model: GenericHeteroGNN): + model.eval() + + intervals = [ + interval for interval in PassLogType if interval != PassLogType.RESULT_0S + ] + + interval_preds = {interval: [] for interval in intervals} + interval_targets = {interval: [] for interval in intervals} + + with torch.no_grad(): + for data in loader: + # 1. Forward pass + logits = model(data.x_dict, data.edge_index_dict) + target = data.y.view(logits.size()) + + logits_len = len(logits[0]) + logits_per_interval = logits_len // len(intervals) + + # 2. Iterate through each time horizon + for i, interval in enumerate(intervals): + logits_start = i * logits_per_interval + logits_end = logits_start + logits_per_interval + + interval_logits = logits[:, logits_start:logits_end] + interval_target = target[:, logits_start:logits_end] + + interval_targets[interval].append(interval_target) + + preds = (torch.sigmoid(interval_logits) > 0.5).float() + interval_preds[interval].append(preds) + + for _, interval in enumerate(intervals): + preds = interval_preds[interval] + targets = interval_targets[interval] + + pred_matrix = torch.cat(preds, dim=0).cpu().numpy() + target_matrix = torch.cat(targets, dim=0).cpu().numpy() + + print(f"\n--- Interval: {interval.name} ---") + + for j, name in enumerate(LABEL_FIELD_NAMES): + f1 = f1_score(target_matrix[:, j], pred_matrix[:, j], zero_division=0) + print(f"{name:20} | F1-Score: {f1:.4f}") + + +def train_and_export_models( + graphs: List[HeteroData], labels: List[List[Any]], label_weights: torch.Tensor +): + for graph, label in zip(graphs, labels): + graph.y = torch.tensor(label, dtype=torch.float) + + random.shuffle(graphs) + split_idx = int(len(graphs) * TRAIN_DATA_CROSS_RATIO) + training_data, test_data = graphs[:split_idx], graphs[split_idx:] + + training_loader = DataLoader(training_data, batch_size=32, shuffle=True) + test_loader = DataLoader(test_data, batch_size=32, shuffle=True) + + for num_layers in LAYER_OPTIONS: + for convolution in CONVOLUTION_OPTIONS: + for num_hidden_dims in HIDDEN_DIMENSION_OPTIONS: + conv_name, args = convolution + + model_name = MODEL_NAME_TEMPLATE.format( + num_layers=num_layers, + conv_type=conv_name.__name__, + num_hidden_dims=num_hidden_dims, + ) + + print(f"Training {model_name}") + + metadata = training_data[0].metadata() + num_labels = training_data[0].y.shape[-1] + + config = HeteroGNNConfig( + edge_specs=convolution, + num_layers=num_layers, + hidden_channels=num_hidden_dims, + out_channels=num_labels, + return_node=NodeType.PASS_DESTINATION.value, + ) + + model = GenericHeteroGNN(config=config, metadata=metadata) + + train_single_model( + loader=training_loader, model=model, label_weights=label_weights + ) + + print(f"Trained {model_name}") + + evaluate_model(loader=test_loader, model=model) + + model.eval() + + x_dict = training_data[0].x_dict + edge_dict = training_data[0].edge_index_dict + + # file_name = os.path.join(onnx_path, f"{model_name}.onnx") + + # torch.onnx.export( + # model, + # (x_dict, edge_dict), + # file_name, + # input_names=["x_dict", "edge_dict"], + # output_names=["output"], + # dynamic_axes={"x_dict": {0: "batch"}, "output": {0: "batch"}}, + # opset_version=15, # Important for complex GNN ops + #) + + print(f"Saved {model_name}.onnx") + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python train_passing.py ") + sys.exit(1) + + labelled_passes = load_labelled_passes( + labelled_pass_file=sys.argv[1], friendly_team=Team.BLUE + ) + + label_weights = calculate_label_weights(labelled_passes=labelled_passes) + + event_scaled_weights = scale_priority_labels(label_weights=label_weights) + + interval_scaled_weights = scale_priority_intervals(label_weights=label_weights) + + undersampled_passes = undersample_passes(labelled_passes) + + graphs, labels = process_all_passes(labelled_passes=undersampled_passes) + + print("Dataset generated!") + + train_and_export_models(graphs, labels, event_scaled_weights) diff --git a/src/software/ml/requirements.in b/src/software/ml/requirements.in new file mode 100644 index 0000000000..18fe0f62a9 --- /dev/null +++ b/src/software/ml/requirements.in @@ -0,0 +1,7 @@ +pandas +numpy +torch +torch_geometric +onnxruntime +scikit-learn +onnxscript diff --git a/src/software/ml/requirements_lock.txt b/src/software/ml/requirements_lock.txt new file mode 100644 index 0000000000..ef707c116d --- /dev/null +++ b/src/software/ml/requirements_lock.txt @@ -0,0 +1,1679 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# bazel run //software/ml:requirements.update +# +aiohappyeyeballs==2.6.1 \ + --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ + --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 + # via aiohttp +aiohttp==3.13.5 \ + --hash=sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5 \ + --hash=sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b \ + --hash=sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9 \ + --hash=sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b \ + --hash=sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8 \ + --hash=sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a \ + --hash=sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2 \ + --hash=sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1 \ + --hash=sha256:147b4f501d0292077f29d5268c16bb7c864a1f054d7001c4c1812c0421ea1ed0 \ + --hash=sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037 \ + --hash=sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416 \ + --hash=sha256:178c7b5e62b454c2bc790786e6058c3cc968613b4419251b478c153a4aec32b1 \ + --hash=sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9 \ + --hash=sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a \ + --hash=sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36 \ + --hash=sha256:206b7b3ef96e4ce211754f0cd003feb28b7d81f0ad26b8d077a5d5161436067f \ + --hash=sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b \ + --hash=sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174 \ + --hash=sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b \ + --hash=sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8 \ + --hash=sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e \ + --hash=sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6 \ + --hash=sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c \ + --hash=sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe \ + --hash=sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9 \ + --hash=sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc \ + --hash=sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800 \ + --hash=sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286 \ + --hash=sha256:347542f0ea3f95b2a955ee6656461fa1c776e401ac50ebce055a6c38454a0adf \ + --hash=sha256:39380e12bd1f2fdab4285b6e055ad48efbaed5c836433b142ed4f5b9be71036a \ + --hash=sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc \ + --hash=sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9 \ + --hash=sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665 \ + --hash=sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832 \ + --hash=sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297 \ + --hash=sha256:4e704c52438f66fdd89588346183d898bb42167cf88f8b7ff1c0f9fc957c348f \ + --hash=sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73 \ + --hash=sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b \ + --hash=sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9 \ + --hash=sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090 \ + --hash=sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49 \ + --hash=sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d \ + --hash=sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46 \ + --hash=sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83 \ + --hash=sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796 \ + --hash=sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be \ + --hash=sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d \ + --hash=sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf \ + --hash=sha256:6f497a6876aa4b1a102b04996ce4c1170c7040d83faa9387dd921c16e30d5c83 \ + --hash=sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25 \ + --hash=sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06 \ + --hash=sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3 \ + --hash=sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6 \ + --hash=sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb \ + --hash=sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88 \ + --hash=sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9 \ + --hash=sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be \ + --hash=sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14 \ + --hash=sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a \ + --hash=sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c \ + --hash=sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3 \ + --hash=sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f \ + --hash=sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d \ + --hash=sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670 \ + --hash=sha256:912d4b6af530ddb1338a66229dac3a25ff11d4448be3ec3d6340583995f56031 \ + --hash=sha256:9277145d36a01653863899c665243871434694bcc3431922c3b35c978061bdb8 \ + --hash=sha256:95d14ca7abefde230f7639ec136ade282655431fd5db03c343b19dda72dd1643 \ + --hash=sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d \ + --hash=sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8 \ + --hash=sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8 \ + --hash=sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1 \ + --hash=sha256:9efcc0f11d850cefcafdd9275b9576ad3bfb539bed96807663b32ad99c4d4b88 \ + --hash=sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb \ + --hash=sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61 \ + --hash=sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4 \ + --hash=sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7 \ + --hash=sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9 \ + --hash=sha256:a8a4d3427e8de1312ddf309cc482186466c79895b3a139fed3259fc01dfa9a5b \ + --hash=sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500 \ + --hash=sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6 \ + --hash=sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2 \ + --hash=sha256:af545c2cffdb0967a96b6249e6f5f7b0d92cdfd267f9d5238d5b9ca63e8edb10 \ + --hash=sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1 \ + --hash=sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3 \ + --hash=sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e \ + --hash=sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a \ + --hash=sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5 \ + --hash=sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95 \ + --hash=sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074 \ + --hash=sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5 \ + --hash=sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b \ + --hash=sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772 \ + --hash=sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a \ + --hash=sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274 \ + --hash=sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94 \ + --hash=sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13 \ + --hash=sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac \ + --hash=sha256:cb979826071c0986a5f08333a36104153478ce6018c58cba7f9caddaf63d5d67 \ + --hash=sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76 \ + --hash=sha256:d147004fede1b12f6013a6dbb2a26a986a671a03c6ea740ddc76500e5f1c399f \ + --hash=sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8 \ + --hash=sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7 \ + --hash=sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8 \ + --hash=sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3 \ + --hash=sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be \ + --hash=sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b \ + --hash=sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c \ + --hash=sha256:e999f0c88a458c836d5fb521814e92ed2172c649200336a6df514987c1488258 \ + --hash=sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c \ + --hash=sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6 \ + --hash=sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56 \ + --hash=sha256:ee5e86776273de1795947d17bddd6bb19e0365fd2af4289c0d2c5454b6b1d36b \ + --hash=sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d \ + --hash=sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a \ + --hash=sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162 \ + --hash=sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1 \ + --hash=sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6 \ + --hash=sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5 \ + --hash=sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540 \ + --hash=sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254 + # via torch-geometric +aiosignal==1.4.0 \ + --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \ + --hash=sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7 + # via aiohttp +attrs==26.1.0 \ + --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ + --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 + # via aiohttp +certifi==2026.2.25 \ + --hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \ + --hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7 + # via requests +charset-normalizer==3.4.7 \ + --hash=sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc \ + --hash=sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c \ + --hash=sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67 \ + --hash=sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4 \ + --hash=sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0 \ + --hash=sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c \ + --hash=sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5 \ + --hash=sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444 \ + --hash=sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153 \ + --hash=sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9 \ + --hash=sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01 \ + --hash=sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217 \ + --hash=sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b \ + --hash=sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c \ + --hash=sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a \ + --hash=sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83 \ + --hash=sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5 \ + --hash=sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7 \ + --hash=sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb \ + --hash=sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c \ + --hash=sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1 \ + --hash=sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42 \ + --hash=sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab \ + --hash=sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df \ + --hash=sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e \ + --hash=sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207 \ + --hash=sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18 \ + --hash=sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734 \ + --hash=sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38 \ + --hash=sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110 \ + --hash=sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18 \ + --hash=sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44 \ + --hash=sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d \ + --hash=sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48 \ + --hash=sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e \ + --hash=sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5 \ + --hash=sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d \ + --hash=sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53 \ + --hash=sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790 \ + --hash=sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c \ + --hash=sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b \ + --hash=sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116 \ + --hash=sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d \ + --hash=sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10 \ + --hash=sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6 \ + --hash=sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2 \ + --hash=sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776 \ + --hash=sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a \ + --hash=sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265 \ + --hash=sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008 \ + --hash=sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943 \ + --hash=sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374 \ + --hash=sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246 \ + --hash=sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e \ + --hash=sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5 \ + --hash=sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616 \ + --hash=sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15 \ + --hash=sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41 \ + --hash=sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960 \ + --hash=sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752 \ + --hash=sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e \ + --hash=sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72 \ + --hash=sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7 \ + --hash=sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8 \ + --hash=sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b \ + --hash=sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4 \ + --hash=sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545 \ + --hash=sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706 \ + --hash=sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366 \ + --hash=sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb \ + --hash=sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a \ + --hash=sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e \ + --hash=sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00 \ + --hash=sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f \ + --hash=sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a \ + --hash=sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1 \ + --hash=sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66 \ + --hash=sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356 \ + --hash=sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319 \ + --hash=sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4 \ + --hash=sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad \ + --hash=sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d \ + --hash=sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5 \ + --hash=sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7 \ + --hash=sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0 \ + --hash=sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686 \ + --hash=sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34 \ + --hash=sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49 \ + --hash=sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c \ + --hash=sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1 \ + --hash=sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e \ + --hash=sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60 \ + --hash=sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0 \ + --hash=sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274 \ + --hash=sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d \ + --hash=sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0 \ + --hash=sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae \ + --hash=sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f \ + --hash=sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d \ + --hash=sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe \ + --hash=sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3 \ + --hash=sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393 \ + --hash=sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1 \ + --hash=sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af \ + --hash=sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44 \ + --hash=sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00 \ + --hash=sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c \ + --hash=sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3 \ + --hash=sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7 \ + --hash=sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd \ + --hash=sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e \ + --hash=sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b \ + --hash=sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8 \ + --hash=sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259 \ + --hash=sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859 \ + --hash=sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46 \ + --hash=sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30 \ + --hash=sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b \ + --hash=sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46 \ + --hash=sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24 \ + --hash=sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a \ + --hash=sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24 \ + --hash=sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc \ + --hash=sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215 \ + --hash=sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063 \ + --hash=sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832 \ + --hash=sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6 \ + --hash=sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79 \ + --hash=sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464 + # via requests +cuda-bindings==13.2.0 \ + --hash=sha256:08b395f79cb89ce0cd8effff07c4a1e20101b873c256a1aeb286e8fd7bd0f556 \ + --hash=sha256:1eba9504ac70667dd48313395fe05157518fd6371b532790e96fbb31bbb5a5e1 \ + --hash=sha256:45815daeb595bf3b405c52671a2542b1f8e9329f3b029494acbfcc74aeaa1f2d \ + --hash=sha256:46d8776a55d6d5da9dd6e9858fba2efcda2abe6743871dee47dd06eb8cb6d955 \ + --hash=sha256:6629ca2df6f795b784752409bcaedbd22a7a651b74b56a165ebc0c9dcbd504d0 \ + --hash=sha256:6ccf14e0c1def3b7200100aafff3a9f7e210ecb6e409329e92dcf6cd2c00d5c7 \ + --hash=sha256:721104c603f059780d287969be3d194a18d0cc3b713ed9049065a1107706759d \ + --hash=sha256:7dca0da053d3b4cc4869eff49c61c03f3c5dbaa0bcd712317a358d5b8f3f385d \ + --hash=sha256:845025438a1b9e20718b9fb42add3e0eb72e85458bcab3eeb80bfd8f0a9dab33 \ + --hash=sha256:8cebe3ce4aeeca5af9c490e175f76c4b569bbf4a35a62294b777bc77bf7ac4d8 \ + --hash=sha256:a6464b30f46692d6c7f65d4a0e0450d81dd29de3afc1bb515653973d01c2cd6e \ + --hash=sha256:bd658bb5c0e55b7b3e5dd0ed509c6addb298c665db26a9bfba35e1e626000ba2 \ + --hash=sha256:d6f3682ec3c4769326aafc67c2ba669d97d688d0b7e63e659d36d2f8b72f32d6 \ + --hash=sha256:debb51b211d246f8326f6b6e982506a5d0d9906672c91bc478b66addc7ecc60a \ + --hash=sha256:df850a1ff8ce1b3385257b08e47b70e959932f5f432d0a4e46a355962b4e4771 \ + --hash=sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788 \ + --hash=sha256:e8a16384c6494e5485f39314b0b4afb04bee48d49edb16d5d8593fd35bbd231b \ + --hash=sha256:f4af9f3e1be603fa12d5ad6cfca7844c9d230befa9792b5abdf7dd79979c3626 + # via torch +cuda-pathfinder==1.5.1 \ + --hash=sha256:b3718097fb57cf9e8a904dd072d806f2c9a27627e35c020b06ab9454bcec08c0 + # via cuda-bindings +cuda-toolkit[cublas,cudart,cufft,cufile,cupti,curand,cusolver,cusparse,nvjitlink,nvrtc,nvtx]==13.0.2 \ + --hash=sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb + # via torch +filelock==3.25.2 \ + --hash=sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694 \ + --hash=sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70 + # via torch +flatbuffers==25.12.19 \ + --hash=sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4 + # via onnxruntime +frozenlist==1.8.0 \ + --hash=sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686 \ + --hash=sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0 \ + --hash=sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121 \ + --hash=sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd \ + --hash=sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7 \ + --hash=sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c \ + --hash=sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84 \ + --hash=sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d \ + --hash=sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b \ + --hash=sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79 \ + --hash=sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967 \ + --hash=sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f \ + --hash=sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4 \ + --hash=sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7 \ + --hash=sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef \ + --hash=sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9 \ + --hash=sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3 \ + --hash=sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd \ + --hash=sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087 \ + --hash=sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068 \ + --hash=sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7 \ + --hash=sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed \ + --hash=sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b \ + --hash=sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f \ + --hash=sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25 \ + --hash=sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe \ + --hash=sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143 \ + --hash=sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e \ + --hash=sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930 \ + --hash=sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37 \ + --hash=sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128 \ + --hash=sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2 \ + --hash=sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675 \ + --hash=sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f \ + --hash=sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746 \ + --hash=sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df \ + --hash=sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8 \ + --hash=sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c \ + --hash=sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0 \ + --hash=sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad \ + --hash=sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82 \ + --hash=sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29 \ + --hash=sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c \ + --hash=sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30 \ + --hash=sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf \ + --hash=sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62 \ + --hash=sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5 \ + --hash=sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383 \ + --hash=sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c \ + --hash=sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52 \ + --hash=sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d \ + --hash=sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1 \ + --hash=sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a \ + --hash=sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714 \ + --hash=sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65 \ + --hash=sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95 \ + --hash=sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1 \ + --hash=sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506 \ + --hash=sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888 \ + --hash=sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6 \ + --hash=sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41 \ + --hash=sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459 \ + --hash=sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a \ + --hash=sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608 \ + --hash=sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa \ + --hash=sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8 \ + --hash=sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1 \ + --hash=sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186 \ + --hash=sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6 \ + --hash=sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed \ + --hash=sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e \ + --hash=sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52 \ + --hash=sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231 \ + --hash=sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450 \ + --hash=sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496 \ + --hash=sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a \ + --hash=sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3 \ + --hash=sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24 \ + --hash=sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178 \ + --hash=sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695 \ + --hash=sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7 \ + --hash=sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4 \ + --hash=sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e \ + --hash=sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e \ + --hash=sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61 \ + --hash=sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca \ + --hash=sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad \ + --hash=sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b \ + --hash=sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a \ + --hash=sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8 \ + --hash=sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51 \ + --hash=sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011 \ + --hash=sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8 \ + --hash=sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103 \ + --hash=sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b \ + --hash=sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda \ + --hash=sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806 \ + --hash=sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042 \ + --hash=sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e \ + --hash=sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b \ + --hash=sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef \ + --hash=sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d \ + --hash=sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567 \ + --hash=sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a \ + --hash=sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2 \ + --hash=sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0 \ + --hash=sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e \ + --hash=sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b \ + --hash=sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d \ + --hash=sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a \ + --hash=sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52 \ + --hash=sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47 \ + --hash=sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1 \ + --hash=sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94 \ + --hash=sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f \ + --hash=sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff \ + --hash=sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822 \ + --hash=sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a \ + --hash=sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11 \ + --hash=sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581 \ + --hash=sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51 \ + --hash=sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565 \ + --hash=sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40 \ + --hash=sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92 \ + --hash=sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2 \ + --hash=sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5 \ + --hash=sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4 \ + --hash=sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93 \ + --hash=sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027 \ + --hash=sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd + # via + # aiohttp + # aiosignal +fsspec==2026.3.0 \ + --hash=sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41 \ + --hash=sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4 + # via + # torch + # torch-geometric +idna==3.11 \ + --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ + --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 + # via + # requests + # yarl +jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 + # via + # torch + # torch-geometric +joblib==1.5.3 \ + --hash=sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713 \ + --hash=sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3 + # via scikit-learn +markupsafe==3.0.3 \ + --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ + --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ + --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ + --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ + --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ + --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ + --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ + --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ + --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ + --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ + --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ + --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ + --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ + --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ + --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ + --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ + --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ + --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ + --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ + --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ + --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ + --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ + --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ + --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ + --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ + --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ + --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ + --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ + --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ + --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ + --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ + --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ + --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ + --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ + --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ + --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ + --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ + --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ + --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ + --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ + --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ + --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ + --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ + --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ + --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ + --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ + --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ + --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ + --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ + --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ + --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ + --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ + --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ + --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ + --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ + --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ + --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ + --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ + --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ + --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ + --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ + --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ + --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ + --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ + --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ + --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ + --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ + --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ + --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ + --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ + --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ + --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ + --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ + --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ + --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ + --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ + --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ + --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ + --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ + --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ + --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ + --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ + --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ + --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ + --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ + --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ + --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ + --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ + --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 + # via jinja2 +ml-dtypes==0.5.4 \ + --hash=sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf \ + --hash=sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d \ + --hash=sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f \ + --hash=sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483 \ + --hash=sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7 \ + --hash=sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22 \ + --hash=sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6 \ + --hash=sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175 \ + --hash=sha256:388d399a2152dd79a3f0456a952284a99ee5c93d3e2f8dfe25977511e0515270 \ + --hash=sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1 \ + --hash=sha256:3d277bf3637f2a62176f4575512e9ff9ef51d00e39626d9fe4a161992f355af2 \ + --hash=sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1 \ + --hash=sha256:4ff7f3e7ca2972e7de850e7b8fcbb355304271e2933dd90814c1cb847414d6e2 \ + --hash=sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298 \ + --hash=sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d \ + --hash=sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de \ + --hash=sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049 \ + --hash=sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d \ + --hash=sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90 \ + --hash=sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb \ + --hash=sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465 \ + --hash=sha256:88c982aac7cb1cbe8cbb4e7f253072b1df872701fcaf48d84ffbb433b6568f24 \ + --hash=sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453 \ + --hash=sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56 \ + --hash=sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48 \ + --hash=sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff \ + --hash=sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460 \ + --hash=sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac \ + --hash=sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900 \ + --hash=sha256:a9b61c19040397970d18d7737375cffd83b1f36a11dd4ad19f83a016f736c3ef \ + --hash=sha256:b4b801ebe0b477be666696bda493a9be8356f1f0057a57f1e35cd26928823e5a \ + --hash=sha256:b95e97e470fe60ed493fd9ae3911d8da4ebac16bd21f87ffa2b7c588bf22ea2c \ + --hash=sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040 \ + --hash=sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9 \ + --hash=sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7 \ + --hash=sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6 \ + --hash=sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b \ + --hash=sha256:d81fdb088defa30eb37bf390bb7dde35d3a83ec112ac8e33d75ab28cc29dd8b0 \ + --hash=sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328 + # via + # onnx + # onnx-ir + # onnxscript +mpmath==1.3.0 \ + --hash=sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f \ + --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c + # via sympy +multidict==6.7.1 \ + --hash=sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0 \ + --hash=sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9 \ + --hash=sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581 \ + --hash=sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2 \ + --hash=sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941 \ + --hash=sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3 \ + --hash=sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43 \ + --hash=sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962 \ + --hash=sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1 \ + --hash=sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f \ + --hash=sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c \ + --hash=sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8 \ + --hash=sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa \ + --hash=sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6 \ + --hash=sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c \ + --hash=sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991 \ + --hash=sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262 \ + --hash=sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd \ + --hash=sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d \ + --hash=sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d \ + --hash=sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5 \ + --hash=sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3 \ + --hash=sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601 \ + --hash=sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505 \ + --hash=sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0 \ + --hash=sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292 \ + --hash=sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed \ + --hash=sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362 \ + --hash=sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511 \ + --hash=sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23 \ + --hash=sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2 \ + --hash=sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb \ + --hash=sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e \ + --hash=sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582 \ + --hash=sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0 \ + --hash=sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2 \ + --hash=sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e \ + --hash=sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d \ + --hash=sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65 \ + --hash=sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a \ + --hash=sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd \ + --hash=sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d \ + --hash=sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108 \ + --hash=sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177 \ + --hash=sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144 \ + --hash=sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5 \ + --hash=sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd \ + --hash=sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5 \ + --hash=sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060 \ + --hash=sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37 \ + --hash=sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56 \ + --hash=sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df \ + --hash=sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963 \ + --hash=sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568 \ + --hash=sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db \ + --hash=sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118 \ + --hash=sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84 \ + --hash=sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f \ + --hash=sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889 \ + --hash=sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71 \ + --hash=sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f \ + --hash=sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0 \ + --hash=sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7 \ + --hash=sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048 \ + --hash=sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8 \ + --hash=sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49 \ + --hash=sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0 \ + --hash=sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9 \ + --hash=sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59 \ + --hash=sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190 \ + --hash=sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709 \ + --hash=sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d \ + --hash=sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c \ + --hash=sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e \ + --hash=sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2 \ + --hash=sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40 \ + --hash=sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3 \ + --hash=sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee \ + --hash=sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609 \ + --hash=sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c \ + --hash=sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445 \ + --hash=sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1 \ + --hash=sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a \ + --hash=sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5 \ + --hash=sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31 \ + --hash=sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8 \ + --hash=sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33 \ + --hash=sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7 \ + --hash=sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca \ + --hash=sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8 \ + --hash=sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92 \ + --hash=sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733 \ + --hash=sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429 \ + --hash=sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9 \ + --hash=sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4 \ + --hash=sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6 \ + --hash=sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2 \ + --hash=sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172 \ + --hash=sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981 \ + --hash=sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5 \ + --hash=sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de \ + --hash=sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52 \ + --hash=sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7 \ + --hash=sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c \ + --hash=sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2 \ + --hash=sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6 \ + --hash=sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf \ + --hash=sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f \ + --hash=sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b \ + --hash=sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961 \ + --hash=sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a \ + --hash=sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3 \ + --hash=sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b \ + --hash=sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358 \ + --hash=sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6 \ + --hash=sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e \ + --hash=sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1 \ + --hash=sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c \ + --hash=sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5 \ + --hash=sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53 \ + --hash=sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872 \ + --hash=sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e \ + --hash=sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df \ + --hash=sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03 \ + --hash=sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8 \ + --hash=sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a \ + --hash=sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122 \ + --hash=sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a \ + --hash=sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee \ + --hash=sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32 \ + --hash=sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3 \ + --hash=sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489 \ + --hash=sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23 \ + --hash=sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34 \ + --hash=sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75 \ + --hash=sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8 \ + --hash=sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a \ + --hash=sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d \ + --hash=sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855 \ + --hash=sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b \ + --hash=sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4 \ + --hash=sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4 \ + --hash=sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d \ + --hash=sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0 \ + --hash=sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba \ + --hash=sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19 + # via + # aiohttp + # yarl +networkx==3.6.1 \ + --hash=sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509 \ + --hash=sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762 + # via torch +numpy==2.4.4 \ + --hash=sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed \ + --hash=sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50 \ + --hash=sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959 \ + --hash=sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827 \ + --hash=sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd \ + --hash=sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233 \ + --hash=sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc \ + --hash=sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b \ + --hash=sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7 \ + --hash=sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e \ + --hash=sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a \ + --hash=sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d \ + --hash=sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3 \ + --hash=sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e \ + --hash=sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb \ + --hash=sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a \ + --hash=sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0 \ + --hash=sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e \ + --hash=sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113 \ + --hash=sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103 \ + --hash=sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93 \ + --hash=sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af \ + --hash=sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5 \ + --hash=sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7 \ + --hash=sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392 \ + --hash=sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c \ + --hash=sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4 \ + --hash=sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40 \ + --hash=sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf \ + --hash=sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44 \ + --hash=sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b \ + --hash=sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5 \ + --hash=sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e \ + --hash=sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74 \ + --hash=sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0 \ + --hash=sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e \ + --hash=sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec \ + --hash=sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015 \ + --hash=sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d \ + --hash=sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d \ + --hash=sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842 \ + --hash=sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150 \ + --hash=sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8 \ + --hash=sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a \ + --hash=sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed \ + --hash=sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f \ + --hash=sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008 \ + --hash=sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e \ + --hash=sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0 \ + --hash=sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e \ + --hash=sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f \ + --hash=sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a \ + --hash=sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40 \ + --hash=sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7 \ + --hash=sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83 \ + --hash=sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d \ + --hash=sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c \ + --hash=sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871 \ + --hash=sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502 \ + --hash=sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252 \ + --hash=sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8 \ + --hash=sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115 \ + --hash=sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f \ + --hash=sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e \ + --hash=sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d \ + --hash=sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0 \ + --hash=sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119 \ + --hash=sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e \ + --hash=sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db \ + --hash=sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121 \ + --hash=sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d \ + --hash=sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e + # via + # -r software/ml/requirements.in + # ml-dtypes + # onnx + # onnx-ir + # onnxruntime + # onnxscript + # pandas + # scikit-learn + # scipy + # torch-geometric +nvidia-cublas==13.1.0.3 \ + --hash=sha256:2a3b94a37def342471c59fad7856caee4926809a72dd5270155d6a31b5b277be \ + --hash=sha256:c86fc7f7ae36d7528288c5d88098edcb7b02c633d262e7ddbb86b0ad91be5df2 \ + --hash=sha256:ee8722c1f0145ab246bccb9e452153b5e0515fd094c3678df50b2a0888b8b171 + # via + # cuda-toolkit + # nvidia-cudnn-cu13 + # nvidia-cusolver +nvidia-cuda-cupti==13.0.85 \ + --hash=sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8 \ + --hash=sha256:683f58d301548deeefcb8f6fac1b8d907691b9d8b18eccab417f51e362102f00 \ + --hash=sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151 + # via cuda-toolkit +nvidia-cuda-nvrtc==13.0.88 \ + --hash=sha256:6bcd4e7f8e205cbe644f5a98f2f799bef9556fefc89dd786e79a16312ce49872 \ + --hash=sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575 \ + --hash=sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b + # via cuda-toolkit +nvidia-cuda-runtime==13.0.96 \ + --hash=sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548 \ + --hash=sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55 \ + --hash=sha256:f79298c8a098cec150a597c8eba58ecdab96e3bdc4b9bc4f9983635031740492 + # via cuda-toolkit +nvidia-cudnn-cu13==9.19.0.56 \ + --hash=sha256:40d8c375005bcb01495f8edf375230b203a411a0c05fb6dc92a3781edcb23eac \ + --hash=sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4 \ + --hash=sha256:d20e1734305e9d68889a96e3f35094d733ff1f83932ebe462753973e53a572bf + # via torch +nvidia-cufft==12.0.0.61 \ + --hash=sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5 \ + --hash=sha256:2abce5b39d2f5ae12730fb7e5db6696533e36c26e2d3e8fd1750bdd2853364eb \ + --hash=sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3 + # via cuda-toolkit +nvidia-cufile==1.15.1.6 \ + --hash=sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44 \ + --hash=sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1 + # via cuda-toolkit +nvidia-curand==10.4.0.35 \ + --hash=sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a \ + --hash=sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc \ + --hash=sha256:65b1710aa6961d326b411e314b374290904c5ddf41dc3f766ebc3f1d7d4ca69f + # via cuda-toolkit +nvidia-cusolver==12.0.4.66 \ + --hash=sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2 \ + --hash=sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112 \ + --hash=sha256:16515bd33a8e76bb54d024cfa068fa68d30e80fc34b9e1090813ea9362e0cb65 + # via cuda-toolkit +nvidia-cusparse==12.6.3.3 \ + --hash=sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b \ + --hash=sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c \ + --hash=sha256:cbcf42feb737bd7ec15b4c0a63e62351886bd3f975027b8815d7f720a2b5ea79 + # via + # cuda-toolkit + # nvidia-cusolver +nvidia-cusparselt-cu13==0.8.0 \ + --hash=sha256:25e30a8a7323935d4ad0340b95a0b69926eee755767e8e0b1cf8dd85b197d3fd \ + --hash=sha256:400c6ed1cf6780fc6efedd64ec9f1345871767e6a1a0a552a1ea0578117ea77c \ + --hash=sha256:e80212ed7b1afc97102fbb2b5c82487aa73f6a0edfa6d26c5a152593e520bb8f + # via torch +nvidia-nccl-cu13==2.28.9 \ + --hash=sha256:01c873ba1626b54caa12272ed228dc5b2781545e0ae8ba3f432a8ef1c6d78643 \ + --hash=sha256:e4553a30f34195f3fa1da02a6da3d6337d28f2003943aa0a3d247bbc25fefc42 + # via torch +nvidia-nvjitlink==13.0.88 \ + --hash=sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b \ + --hash=sha256:634e96e3da9ef845ae744097a1f289238ecf946ce0b82e93cdce14b9782e682f \ + --hash=sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c + # via + # cuda-toolkit + # nvidia-cufft + # nvidia-cusolver + # nvidia-cusparse +nvidia-nvshmem-cu13==3.4.5 \ + --hash=sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80 \ + --hash=sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9 + # via torch +nvidia-nvtx==13.0.85 \ + --hash=sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4 \ + --hash=sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6 \ + --hash=sha256:d66ea44254dd3c6eacc300047af6e1288d2269dd072b417e0adffbf479e18519 + # via cuda-toolkit +onnx==1.21.0 \ + --hash=sha256:10c3185a232089335581fabb98fba4e86d3e8246b8140f2e406082438100ebda \ + --hash=sha256:19d9971a3e52a12968ae6c70fd0f86c349536de0b0c33922ecdbe52d1972fe60 \ + --hash=sha256:1a9baf882562c4cebf79589bebb7cd71a20e30b51158cac3e3bbaf27da6163bd \ + --hash=sha256:257d1d1deb6a652913698f1e3f33ef1ca0aa69174892fe38946d4572d89dd94f \ + --hash=sha256:2aca19949260875c14866fc77ea0bc37e4e809b24976108762843d328c92d3ce \ + --hash=sha256:3abd09872523c7e0362d767e4e63bd7c6bac52a5e2c3edbf061061fe540e2027 \ + --hash=sha256:458d91948ad9a7729a347550553b49ab6939f9af2cddf334e2116e45467dc61f \ + --hash=sha256:4d8b67d0aaec5864c87633188b91cc520877477ec0254eda122bef8be43cd764 \ + --hash=sha256:5489f25fe461e7f32128218251a466cabbeeaf1eaa791c79daebf1a80d5a2cc9 \ + --hash=sha256:5f78c411743db317a76e5d009f84f7e3d5380411a1567a868e82461a1e5c775d \ + --hash=sha256:7b58a4cfec8d9311b73dc083e4c1fa362069267881144c05139b3eba5dc3a840 \ + --hash=sha256:7cd7cb8f6459311bdb557cbf6c0ccc6d8ace11c304d1bba0a30b4a4688e245f8 \ + --hash=sha256:7ee9d8fd6a4874a5fa8b44bbcabea104ce752b20469b88bc50c7dcf9030779ad \ + --hash=sha256:82aa6ab51144df07c58c4850cb78d4f1ae969d8c0bf657b28041796d49ba6974 \ + --hash=sha256:9003d5206c01fa2ff4b46311566865d8e493e1a6998d4009ec6de39843f1b59b \ + --hash=sha256:9ea4e824964082811938a9250451d89c4ec474fe42dd36c038bfa5df31993d1e \ + --hash=sha256:a9261bd580fb8548c9c37b3c6750387eb8f21ea43c63880d37b2c622e1684285 \ + --hash=sha256:ab6a488dabbb172eebc9f3b3e7ac68763f32b0c571626d4a5004608f866cc83d \ + --hash=sha256:bba12181566acf49b35875838eba49536a327b2944664b17125577d230c637ad \ + --hash=sha256:c9b56ad04039fac6b028c07e54afa1ec7f75dd340f65311f2c292e41ed7aa4d9 \ + --hash=sha256:ca14bc4842fccc3187eb538f07eabeb25a779b39388b006db4356c07403a7bbb \ + --hash=sha256:db17fc0fec46180b6acbd1d5d8650a04e5527c02b09381da0b5b888d02a204c8 \ + --hash=sha256:e0c21cc5c7a41d1a509828e2b14fe9c30e807c6df611ec0fd64a47b8d4b16abd \ + --hash=sha256:e1931bfcc222a4c9da6475f2ffffb84b97ab3876041ec639171c11ce802bee6a \ + --hash=sha256:efba467efb316baf2a9452d892c2f982b9b758c778d23e38c7f44fa211b30bb9 \ + --hash=sha256:f2c7c234c568402e10db74e33d787e4144e394ae2bcbbf11000fbfe2e017ad68 \ + --hash=sha256:f53b3c15a3b539c16b99655c43c365622046d68c49b680c48eba4da2a4fb6f27 \ + --hash=sha256:fc2635400fe39ff37ebc4e75342cc54450eadadf39c540ff132c319bf4960095 + # via + # onnx-ir + # onnxscript +onnx-ir==0.2.1 \ + --hash=sha256:8b8b10a93f43e65962104de6070c43c5dacb0e3cdfefc7c8059dd83c9db64f35 \ + --hash=sha256:c7285da889312f91882de2092e298a9eeeefbfc1d1951c49d983992967eb09a7 + # via onnxscript +onnxruntime==1.24.4 \ + --hash=sha256:046ff290045a387676941a02a8ae5c3ebec6b4f551ae228711968c4a69d8f6b7 \ + --hash=sha256:0bdfce8e9a6497cec584aab407b71bf697dac5e1b7b7974adc50bf7533bdb3a2 \ + --hash=sha256:0d640eb9f3782689b55cfa715094474cd5662f2f137be6a6f847a594b6e9705c \ + --hash=sha256:14ed1f197fab812b695a5eaddb536c635e58a2fbbe50a517c78f082cc6ce9177 \ + --hash=sha256:1700f559c8086d06b2a4d5de51e62cb4ff5e2631822f71a36db8c72383db71ee \ + --hash=sha256:1a5c5a544b22f90859c88617ecb30e161ee3349fcc73878854f43d77f00558b5 \ + --hash=sha256:311e309f573bf3c12aa5723e23823077f83d5e412a18499d4485c7eb41040858 \ + --hash=sha256:3b6ba8b0181a3aa88edab00eb01424ffc06f42e71095a91186c2249415fcff93 \ + --hash=sha256:3f0b910e86b759a4732663ec61fd57ac42ee1b0066f68299de164220b660546d \ + --hash=sha256:4c74e268dc808e61e63784d43f9ddcdaf50a776c2819e8bd1d1b11ef64bf7e36 \ + --hash=sha256:535b29475ca42b593c45fbb2152fbf1cdf3f287315bf650e6a724a0a1d065cdb \ + --hash=sha256:71d6a5c1821d6e8586a024000ece458db8f2fc0ecd050435d45794827ce81e19 \ + --hash=sha256:aa12ddc54c9c4594073abcaa265cd9681e95fb89dae982a6f508a794ca42e661 \ + --hash=sha256:b43b63eb24a2bc8fc77a09be67587a570967a412cccb837b6245ccb546691153 \ + --hash=sha256:cad1c2b3f455c55678ab2a8caa51fb420c25e6e3cf10f4c23653cdabedc8de78 \ + --hash=sha256:dc4aaed1e5e1aaacf2343c838a30a7c3ade78f13eeb16817411f929d04040a13 \ + --hash=sha256:e1cc6a518255f012134bc791975a6294806be9a3b20c4a54cca25194c90cf731 \ + --hash=sha256:e26478356dba25631fb3f20112e345f8e8bf62c499bb497e8a559f7d69cf7e7b \ + --hash=sha256:e2b7969e72d8cb53ffc88ab6d49dd5e75c1c663bda7be7eb0ece192f127343d1 \ + --hash=sha256:e30c972bc02e072911aabb6891453ec73795386c0af2b761b65444b8a4c4745f \ + --hash=sha256:e54ad52e61d2d4618dcff8fa1480ac66b24ee2eab73331322db1049f11ccf330 \ + --hash=sha256:e6214096e14b7b52e3bee1903dc12dc7ca09cb65e26664668a4620cc5e6f9a90 \ + --hash=sha256:e99a48078baaefa2b50fe5836c319499f71f13f76ed32d0211f39109147a49e0 \ + --hash=sha256:fbff2a248940e3398ae78374c5a839e49a2f39079b488bc64439fa0ec327a3e4 + # via -r software/ml/requirements.in +onnxscript==0.7.0 \ + --hash=sha256:5b356907d4501e9919f8599c91d8da967406a37b1fac2b40caa55a49acf242ea \ + --hash=sha256:c95ed7b339b02cface56ee27689565c46612e1fc542c562298dddfdad5268dc5 + # via -r software/ml/requirements.in +packaging==26.0 \ + --hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \ + --hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 + # via + # onnxruntime + # onnxscript +pandas==3.0.2 \ + --hash=sha256:01f31a546acd5574ef77fe199bc90b55527c225c20ccda6601cf6b0fd5ed597c \ + --hash=sha256:0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288 \ + --hash=sha256:07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991 \ + --hash=sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd \ + --hash=sha256:0f48afd9bb13300ffb5a3316973324c787054ba6665cda0da3fbd67f451995db \ + --hash=sha256:140f0cffb1fa2524e874dde5b477d9defe10780d8e9e220d259b2c0874c89d9d \ + --hash=sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18 \ + --hash=sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d \ + --hash=sha256:339dda302bd8369dedeae979cb750e484d549b563c3f54f3922cb8ff4978c5eb \ + --hash=sha256:4544c7a54920de8eeacaa1466a6b7268ecfbc9bc64ab4dbb89c6bbe94d5e0660 \ + --hash=sha256:4d888a5c678a419a5bb41a2a93818e8ed9fd3172246555c0b37b7cc27027effd \ + --hash=sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482 \ + --hash=sha256:57a07209bebcbcf768d2d13c9b78b852f9a15978dac41b9e6421a81ad4cdd276 \ + --hash=sha256:5880314e69e763d4c8b27937090de570f1fb8d027059a7ada3f7f8e98bdcb677 \ + --hash=sha256:5d3cfe227c725b1f3dff4278b43d8c784656a42a9325b63af6b1492a8232209e \ + --hash=sha256:5fdbfa05931071aba28b408e59226186b01eb5e92bea2ab78b65863ca3228d84 \ + --hash=sha256:60a80bb4feacbef5e1447a3f82c33209c8b7e07f28d805cfd1fb951e5cb443aa \ + --hash=sha256:61c2fd96d72b983a9891b2598f286befd4ad262161a609c92dc1652544b46b76 \ + --hash=sha256:63d141b56ef686f7f0d714cfb8de4e320475b86bf4b620aa0b7da89af8cbdbbb \ + --hash=sha256:6c4d8458b97a35717b62469a4ea0e85abd5ed8687277f5ccfc67f8a5126f8c53 \ + --hash=sha256:710246ba0616e86891b58ab95f2495143bb2bc83ab6b06747c74216f583a6ac9 \ + --hash=sha256:734be7551687c00fbd760dc0522ed974f82ad230d4a10f54bf51b80d44a08702 \ + --hash=sha256:7cadd7e9a44ec13b621aec60f9150e744cfc7a3dd32924a7e2f45edff31823b0 \ + --hash=sha256:81526c4afd31971f8b62671442a4b2b51e0aa9acc3819c9f0f12a28b6fcf85f1 \ + --hash=sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14 \ + --hash=sha256:a3096110bf9eac0070b7208465f2740e2d8a670d5cb6530b5bb884eca495fd39 \ + --hash=sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4 \ + --hash=sha256:a727a73cbdba2f7458dc82449e2315899d5140b449015d822f515749a46cbbe0 \ + --hash=sha256:ae37e833ff4fed0ba352f6bdd8b73ba3ab3256a85e54edfd1ab51ae40cca0af8 \ + --hash=sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d \ + --hash=sha256:b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf \ + --hash=sha256:b444dc64c079e84df91baa8bf613d58405645461cabca929d9178f2cd392398d \ + --hash=sha256:b5329e26898896f06035241a626d7c335daa479b9bbc82be7c2742d048e41172 \ + --hash=sha256:b5918ba197c951dec132b0c5929a00c0bf05d5942f590d3c10a807f6e15a57d3 \ + --hash=sha256:b75c347eff42497452116ce05ef461822d97ce5b9ff8df6edacb8076092c855d \ + --hash=sha256:c3b723df9087a9a9a840e263ebd9f88b64a12075d1bf2ea401a5a42f254f084d \ + --hash=sha256:c934008c733b8bbea273ea308b73b3156f0181e5b72960790b09c18a2794fe1e \ + --hash=sha256:d1478075142e83a5571782ad007fb201ed074bdeac7ebcc8890c71442e96adf7 \ + --hash=sha256:d606a041c89c0a474a4702d532ab7e73a14fe35c8d427b972a625c8e46373668 \ + --hash=sha256:db0dbfd2a6cdf3770aa60464d50333d8f3d9165b2f2671bcc299b72de5a6677b \ + --hash=sha256:dbbd4aa20ca51e63b53bbde6a0fa4254b1aaabb74d2f542df7a7959feb1d760c \ + --hash=sha256:dbc20dea3b9e27d0e66d74c42b2d0c1bed9c2ffe92adea33633e3bedeb5ac235 \ + --hash=sha256:deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535 \ + --hash=sha256:ed72cb3f45190874eb579c64fa92d9df74e98fd63e2be7f62bce5ace0ade61df \ + --hash=sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f \ + --hash=sha256:f12b1a9e332c01e09510586f8ca9b108fd631fd656af82e452d7315ef6df5f9f \ + --hash=sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043 \ + --hash=sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab + # via -r software/ml/requirements.in +propcache==0.4.1 \ + --hash=sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e \ + --hash=sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4 \ + --hash=sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be \ + --hash=sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3 \ + --hash=sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85 \ + --hash=sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b \ + --hash=sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367 \ + --hash=sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf \ + --hash=sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393 \ + --hash=sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888 \ + --hash=sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37 \ + --hash=sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8 \ + --hash=sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60 \ + --hash=sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1 \ + --hash=sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4 \ + --hash=sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717 \ + --hash=sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7 \ + --hash=sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc \ + --hash=sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe \ + --hash=sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb \ + --hash=sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75 \ + --hash=sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6 \ + --hash=sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e \ + --hash=sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff \ + --hash=sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566 \ + --hash=sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12 \ + --hash=sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367 \ + --hash=sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874 \ + --hash=sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf \ + --hash=sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566 \ + --hash=sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a \ + --hash=sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc \ + --hash=sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a \ + --hash=sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1 \ + --hash=sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6 \ + --hash=sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61 \ + --hash=sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726 \ + --hash=sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49 \ + --hash=sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44 \ + --hash=sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af \ + --hash=sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa \ + --hash=sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153 \ + --hash=sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc \ + --hash=sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5 \ + --hash=sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938 \ + --hash=sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf \ + --hash=sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925 \ + --hash=sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8 \ + --hash=sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c \ + --hash=sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85 \ + --hash=sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e \ + --hash=sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0 \ + --hash=sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1 \ + --hash=sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0 \ + --hash=sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992 \ + --hash=sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db \ + --hash=sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f \ + --hash=sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d \ + --hash=sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1 \ + --hash=sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e \ + --hash=sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900 \ + --hash=sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89 \ + --hash=sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a \ + --hash=sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b \ + --hash=sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f \ + --hash=sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f \ + --hash=sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1 \ + --hash=sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183 \ + --hash=sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66 \ + --hash=sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21 \ + --hash=sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db \ + --hash=sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded \ + --hash=sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb \ + --hash=sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19 \ + --hash=sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0 \ + --hash=sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165 \ + --hash=sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778 \ + --hash=sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455 \ + --hash=sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f \ + --hash=sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b \ + --hash=sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237 \ + --hash=sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81 \ + --hash=sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859 \ + --hash=sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c \ + --hash=sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835 \ + --hash=sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393 \ + --hash=sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5 \ + --hash=sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641 \ + --hash=sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144 \ + --hash=sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74 \ + --hash=sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db \ + --hash=sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac \ + --hash=sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403 \ + --hash=sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9 \ + --hash=sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f \ + --hash=sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311 \ + --hash=sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581 \ + --hash=sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36 \ + --hash=sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00 \ + --hash=sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a \ + --hash=sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f \ + --hash=sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2 \ + --hash=sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7 \ + --hash=sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239 \ + --hash=sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757 \ + --hash=sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72 \ + --hash=sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9 \ + --hash=sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4 \ + --hash=sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24 \ + --hash=sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207 \ + --hash=sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e \ + --hash=sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1 \ + --hash=sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d \ + --hash=sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37 \ + --hash=sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c \ + --hash=sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e \ + --hash=sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570 \ + --hash=sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af \ + --hash=sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f \ + --hash=sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88 \ + --hash=sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48 \ + --hash=sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781 + # via + # aiohttp + # yarl +protobuf==7.34.1 \ + --hash=sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a \ + --hash=sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a \ + --hash=sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b \ + --hash=sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4 \ + --hash=sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280 \ + --hash=sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11 \ + --hash=sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7 \ + --hash=sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c + # via + # onnx + # onnxruntime +psutil==7.2.2 \ + --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ + --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \ + --hash=sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841 \ + --hash=sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63 \ + --hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \ + --hash=sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a \ + --hash=sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b \ + --hash=sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9 \ + --hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \ + --hash=sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312 \ + --hash=sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b \ + --hash=sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9 \ + --hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \ + --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \ + --hash=sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1 \ + --hash=sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf \ + --hash=sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea \ + --hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \ + --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \ + --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ + --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 + # via torch-geometric +pyparsing==3.3.2 \ + --hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \ + --hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc + # via torch-geometric +python-dateutil==2.9.0.post0 \ + --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ + --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 + # via pandas +requests==2.33.1 \ + --hash=sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517 \ + --hash=sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a + # via torch-geometric +scikit-learn==1.8.0 \ + --hash=sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2 \ + --hash=sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a \ + --hash=sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da \ + --hash=sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9 \ + --hash=sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961 \ + --hash=sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6 \ + --hash=sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271 \ + --hash=sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809 \ + --hash=sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242 \ + --hash=sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4 \ + --hash=sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7 \ + --hash=sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76 \ + --hash=sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6 \ + --hash=sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b \ + --hash=sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e \ + --hash=sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7 \ + --hash=sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e \ + --hash=sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57 \ + --hash=sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735 \ + --hash=sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb \ + --hash=sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb \ + --hash=sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e \ + --hash=sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd \ + --hash=sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a \ + --hash=sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9 \ + --hash=sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1 \ + --hash=sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde \ + --hash=sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3 \ + --hash=sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f \ + --hash=sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b \ + --hash=sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3 \ + --hash=sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e \ + --hash=sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702 \ + --hash=sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c \ + --hash=sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1 \ + --hash=sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4 \ + --hash=sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd + # via -r software/ml/requirements.in +scipy==1.17.1 \ + --hash=sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0 \ + --hash=sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458 \ + --hash=sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118 \ + --hash=sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39 \ + --hash=sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e \ + --hash=sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6 \ + --hash=sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec \ + --hash=sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21 \ + --hash=sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1 \ + --hash=sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6 \ + --hash=sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce \ + --hash=sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8 \ + --hash=sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448 \ + --hash=sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19 \ + --hash=sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b \ + --hash=sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87 \ + --hash=sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4 \ + --hash=sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9 \ + --hash=sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b \ + --hash=sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082 \ + --hash=sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464 \ + --hash=sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87 \ + --hash=sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c \ + --hash=sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369 \ + --hash=sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad \ + --hash=sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f \ + --hash=sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c \ + --hash=sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475 \ + --hash=sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd \ + --hash=sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866 \ + --hash=sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d \ + --hash=sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6 \ + --hash=sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb \ + --hash=sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca \ + --hash=sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0 \ + --hash=sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca \ + --hash=sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d \ + --hash=sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee \ + --hash=sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4 \ + --hash=sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717 \ + --hash=sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49 \ + --hash=sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2 \ + --hash=sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a \ + --hash=sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350 \ + --hash=sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950 \ + --hash=sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b \ + --hash=sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086 \ + --hash=sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444 \ + --hash=sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068 \ + --hash=sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff \ + --hash=sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a \ + --hash=sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50 \ + --hash=sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696 \ + --hash=sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21 \ + --hash=sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c \ + --hash=sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484 \ + --hash=sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118 \ + --hash=sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3 \ + --hash=sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea \ + --hash=sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293 \ + --hash=sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76 + # via scikit-learn +six==1.17.0 \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ + --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 + # via python-dateutil +sympy==1.14.0 \ + --hash=sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517 \ + --hash=sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 + # via + # onnx-ir + # onnxruntime + # torch +threadpoolctl==3.6.0 \ + --hash=sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb \ + --hash=sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e + # via scikit-learn +torch==2.11.0 \ + --hash=sha256:01018087326984a33b64e04c8cb5c2795f9120e0d775ada1f6638840227b04d7 \ + --hash=sha256:0f68f4ac6d95d12e896c3b7a912b5871619542ec54d3649cf48cc1edd4dd2756 \ + --hash=sha256:1b32ceda909818a03b112006709b02be1877240c31750a8d9c6b7bf5f2d8a6e5 \ + --hash=sha256:1e6debd97ccd3205bbb37eb806a9d8219e1139d15419982c09e23ef7d4369d18 \ + --hash=sha256:2658f34ce7e2dabf4ec73b45e2ca68aedad7a5be87ea756ad656eaf32bf1e1ea \ + --hash=sha256:2b4e811728bd0cc58fb2b0948fe939a1ee2bf1422f6025be2fca4c7bd9d79718 \ + --hash=sha256:2bb3cc54bd0dea126b0060bb1ec9de0f9c7f7342d93d436646516b0330cd5be7 \ + --hash=sha256:2c0d7fcfbc0c4e8bb5ebc3907cbc0c6a0da1b8f82b1fc6e14e914fa0b9baf74e \ + --hash=sha256:4b5866312ee6e52ea625cd211dcb97d6a2cdc1131a5f15cc0d87eec948f6dd34 \ + --hash=sha256:4cf8687f4aec3900f748d553483ef40e0ac38411c3c48d0a86a438f6d7a99b18 \ + --hash=sha256:4dc8b3809469b6c30b411bb8c4cad3828efd26236153d9beb6a3ec500f211a60 \ + --hash=sha256:4dda3b3f52d121063a731ddb835f010dc137b920d7fec2778e52f60d8e4bf0cd \ + --hash=sha256:563ed3d25542d7e7bbc5b235ccfacfeb97fb470c7fee257eae599adb8005c8a2 \ + --hash=sha256:63a68fa59de8f87acc7e85a5478bb2dddbb3392b7593ec3e78827c793c4b73fd \ + --hash=sha256:73e24aaf8f36ab90d95cd1761208b2eb70841c2a9ca1a3f9061b39fc5331b708 \ + --hash=sha256:7aa2f9bbc6d4595ba72138026b2074be1233186150e9292865e04b7a63b8c67a \ + --hash=sha256:7b6a60d48062809f58595509c524b88e6ddec3ebe25833d6462eeab81e5f2ce4 \ + --hash=sha256:8245477871c3700d4370352ffec94b103cfcb737229445cf9946cddb7b2ca7cd \ + --hash=sha256:8b394322f49af4362d4f80e424bcaca7efcd049619af03a4cf4501520bdf0fb4 \ + --hash=sha256:98bb213c3084cfe176302949bdc360074b18a9da7ab59ef2edc9d9f742504778 \ + --hash=sha256:a97b94bbf62992949b4730c6cd2cc9aee7b335921ee8dc207d930f2ed09ae2db \ + --hash=sha256:ab9a8482f475f9ba20e12db84b0e55e2f58784bdca43a854a6ccd3fd4b9f75e6 \ + --hash=sha256:b2a43985ff5ef6ddd923bbcf99943e5f58059805787c5c9a2622bf05ca2965b0 \ + --hash=sha256:b3c712ae6fb8e7a949051a953fc412fe0a6940337336c3b6f905e905dac5157f \ + --hash=sha256:cc89b9b173d9adfab59fd227f0ab5e5516d9a52b658ae41d64e59d2e55a418db \ + --hash=sha256:d91aac77f24082809d2c5a93f52a5f085032740a1ebc9252a7b052ef5a4fddc6 \ + --hash=sha256:f99924682ef0aa6a4ab3b1b76f40dc6e273fca09f367d15a524266db100a723f \ + --hash=sha256:fbf39280699d1b869f55eac536deceaa1b60bd6788ba74f399cc67e60a5fab10 + # via -r software/ml/requirements.in +torch-geometric==2.7.0 \ + --hash=sha256:6e0cd3ad824d484651ef5d308fc66c687bfcf5ba040d56d1e0fe0f81f365e292 \ + --hash=sha256:f9099e4aece1a9f618c84dbaac33a77f43139736698c7e8bddf3301ef1f2e8d4 + # via -r software/ml/requirements.in +tqdm==4.67.3 \ + --hash=sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb \ + --hash=sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf + # via torch-geometric +triton==3.6.0 \ + --hash=sha256:0b3a97e8ed304dfa9bd23bb41ca04cdf6b2e617d5e782a8653d616037a5d537d \ + --hash=sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9 \ + --hash=sha256:1722e172d34e32abc3eb7711d0025bb69d7959ebea84e3b7f7a341cd7ed694d6 \ + --hash=sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4 \ + --hash=sha256:448e02fe6dc898e9e5aa89cf0ee5c371e99df5aa5e8ad976a80b93334f3494fd \ + --hash=sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7 \ + --hash=sha256:49df5ef37379c0c2b5c0012286f80174fcf0e073e5ade1ca9a86c36814553651 \ + --hash=sha256:6c723cfb12f6842a0ae94ac307dba7e7a44741d720a40cf0e270ed4a4e3be781 \ + --hash=sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca \ + --hash=sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803 \ + --hash=sha256:a6550fae429e0667e397e5de64b332d1e5695b73650ee75a6146e2e902770bea \ + --hash=sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f \ + --hash=sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3 \ + --hash=sha256:ef5523241e7d1abca00f1d240949eebdd7c673b005edbbce0aca95b8191f1d43 + # via torch +typing-extensions==4.15.0 \ + --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ + --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 + # via + # aiosignal + # onnx + # onnx-ir + # onnxscript + # torch +urllib3==2.6.3 \ + --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ + --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 + # via requests +xxhash==3.6.0 \ + --hash=sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad \ + --hash=sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c \ + --hash=sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3 \ + --hash=sha256:01be0c5b500c5362871fc9cfdf58c69b3e5c4f531a82229ddb9eb1eb14138004 \ + --hash=sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b \ + --hash=sha256:02ea4cb627c76f48cd9fb37cf7ab22bd51e57e1b519807234b473faebe526796 \ + --hash=sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f \ + --hash=sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c \ + --hash=sha256:0d50101e57aad86f4344ca9b32d091a2135a9d0a4396f19133426c88025b09f1 \ + --hash=sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1 \ + --hash=sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0 \ + --hash=sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec \ + --hash=sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d \ + --hash=sha256:18b242455eccdfcd1fa4134c431a30737d2b4f045770f8fe84356b3469d4b919 \ + --hash=sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67 \ + --hash=sha256:1fc1ed882d1e8df932a66e2999429ba6cc4d5172914c904ab193381fba825360 \ + --hash=sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799 \ + --hash=sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679 \ + --hash=sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef \ + --hash=sha256:2762bfff264c4e73c0e507274b40634ff465e025f0eaf050897e88ec8367575d \ + --hash=sha256:277175a73900ad43a8caeb8b99b9604f21fe8d7c842f2f9061a364a7e220ddb7 \ + --hash=sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8 \ + --hash=sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa \ + --hash=sha256:2ab89a6b80f22214b43d98693c30da66af910c04f9858dd39c8e570749593d7e \ + --hash=sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa \ + --hash=sha256:2f171a900d59d51511209f7476933c34a0c2c711078d3c80e74e0fe4f38680ec \ + --hash=sha256:339f518c3c7a850dd033ab416ea25a692759dc7478a71131fe8869010d2b75e4 \ + --hash=sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad \ + --hash=sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7 \ + --hash=sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5 \ + --hash=sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11 \ + --hash=sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae \ + --hash=sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d \ + --hash=sha256:44e342e8cc11b4e79dae5c57f2fb6360c3c20cc57d32049af8f567f5b4bcb5f4 \ + --hash=sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6 \ + --hash=sha256:45aae0c9df92e7fa46fbb738737324a563c727990755ec1965a6a339ea10a1df \ + --hash=sha256:48e6f2ffb07a50b52465a1032c3cf1f4a5683f944acaca8a134a2f23674c2058 \ + --hash=sha256:4903530e866b7a9c1eadfd3fa2fbe1b97d3aed4739a80abf506eb9318561c850 \ + --hash=sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2 \ + --hash=sha256:4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d \ + --hash=sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89 \ + --hash=sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e \ + --hash=sha256:4da8168ae52c01ac64c511d6f4a709479da8b7a4a1d7621ed51652f93747dffa \ + --hash=sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6 \ + --hash=sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb \ + --hash=sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3 \ + --hash=sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b \ + --hash=sha256:5576b002a56207f640636056b4160a378fe36a58db73ae5c27a7ec8db35f71d4 \ + --hash=sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db \ + --hash=sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119 \ + --hash=sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec \ + --hash=sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518 \ + --hash=sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296 \ + --hash=sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033 \ + --hash=sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729 \ + --hash=sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca \ + --hash=sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063 \ + --hash=sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5 \ + --hash=sha256:6551880383f0e6971dc23e512c9ccc986147ce7bfa1cd2e4b520b876c53e9f3d \ + --hash=sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f \ + --hash=sha256:6965e0e90f1f0e6cb78da568c13d4a348eeb7f40acfd6d43690a666a459458b8 \ + --hash=sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42 \ + --hash=sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e \ + --hash=sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392 \ + --hash=sha256:780b90c313348f030b811efc37b0fa1431163cb8db8064cf88a7936b6ce5f222 \ + --hash=sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f \ + --hash=sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd \ + --hash=sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77 \ + --hash=sha256:7c35c4cdc65f2a29f34425c446f2f5cdcd0e3c34158931e1cc927ece925ab802 \ + --hash=sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d \ + --hash=sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1 \ + --hash=sha256:7dac94fad14a3d1c92affb661021e1d5cbcf3876be5f5b4d90730775ccb7ac41 \ + --hash=sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374 \ + --hash=sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263 \ + --hash=sha256:87ff03d7e35c61435976554477a7f4cd1704c3596a89a8300d5ce7fc83874a71 \ + --hash=sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13 \ + --hash=sha256:89952ea539566b9fed2bbd94e589672794b4286f342254fad28b149f9615fef8 \ + --hash=sha256:8a8f1972e75ebdd161d7896743122834fe87378160c20e97f8b09166213bf8cc \ + --hash=sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62 \ + --hash=sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11 \ + --hash=sha256:9085e798c163ce310d91f8aa6b325dda3c2944c93c6ce1edb314030d4167cc65 \ + --hash=sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0 \ + --hash=sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b \ + --hash=sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2 \ + --hash=sha256:97460eec202017f719e839a0d3551fbc0b2fcc9c6c6ffaa5af85bbd5de432788 \ + --hash=sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6 \ + --hash=sha256:9e040d3e762f84500961791fa3709ffa4784d4dcd7690afc655c095e02fff05f \ + --hash=sha256:a034590a727b44dd8ac5914236a7b8504144447a9682586c3327e935f33ec8cc \ + --hash=sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e \ + --hash=sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702 \ + --hash=sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405 \ + --hash=sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f \ + --hash=sha256:a75ffc1bd5def584129774c158e108e5d768e10b75813f2b32650bb041066ed6 \ + --hash=sha256:a87f271a33fad0e5bf3be282be55d78df3a45ae457950deb5241998790326f87 \ + --hash=sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3 \ + --hash=sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a \ + --hash=sha256:aed058764db109dc9052720da65fafe84873b05eb8b07e5e653597951af57c3b \ + --hash=sha256:af1f3278bd02814d6dedc5dec397993b549d6f16c19379721e5a1d31e132c49b \ + --hash=sha256:b0359391c3dad6de872fefb0cf5b69d55b0655c55ee78b1bb7a568979b2ce96b \ + --hash=sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8 \ + --hash=sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db \ + --hash=sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99 \ + --hash=sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a \ + --hash=sha256:b5b848ad6c16d308c3ac7ad4ba6bede80ed5df2ba8ed382f8932df63158dd4b2 \ + --hash=sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204 \ + --hash=sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b \ + --hash=sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546 \ + --hash=sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95 \ + --hash=sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9 \ + --hash=sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54 \ + --hash=sha256:bf48889c9630542d4709192578aebbd836177c9f7a4a2778a7d6340107c65f06 \ + --hash=sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c \ + --hash=sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152 \ + --hash=sha256:c2f9ccd5c4be370939a2e17602fbc49995299203da72a3429db013d44d590e86 \ + --hash=sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4 \ + --hash=sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93 \ + --hash=sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd \ + --hash=sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd \ + --hash=sha256:cc604dc06027dbeb8281aeac5899c35fcfe7c77b25212833709f0bff4ce74d2a \ + --hash=sha256:cfbc5b91397c8c2972fdac13fb3e4ed2f7f8ccac85cd2c644887557780a9b6e2 \ + --hash=sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248 \ + --hash=sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd \ + --hash=sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6 \ + --hash=sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf \ + --hash=sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7 \ + --hash=sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490 \ + --hash=sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0 \ + --hash=sha256:e4ff728a2894e7f436b9e94c667b0f426b9c74b71f900cf37d5468c6b5da0536 \ + --hash=sha256:e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb \ + --hash=sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829 \ + --hash=sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746 \ + --hash=sha256:ee34327b187f002a596d7b167ebc59a1b729e963ce645964bbc050d2f1b73d07 \ + --hash=sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292 \ + --hash=sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6 \ + --hash=sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd \ + --hash=sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7 \ + --hash=sha256:f572dfd3d0e2eb1a57511831cf6341242f5a9f8298a45862d085f5b93394a27d \ + --hash=sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0 \ + --hash=sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee \ + --hash=sha256:ffc578717a347baf25be8397cb10d2528802d24f94cfc005c0e44fef44b5cdd6 + # via torch-geometric +yarl==1.23.0 \ + --hash=sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc \ + --hash=sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4 \ + --hash=sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85 \ + --hash=sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993 \ + --hash=sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222 \ + --hash=sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de \ + --hash=sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25 \ + --hash=sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e \ + --hash=sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2 \ + --hash=sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e \ + --hash=sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860 \ + --hash=sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957 \ + --hash=sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760 \ + --hash=sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52 \ + --hash=sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788 \ + --hash=sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912 \ + --hash=sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719 \ + --hash=sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035 \ + --hash=sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220 \ + --hash=sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412 \ + --hash=sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05 \ + --hash=sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41 \ + --hash=sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4 \ + --hash=sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4 \ + --hash=sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd \ + --hash=sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748 \ + --hash=sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a \ + --hash=sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4 \ + --hash=sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34 \ + --hash=sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069 \ + --hash=sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25 \ + --hash=sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2 \ + --hash=sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb \ + --hash=sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f \ + --hash=sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5 \ + --hash=sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8 \ + --hash=sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c \ + --hash=sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512 \ + --hash=sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6 \ + --hash=sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5 \ + --hash=sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9 \ + --hash=sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072 \ + --hash=sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5 \ + --hash=sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277 \ + --hash=sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a \ + --hash=sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6 \ + --hash=sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae \ + --hash=sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26 \ + --hash=sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2 \ + --hash=sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4 \ + --hash=sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70 \ + --hash=sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723 \ + --hash=sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c \ + --hash=sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9 \ + --hash=sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5 \ + --hash=sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e \ + --hash=sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c \ + --hash=sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4 \ + --hash=sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0 \ + --hash=sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2 \ + --hash=sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b \ + --hash=sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7 \ + --hash=sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750 \ + --hash=sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2 \ + --hash=sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474 \ + --hash=sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716 \ + --hash=sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7 \ + --hash=sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123 \ + --hash=sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007 \ + --hash=sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595 \ + --hash=sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe \ + --hash=sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea \ + --hash=sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598 \ + --hash=sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679 \ + --hash=sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8 \ + --hash=sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83 \ + --hash=sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6 \ + --hash=sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f \ + --hash=sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94 \ + --hash=sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51 \ + --hash=sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120 \ + --hash=sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039 \ + --hash=sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1 \ + --hash=sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05 \ + --hash=sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb \ + --hash=sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144 \ + --hash=sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa \ + --hash=sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a \ + --hash=sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99 \ + --hash=sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928 \ + --hash=sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d \ + --hash=sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3 \ + --hash=sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434 \ + --hash=sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86 \ + --hash=sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46 \ + --hash=sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319 \ + --hash=sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67 \ + --hash=sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c \ + --hash=sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169 \ + --hash=sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c \ + --hash=sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59 \ + --hash=sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107 \ + --hash=sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4 \ + --hash=sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a \ + --hash=sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb \ + --hash=sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f \ + --hash=sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769 \ + --hash=sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432 \ + --hash=sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090 \ + --hash=sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764 \ + --hash=sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d \ + --hash=sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4 \ + --hash=sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b \ + --hash=sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d \ + --hash=sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543 \ + --hash=sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24 \ + --hash=sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5 \ + --hash=sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b \ + --hash=sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d \ + --hash=sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b \ + --hash=sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6 \ + --hash=sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735 \ + --hash=sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e \ + --hash=sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28 \ + --hash=sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3 \ + --hash=sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401 \ + --hash=sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6 \ + --hash=sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d + # via aiohttp + +# The following packages are considered to be unsafe in a requirements file: +setuptools==81.0.0 \ + --hash=sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a \ + --hash=sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6 + # via torch diff --git a/src/software/py_constants.cpp b/src/software/py_constants.cpp index ce7b5f1f1a..4b6c68e0c9 100644 --- a/src/software/py_constants.cpp +++ b/src/software/py_constants.cpp @@ -40,6 +40,9 @@ PYBIND11_MODULE(py_constants, m) ACCELERATION_DUE_TO_GRAVITY_METERS_PER_SECOND_SQUARED; m.attr("ENEMY_BALL_PLACEMENT_DISTANCE_METERS") = ENEMY_BALL_PLACEMENT_DISTANCE_METERS; + m.attr("BALL_TO_FRONT_OF_ROBOT_DISTANCE_WHEN_DRIBBLING") = + BALL_TO_FRONT_OF_ROBOT_DISTANCE_WHEN_DRIBBLING; + m.attr("TACTIC_OVERRIDE_PATH") = TACTIC_OVERRIDE_PATH; m.attr("PLAY_OVERRIDE_PATH") = PLAY_OVERRIDE_PATH; diff --git a/src/software/thunderscope/BUILD b/src/software/thunderscope/BUILD index 769f9781f2..3b453ad7f4 100644 --- a/src/software/thunderscope/BUILD +++ b/src/software/thunderscope/BUILD @@ -19,11 +19,12 @@ py_binary( ":estop_helpers", ":thunderscope", ":util", + "//software/evaluation/loggers:pass_logger", + "//software/evaluation/loggers:stats_logger", "//software/thunderscope/binary_context_managers:full_system", "//software/thunderscope/binary_context_managers:game_controller", "//software/thunderscope/binary_context_managers:runtime_manager", "//software/thunderscope/binary_context_managers:simulator", - "//software/thunderscope/log/stats", ], ) diff --git a/src/software/thunderscope/constants.py b/src/software/thunderscope/constants.py index eb5bc0bb00..ac5973b8f2 100644 --- a/src/software/thunderscope/constants.py +++ b/src/software/thunderscope/constants.py @@ -402,18 +402,16 @@ class RuntimeManagerConstants: EXTERNAL_RUNTIMES_PATH = "/opt/tbotspython/external_runtimes" RUNTIME_CONFIG_PATH = f"{EXTERNAL_RUNTIMES_PATH}/runtime_config.toml" - RUNTIME_STATS_DIRECTORY_PATH = "/tmp/tbots/stats" - RUNTIME_FRIENDLY_STATS_FILE = "blue.toml" - RUNTIME_ENEMY_FROM_FRIENDLY_STATS_FILE = "yellow_from_blue.toml" - RUNTIME_ENEMY_STATS_FILE = "yellow.toml" - RUNTIME_FRIENDLY_FROM_ENEMY_STATS_FILE = "blue_from_yellow.toml" - - RUNTIME_STATS_SCORE_KEY = "goals" - RUNTIME_STATS_RED_CARDS_KEY = "red_cards" - RUNTIME_STATS_YELLOW_CARDS_KEY = "yellow_cards" - RUNTIME_STATS_SHOTS_ON_NET = "shots_on_net" - RUNTIME_STATS_SHOTS_BLOCKED = "shots_blocked" + RUNTIME_EVENTS_DIRECTORY_PATH = ( + "/home/thunderbots/Software/src/software/ml/datasets" + ) + RUNTIME_EVENTS_FILE = "game_events.csv" 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 = "/home/thunderbots/Software/src/software/ml/datasets" + PASS_RESULTS_FILE_NAME = "pass_results.csv" diff --git a/src/software/thunderscope/log/stats/BUILD b/src/software/thunderscope/log/stats/BUILD deleted file mode 100644 index 20f800a11d..0000000000 --- a/src/software/thunderscope/log/stats/BUILD +++ /dev/null @@ -1,18 +0,0 @@ -package(default_visibility = ["//visibility:public"]) - -py_library( - name = "fullsystem_stats", - srcs = ["fullsystem_stats.py"], - deps = [ - "//software/thunderscope/log/trackers:tracker", - ], -) - -py_library( - name = "stats", - srcs = ["stats.py"], - deps = [ - ":fullsystem_stats", - "//software/thunderscope:thread_safe_buffer", - ], -) diff --git a/src/software/thunderscope/log/stats/fullsystem_stats.py b/src/software/thunderscope/log/stats/fullsystem_stats.py deleted file mode 100644 index 77049dee15..0000000000 --- a/src/software/thunderscope/log/stats/fullsystem_stats.py +++ /dev/null @@ -1,239 +0,0 @@ -import os - -from software.thunderscope.log.trackers import ( - PossessionTracker, - ShotTracker, - TrackerBuilder, - RefereeTracker, - GoalieTracker, -) -from dataclasses import dataclass -from software.thunderscope.proto_unix_io import ProtoUnixIO -from software.thunderscope.constants import RuntimeManagerConstants -import logging -from proto.import_all_protos import * -from rich import print - - -@dataclass -class FSStats: - """Stats for how well a FullSystem is performing""" - - num_yellow_cards: int = 0 - num_red_cards: int = 0 - num_scores: int = 0 - - num_shots_on_net: int = 0 - num_enemy_shots_blocked: int = 0 - - -class FullSystemStats: - # From GoalieTacticConfig - INCOMING_SHOT_MIN_VELOCITY = 0.2 - - def __init__( - self, - proto_unix_io: ProtoUnixIO, - friendly_colour_yellow: bool, - buffer_size: int = 5, - record_enemy_stats: bool = False, - ): - """Initializes the FullSystem Stats Tracker - - :param friendly_colour_yellow: if the friendly colour is yellow - :param buffer_size: the buffer size for protocol buffers - :param record_enemy_stats: if this should record both friendly and enemy stats or just friendly - """ - self.friendly_colour_yellow = friendly_colour_yellow - - # True if friendly had the last possession, False if enemy - # None if neither - self.last_possession_friendly: bool | None = None - - # use both trackers to keep track of shots on net - # take the min to avoid noise from the attacker tactic visualization - self.num_shots_on_net_attacker: int = 0 - self.num_shots_on_net_goalie: int = 0 - - self.stats = FSStats() - - # these should be set up using the setup method - self.stats_file = None - self.enemy_stats_file = None - - # the python __del__ destructor isn't called reliably - # so printing this at the start instead - print(f"[bold red]Writing FS Stats to {self._get_stats_file()}") - - self.tracker = ( - TrackerBuilder(proto_unix_io=proto_unix_io) - .add_tracker( - ShotTracker, callback=self._update_shot_count, buffer_size=buffer_size - ) - .add_tracker( - PossessionTracker, - callback=self._update_posession, - buffer_size=buffer_size, - ) - .add_tracker( - RefereeTracker, - callback=self._update_referee_info_friendly, - friendly_color_yellow=self.friendly_colour_yellow, - buffer_size=buffer_size, - ) - .add_tracker( - GoalieTracker, - callback=self._update_goalie_shot_friendly, - for_friendly=True, - buffer_size=buffer_size, - ) - ) - - self.record_enemy_stats = record_enemy_stats - if self.record_enemy_stats: - self.enemy_stats = FSStats() - self.tracker = self.tracker.add_tracker( - RefereeTracker, - callback=self._update_referee_info_enemy, - friendly_color_yellow=(not self.friendly_colour_yellow), - ).add_tracker( - GoalieTracker, - callback=self._update_goalie_shot_enemy, - for_friendly=False, - buffer_size=buffer_size, - ) - - print(f"[bold red]Writing Enemy FS Stats to {self._get_enemy_stats_file()}") - - def refresh(self) -> None: - """Refreshes the stats for the game so far""" - self.tracker.refresh() - - self._flush_stats() - - def _update_shot_count(self, _: Shot): - self.stats.num_shots_on_net += 1 - - def _update_posession(self, friendly_posession: bool | None): - self.last_possession_friendly = not friendly_posession - - def _update_referee_info_friendly( - self, num_goals: int, num_yellow_cards: int, num_red_cards: int - ) -> None: - # the callback for tracking incoming shots can't differentiate between a blocked shot vs a goal - # so we subtract all "blocked" shots that were actually goals, so not blocked - if self.stats.num_scores < num_goals: - self.stats.num_enemy_shots_blocked -= 1 - - self.stats.num_scores = num_goals - self.stats.num_yellow_cards = num_yellow_cards - self.stats.num_red_cards = num_red_cards - - def _update_referee_info_enemy( - self, num_goals: int, num_yellow_cards: int, num_red_cards: int - ) -> None: - # the callback for tracking incoming shots can't differentiate between a blocked shot vs a goal - # so we subtract all "blocked" shots that were actually goals, so not blocked - if self.enemy_stats.num_scores < num_goals: - self.enemy_stats.num_enemy_shots_blocked -= 1 - - self.enemy_stats.num_scores = num_goals - self.enemy_stats.num_yellow_cards = num_yellow_cards - self.enemy_stats.num_red_cards = num_red_cards - - def _update_goalie_shot_friendly( - self, is_shot_incoming: bool, last_shot_incoming: bool - ) -> None: - if not is_shot_incoming and last_shot_incoming: - self.stats.num_enemy_shots_blocked += 1 - - if self.record_enemy_stats: - if is_shot_incoming and not last_shot_incoming: - self.enemy_stats.num_shots_on_net += 1 - - def _update_goalie_shot_enemy( - self, is_shot_incoming: bool, last_shot_incoming: bool - ) -> None: - if self.record_enemy_stats: - if not is_shot_incoming and last_shot_incoming: - self.enemy_stats.num_enemy_shots_blocked += 1 - - def _get_stats_file(self): - return os.path.join( - RuntimeManagerConstants.RUNTIME_STATS_DIRECTORY_PATH, - RuntimeManagerConstants.RUNTIME_ENEMY_STATS_FILE - if self.friendly_colour_yellow - else RuntimeManagerConstants.RUNTIME_FRIENDLY_STATS_FILE, - ) - - def _get_enemy_stats_file(self): - return os.path.join( - RuntimeManagerConstants.RUNTIME_STATS_DIRECTORY_PATH, - RuntimeManagerConstants.RUNTIME_FRIENDLY_FROM_ENEMY_STATS_FILE - if self.friendly_colour_yellow - else RuntimeManagerConstants.RUNTIME_ENEMY_FROM_FRIENDLY_STATS_FILE, - ) - - def setup(self): - """Sets up the file resources for logging - Creates any missing directories and stores the file handle - """ - stats_file_name = self._get_stats_file() - - # create temp stats directory if it doesn't exist - os.makedirs(os.path.dirname(stats_file_name), exist_ok=True) - - self.stats_file = open(stats_file_name, "w") - - if self.record_enemy_stats: - enemy_stats_file_name = self._get_enemy_stats_file() - - # create temp stats directory if it doesn't exist - os.makedirs(os.path.dirname(enemy_stats_file_name), exist_ok=True) - - self.enemy_stats_file = open(enemy_stats_file_name, "w") - - def cleanup(self): - """Writes all logs back to file, and cleans up any created file resources after logging""" - self._flush_stats() - - if self.stats_file: - self.stats_file.flush() - self.stats_file.close() - - if self.record_enemy_stats and self.enemy_stats_file: - self.enemy_stats_file.flush() - self.enemy_stats_file.close() - - def _flush_stats(self): - """Write the current stats to disk""" - self._write_stats_to_file(self.stats, self.stats_file) - - if self.record_enemy_stats: - self._write_stats_to_file(self.enemy_stats, self.enemy_stats_file) - - def _write_stats_to_file(self, stats: FSStats, stats_file) -> None: - """Write the given stats to the given file - - :param stats: the stats to write - :param stats_file: handle to the file to write to - """ - if not stats_file: - return - - try: - # formatted as key-value pairs in TOML - stats_to_write = ( - f'{RuntimeManagerConstants.RUNTIME_STATS_SCORE_KEY} = "{stats.num_scores}"\n' - f'{RuntimeManagerConstants.RUNTIME_STATS_RED_CARDS_KEY} = "{stats.num_red_cards}"\n' - f'{RuntimeManagerConstants.RUNTIME_STATS_YELLOW_CARDS_KEY} = "{stats.num_yellow_cards}"\n' - f'{RuntimeManagerConstants.RUNTIME_STATS_SHOTS_ON_NET} = "{stats.num_shots_on_net}"\n' - f'{RuntimeManagerConstants.RUNTIME_STATS_SHOTS_BLOCKED} = "{stats.num_enemy_shots_blocked}"' - ) - - stats_file.seek(0) - stats_file.write(stats_to_write) - stats_file.truncate() - - except (FileNotFoundError, PermissionError): - logging.warning("Failed to write TOML FS stats file") diff --git a/src/software/thunderscope/log/stats/stats.py b/src/software/thunderscope/log/stats/stats.py deleted file mode 100644 index de7e9074fc..0000000000 --- a/src/software/thunderscope/log/stats/stats.py +++ /dev/null @@ -1,33 +0,0 @@ -from software.thunderscope.log.stats.fullsystem_stats import FullSystemStats -from software.thunderscope.proto_unix_io import ProtoUnixIO -from proto.import_all_protos import * - - -class Stats: - """This class is a wrapper for all Statistics related operations we want to do with FullSystem or Thunderscope""" - - def __init__( - self, - proto_unix_io: ProtoUnixIO, - friendly_color_yellow: bool = False, - record_enemy_stats: bool = False, - buffer_size: int = 5, - ): - self.proto_unix_io = proto_unix_io - - self.fs_stats = FullSystemStats( - friendly_colour_yellow=friendly_color_yellow, - proto_unix_io=proto_unix_io, - buffer_size=buffer_size, - record_enemy_stats=record_enemy_stats, - ) - - def refresh(self): - self.fs_stats.refresh() - - def __enter__(self): - self.fs_stats.setup() - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.fs_stats.cleanup() diff --git a/src/software/thunderscope/log/trackers/__init__.py b/src/software/thunderscope/log/trackers/__init__.py index 75eb8e1915..0e3e3b41cb 100644 --- a/src/software/thunderscope/log/trackers/__init__.py +++ b/src/software/thunderscope/log/trackers/__init__.py @@ -4,7 +4,6 @@ from software.thunderscope.log.trackers.referee_tracker import RefereeTracker from software.thunderscope.log.trackers.goalie_tracker import GoalieTracker -from software.thunderscope.log.trackers.tracked_event import EventType, TrackedEvent __all__ = [ "PossessionTracker", @@ -13,6 +12,5 @@ "TrackerBuilder", "RefereeTracker", "GoalieTracker", - "TrackedEvent", "EventType", ] diff --git a/src/software/thunderscope/log/trackers/possession_tracker.py b/src/software/thunderscope/log/trackers/possession_tracker.py deleted file mode 100644 index 5dd3a7b4a8..0000000000 --- a/src/software/thunderscope/log/trackers/possession_tracker.py +++ /dev/null @@ -1,87 +0,0 @@ -from software.thunderscope.log.trackers.tracker import Tracker -from typing import override, Callable -from software.thunderscope.thread_safe_buffer import ThreadSafeBuffer -from software.thunderscope.proto_unix_io import ProtoUnixIO -from proto.import_all_protos import * -import software.python_bindings as tbots_cpp - - -class PossessionTracker(Tracker): - """Tracker to track when ball possession changes""" - - def __init__(self, callback: Callable[[bool | None], None], buffer_size: int = 5): - """Initializes the Possession tracker - - :param callback: function to call when possession changes - called with an optional bool value: - - True: friendly possession - - False: enemy possession - - None: neither - :param buffer_size: buffer size for the tracker's io - """ - super().__init__(callback=callback, buffer_size=buffer_size) - - self.world_buffer = ThreadSafeBuffer(self.buffer_size, World) - - @override - def set_proto_unix_io(self, proto_unix_io: ProtoUnixIO) -> None: - super().set_proto_unix_io( - proto_unix_io, - [ - (World, self.world_buffer), - ], - ) - - @override - def refresh(self): - """Refresh and update the callback with the latest ball possession""" - world_msg = self.world_buffer.get(block=False, return_cached=True) - - if world_msg is None: - return - - world = tbots_cpp.World(world_msg) - - if self.callback: - self.callback( - self._check_posession_for_friendly( - world.friendlyTeam(), world.enemyTeam(), world.ball().position() - ) - ) - - def _check_posession_for_friendly( - self, - friendly_team: tbots_cpp.Team, - enemy_team: tbots_cpp.Team, - ball_position: tbots_cpp.Point, - ) -> bool | None: - """Check for if the friendly team has possession of the ball - True if they do, False if enemy team has possession, None if neither - - :param friendly_team: the friendly team - :param enemy_team: the enemy team - :param ball_position: the current ball position - :return: True / False / None depending on which team has possession - """ - if self._check_posession_for_team(friendly_team, ball_position): - return True - - if self._check_posession_for_team(enemy_team, ball_position): - return False - - return None - - def _check_posession_for_team( - self, team: tbots_cpp.Team, ball_position: tbots_cpp.Point - ) -> bool: - """Check if the given team has possession of the ball - - :param team: the team to check - :param ball_position: the current ball position - :return: True if the team has possession, False otherwise - """ - for robot in team.getAllRobots(): - if robot.isNearDribbler(ball_position): - return True - - return False diff --git a/src/software/thunderscope/log/trackers/referee_tracker.py b/src/software/thunderscope/log/trackers/referee_tracker.py deleted file mode 100644 index b7d824d08b..0000000000 --- a/src/software/thunderscope/log/trackers/referee_tracker.py +++ /dev/null @@ -1,67 +0,0 @@ -from typing import override, Callable -from software.thunderscope.log.trackers.tracker import Tracker -from proto.import_all_protos import * -from software.thunderscope.thread_safe_buffer import ThreadSafeBuffer -from software.thunderscope.proto_unix_io import ProtoUnixIO - - -class RefereeTracker(Tracker): - """Tracks Referee events, like goals and yellow / red cards for the friendly team only""" - - def __init__( - self, - friendly_color_yellow: bool, - callback: Callable[[int, int, int], None], - buffer_size: int = 5, - ): - """Initializes the Referee tracker - - :param friendly_color_yellow: if the friendly color is yellow or not - determines which goals, etc. the tracker tracks - :param callback: function to call when there is any new Referee event - called with the current goals, yellow cards, and red cards - :param buffer_size: buffer size for the tracker's io - """ - super().__init__(callback=callback, buffer_size=buffer_size) - - self.referee_buffer = ThreadSafeBuffer(buffer_size, Referee) - - self.friendly_color_yellow = friendly_color_yellow - - @override - def set_proto_unix_io(self, proto_unix_io: ProtoUnixIO) -> None: - super().set_proto_unix_io( - proto_unix_io, - [ - (Referee, self.referee_buffer), - ], - ) - - @override - def refresh(self): - """Refresh and update the callback with the latest referee information""" - refree_msg = self.referee_buffer.get(block=False, return_cached=True) - - if not refree_msg: - return - - if refree_msg.HasField("yellow" if self.friendly_color_yellow else "blue"): - team_info = ( - refree_msg.yellow if self.friendly_color_yellow else refree_msg.blue - ) - - num_goals = 0 - num_yellow_cards = 0 - num_red_cards = 0 - - if team_info.HasField("score"): - num_goals = team_info.score - - if team_info.HasField("yellow_cards"): - num_yellow_cards = team_info.yellow_cards - - if team_info.HasField("red_cards"): - num_red_cards = team_info.red_cards - - if self.callback: - self.callback(num_goals, num_yellow_cards, num_red_cards) diff --git a/src/software/thunderscope/log/trackers/tracked_event.py b/src/software/thunderscope/log/trackers/tracked_event.py deleted file mode 100644 index 0ae6a22a3a..0000000000 --- a/src/software/thunderscope/log/trackers/tracked_event.py +++ /dev/null @@ -1,162 +0,0 @@ -from dataclasses import dataclass -from enum import StrEnum, auto -from proto.import_all_protos import * -from typing import Any -from software.py_constants import DIV_B_NUM_ROBOTS -from google.protobuf.descriptor import Descriptor, FieldDescriptor -from software.thunderscope.time_provider import time_provider_instance - - -def count_primitive_fields(descriptor: Descriptor): - """Recursively counts the number of primitive fields in a Protobuf message - using its descriptor. - - :param message: the message descriptor to count all leaf-level primitive fields for - :return: the count of primitive fields - """ - count = 0 - - for field in descriptor.fields: - # Check if the field is a nested message - if field.type == FieldDescriptor.TYPE_MESSAGE: - # Get the nested message class to recurse into its descriptor - nested_message = field.message_type - # Recurse using the nested message's descriptor - count += count_primitive_fields(nested_message) - else: - # It's a primitive type (double, float, int, bool, string, etc.) - count += 1 - return count - - -NUM_ROBOT_FIELDS = count_primitive_fields(RobotState.DESCRIPTOR) - - -class EventType(StrEnum): - """Enum for the different types of events we want to track""" - - PASS = auto() - SHOT_ON_GOAL = auto() - ENEMY_SHOT_ON_GOAL = auto() - SHOT_BLOCKED = auto() - FRIENDLY_POSSESSION_START = auto() - FRIENDLY_POSSESSION_END = auto() - ENEMY_POSSESSION_START = auto() - ENEMY_POSSESSION_END = auto() - GAME_START = auto() - GAME_END = auto() - GOAL_SCORED = auto() - YELLOW_CARD = auto() - RED_CARD = auto() - - -class Team(StrEnum): - """The teams present in the game""" - - BLUE = auto() - YELLOW = auto() - - -@dataclass -class Robot: - """Represents a single robot on the field, with ID and current state.""" - - id: int - state: RobotState - - -@dataclass -class TrackedEvent: - """Represents a single event being tracked, where and for whom the event is, and the game state at the time of the event""" - - event_type: EventType - timestamp: float - from_team: Team - for_team: Team - ball_state: BallState - friendly_robots: list[Robot] - enemy_robots: list[Robot] - - -def get_event_from_world( - world_msg: World, event_type: EventType, from_team: Team, for_team: Team -) -> TrackedEvent: - """Creates a TrackedEvent from a world protobuf message - - :param world_msg: the world object containing the state of the game - :param event_type: the type of event being recorded - :param from_team: the team that the event is coming from - :param for_team: the team that the event is for - :return: a fully populated TrackedEvent including ball and robot states - """ - ball_state = world_msg.ball.current_state - - friendly_robots = [ - Robot(id=robot.id, state=robot.current_state) - for robot in world_msg.friendly_team.team_robots - ] - enemy_robots = [ - Robot(id=robot.id, state=robot.current_state) - for robot in world_msg.enemy_team.team_robots - ] - - return TrackedEvent( - timestamp=time_provider_instance.elapsed_time_ns(), - event_type=event_type, - from_team=from_team, - for_team=for_team, - ball_state=ball_state, - friendly_robots=friendly_robots, - enemy_robots=enemy_robots, - ) - - -def add_robots_to_row(row: list[Any], robots: list[Robot]) -> None: - """Serializes robots into flattened columns within a list row - - :param row: the existing list representing a CSV row to be appended to - :param robot_states: the list of RobotState objects to flatten and add - :return: None (the row list is modified in place) - """ - # robot state columns will be added based on robot id - robot_state_map = {robot.id: robot.state for robot in robots} - - # Add friendly robots: [r1_data, r2_data...] based on id - for idx in range(DIV_B_NUM_ROBOTS): - if idx not in robot_state_map: - row.extend([None] * NUM_ROBOT_FIELDS) - else: - robot_state = robot_state_map[idx] - robot_row = [ - robot_state.global_position.x_meters, - robot_state.global_position.y_meters, - robot_state.global_orientation.radians, - robot_state.global_velocity.x_component_meters, - robot_state.global_velocity.y_component_meters, - robot_state.global_angular_velocity.radians_per_second, - ] - - assert len(robot_row) == NUM_ROBOT_FIELDS - - row.extend(robot_row) - - -def event_to_csv_row(event: TrackedEvent) -> str: - """Serializes a TrackedEvent into a flat CSV string row - - :param event: the TrackedEvent object to convert - :return: a comma-separated string of all event attributes and robot states - """ - row = [event.event_type.value, event.timestamp] - - row = row + [ - event.ball_state.global_position.x_meters, - event.ball_state.global_position.y_meters, - event.ball_state.global_velocity.x_component_meters, - event.ball_state.global_velocity.y_component_meters, - ] - - add_robots_to_row(row, event.friendly_robots) - add_robots_to_row(row, event.enemy_robots) - - return ",".join([str(elem) for elem in row]) diff --git a/src/software/thunderscope/log/trackers/tracker.py b/src/software/thunderscope/log/trackers/tracker.py deleted file mode 100644 index f5f4aa319a..0000000000 --- a/src/software/thunderscope/log/trackers/tracker.py +++ /dev/null @@ -1,36 +0,0 @@ -from software.thunderscope.proto_unix_io import ProtoUnixIO -from typing import Callable, Optional, Tuple, Type -from software.thunderscope.thread_safe_buffer import ThreadSafeBuffer -from google.protobuf.message import Message - - -class Tracker: - """Generic tracker base class.""" - - def __init__( - self, callback: Optional[Callable[..., None]] = None, buffer_size: int = 5 - ): - """Initializes the tracker with the given callback and buffer size - - :param callback: the function to call when the tracker tracks an event - :param buffer_size: buffer size for the tracker's io - """ - self.callback = callback - self.buffer_size = buffer_size - - def set_proto_unix_io( - self, - proto_unix_io: ProtoUnixIO, - type_buffers: list[Tuple[Type[Message], ThreadSafeBuffer]], - ) -> None: - """Registers the given message types and buffers to the given proto unix io connection - - :param proto_unix_io: the io connection to listen on - :param type_buffers: a list of (Message Type, Buffer) tuples. - messages of each type will be placed into their corresponding buffer - """ - for message_type, buffer in type_buffers: - proto_unix_io.register_observer(message_type, buffer) - - def refresh(self) -> None: - raise Exception("Not Implemented, please use the appropriate subclass!") diff --git a/src/software/thunderscope/log/trackers/tracker_builder.py b/src/software/thunderscope/log/trackers/tracker_builder.py deleted file mode 100644 index 903458b6e7..0000000000 --- a/src/software/thunderscope/log/trackers/tracker_builder.py +++ /dev/null @@ -1,38 +0,0 @@ -from software.thunderscope.proto_unix_io import ProtoUnixIO -from software.thunderscope.log.trackers.tracker import Tracker -from typing import Callable, Any, Optional, Type, Self - - -class TrackerBuilder: - """Builder class to combine different trackers and update them together""" - - def __init__(self, proto_unix_io: ProtoUnixIO) -> None: - """Initializes the builder - - :param proto_unix_io: the unix io that the trackers should listen on - """ - self.proto_unix_io = proto_unix_io - - self.trackers = [] - - def add_tracker( - self, - tracker_cls: Type[Tracker], - callback: Optional[Callable[[Any], None]] = None, - **kwargs, - ) -> Self: - """Adds a single tracker to the list - - :param tracker_cls: The class of the tracker to instantiate - :param callback: function that the tracker should call when it tracks an event - :param **kwargs: tracker-specific arguments - """ - tracker = tracker_cls(callback=callback, **kwargs) - tracker.set_proto_unix_io(self.proto_unix_io) - self.trackers.append(tracker) - return self - - def refresh(self) -> None: - """Refreshes all the trackers""" - for tracker in self.trackers: - tracker.refresh() diff --git a/src/software/thunderscope/thunderscope_main.py b/src/software/thunderscope/thunderscope_main.py index 3290b65ca1..a6da715deb 100644 --- a/src/software/thunderscope/thunderscope_main.py +++ b/src/software/thunderscope/thunderscope_main.py @@ -11,7 +11,8 @@ from software.thunderscope.binary_context_managers.runtime_manager import ( runtime_manager_instance, ) -from software.thunderscope.log.stats.stats import Stats +from software.evaluation.loggers.stats_logger import StatsLogger +from software.evaluation.loggers.pass_logger import PassLogger from software.thunderscope.thunderscope import Thunderscope from software.thunderscope.constants import LogLevels @@ -245,6 +246,22 @@ help="Record stats about fullsystem performance (during AI vs AI) for a set amount of time in minutes", ) + parser.add_argument( + "--pass_results_file", + action="store", + type=str, + default="pass_results.csv", + help="File name within ml/datasets to store pass results", + ) + + parser.add_argument( + "--game_events_file", + action="store", + type=str, + default="game_events.csv", + help="File name within ml/datasets to store game events", + ) + args = parser.parse_args() # we only have --launch_gc parameter but not args.run_yellow and args.run_blue @@ -492,17 +509,27 @@ def __ticker(tick_rate_ms: int) -> None: if args.enable_autoref else contextlib.nullcontext() ) as autoref, ( - Stats( + StatsLogger( proto_unix_io=tscope.proto_unix_io_map[ProtoUnixIOTypes.BLUE], record_enemy_stats=True, + friendly_colour_yellow=False, + out_file_name=args.game_events_file, ) if args.record_stats else contextlib.nullcontext() - ) as blue_stats, ( - Stats(proto_unix_io=tscope.proto_unix_io_map[ProtoUnixIOTypes.YELLOW]) + ) as blue_stats_logger, ( + StatsLogger( + proto_unix_io=tscope.proto_unix_io_map[ProtoUnixIOTypes.YELLOW], + friendly_colour_yellow=True, + out_file_name=args.game_events_file, + ) if args.record_stats else contextlib.nullcontext() - ) as yellow_stats: + ) as yellow_stats_logger, PassLogger( + proto_unix_io=tscope.proto_unix_io_map[ProtoUnixIOTypes.YELLOW], + friendly_colour_yellow=True, + out_file_name=args.pass_results_file, + ) as blue_pass_logger: tscope.register_refresh_function(gamecontroller.refresh) autoref_proto_unix_io = ProtoUnixIO() @@ -513,9 +540,11 @@ def __ticker(tick_rate_ms: int) -> None: tscope.proto_unix_io_map[ProtoUnixIOTypes.YELLOW] ) - if args.record_stats: - tscope.register_refresh_function(blue_stats.refresh) - tscope.register_refresh_function(yellow_stats.refresh) + if args.record_stats and blue_stats_logger and yellow_stats_logger: + tscope.register_refresh_function(blue_stats_logger.refresh) + tscope.register_refresh_function(yellow_stats_logger.refresh) + + tscope.register_refresh_function(blue_pass_logger.refresh) simulator.setup_proto_unix_io( tscope.proto_unix_io_map[ProtoUnixIOTypes.SIM],