diff --git a/src/proto/message_translation/tbots_protobuf.cpp b/src/proto/message_translation/tbots_protobuf.cpp index cc21417cd7..1f02d36b2b 100644 --- a/src/proto/message_translation/tbots_protobuf.cpp +++ b/src/proto/message_translation/tbots_protobuf.cpp @@ -544,3 +544,19 @@ std::unique_ptr createShapeProto(const Stadium& stadium) (*shape_msg->mutable_stadium()) = *createStadiumProto(stadium); return shape_msg; } + +std::unique_ptr createPassFeaturesProto(const Pass& pass, + const World& world, + double score) +{ + auto pass_features_msg = std::make_unique(); + (*pass_features_msg->mutable_passer_point()) = *createPointProto(pass.passerPoint()); + (*pass_features_msg->mutable_receiver_point()) = + *createPointProto(pass.receiverPoint()); + + (*pass_features_msg->mutable_world_state()) = *createWorld(world); + + pass_features_msg->set_score(score); + + return pass_features_msg; +} diff --git a/src/proto/message_translation/tbots_protobuf.h b/src/proto/message_translation/tbots_protobuf.h index 50ab53c374..33905f93a4 100644 --- a/src/proto/message_translation/tbots_protobuf.h +++ b/src/proto/message_translation/tbots_protobuf.h @@ -213,6 +213,19 @@ BallState createBallState(const TbotsProto::BallState ball_state); std::unique_ptr createPassVisualization( const std::vector& passes_with_rating); +/** + * Returns a pass features message with the given pass, game state, and score + * + * @param pass the pass to get features from + * @param world the current world state + * @param score the score for the pass in the current world + * + * @return The unique_ptr to a PassFeatures proto + */ +std::unique_ptr createPassFeaturesProto(const Pass& pass, + const World& world, + double score); + /** * Returns the WorldStateReceivedTrigger given the world state received trigger * diff --git a/src/proto/visualization.proto b/src/proto/visualization.proto index eca602a794..d535eb81a1 100644 --- a/src/proto/visualization.proto +++ b/src/proto/visualization.proto @@ -97,3 +97,14 @@ message DebugShapes // Unique ID to a named shape repeated DebugShape debug_shapes = 1; } + +message PassFeatures +{ + Point passer_point = 1; + Point receiver_point = 2; + double pass_speed_m_per_s = 3; + + World world_state = 4; + + double score = 5; +} diff --git a/src/shared/constants.h b/src/shared/constants.h index b8afca9183..bc395ed4ef 100644 --- a/src/shared/constants.h +++ b/src/shared/constants.h @@ -2,7 +2,7 @@ #include // Some platformio targets don't support STL, so we can't -// use unordered_map, string, .... We guard all networking stuff with +// use unordered_map, string, .... We guard all constants that need these types with #ifndef PLATFORMIO_BUILD #include #include @@ -32,6 +32,10 @@ static const std::string REPLAY_METADATA_DELIMITER = ","; static const std::string REPLAY_FILE_VERSION_PREFIX = "version:"; static const unsigned int REPLAY_FILE_VERSION = 2; +static const std::string PASS_FEATURE_DELIMITER = ","; +static const std::string PASS_FEATURE_DIR = "/tmp/tbots/ml"; +static const std::string PASS_FEATURE_FILE = "pass_features.csv"; + #endif // PLATFORMIO_BUILD // TOML config file path for robot configuration @@ -102,6 +106,8 @@ static const double COLLISION_ALLOWED_ROBOT_MAX_SPEED_METERS_PER_SECOND = 0.5; static const double STOP_COMMAND_BALL_AVOIDANCE_DISTANCE_M = 0.5; // The maximum speed attainable by enemy robots static const double ENEMY_ROBOT_MAX_SPEED_METERS_PER_SECOND = 3.0; +// The speed at which enemy robots can intercept a moving ball +static const double ENEMY_ROBOT_INTERCEPTION_SPEED_METERS_PER_SECOND = 0.5; // The maximum acceleration achievable by enemy robots, in metres per seconds squared. static const double ENEMY_ROBOT_MAX_ACCELERATION_METERS_PER_SECOND_SQUARED = 4.0; @@ -228,3 +234,9 @@ static const unsigned NUM_GENEVA_ANGLES = 5; constexpr double AUTO_CHIP_DISTANCE_DEFAULT_M = 1.5; constexpr double AUTO_KICK_SPEED_DEFAULT_M_PER_S = 1.5; constexpr double WHEEL_ROTATION_MAX_SPEED_M_PER_S = 15.2; + +// how often to sample pass feature data, so once for every n passes considered +static constexpr unsigned int PASS_SAMPLING_FREQUENCY = 10; + +// how often to commit to a random pass instead of the best pass, so once every n passes +static constexpr unsigned int BEST_PASS_OVERRIDE_FREQUENCY = 10; diff --git a/src/software/ai/passing/BUILD b/src/software/ai/passing/BUILD index a959236031..0009a99869 100644 --- a/src/software/ai/passing/BUILD +++ b/src/software/ai/passing/BUILD @@ -131,6 +131,7 @@ cc_library( hdrs = ["pass_generator.h"], deps = [ ":cost_functions", + ":pass_feature_collector", ":pass_with_rating", "//software/optimization:gradient_descent", "//software/world", @@ -147,3 +148,15 @@ cc_test( "//software/world", ], ) + +cc_library( + name = "pass_feature_collector", + srcs = ["pass_feature_collector.cpp"], + hdrs = ["pass_feature_collector.h"], + deps = [ + ":cost_functions", + ":pass", + "//software/ai/evaluation:calc_best_shot", + "//software/world", + ], +) diff --git a/src/software/ai/passing/cost_function.cpp b/src/software/ai/passing/cost_function.cpp index 7b5506911a..8588fe8bfa 100644 --- a/src/software/ai/passing/cost_function.cpp +++ b/src/software/ai/passing/cost_function.cpp @@ -144,6 +144,24 @@ double calculateInterceptRisk(const Team& enemy_team, const Pass& pass, return *std::max_element(enemy_intercept_risks.begin(), enemy_intercept_risks.end()); } +Duration getEnemyTimeToInterceptPoint(const Robot& enemy_robot, + const Point& interception_point) +{ + Vector enemy_interception_vector = interception_point - enemy_robot.position(); + // Take into account the enemy robot's radius for minimum min_interception_distance + // required to travel to intercept the pass. + double min_interception_distance = + std::max(0.0, enemy_interception_vector.length() - ROBOT_MAX_RADIUS_METERS); + + double signed_1d_enemy_vel = + enemy_robot.velocity().dot(enemy_interception_vector.normalize()); + + return getTimeToTravelDistance( + min_interception_distance, ENEMY_ROBOT_MAX_SPEED_METERS_PER_SECOND, + ENEMY_ROBOT_MAX_ACCELERATION_METERS_PER_SECOND_SQUARED, signed_1d_enemy_vel, + ENEMY_ROBOT_INTERCEPTION_SPEED_METERS_PER_SECOND); +} + double calculateInterceptRisk(const Robot& enemy_robot, const Pass& pass, const TbotsProto::PassingConfig& passing_config) { @@ -157,26 +175,13 @@ double calculateInterceptRisk(const Robot& enemy_robot, const Pass& pass, // point on the pass before the ball Point closest_interception_point = closestPoint( enemy_robot.position(), Segment(pass.passerPoint(), pass.receiverPoint())); - Vector enemy_interception_vector = - closest_interception_point - enemy_robot.position(); - // Take into account the enemy robot's radius for minimum min_interception_distance - // required to travel to intercept the pass. - double min_interception_distance = - std::max(0.0, enemy_interception_vector.length() - ROBOT_MAX_RADIUS_METERS); - const double ENEMY_ROBOT_INTERCEPTION_SPEED_METERS_PER_SECOND = 0.5; - double signed_1d_enemy_vel = - enemy_robot.velocity().dot(enemy_interception_vector.normalize()); - double enemy_robot_time_to_interception_point_sec = - getTimeToTravelDistance( - min_interception_distance, ENEMY_ROBOT_MAX_SPEED_METERS_PER_SECOND, - ENEMY_ROBOT_MAX_ACCELERATION_METERS_PER_SECOND_SQUARED, signed_1d_enemy_vel, - ENEMY_ROBOT_INTERCEPTION_SPEED_METERS_PER_SECOND) - .toSeconds(); + double time_to_intercept_s = + getEnemyTimeToInterceptPoint(enemy_robot, closest_interception_point).toSeconds(); + // Scale the time to interception point by the enemy robot's interception capability - Duration enemy_robot_time_to_interception_point = - Duration::fromSeconds(enemy_robot_time_to_interception_point_sec * - passing_config.enemy_interception_time_multiplier()); + Duration scaled_time_to_intercept = Duration::fromSeconds( + time_to_intercept_s * passing_config.enemy_interception_time_multiplier()); // TODO (#2988): We should generate a more realistic ball trajectory Duration ball_time_to_interception_point = @@ -185,7 +190,7 @@ double calculateInterceptRisk(const Robot& enemy_robot, const Pass& pass, Duration::fromSeconds(passing_config.pass_delay_sec()); Duration interception_delta_time = - ball_time_to_interception_point - enemy_robot_time_to_interception_point; + ball_time_to_interception_point - scaled_time_to_intercept; // Whether or not the enemy will be able to intercept the pass can be determined // by whether or not they will be able to reach the pass receive position before @@ -195,6 +200,54 @@ double calculateInterceptRisk(const Robot& enemy_robot, const Pass& pass, 0.0, 1.0); } +std::optional getClosestReceiverToPass(const Team& friendly_team, + const Pass& pass) +{ + if (friendly_team.getAllRobots().empty()) + return std::nullopt; + + auto best_receiver = friendly_team.getAllRobots().at(0); + double curr_best_distance = std::numeric_limits::max(); + + for (const Robot& robot : friendly_team.getAllRobots()) + { + double distance = (robot.position() - pass.receiverPoint()).length(); + if (distance < curr_best_distance) + { + best_receiver = robot; + curr_best_distance = distance; + } + } + + return best_receiver; +} + +Duration getBallTravelTime(const Pass& pass, + const TbotsProto::PassingConfig& passing_config) +{ + return Duration::fromSeconds((pass.receiverPoint() - pass.passerPoint()).length() / + pass.speed()) + + Duration::fromSeconds(passing_config.pass_delay_sec()); +} + +Timestamp getEarliestReceiveTime(const Robot& best_receiver, const Pass& pass, + const TbotsProto::PassingConfig& passing_config) +{ + Duration min_robot_travel_time = + best_receiver.getTimeToPosition(pass.receiverPoint()); + Timestamp earliest_time_to_receive_point = + best_receiver.timestamp() + min_robot_travel_time; + + return earliest_time_to_receive_point; +} + +Timestamp getEarliestTimeToAngle(const Robot& best_receiver, const Pass& pass) +{ + Angle receive_angle = (pass.passerPoint() - best_receiver.position()).orientation(); + Duration time_to_receive_angle = best_receiver.getTimeToOrientation(receive_angle); + return best_receiver.timestamp() + time_to_receive_angle; +} + double ratePassFriendlyCapability(const Team& friendly_team, const Pass& pass, const TbotsProto::PassingConfig& passing_config) { @@ -211,37 +264,27 @@ double ratePassFriendlyCapability(const Team& friendly_team, const Pass& pass, } // Get the robot that is closest to where the pass would be received - Robot best_receiver = friendly_team.getAllRobots()[0]; - for (const Robot& robot : friendly_team.getAllRobots()) + auto best_receiver_opt = getClosestReceiverToPass(friendly_team, pass); + + if (!best_receiver_opt.has_value()) { - double distance = (robot.position() - pass.receiverPoint()).length(); - double curr_best_distance = - (best_receiver.position() - pass.receiverPoint()).length(); - if (distance < curr_best_distance) - { - best_receiver = robot; - } + return 0; } + const Robot& best_receiver = best_receiver_opt.value(); + + Duration ball_travel_time = getBallTravelTime(pass, passing_config); + Timestamp receive_time = best_receiver.timestamp() + ball_travel_time; + // Figure out what time the robot would have to receive the ball at + // and how long it would take our robot to get there // TODO (#2988): We should generate a more realistic ball trajectory - Duration ball_travel_time = - Duration::fromSeconds((pass.receiverPoint() - pass.passerPoint()).length() / - pass.speed()) + - Duration::fromSeconds(passing_config.pass_delay_sec()); - Timestamp receive_time = best_receiver.timestamp() + ball_travel_time; - - // Figure out how long it would take our robot to get there - Duration min_robot_travel_time = - best_receiver.getTimeToPosition(pass.receiverPoint()); - Timestamp earliest_time_to_receive_point = - best_receiver.timestamp() + min_robot_travel_time; + const Timestamp earliest_time_to_receive_point = + getEarliestReceiveTime(best_receiver, pass, passing_config); // Figure out what angle the robot would have to be at to receive the ball - Angle receive_angle = (pass.passerPoint() - best_receiver.position()).orientation(); - Duration time_to_receive_angle = best_receiver.getTimeToOrientation(receive_angle); - Timestamp earliest_time_to_receive_angle = - best_receiver.timestamp() + time_to_receive_angle; + const Timestamp earliest_time_to_receive_angle = + getEarliestTimeToAngle(best_receiver, pass); // Figure out if rotation or moving will take us longer Timestamp latest_time_to_receiver_state = @@ -259,30 +302,36 @@ double ratePassFriendlyCapability(const Team& friendly_team, const Pass& pass, sigmoid_width); } -double getStaticPositionQuality(const Field& field, const Point& position, - const TbotsProto::PassingConfig& passing_config) +Rectangle getReducedField(const Field& field, TbotsProto::PassingConfig passing_config) { - // This constant is used to determine how steep the sigmoid slopes below are - static const double sig_width = 0.1; - // The offset from the sides of the field for the center of the sigmoid functions double x_offset = passing_config.static_field_position_quality_x_offset(); double y_offset = passing_config.static_field_position_quality_y_offset(); - double friendly_goal_weight = - passing_config.static_field_position_quality_friendly_goal_distance_weight(); - // Make a slightly smaller field, and positive weight values in this reduced field double half_field_length = field.xLength() / 2; double half_field_width = field.yLength() / 2; Rectangle reduced_size_field( Point(-half_field_length + x_offset, -half_field_width + y_offset), Point(half_field_length - x_offset, half_field_width - y_offset)); + return reduced_size_field; +} + +double getStaticPositionQuality(const Field& field, const Point& position, + const TbotsProto::PassingConfig& passing_config) +{ + // This constant is used to determine how steep the sigmoid slopes below are + static const double sig_width = 0.1; + + // Make a slightly smaller field, and positive weight values in this reduced field + const auto reduced_size_field = getReducedField(field, passing_config); double on_field_quality = rectangleSigmoid(reduced_size_field, position, sig_width); // Add a negative weight for positions closer to our goal Vector vec_to_friendly_goal = Vector(field.friendlyGoalCenter().x() - position.x(), field.friendlyGoalCenter().y() - position.y()); double distance_to_friendly_goal = vec_to_friendly_goal.length(); + double friendly_goal_weight = + passing_config.static_field_position_quality_friendly_goal_distance_weight(); double near_friendly_goal_quality = (1 - std::exp(-friendly_goal_weight * (std::pow(5, -2 + distance_to_friendly_goal)))); diff --git a/src/software/ai/passing/cost_function.h b/src/software/ai/passing/cost_function.h index 4235015edb..648eb543d4 100644 --- a/src/software/ai/passing/cost_function.h +++ b/src/software/ai/passing/cost_function.h @@ -117,6 +117,18 @@ double ratePassNotTooClose(const Pass& pass, double calculateInterceptRisk(const Team& enemy_team, const Pass& pass, const TbotsProto::PassingConfig& passing_config); +/** + * Calculates the time taken by an enemy to get to a specific intercept point + * + * @param enemy_robot The robot that might intercept our pass + * @param interception_point The point on the pass's path that the enemy is trying to + * intercept the ball at + * @return A Duration value, which is the time taken by the enemy to get to the + * interception point + */ +Duration getEnemyTimeToInterceptPoint(const Robot& enemy_robot, + const Point& interception_point); + /** * Calculates the likelihood that the given pass will be intercepted by a given robot * @@ -131,6 +143,48 @@ double calculateInterceptRisk(const Team& enemy_team, const Pass& pass, double calculateInterceptRisk(const Robot& enemy_robot, const Pass& pass, const TbotsProto::PassingConfig& passing_config); +/** + * Calculates the time it takes for the ball to travel the length of the given pass + * + * @param pass the pass to calculate travel time for + * @param passing_config The passing config used for tuning + * @return A Duration value, which is the travel time + */ +Duration getBallTravelTime(const Pass& pass, + const TbotsProto::PassingConfig& passing_config); + +/** + * From the given team of robots, gets the closest receiver for the given pass + * i.e the robot closest to the receive point of the pass + * + * @param friendly_team the team of robots + * @param pass the pass to find the best receiver for + * @return A robot, or nullopt if no robot is found + */ +std::optional getClosestReceiverToPass(const Team& friendly_team, + const Pass& pass); + +/** + * Gets the timestamp of the earliest time the given receiver can receive the pass + * i.e make it to the pass's receive point + * + * @param best_receiver the best receiver robot for this pass + * @param pass the pass to find the receive timestamp for + * @param passing_config The passing config used for tuning + * @return A Timestamp indicating the earliest receive time + */ +Timestamp getEarliestReceiveTime(const Robot& best_receiver, const Pass& pass, + const TbotsProto::PassingConfig& passing_config); + +/** + * Gets the timestamp of the earliest time the given receiver can turn to the right + * orientation to receive the pass + * + * @param best_receiver the best receiver robot for this pass + * @param pass the pass to find the receive timestamp for + * @return A Timestamp indicating the earliest time to that orientation + */ +Timestamp getEarliestTimeToAngle(const Robot& best_receiver, const Pass& pass); /** * Calculate the probability of a friendly robot receiving the given pass @@ -149,6 +203,17 @@ double calculateInterceptRisk(const Robot& enemy_robot, const Pass& pass, double ratePassFriendlyCapability(const Team& friendly_team, const Pass& pass, const TbotsProto::PassingConfig& passing_config); +/** + * Gets a smaller field according to the passing config + * to indicate the boundaries of where we'd ideally want our passes to be + * + * @param field The field object to shrink + * @param passing_config The passing config used for tuning + * + * @return A Rectangle representing the smaller field + */ +Rectangle getReducedField(const Field& field, TbotsProto::PassingConfig passing_config); + /** * Calculates the static position quality for a given position on a given field * diff --git a/src/software/ai/passing/pass_feature_collector.cpp b/src/software/ai/passing/pass_feature_collector.cpp new file mode 100644 index 0000000000..601a99f2d7 --- /dev/null +++ b/src/software/ai/passing/pass_feature_collector.cpp @@ -0,0 +1,104 @@ + +#include "pass_feature_collector.h" + +#include + +#include "cost_function.h" +#include "proto/message_translation/tbots_protobuf.h" +#include "software/ai/evaluation/calc_best_shot.h" +#include "software/geom/algorithms/closest_point.h" +#include "software/geom/algorithms/contains.h" +#include "software/geom/algorithms/distance.h" +#include "software/logger/logger.h" + +PassFeatureCollector::PassFeatureCollector() {} + +void PassFeatureCollector::logPassFeatures( + const Pass& pass, const World& world, const TbotsProto::PassingConfig& passing_config) +{ + double score = getPassScore(pass, world, passing_config); + + LOG(VISUALIZE) << *createPassFeaturesProto(pass, world, score); +} + +double PassFeatureCollector::getEnemyInterceptTimeDelta( + const Robot& enemy_robot, const Pass& pass, + const TbotsProto::PassingConfig& passing_config) +{ + Point closest_interception_point = closestPoint( + enemy_robot.position(), Segment(pass.passerPoint(), pass.receiverPoint())); + + double time_to_interception_s = + getEnemyTimeToInterceptPoint(enemy_robot, closest_interception_point).toSeconds(); + + Duration ball_time_to_interception_point = + Duration::fromSeconds(distance(pass.passerPoint(), closest_interception_point) / + pass.speed()) + + Duration::fromSeconds(passing_config.pass_delay_sec()); + + return time_to_interception_s - ball_time_to_interception_point.toSeconds(); +} + +double PassFeatureCollector::getPassScore(const Pass& pass, const World& world, + const TbotsProto::PassingConfig passing_config) +{ + double score = NEUTRAL_SCORE; + + // if the pass has 0 speed + if (pass.speed() == 0) + return DEFINITELY_BAD_SCORE; + + // if there are no receivers on the friendly team + if (world.friendlyTeam().getAllRobots().size() <= 0) + return DEFINITELY_BAD_SCORE; + + const Field& field = world.field(); + + // in_enemy_defense_area_quality -> if pass receive point is in the enemy defense area + // (illegal) + if (contains(field.enemyDefenseArea(), pass.receiverPoint())) + return DEFINITELY_BAD_SCORE; + + // on_field_quality -> Make a slightly smaller field, and check + // if pass receive point not in the reduced field boundaries + if (!contains(getReducedField(field, passing_config), pass.receiverPoint())) + score += BAD_SCORE; + + // how well the receiver can receive the ball + double friendlyReceiveCapabilitySigmoid = + ratePassFriendlyCapability(world.friendlyTeam(), pass, passing_config); + + // robot arrives / turns too late + if (friendlyReceiveCapabilitySigmoid < BAD_SIGMOID_SCORE) + return DEFINITELY_BAD_SCORE; + else if (friendlyReceiveCapabilitySigmoid < NEUTRAL_SIGMOID_SCORE) + score += BAD_SCORE; + + // how well any enemy can intercept the ball + for (const auto& robot : world.enemyTeam().getAllRobots()) + { + double intercept_time_delta = + getEnemyInterceptTimeDelta(robot, pass, passing_config); + + // if any enemy can intercept successfully + if (intercept_time_delta <= 0) + return DEFINITELY_BAD_SCORE; + // if the interception is close (risky) + else if (intercept_time_delta < RISKY_INTERCEPT_DELTA) + score += SLIGHTLY_BAD_SCORE; + } + + double passForwardSigmoid = ratePassForwardQuality(pass, passing_config); + if (passForwardSigmoid < BAD_SIGMOID_SCORE) + score += BAD_SCORE; + + auto shot_opt = calcBestShotOnGoal( + Segment(field.enemyGoalpostPos(), field.enemyGoalpostNeg()), pass.receiverPoint(), + world.enemyTeam().getAllRobots(), TeamType::ENEMY); + + // if there's no shot on goal, rate it very slightly bad + if (!shot_opt) + score += VERY_SLIGHTLY_BAD_SCORE; + + return score; +} diff --git a/src/software/ai/passing/pass_feature_collector.h b/src/software/ai/passing/pass_feature_collector.h new file mode 100644 index 0000000000..3cf8057b8f --- /dev/null +++ b/src/software/ai/passing/pass_feature_collector.h @@ -0,0 +1,106 @@ +#pragma once +#include + +#include "pass.h" +#include "software/world/world.h" + +/* + * This class is responsible for collecting the features and the results + * for each pass considered by the Pass Generator + * + * These will tell the model what a "bad" pass is: + * We can't make any determinations on if the pass is good, since + * we don't know the game state outcome yet. But, we know which + * passes we'd like to exclude / weigh down + * + * These are the features we want to collect: + * + * Pass Receive Position x and y + * Ball Position x and y + * Positions of friendly and enemy robots, x and y + * The Score we assigned to the pass + * + * These will all be sent within the PassFeatures proto to be consumed later + * + * + * This is how we determine the final result for each pass: + * + * Definitely Bad: These are passes that either break an SSL rule + * or passes we just want to avoid completely + * they get a very negative score. + * + * Eg: a pass into the enemy defense area (illegal) + * a pass that the receiver cannot physically receive + * + * Bad, Slightly Bad, : These are additive scores for passes + * Very Slightly Bad that are not illegal, but we would want to avoid. + * These are heuristics based and so have some bias + * but that's unavoidable. + * We make the score more negative the worse the pass is. + * + * Eg: a pass that the receive would reached just in time (risky) + * + * Everything else gets a score of 0, so a neutral pass. + * + * + * A lot of the logic is ported over from existing cost functions. However, + * we want to exclude the more "subjective" metrics when scoring passes. Specifically: + * + * 1. getStaticPositionQuality + * - exclude near_friendly_goal_quality + * - include in_enemy_defense_area_quality, on_field_quality + * 2. ratePassNotTooClose -> exclude completely + * 3. ratePassFriendlyCapability -> use completely + * 4. ratePassForwardQuality -> use completely + * 5. ratePassEnemyRisk + * - exclude enemy_receiver_proximity_risk + * - include intercept_risk, but risk -> score translation is different + * 6. ratePassShootScore -> check if there's shot on goal the same + */ +class PassFeatureCollector +{ + public: + PassFeatureCollector(); + + /** + * Gets the features and score for the given pass within the given world state + * And sends out a proto with the feature info + * + * @param pass the pass to get features from + * @param world the current world state + * @param passing_config the static config for passes, to compare against for scoring + */ + void logPassFeatures(const Pass& pass, const World& world, + const TbotsProto::PassingConfig& passing_config); + + private: + /** + * Gets the score for the given pass in the given world state + * Compares against the passing config for scoring + * @param pass the pass to score + * @param world the current world state + * @param passing_config the config containing values to compare against + * @return a numerical score to quantify if the pass is bad or neutral + */ + double getPassScore(const Pass& pass, const World& world, + const TbotsProto::PassingConfig passing_config); + + double getEnemyInterceptTimeDelta(const Robot& enemy_robot, const Pass& pass, + const TbotsProto::PassingConfig& passing_config); + + // Labels for bad passes + static constexpr double DEFINITELY_BAD_SCORE = -100; + static constexpr double BAD_SCORE = -3; + static constexpr double SLIGHTLY_BAD_SCORE = -1; + static constexpr double VERY_SLIGHTLY_BAD_SCORE = -0.1; + static constexpr double NEUTRAL_SCORE = 0; + + // Translating Sigmoid scores in [0, 1] into heuristics + static constexpr double BAD_SIGMOID_SCORE = 0.25; + static constexpr double NEUTRAL_SIGMOID_SCORE = 0.5; + static constexpr double GOOD_SIGMOID_SCORE = 0.75; + + // If the enemy can get in the ball's path before the ball within n seconds, it's + // risky + static constexpr double RISKY_INTERCEPT_DELTA = 0.75; +}; diff --git a/src/software/ai/passing/pass_generator.cpp b/src/software/ai/passing/pass_generator.cpp index ab9618d9f4..32e8149723 100644 --- a/src/software/ai/passing/pass_generator.cpp +++ b/src/software/ai/passing/pass_generator.cpp @@ -8,7 +8,10 @@ PassGenerator::PassGenerator(const TbotsProto::PassingConfig& passing_config) : optimizer_(optimizer_param_weights), random_num_gen_(RNG_SEED), - passing_config_(passing_config) + passing_config_(passing_config), + num_passes_since_sample_(0), + num_picks_since_random_(0), + sample_pass_features_(false) { } @@ -136,6 +139,10 @@ PassWithRating PassGenerator::optimizeReceivingPositions( }; PassWithRating best_pass{Pass(Point(), Point(), 1.0), -1.0}; + + // store all considered passes for later + std::vector considered_passes; + for (const auto& [robot_id, receiving_positions] : receiving_positions_map) { PassWithRating best_pass_for_robot{Pass(Point(), Point(), 1.0), -1.0}; @@ -152,9 +159,22 @@ PassWithRating PassGenerator::optimizeReceivingPositions( passing_config_); double score = ratePass(world, optimized_pass, passing_config_); + if (sample_pass_features_) + { + if (num_passes_since_sample_ == 0) + { + pass_feature_collector_.logPassFeatures(optimized_pass, world, + passing_config_); + } + + num_passes_since_sample_ = + (num_passes_since_sample_ + 1) % PASS_SAMPLING_FREQUENCY; + } + if (score > best_pass_for_robot.rating) { best_pass_for_robot = PassWithRating{optimized_pass, score}; + considered_passes.push_back(best_pass_for_robot); } } @@ -176,5 +196,22 @@ PassWithRating PassGenerator::optimizeReceivingPositions( } } + // for sampling, we'll occasionally consider a random pass instead of the best one + // so we can get more varied data + if (sample_pass_features_) + { + if (num_picks_since_random_ == 0) + { + std::uniform_int_distribution distribution( + 0, considered_passes.size() - 1); + std::size_t random_index = distribution(random_num_gen_); + + best_pass = considered_passes[random_index]; + } + + num_picks_since_random_ = + (num_picks_since_random_ + 1) % BEST_PASS_OVERRIDE_FREQUENCY; + } + return best_pass; } diff --git a/src/software/ai/passing/pass_generator.h b/src/software/ai/passing/pass_generator.h index 6e1c6de902..b046dafd47 100644 --- a/src/software/ai/passing/pass_generator.h +++ b/src/software/ai/passing/pass_generator.h @@ -2,6 +2,7 @@ #include +#include "pass_feature_collector.h" #include "proto/parameters.pb.h" #include "software/ai/passing/cost_function.h" #include "software/ai/passing/pass_with_rating.h" @@ -80,4 +81,10 @@ class PassGenerator // Passing configuration TbotsProto::PassingConfig passing_config_; + + // Pass Feature Collector + PassFeatureCollector pass_feature_collector_; + unsigned int num_passes_since_sample_; + unsigned int num_picks_since_random_; + bool sample_pass_features_; }; diff --git a/src/software/embedded/ansible/requirements_lock.txt b/src/software/embedded/ansible/requirements_lock.txt index 77dfeb01c6..a8fd6ad385 100644 --- a/src/software/embedded/ansible/requirements_lock.txt +++ b/src/software/embedded/ansible/requirements_lock.txt @@ -8,9 +8,9 @@ ansible==11.9.0 \ --hash=sha256:528ca5a408f11cf1fea00daea7570e68d40e167be38b90c119a7cb45729e4921 \ --hash=sha256:79b087ef38105b93e0e092e7013a0f840e154a6a8ce9b5fddd1b47593adc542a # via -r software/embedded/ansible/requirements.in -ansible-core==2.18.9 \ - --hash=sha256:25206e1aac3bd30d95649a5ccf0d3646461d02b4dc265b5959e33b7ccd6f23f8 \ - --hash=sha256:a5f4a02aad5843e990ff7be1b92dd658a8b230de713ea643920e683ebf980da1 +ansible-core==2.18.15 \ + --hash=sha256:080b8b19bd23503503357f32eff7fd1beafe208cc0f8ae1bab291b1b121cf20f \ + --hash=sha256:577effbe1dba09cecc2b03ca767f693b3f16a773d5c3cbdb8348a633b41f3e72 # via ansible cffi==2.0.0 \ --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ @@ -98,61 +98,56 @@ cffi==2.0.0 \ --hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \ --hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf # via cryptography -cryptography==46.0.1 \ - --hash=sha256:0a17377fa52563d730248ba1f68185461fff36e8bc75d8787a7dd2e20a802b7a \ - --hash=sha256:0ca4be2af48c24df689a150d9cd37404f689e2968e247b6b8ff09bff5bcd786f \ - --hash=sha256:0d1922d9280e08cde90b518a10cd66831f632960a8d08cb3418922d83fce6f12 \ - --hash=sha256:0dfb7c88d4462a0cfdd0d87a3c245a7bc3feb59de101f6ff88194f740f72eda6 \ - --hash=sha256:0ff483716be32690c14636e54a1f6e2e1b7bf8e22ca50b989f88fa1b2d287080 \ - --hash=sha256:13e67c4d3fb8b6bc4ef778a7ccdd8df4cd15b4bcc18f4239c8440891a11245cc \ - --hash=sha256:15b5fd9358803b0d1cc42505a18d8bca81dabb35b5cfbfea1505092e13a9d96d \ - --hash=sha256:1cd6d50c1a8b79af1a6f703709d8973845f677c8e97b1268f5ff323d38ce8475 \ - --hash=sha256:2dd339ba3345b908fa3141ddba4025568fa6fd398eabce3ef72a29ac2d73ad75 \ - --hash=sha256:341fb7a26bc9d6093c1b124b9f13acc283d2d51da440b98b55ab3f79f2522ead \ - --hash=sha256:34f04b7311174469ab3ac2647469743720f8b6c8b046f238e5cb27905695eb2a \ - --hash=sha256:41c281a74df173876da1dc9a9b6953d387f06e3d3ed9284e3baae3ab3f40883a \ - --hash=sha256:449ef2b321bec7d97ef2c944173275ebdab78f3abdd005400cc409e27cd159ab \ - --hash=sha256:45f790934ac1018adeba46a0f7289b2b8fe76ba774a88c7f1922213a56c98bc1 \ - --hash=sha256:48948940d0ae00483e85e9154bb42997d0b77c21e43a77b7773c8c80de532ac5 \ - --hash=sha256:4c49eda9a23019e11d32a0eb51a27b3e7ddedde91e099c0ac6373e3aacc0d2ee \ - --hash=sha256:504e464944f2c003a0785b81668fe23c06f3b037e9cb9f68a7c672246319f277 \ - --hash=sha256:534b96c0831855e29fc3b069b085fd185aa5353033631a585d5cd4dd5d40d657 \ - --hash=sha256:6ef1488967e729948d424d09c94753d0167ce59afba8d0f6c07a22b629c557b2 \ - --hash=sha256:7176a5ab56fac98d706921f6416a05e5aff7df0e4b91516f450f8627cda22af3 \ - --hash=sha256:7411c910fb2a412053cf33cfad0153ee20d27e256c6c3f14d7d7d1d9fec59fd5 \ - --hash=sha256:757af4f6341ce7a1e47c326ca2a81f41d236070217e5fbbad61bbfe299d55d28 \ - --hash=sha256:7823bc7cdf0b747ecfb096d004cc41573c2f5c7e3a29861603a2871b43d3ef32 \ - --hash=sha256:7fab1187b6c6b2f11a326f33b036f7168f5b996aedd0c059f9738915e4e8f53a \ - --hash=sha256:84ef1f145de5aee82ea2447224dc23f065ff4cc5791bb3b506615957a6ba8128 \ - --hash=sha256:92e8cfe8bd7dd86eac0a677499894862cd5cc2fd74de917daa881d00871ac8e7 \ - --hash=sha256:9394c7d5a7565ac5f7d9ba38b2617448eba384d7b107b262d63890079fad77ca \ - --hash=sha256:9495d78f52c804b5ec8878b5b8c7873aa8e63db9cd9ee387ff2db3fffe4df784 \ - --hash=sha256:9873bf7c1f2a6330bdfe8621e7ce64b725784f9f0c3a6a55c3047af5849f920e \ - --hash=sha256:9babb7818fdd71394e576cf26c5452df77a355eac1a27ddfa24096665a27f8fd \ - --hash=sha256:9e8776dac9e660c22241b6587fae51a67b4b0147daa4d176b172c3ff768ad736 \ - --hash=sha256:9ed64e5083fa806709e74fc5ea067dfef9090e5b7a2320a49be3c9df3583a2d8 \ - --hash=sha256:9f2c4cc63be3ef43c0221861177cee5d14b505cd4d4599a89e2cd273c4d3542a \ - --hash=sha256:9f40642a140c0c8649987027867242b801486865277cbabc8c6059ddef16dc8b \ - --hash=sha256:af84e8e99f1a82cea149e253014ea9dc89f75b82c87bb6c7242203186f465129 \ - --hash=sha256:b9c79af2c3058430d911ff1a5b2b96bbfe8da47d5ed961639ce4681886614e70 \ - --hash=sha256:c52fded6383f7e20eaf70a60aeddd796b3677c3ad2922c801be330db62778e05 \ - --hash=sha256:cbb8e769d4cac884bb28e3ff620ef1001b75588a5c83c9c9f1fdc9afbe7f29b0 \ - --hash=sha256:d84c40bdb8674c29fa192373498b6cb1e84f882889d21a471b45d1f868d8d44b \ - --hash=sha256:db5597a4c7353b2e5fb05a8e6cb74b56a4658a2b7bf3cb6b1821ae7e7fd6eaa0 \ - --hash=sha256:e22801b61613ebdebf7deb18b507919e107547a1d39a3b57f5f855032dd7cfb8 \ - --hash=sha256:e34da95e29daf8a71cb2841fd55df0511539a6cdf33e6f77c1e95e44006b9b46 \ - --hash=sha256:e46710a240a41d594953012213ea8ca398cd2448fbc5d0f1be8160b5511104a0 \ - --hash=sha256:e94eb5fa32a8a9f9bf991f424f002913e3dd7c699ef552db9b14ba6a76a6313b \ - --hash=sha256:ec13b7105117dbc9afd023300fb9954d72ca855c274fe563e72428ece10191c0 \ - --hash=sha256:ed570874e88f213437f5cf758f9ef26cbfc3f336d889b1e592ee11283bb8d1c7 \ - --hash=sha256:ed957044e368ed295257ae3d212b95456bd9756df490e1ac4538857f67531fcc \ - --hash=sha256:ef648d2c690703501714588b2ba640facd50fd16548133b11b2859e8655a69da \ - --hash=sha256:efc9e51c3e595267ff84adf56e9b357db89ab2279d7e375ffcaf8f678606f3d9 \ - --hash=sha256:f736ab8036796f5a119ff8211deda416f8c15ce03776db704a7a4e17381cb2ef \ - --hash=sha256:f7a24ea78de345cfa7f6a8d3bde8b242c7fac27f2bd78fa23474ca38dfaeeab9 \ - --hash=sha256:f7de12fa0eee6234de9a9ce0ffcfa6ce97361db7a50b09b65c63ac58e5f22fc7 \ - --hash=sha256:f9b55038b5c6c47559aa33626d8ecd092f354e23de3c6975e4bb205df128a2a0 \ - --hash=sha256:fd4b5e2ee4e60425711ec65c33add4e7a626adef79d66f62ba0acfd493af282d +cryptography==46.0.6 \ + --hash=sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70 \ + --hash=sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d \ + --hash=sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a \ + --hash=sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0 \ + --hash=sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97 \ + --hash=sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30 \ + --hash=sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759 \ + --hash=sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c \ + --hash=sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead \ + --hash=sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275 \ + --hash=sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58 \ + --hash=sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f \ + --hash=sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361 \ + --hash=sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507 \ + --hash=sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa \ + --hash=sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b \ + --hash=sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b \ + --hash=sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8 \ + --hash=sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8 \ + --hash=sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72 \ + --hash=sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175 \ + --hash=sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e \ + --hash=sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124 \ + --hash=sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a \ + --hash=sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c \ + --hash=sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f \ + --hash=sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d \ + --hash=sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4 \ + --hash=sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c \ + --hash=sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290 \ + --hash=sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca \ + --hash=sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d \ + --hash=sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a \ + --hash=sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed \ + --hash=sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a \ + --hash=sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb \ + --hash=sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8 \ + --hash=sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707 \ + --hash=sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410 \ + --hash=sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736 \ + --hash=sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2 \ + --hash=sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4 \ + --hash=sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013 \ + --hash=sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19 \ + --hash=sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b \ + --hash=sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738 \ + --hash=sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463 \ + --hash=sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77 \ + --hash=sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4 # via ansible-core jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ @@ -249,13 +244,13 @@ markupsafe==3.0.3 \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via jinja2 -packaging==25.0 \ - --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ - --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f +packaging==26.0 \ + --hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \ + --hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 # via ansible-core -pycparser==2.23 \ - --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \ - --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi pyyaml==6.0.3 \ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ @@ -269,12 +264,14 @@ pyyaml==6.0.3 \ --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ @@ -283,10 +280,12 @@ pyyaml==6.0.3 \ --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ @@ -303,6 +302,7 @@ pyyaml==6.0.3 \ --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ @@ -314,6 +314,7 @@ pyyaml==6.0.3 \ --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ @@ -321,6 +322,7 @@ pyyaml==6.0.3 \ --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 diff --git a/src/software/thunderscope/binary_context_managers/full_system.py b/src/software/thunderscope/binary_context_managers/full_system.py index 25f687756b..8af4b90454 100644 --- a/src/software/thunderscope/binary_context_managers/full_system.py +++ b/src/software/thunderscope/binary_context_managers/full_system.py @@ -224,6 +224,7 @@ def setup_proto_unix_io(self, proto_unix_io: ProtoUnixIO) -> None: ObstacleList, DebugShapes, BallPlacementVisualization, + PassFeatures, ]: proto_unix_io.attach_unix_receiver( runtime_dir=self.full_system_runtime_dir, diff --git a/src/software/thunderscope/thunderscope_main.py b/src/software/thunderscope/thunderscope_main.py index 3bd6c9d68a..ce4263c25b 100644 --- a/src/software/thunderscope/thunderscope_main.py +++ b/src/software/thunderscope/thunderscope_main.py @@ -287,6 +287,7 @@ {"proto_class": World}, {"proto_class": PlayInfo}, {"proto_class": BallPlacementVisualization}, + {"proto_class": PassFeatures}, ]: proto_unix_io.attach_unix_receiver( runtime_dir, from_log_visualize=True, **arg