Thunderbots Project
Loading...
Searching...
No Matches
receiver_position_generator.hpp
1#pragma once
2
3#include <random>
4
5#include "proto/message_translation/tbots_protobuf.h"
6#include "proto/parameters.pb.h"
7#include "software/ai/passing/cost_function.h"
8#include "software/ai/passing/field_pitch_division.h"
9#include "software/ai/passing/pass.h"
10#include "software/ai/passing/pass_with_rating.h"
11#include "software/logger/logger.h"
12#include "software/world/world.h"
13
18template <class ZoneEnum>
20{
21 static_assert(std::is_enum<ZoneEnum>::value,
22 "PassGenerator: ZoneEnum must be a zone id enum");
23
24 public:
33 std::shared_ptr<const FieldPitchDivision<ZoneEnum>> pitch_division,
34 TbotsProto::PassingConfig passing_config);
35
51 std::vector<Point> getBestReceivingPositions(
52 const World &world, unsigned int num_positions,
53 const std::vector<Point> &existing_receiver_positions = {},
54 const std::optional<Point> &pass_origin_override = std::nullopt);
55
56
57 private:
69 void updateBestReceiverPositions(
70 std::map<ZoneEnum, PassWithRating> &best_receiving_positions, const World &world,
71 const Point &pass_origin, const std::vector<ZoneEnum> &zones_to_sample,
72 unsigned int num_samples_per_zone);
73
86 std::vector<ZoneEnum> getTopZones(
87 const std::map<ZoneEnum, PassWithRating> &best_receiving_positions,
88 unsigned int num_positions, const Point &pass_origin,
89 const std::vector<Point> &existing_receiver_positions);
90
96 void visualizeBestReceivingPositionsAndZones(
97 const std::map<ZoneEnum, PassWithRating> &best_receiving_positions,
98 const std::vector<ZoneEnum> &top_zones);
99
100 // Pitch division
101 std::shared_ptr<const FieldPitchDivision<ZoneEnum>> pitch_division_;
102
103 // The best receiving position for each zone from the previous iteration
104 std::map<ZoneEnum, Point> prev_best_receiving_positions;
105
106 // Passing configuration
107 TbotsProto::PassingConfig passing_config_;
108
109 // A vector of shapes that will be visualized
110 std::vector<TbotsProto::DebugShapes::DebugShape> debug_shapes;
111
112 // A random number generator for use across the class
113 std::mt19937 random_num_gen_;
114
115 // The random seed to initialize the random number generator
116 static constexpr int RNG_SEED = 1010;
117};
118
119template <class ZoneEnum>
121 std::shared_ptr<const FieldPitchDivision<ZoneEnum>> pitch_division,
122 TbotsProto::PassingConfig passing_config)
123 : pitch_division_(pitch_division),
124 passing_config_(passing_config),
125 random_num_gen_(RNG_SEED)
126{
127}
128
129template <class ZoneEnum>
131 const World &world, unsigned int num_positions,
132 const std::vector<Point> &existing_receiver_positions,
133 const std::optional<Point> &pass_origin_override)
134{
135 std::map<ZoneEnum, PassWithRating> best_receiving_positions;
136 debug_shapes.clear();
137
138 Point pass_origin = pass_origin_override.value_or(world.ball().position());
139 const auto &receiver_config = passing_config_.receiver_position_generator_config();
140
141 // Verify that the number of receiver positions requested is valid
142 if (num_positions >
143 (world.friendlyTeam().numRobots() - existing_receiver_positions.size()))
144 {
145 LOG(WARNING) << "Not enough friendly robots to assign " << num_positions
146 << " receiver positions. Assigning "
147 << world.friendlyTeam().numRobots() -
148 existing_receiver_positions.size()
149 << " receiver positions instead";
150 num_positions = static_cast<unsigned int>(world.friendlyTeam().numRobots() -
151 existing_receiver_positions.size());
152 }
153
154 // Add the previous best sampled receiving positions with their updated rating
155 for (const auto &[zone, prev_best_receiving_position] : prev_best_receiving_positions)
156 {
157 Pass pass = Pass::fromDestReceiveSpeed(pass_origin, prev_best_receiving_position,
158 passing_config_);
159 // Increase the rating of the previous best receiving positions to
160 // discourage changing the receiver positions too much.
161 double receiver_position_rating =
162 rateReceivingPosition(world, pass, passing_config_) *
163 receiver_config.previous_best_receiver_position_score_multiplier();
164 best_receiving_positions.insert_or_assign(
165 zone, PassWithRating{pass, receiver_position_rating});
166 }
167
168 // Begin by sampling a few passes per zone to get an initial estimate of the best
169 // receiving zones
170 updateBestReceiverPositions(best_receiving_positions, world, pass_origin,
171 pitch_division_->getAllZoneIds(),
172 receiver_config.num_initial_samples_per_zone());
173
174 // Get the top zones based on the initial sampling
175 std::vector<ZoneEnum> top_zones =
176 getTopZones(best_receiving_positions, num_positions, pass_origin,
177 existing_receiver_positions);
178
179 // Sample more passes from only the top zones and update their ranking
180 updateBestReceiverPositions(best_receiving_positions, world, pass_origin, top_zones,
181 receiver_config.num_additional_samples_per_top_zone());
182 std::sort(top_zones.begin(), top_zones.end(),
183 [&](const ZoneEnum &z1, const ZoneEnum &z2) {
184 return best_receiving_positions.find(z1)->second.rating >
185 best_receiving_positions.find(z2)->second.rating;
186 });
187
188 // Get the top best receiving positions and update the previous best
189 std::vector<Point> best_positions;
190 prev_best_receiving_positions.clear();
191 for (const auto zone : top_zones)
192 {
193 Point best_position =
194 best_receiving_positions.find(zone)->second.pass.receiverPoint();
195 best_positions.push_back(best_position);
196 prev_best_receiving_positions.insert_or_assign(zone, best_position);
197 }
198
199 // Visualize the receiving positions and zones
200 if (receiver_config.receiver_vis_config()
201 .visualize_best_receiving_positions_and_zones())
202 {
203 visualizeBestReceivingPositionsAndZones(best_receiving_positions, top_zones);
204 }
205
206 return best_positions;
207}
208
209template <class ZoneEnum>
211 const std::map<ZoneEnum, PassWithRating> &best_receiving_positions,
212 const std::vector<ZoneEnum> &top_zones)
213{
214 for (unsigned int i = 0; i < top_zones.size(); i++)
215 {
216 debug_shapes.push_back(*createDebugShape(pitch_division_->getZone(top_zones[i]),
217 std::to_string(i + 1),
218 std::to_string(i + 1)));
219
220 debug_shapes.push_back(*createDebugShape(
221 Circle(
222 best_receiving_positions.find(top_zones[i])->second.pass.receiverPoint(),
223 0.15),
224 std::to_string(i + 1) + "rpg", std::to_string(i + 1) + "rpg"));
225 }
226
227 LOG(VISUALIZE) << *createDebugShapes(debug_shapes);
228}
229
230template <class ZoneEnum>
232 std::map<ZoneEnum, PassWithRating> &best_receiving_positions, const World &world,
233 const Point &pass_origin, const std::vector<ZoneEnum> &zones_to_sample,
234 unsigned int num_samples_per_zone)
235{
236 for (const auto &zone_id : zones_to_sample)
237 {
238 auto zone = pitch_division_->getZone(zone_id);
239 std::uniform_real_distribution x_distribution(zone.xMin(), zone.xMax());
240 std::uniform_real_distribution y_distribution(zone.yMin(), zone.yMax());
241
242 PassWithRating best_pass_for_receiving{Pass(Point(0, 0), Point(0, 0), 1.0), -1.0};
243
244 // Check if we have already sampled some passes for this zone
245 const auto &best_sampled_pass_iter = best_receiving_positions.find(zone_id);
246 if (best_sampled_pass_iter != best_receiving_positions.end())
247 {
248 best_pass_for_receiving = best_sampled_pass_iter->second;
249 }
250
251 // Randomly sample receiving positions in the zone
252 for (unsigned int i = 0; i < num_samples_per_zone; ++i)
253 {
254 auto pass = Pass::fromDestReceiveSpeed(
255 pass_origin,
256 Point(x_distribution(random_num_gen_), y_distribution(random_num_gen_)),
257 passing_config_);
258 double rating = rateReceivingPosition(world, pass, passing_config_);
259
260 if (rating > best_pass_for_receiving.rating)
261 {
262 best_pass_for_receiving = PassWithRating{pass, rating};
263 }
264 }
265
266 best_receiving_positions.insert_or_assign(zone_id, best_pass_for_receiving);
267 }
268}
269
270template <class ZoneEnum>
272 const std::map<ZoneEnum, PassWithRating> &best_receiving_positions,
273 unsigned int num_positions, const Point &pass_origin,
274 const std::vector<Point> &existing_receiver_positions)
275{
276 std::vector<ZoneEnum> top_zones;
277
278 // Sort the zones based on initial ratings
279 auto all_zones = pitch_division_->getAllZoneIds();
280 std::sort(all_zones.begin(), all_zones.end(),
281 [&](const ZoneEnum &z1, const ZoneEnum &z2) {
282 return best_receiving_positions.find(z1)->second.rating >
283 best_receiving_positions.find(z2)->second.rating;
284 });
285
286 // Iterate through the zones in descending order of rating and select them as top
287 // zones if they are not too close to the previous selected zones.
288 const Angle min_angle_diff_between_receivers =
289 Angle::fromDegrees(passing_config_.receiver_position_generator_config()
290 .min_angle_between_receivers_deg());
291
292 for (unsigned int i = 0; i < all_zones.size() && top_zones.size() < num_positions;
293 i++)
294 {
295 Angle curr_pass_angle =
296 best_receiving_positions.find(all_zones[i])->second.pass.passerOrientation();
297
298 // Check that none of the previously selected top zones are close to the current
299 // candidate zone
300 bool no_prev_receivers_close =
301 std::none_of(top_zones.begin(), top_zones.end(), [&](const ZoneEnum &zone) {
302 return curr_pass_angle.minDiff(best_receiving_positions.find(zone)
303 ->second.pass.passerOrientation()) <
304 min_angle_diff_between_receivers;
305 });
306
307 // and none of the existing receiver positions are close to the current
308 // candidate zone
309 no_prev_receivers_close =
310 no_prev_receivers_close &&
311 std::none_of(
312 existing_receiver_positions.begin(), existing_receiver_positions.end(),
313 [&](const Point &existing_receiver_position) {
314 return curr_pass_angle.minDiff(
315 (existing_receiver_position - pass_origin).orientation()) <
316 min_angle_diff_between_receivers;
317 });
318
319 if (no_prev_receivers_close)
320 {
321 top_zones.push_back(all_zones[i]);
322 }
323 }
324
325 // If we did not find enough receiver positions, add the remaining top zones
326 if (top_zones.size() < num_positions)
327 {
328 LOG(WARNING)
329 << "Not enough receiver positions were found. Expected to find "
330 << num_positions << " receiver positions, but only found " << top_zones.size()
331 << ". Consider reducing 'min_angle_between_receivers_deg' in the dynamic parameters";
332 for (unsigned int i = 0; i < all_zones.size() && top_zones.size() < num_positions;
333 i++)
334 {
335 if (std::find(top_zones.begin(), top_zones.end(), all_zones[i]) ==
336 top_zones.end())
337 {
338 top_zones.push_back(all_zones[i]);
339 }
340 }
341 }
342
343 return top_zones;
344}
Definition angle.h:15
static constexpr Angle fromDegrees(double deg)
Definition angle.h:408
Point position() const
Definition ball.cpp:47
Definition circle.h:10
Definition field_pitch_division.h:13
Definition pass.h:20
static Pass fromDestReceiveSpeed(const Point &ball_position, const Point &pass_destination, double dest_speed_m_per_s, double min_pass_speed_m_per_s, double max_pass_speed_m_per_s)
Definition pass.cpp:48
Definition point.h:14
Definition receiver_position_generator.hpp:20
std::vector< Point > getBestReceivingPositions(const World &world, unsigned int num_positions, const std::vector< Point > &existing_receiver_positions={}, const std::optional< Point > &pass_origin_override=std::nullopt)
Definition receiver_position_generator.hpp:130
ReceiverPositionGenerator(std::shared_ptr< const FieldPitchDivision< ZoneEnum > > pitch_division, TbotsProto::PassingConfig passing_config)
Definition receiver_position_generator.hpp:120
size_t numRobots() const
Definition team.cpp:128
Definition world.h:23
const Ball & ball() const
Definition world.cpp:66
const Team & friendlyTeam() const
Definition world.cpp:71
Definition pass_with_rating.h:6