Carma-platform v4.11.0
CARMA Platform is built on robot operating system (ROS) and utilizes open source software (OSS) that enables Cooperative Driving Automation (CDA) features to allow Automated Driving Systems to interact and cooperate with infrastructure and other vehicles through communication.
yield_plugin.cpp
Go to the documentation of this file.
1/*
2 * Copyright (C) 2022-2026 LEIDOS.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17#include <rclcpp/rclcpp.hpp>
18#include <string>
19#include <algorithm>
20#include <memory>
21#include <limits>
22#include <boost/uuid/uuid_generators.hpp>
23#include <boost/uuid/uuid_io.hpp>
24#include <lanelet2_core/geometry/Point.h>
25#include <trajectory_utils/trajectory_utils.hpp>
26#include <trajectory_utils/conversions/conversions.hpp>
27#include <sstream>
28#include <carma_ros2_utils/carma_lifecycle_node.hpp>
29#include <Eigen/Core>
30#include <Eigen/Geometry>
31#include <Eigen/LU>
32#include <Eigen/SVD>
34#include <carma_v2x_msgs/msg/location_ecef.hpp>
35#include <carma_v2x_msgs/msg/trajectory.hpp>
36#include <carma_v2x_msgs/msg/plan_type.hpp>
38#include <future>
39#include <thread>
40#include <unordered_set>
43#include <yield_plugin/yield_plugin_cuda.cuh>
44
45using oss = std::ostringstream;
46constexpr auto EPSILON {0.01}; //small value to compare doubles
47
48namespace yield_plugin
49{
50 YieldPlugin::YieldPlugin(std::shared_ptr<carma_ros2_utils::CarmaLifecycleNode> nh, carma_wm::WorldModelConstPtr wm, YieldPluginConfig config,
51 MobilityResponseCB mobility_response_publisher,
52 LaneChangeStatusCB lc_status_publisher)
53 : nh_(nh), wm_(wm), config_(config),mobility_response_publisher_(mobility_response_publisher), lc_status_publisher_(lc_status_publisher)
54 {
55 basic_autonomy::set_logger(nh_->get_logger().get_child("basic_autonomy"));
56 }
57
58 double get_trajectory_end_time(const carma_planning_msgs::msg::TrajectoryPlan& trajectory)
59 {
60 return rclcpp::Time(trajectory.trajectory_points.back().target_time).seconds();
61 }
62
63 double get_trajectory_start_time(const carma_planning_msgs::msg::TrajectoryPlan& trajectory)
64 {
65 return rclcpp::Time(trajectory.trajectory_points.front().target_time).seconds();
66 }
67
68 double get_trajectory_duration(const carma_planning_msgs::msg::TrajectoryPlan& trajectory)
69 {
70 return fabs(get_trajectory_end_time(trajectory) - get_trajectory_start_time(trajectory));
71 }
72
73 double get_trajectory_duration(const std::vector<carma_perception_msgs::msg::PredictedState>& trajectory)
74 {
75 return (rclcpp::Time(trajectory.back().header.stamp) - rclcpp::Time(trajectory.front().header.stamp)).seconds();
76 }
77
78 std::vector<std::pair<int, lanelet::BasicPoint2d>> YieldPlugin::detect_trajectories_intersection(std::vector<lanelet::BasicPoint2d> self_trajectory, std::vector<lanelet::BasicPoint2d> incoming_trajectory) const
79 {
80 std::vector<std::pair<int, lanelet::BasicPoint2d>> intersection_points;
81 boost::geometry::model::linestring<lanelet::BasicPoint2d> self_traj;
82 for (auto tpp:self_trajectory)
83 {
84 boost::geometry::append(self_traj, tpp);
85 }
86 // distance to consider trajectories colliding (chosen based on lane width and vehicle size)
87 for (size_t i=0; i<incoming_trajectory.size(); i++)
88 {
89 double res = boost::geometry::distance(incoming_trajectory.at(i), self_traj);
90
92 {
93 intersection_points.push_back(std::make_pair(i, incoming_trajectory.at(i)));
94 }
95 }
96 return intersection_points;
97 }
98
99 std::vector<lanelet::BasicPoint2d> YieldPlugin::convert_eceftrajectory_to_mappoints(const carma_v2x_msgs::msg::Trajectory& ecef_trajectory) const
100 {
101 carma_planning_msgs::msg::TrajectoryPlan trajectory_plan;
102 std::vector<lanelet::BasicPoint2d> map_points;
103
104 lanelet::BasicPoint2d first_point = ecef_to_map_point(ecef_trajectory.location);
105
106 map_points.push_back(first_point);
107 auto curr_point = ecef_trajectory.location;
108
109 for (size_t i = 0; i<ecef_trajectory.offsets.size(); i++)
110 {
111 lanelet::BasicPoint2d offset_point;
112 curr_point.ecef_x += ecef_trajectory.offsets.at(i).offset_x;
113 curr_point.ecef_y += ecef_trajectory.offsets.at(i).offset_y;
114 curr_point.ecef_z += ecef_trajectory.offsets.at(i).offset_z;
115
116 offset_point = ecef_to_map_point(curr_point);
117
118 map_points.push_back(offset_point);
119 }
120
121 return map_points;
122 }
123
124 lanelet::BasicPoint2d YieldPlugin::ecef_to_map_point(const carma_v2x_msgs::msg::LocationECEF& ecef_point) const
125 {
126
127 if (!map_projector_) {
128 throw std::invalid_argument("No map projector available for ecef conversion");
129 }
130
131 lanelet::BasicPoint3d map_point = map_projector_->projectECEF( { static_cast<double>(ecef_point.ecef_x)/100.0, static_cast<double>(ecef_point.ecef_y)/100.0, static_cast<double>(ecef_point.ecef_z)/100.0 } , 1);
132
133 return lanelet::traits::to2D(map_point);
134 }
135
136
137
138 carma_v2x_msgs::msg::MobilityResponse YieldPlugin::compose_mobility_response(const std::string& resp_recipient_id, const std::string& req_plan_id, bool response) const
139 {
140 carma_v2x_msgs::msg::MobilityResponse out_mobility_response;
141 out_mobility_response.m_header.sender_id = config_.vehicle_id;
142 out_mobility_response.m_header.recipient_id = resp_recipient_id;
143 out_mobility_response.m_header.sender_bsm_id = host_bsm_id_;
144 out_mobility_response.m_header.plan_id = req_plan_id;
145 out_mobility_response.m_header.timestamp = nh_->now().seconds()*1000;
146
147
149 {
150 out_mobility_response.is_accepted = true;
151 }
152 else out_mobility_response.is_accepted = false;
153
154 return out_mobility_response;
155 }
156
157
158 void YieldPlugin::mobilityrequest_cb(const carma_v2x_msgs::msg::MobilityRequest::UniquePtr msg)
159 {
160 carma_v2x_msgs::msg::MobilityRequest incoming_request = *msg;
161 carma_planning_msgs::msg::LaneChangeStatus lc_status_msg;
162 if (incoming_request.strategy == "carma/cooperative-lane-change")
163 {
164 if (!map_projector_) {
165 RCLCPP_ERROR(nh_->get_logger(),"Cannot process mobility request as map projection is not yet set!");
166 return;
167 }
168 if (incoming_request.plan_type.type == carma_v2x_msgs::msg::PlanType::CHANGE_LANE_LEFT || incoming_request.plan_type.type == carma_v2x_msgs::msg::PlanType::CHANGE_LANE_RIGHT)
169 {
170 RCLCPP_DEBUG(nh_->get_logger(),"Cooperative Lane Change Request Received");
171 lc_status_msg.status = carma_planning_msgs::msg::LaneChangeStatus::REQUEST_RECEIVED;
172 lc_status_msg.description = "Received lane merge request";
173
174 if (incoming_request.m_header.recipient_id == config_.vehicle_id)
175 {
176 RCLCPP_DEBUG(nh_->get_logger(),"CLC Request correctly received");
177 }
178
179 // extract mobility header
180 std::string req_sender_id = incoming_request.m_header.sender_id;
181 std::string req_plan_id = incoming_request.m_header.plan_id;
182 // extract mobility request
183 carma_v2x_msgs::msg::LocationECEF ecef_location = incoming_request.location;
184 carma_v2x_msgs::msg::Trajectory incoming_trajectory = incoming_request.trajectory;
185 std::string req_strategy_params = incoming_request.strategy_params;
186 clc_urgency_ = incoming_request.urgency;
187 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"received urgency: " << clc_urgency_);
188
189 // Parse strategy parameters
190 using boost::property_tree::ptree;
191 ptree pt;
192 std::istringstream strstream(req_strategy_params);
193 boost::property_tree::json_parser::read_json(strstream, pt);
194 int req_traj_speed_full = pt.get<int>("s");
195 int req_traj_fractional = pt.get<int>("f");
196 int start_lanelet_id = pt.get<int>("sl");
197 int end_lanelet_id = pt.get<int>("el");
198 double req_traj_speed = static_cast<double>(req_traj_speed_full) + static_cast<double>(req_traj_fractional)/10.0;
199 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"req_traj_speed" << req_traj_speed);
200 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"start_lanelet_id" << start_lanelet_id);
201 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"end_lanelet_id" << end_lanelet_id);
202
203 std::vector<lanelet::BasicPoint2d> req_traj_plan = {};
204
205 req_traj_plan = convert_eceftrajectory_to_mappoints(incoming_trajectory);
206
207 double req_expiration_sec = static_cast<double>(incoming_request.expiration);
208 double current_time_sec = nh_->now().seconds();
209
210 bool response_to_clc_req = false;
211 // ensure there is enough time for the yield
212 double req_plan_time = req_expiration_sec - current_time_sec;
213 double req_timestamp = static_cast<double>(incoming_request.m_header.timestamp) / 1000.0 - current_time_sec;
214 set_incoming_request_info(req_traj_plan, req_traj_speed, req_plan_time, req_timestamp);
215
216
217 if (req_expiration_sec - current_time_sec >= config_.min_obj_avoidance_plan_time_in_s && cooperative_request_acceptable_)
218 {
220 lc_status_msg.status = carma_planning_msgs::msg::LaneChangeStatus::REQUEST_ACCEPTED;
221 lc_status_msg.description = "Accepted lane merge request";
222 response_to_clc_req = true;
223 RCLCPP_DEBUG(nh_->get_logger(),"CLC accepted");
224 }
225 else
226 {
227 lc_status_msg.status = carma_planning_msgs::msg::LaneChangeStatus::REQUEST_REJECTED;
228 lc_status_msg.description = "Rejected lane merge request";
229 response_to_clc_req = false;
230 RCLCPP_DEBUG(nh_->get_logger(),"CLC rejected");
231 }
232 carma_v2x_msgs::msg::MobilityResponse outgoing_response = compose_mobility_response(req_sender_id, req_plan_id, response_to_clc_req);
233 mobility_response_publisher_(outgoing_response);
234 lc_status_msg.status = carma_planning_msgs::msg::LaneChangeStatus::RESPONSE_SENT;
235 RCLCPP_DEBUG(nh_->get_logger(),"response sent");
236 }
237 }
238 lc_status_publisher_(lc_status_msg);
239
240 }
241
242 void YieldPlugin::set_incoming_request_info(std::vector <lanelet::BasicPoint2d> req_trajectory, double req_speed, double req_planning_time, double req_timestamp)
243 {
244 req_trajectory_points_ = req_trajectory;
245 req_target_speed_ = req_speed;
246 req_target_plan_time_ = req_planning_time;
247 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"req_target_plan_time_" << req_target_plan_time_);
248 req_timestamp_ = req_timestamp;
249 }
250
251 void YieldPlugin::bsm_cb(const carma_v2x_msgs::msg::BSM::UniquePtr msg)
252 {
253 carma_v2x_msgs::msg::BSMCoreData bsm_core_ = msg->core_data;
254 host_bsm_id_ = bsmIDtoString(bsm_core_);
255 }
256
258 carma_planning_msgs::srv::PlanTrajectory::Request::SharedPtr req,
259 carma_planning_msgs::srv::PlanTrajectory::Response::SharedPtr resp)
260{
261 RCLCPP_DEBUG(nh_->get_logger(),"Yield_plugin was called!");
262 if (req->initial_trajectory_plan.trajectory_points.size() < 2){
263 throw std::invalid_argument("Empty Trajectory received by Yield");
264 }
265 rclcpp::Clock system_clock(RCL_SYSTEM_TIME);
266 rclcpp::Time start_time = system_clock.now(); // Start timing the execution time for planning so it can be logged
267
268 carma_planning_msgs::msg::TrajectoryPlan original_trajectory = req->initial_trajectory_plan;
269 carma_planning_msgs::msg::TrajectoryPlan yield_trajectory;
270
271 // if ego is not stopped and we committed to stopping, use the last committed trajectory
272 if (req->vehicle_state.longitudinal_vel > EPSILON &&
274 {
275 RCLCPP_DEBUG(nh_->get_logger(), "Using last committed trajectory to stopping");
276 lanelet::BasicPoint2d veh_pos(req->vehicle_state.x_pos_global,
277 req->vehicle_state.y_pos_global);
278 auto updated_trajectory = last_traj_plan_committed_to_stopping_.value();
279
280 // Find closest point in last trajectory to current vehicle position
281 size_t idx_to_start_new_traj =
283 updated_trajectory.trajectory_points,
284 veh_pos);
285
286 // Update last trajectory to start from closest point (remove passed points)
287 if (!updated_trajectory.trajectory_points.empty()) {
288 updated_trajectory.trajectory_points =
289 std::vector<carma_planning_msgs::msg::TrajectoryPlanPoint>
290 (updated_trajectory.trajectory_points.begin() + idx_to_start_new_traj,
291 updated_trajectory.trajectory_points.end());
292 }
293 last_traj_plan_committed_to_stopping_ = updated_trajectory;
294 resp->trajectory_plan = last_traj_plan_committed_to_stopping_.value();
295
296 rclcpp::Time end_time = system_clock.now(); // Planning complete
297
298 auto duration = end_time - start_time;
299 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),
300 "ExecutionTime Yield: " << std::to_string(duration.seconds()));
301 return;
302 }
303
304 // Otherwise, we are planning a new trajectory by checking collision
305 try
306 {
307 // NOTE: Wrapping entire plan_trajectory logic with try catch because there is intermittent
308 // open issue of which cause is uncertain:
309 // https://github.com/usdot-fhwa-stol/carma-platform/issues/2501
310
311 double initial_velocity = req->vehicle_state.longitudinal_vel;
312 // If vehicle_state is stopped, non-zero velocity from the trajectory
313 // should be used. Otherwise, vehicle will not move.
314 if (initial_velocity < EPSILON)
315 {
316 initial_velocity = original_trajectory.initial_longitudinal_velocity;
317 // Record the time when vehicle was stopped first due to collision avoidance
320 {
322 RCLCPP_DEBUG(nh_->get_logger(), "First time stopped to prevent collision: %f",
324 }
325 }
326
327 // seperating cooperative yield with regular object detection for better performance.
329 {
330 RCLCPP_DEBUG(nh_->get_logger(),"Only consider high urgency clc");
332 {
333 RCLCPP_DEBUG(nh_->get_logger(),"Yield for CLC. We haven't received an updated negotiation this timestep");
334 yield_trajectory = update_traj_for_cooperative_behavior(original_trajectory, initial_velocity);
336 }
337 else
338 {
339 RCLCPP_DEBUG(nh_->get_logger(),"unreliable CLC communication, switching to object avoidance");
340 yield_trajectory = update_traj_for_object(original_trajectory, external_objects_, initial_velocity); // Compute the trajectory
341 }
342 }
343 else
344 {
345 RCLCPP_DEBUG(nh_->get_logger(),"Yield for object avoidance");
346 auto _t0_upd = std::chrono::steady_clock::now();
347 yield_trajectory = update_traj_for_object(original_trajectory, external_objects_, initial_velocity); // Compute the trajectory
348 const double _upd_ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - _t0_upd).count();
349 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),
350 "[timing] update_traj_for_object: " << _upd_ms << " ms");
351 }
352
353
354 // return original trajectory if no difference in trajectory points a.k.a no collision
355 if (fabs(get_trajectory_end_time(original_trajectory) - get_trajectory_end_time(yield_trajectory)) < EPSILON)
356 {
357
358 resp->trajectory_plan = original_trajectory;
359 // Reset the collision prevention variables
362
363 }
364 else
365 {
366 yield_trajectory.header.frame_id = "map";
367 yield_trajectory.header.stamp = nh_->now();
368 yield_trajectory.trajectory_id = original_trajectory.trajectory_id;
369 resp->trajectory_plan = yield_trajectory;
370 }
371 }
372 catch(const std::runtime_error& e) {
373 RCLCPP_WARN_STREAM(nh_->get_logger(), "Yield Plugin failed to plan trajectory due to known negative time issue: " << e.what());
374 RCLCPP_WARN_STREAM(nh_->get_logger(), "Returning the original trajectory, and retrying at the next call.");
375 resp->trajectory_plan = original_trajectory;
376 }
377
378 rclcpp::Time end_time = system_clock.now(); // Planning complete
379
380 auto duration = end_time - start_time;
381 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),
382 "ExecutionTime Yield: " << std::to_string(duration.seconds()));
383 }
384
385 carma_planning_msgs::msg::TrajectoryPlan YieldPlugin::update_traj_for_cooperative_behavior(const carma_planning_msgs::msg::TrajectoryPlan& original_tp, double current_speed)
386 {
387 carma_planning_msgs::msg::TrajectoryPlan cooperative_trajectory;
388
389 double initial_pos = 0;
390 double goal_pos;
391 double initial_velocity = current_speed;
392 double goal_velocity = req_target_speed_;
393 double planning_time = req_target_plan_time_;
394
395 std::vector<lanelet::BasicPoint2d> host_traj_points = {};
396 for (size_t i=0; i<original_tp.trajectory_points.size(); i++)
397 {
398 lanelet::BasicPoint2d traj_point;
399 traj_point.x() = original_tp.trajectory_points.at(i).x;
400 traj_point.y() = original_tp.trajectory_points.at(i).y;
401 host_traj_points.push_back(traj_point);
402 }
403
404 std::vector<std::pair<int, lanelet::BasicPoint2d>> intersection_points = detect_trajectories_intersection(host_traj_points, req_trajectory_points_);
405 if (!intersection_points.empty())
406 {
407 lanelet::BasicPoint2d intersection_point = intersection_points[0].second;
408 double dx = original_tp.trajectory_points[0].x - intersection_point.x();
409 double dy = original_tp.trajectory_points[0].y - intersection_point.y();
410 // check if a digital_gap is available
411 double digital_gap = check_traj_for_digital_min_gap(original_tp);
412 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"digital_gap: " << digital_gap);
413 goal_pos = sqrt(dx*dx + dy*dy) - std::max(config_.minimum_safety_gap_in_meters, digital_gap);
414 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"Goal position (goal_pos): " << goal_pos);
415 double collision_time = req_timestamp_ + (intersection_points[0].first * ecef_traj_timestep_) - config_.safety_collision_time_gap_in_s;
416 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"req time stamp: " << req_timestamp_);
417 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"Collision time: " << collision_time);
418 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"intersection num: " << intersection_points[0].first);
419 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"Planning time: " << planning_time);
420 // calculate distance traveled from beginning of trajectory to collision point
421 double dx2 = intersection_point.x() - req_trajectory_points_[0].x();
422 double dy2 = intersection_point.y() - req_trajectory_points_[0].y();
423 // calculate incoming trajectory speed from time and distance between trajectory points
424 double incoming_trajectory_speed = sqrt(dx2*dx2 + dy2*dy2)/(intersection_points[0].first * ecef_traj_timestep_);
425 // calculate goal velocity from request trajectory
426 goal_velocity = std::min(goal_velocity, incoming_trajectory_speed);
427 double min_time = (initial_velocity - goal_velocity)/config_.yield_max_deceleration_in_ms2;
428
429 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"goal_velocity: " << goal_velocity);
430 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"incoming_trajectory_speed: " << incoming_trajectory_speed);
431
432 if (planning_time > min_time)
433 {
435 double original_max_speed = max_trajectory_speed(original_tp.trajectory_points, get_trajectory_end_time(original_tp));
436 cooperative_trajectory = generate_JMT_trajectory(original_tp, initial_pos, goal_pos, initial_velocity, goal_velocity, planning_time, original_max_speed);
437 }
438 else
439 {
441 RCLCPP_DEBUG(nh_->get_logger(),"The incoming requested trajectory is rejected, due to insufficient gap");
442 cooperative_trajectory = original_tp;
443 }
444
445 }
446 else
447 {
449 RCLCPP_DEBUG(nh_->get_logger(),"The incoming requested trajectory does not overlap with host vehicle's trajectory");
450 cooperative_trajectory = original_tp;
451 }
452
453 return cooperative_trajectory;
454 }
455
456 double get_smallest_time_step_of_traj(const carma_planning_msgs::msg::TrajectoryPlan& original_tp)
457 {
458 double smallest_time_step = std::numeric_limits<double>::infinity();
459 for (size_t i = 0; i < original_tp.trajectory_points.size() - 1; i ++)
460 {
461 smallest_time_step = std::min(smallest_time_step,
462 (rclcpp::Time(original_tp.trajectory_points.at(i + 1).target_time)
463 - rclcpp::Time(original_tp.trajectory_points.at(i).target_time)).seconds());
464 }
465 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),"smallest_time_step: " << smallest_time_step);
466
467 return smallest_time_step;
468 }
469
470 carma_planning_msgs::msg::TrajectoryPlan YieldPlugin::generate_JMT_trajectory(const carma_planning_msgs::msg::TrajectoryPlan& original_tp, double initial_pos, double goal_pos,
471 double initial_velocity, double goal_velocity, double planning_time, double original_max_speed)
472 {
473 carma_planning_msgs::msg::TrajectoryPlan jmt_trajectory;
474 std::vector<carma_planning_msgs::msg::TrajectoryPlanPoint> jmt_trajectory_points;
475 jmt_trajectory_points.push_back(original_tp.trajectory_points[0]);
476
477 std::vector<double> original_traj_relative_downtracks = get_relative_downtracks(original_tp);
478 std::vector<double> calculated_speeds = {};
479 std::vector<double> new_relative_downtracks = {};
480 new_relative_downtracks.push_back(0.0);
481 calculated_speeds.push_back(initial_velocity);
482 double new_traj_accumulated_downtrack = 0.0;
483 double original_traj_accumulated_downtrack = original_traj_relative_downtracks.at(1);
484
485 // Up until goal_pos (which also can be until end of the entire original trajectory), generate new speeds at
486 // or near original trajectory points by generating them at a fixed time interval using the JMT polynomial equation
487 const double initial_time = 0;
488 double initial_accel = 0;
490 {
491 initial_accel = (initial_velocity - last_speed_.value()) /
492 (nh_->now() - last_speed_time_.value()).seconds();
493
494 if (!std::isnormal(initial_accel))
495 {
496 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),"Detecting nan initial_accel set to 0");
497 initial_accel = 0.0;
498 }
499
500 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),"Detecting initial_accel: " << initial_accel
501 << ", initial_velocity:" << initial_velocity
502 << ", last_speed_: " << last_speed_.value()
503 << ", nh_->now(): " << nh_->now().seconds()
504 << ", last_speed_time_.get(): " << last_speed_time_.value().seconds());
505 }
506
507 const double goal_accel = 0;
508 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),"Following parameters used for JMT: "
509 "\ninitial_pos: " << initial_pos <<
510 "\ngoal_pos: " << goal_pos <<
511 "\ninitial_velocity: " << initial_velocity <<
512 "\ngoal_velocity: " << goal_velocity <<
513 "\ninitial_accel: " << initial_accel <<
514 "\ngoal_accel: " << goal_accel <<
515 "\nplanning_time: " << planning_time <<
516 "\noriginal_max_speed: " << original_max_speed);
517
518 // Get the polynomial solutions used to generate the trajectory
519 std::vector<double> polynomial_coefficients = quintic_coefficient_calculator::quintic_coefficient_calculator(initial_pos,
520 goal_pos,
521 initial_velocity,
522 goal_velocity,
523 initial_accel,
524 goal_accel,
525 initial_time,
526 planning_time);
527 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),"Used original_max_speed: " << original_max_speed);
528 for (size_t i = 0; i < polynomial_coefficients.size(); i++) {
529 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),"Coefficient " << i << ": " << polynomial_coefficients[i]);
530 }
531 // Cap at 0.1 s: finer steps gave no accuracy benefit but caused O(1/dt) loop blow-up
532 // when upstream planners produced sub-10 ms trajectory timesteps.
533 const double smallest_time_step = std::max(get_smallest_time_step_of_traj(original_tp), 0.1);
534 int new_traj_idx = 1;
535 int original_traj_idx = 1;
536 while (new_traj_accumulated_downtrack < goal_pos - EPSILON && original_traj_idx < original_traj_relative_downtracks.size())
537 {
538 const double target_time = new_traj_idx * smallest_time_step;
539 const double downtrack_at_target_time = polynomial_calc(polynomial_coefficients, target_time);
540 double velocity_at_target_time = polynomial_calc_d(polynomial_coefficients, target_time);
541
542 // if the speed becomes negative, the downtrack starts reversing to negative as well
543 // which will never reach the goal_pos, so break here.
544 if (velocity_at_target_time < 0.0)
545 {
546 break;
547 }
548
549 // Cannot have a negative speed or have a higher speed than that of the original trajectory
550 velocity_at_target_time = std::clamp(velocity_at_target_time, 0.0, original_max_speed);
551
552 // Pick the speed if it matches with the original downtracks
553 if (downtrack_at_target_time >= original_traj_accumulated_downtrack)
554 {
555 // velocity_at_target_time doesn't exactly correspond to original_traj_accumulated_downtrack but does for new_traj_accumulated_downtrack.
556 // however, the logic is assuming they are close enough that the speed is usable
557 calculated_speeds.push_back(velocity_at_target_time);
558 original_traj_accumulated_downtrack += original_traj_relative_downtracks.at(original_traj_idx);
559 original_traj_idx ++;
560 }
561 new_traj_accumulated_downtrack = downtrack_at_target_time;
562 new_traj_idx++;
563
564 }
565
566 // if the loop above finished prematurely due to negative speed, fill with 0.0 speeds
567 // since the speed crossed 0.0 and algorithm indicates stopping
568 std::fill_n(std::back_inserter(calculated_speeds),
569 std::size(original_traj_relative_downtracks) - std::size(calculated_speeds),
570 0.0);
571
572 // Moving average filter to smoothen the speeds
573 std::vector<double> filtered_speeds = basic_autonomy::smoothing::moving_average_filter(calculated_speeds, config_.speed_moving_average_window_size);
574 // Replace the original trajectory's associated timestamps based on the newly calculated speeds
575 double prev_speed = filtered_speeds.at(0);
576 last_speed_ = prev_speed;
577 last_speed_time_ = nh_->now();
578
579 for(size_t i = 1; i < original_tp.trajectory_points.size(); i++)
580 {
581 carma_planning_msgs::msg::TrajectoryPlanPoint jmt_tpp = original_tp.trajectory_points.at(i);
582
583 // In case only subset of original trajectory needs modification,
584 // the rest of the points should keep the last speed to cruise
585 double current_speed = goal_velocity;
586
587 if (i < filtered_speeds.size())
588 {
589 current_speed = filtered_speeds.at(i);
590 }
591
592 //Force the speed to 0 if below configured value for more control over stopping behavior
593 if (current_speed < config_.max_stop_speed_in_ms)
594 {
595 current_speed = 0;
596 }
597
598 // Derived from constant accelaration kinematic equation: (vi + vf) / 2 * dt = d_dist
599 // This also handles a case correctly when current_speed is 0, but prev_speed is not 0 yet
600 const double dt = (2 * original_traj_relative_downtracks.at(i)) / (current_speed + prev_speed);
601 jmt_tpp.target_time = rclcpp::Time(jmt_trajectory_points.back().target_time) + rclcpp::Duration::from_nanoseconds(dt*1e9);
602
603 if (prev_speed < EPSILON) // Handle a special case if prev_speed (thus current_speed too) is 0
604 {
605 // NOTE: Assigning arbitrary 100 mins dt between points where normally dt is only 1 sec to model a stopping behavior.
606 // Another way to model it is to keep the trajectory point at a same location and increment time slightly. However,
607 // if the vehicle goes past the point, it may cruise toward undesirable location (for example into the intersection).
608 // Keeping the points help the controller steer the vehicle toward direction of travel even when stopping.
609 // Only downside is the trajectory plan is huge where only 15 sec is expected, but since this is stopping case, it shouldn't matter.
610 jmt_tpp.target_time = rclcpp::Time(jmt_trajectory_points.back().target_time) + rclcpp::Duration::from_nanoseconds(6000 * 1e9);
611 }
612
613
614 jmt_trajectory_points.push_back(jmt_tpp);
615 prev_speed = current_speed;
616 }
617
618 jmt_trajectory.header = original_tp.header;
619 jmt_trajectory.trajectory_id = original_tp.trajectory_id;
620 jmt_trajectory.trajectory_points = jmt_trajectory_points;
621 jmt_trajectory.initial_longitudinal_velocity = initial_velocity;
622 return jmt_trajectory;
623 }
624
625 std::optional<GetCollisionResult> YieldPlugin::get_collision(const carma_planning_msgs::msg::TrajectoryPlan& ego_trajectory,
626 const std::vector<carma_perception_msgs::msg::PredictedState>& object_predictions, double collision_radius, double ego_max_speed)
627 {
628
629 // Iterate through each pair of consecutive points in the trajectories
630 RCLCPP_DEBUG_STREAM(nh_->get_logger(), "Starting a new collision detection, trajectory size: "
631 << ego_trajectory.trajectory_points.size() << ". prediction size: " << object_predictions.size());
632
633 // Iterate through the object to check if it's on the route
634 bool on_route = false;
635 int on_route_idx = 0;
636
637 // A flag to stop searching more than one lanelet if the object has no velocity
638 const auto object_speed{std::hypot(object_predictions.front().predicted_velocity.linear.x,
639 object_predictions.front().predicted_velocity.linear.y)};
640 bool object_has_zero_speed = object_speed < config_.obstacle_zero_speed_threshold_in_ms;
641
642 if (object_predictions.size() < 2)
643 {
644 throw std::invalid_argument("Object on ther road doesn't have enough predicted states! Please check motion_computation is correctly applying predicted states");
645 }
646 const double object_prediction_step_duration = (rclcpp::Time(object_predictions.at(1).header.stamp) - rclcpp::Time(object_predictions.front().header.stamp)).seconds();
647 const double object_prediction_total_duration = get_trajectory_duration(object_predictions);
648
649 if (object_prediction_step_duration < 0.0)
650 {
651 throw std::invalid_argument("Predicted states of the object is malformed. Detected trajectory going backwards in time!");
652 }
653
654 // In order to optimize the for loops for comparing two trajectories, following logic skips every iteration_stride-th points of the object_predictions.
655 // Since skipping number of points from the object_predictions may result in ignoring potential collisions, its value is dependent on two
656 // trajectories' speeds and intervehicle_collision_distance_in_m radius.
657 // Therefore, the derivation first calculates the max time, t, that both actors can move while still being in collision radius:
658 // sqrt( (v1 * t / 2)^2 + (v2 * t / 2)^2 ) = collision_radius. Here v1 and v2 are assumed to be perpendicular to each other and
659 // intersecting at t/2 to get max possible collision_radius. Solving for t gives following:
660 double iteration_stride_max_time_s = 2 * config_.intervehicle_collision_distance_in_m / sqrt(pow(object_speed, 2) + pow(ego_max_speed, 2));
661 int iteration_stride = std::max(1, static_cast<int>(iteration_stride_max_time_s / object_prediction_step_duration));
662
663 RCLCPP_DEBUG_STREAM(nh_->get_logger(), "Determined iteration_stride: " << iteration_stride
664 << ", with object_speed: " << object_speed
665 << ", with ego_max_speed: " << ego_max_speed
666 << ", with object_prediction_step_duration: " << object_prediction_step_duration
667 << ", iteration_stride_max_time_s: " << iteration_stride_max_time_s);
668
669 for (size_t j = 0; j < object_predictions.size(); j += iteration_stride)
670 {
671 const lanelet::BasicPoint2d point(object_predictions.at(j).predicted_position.position.x,
672 object_predictions.at(j).predicted_position.position.y);
673 for (const auto& llt : route_llt_polygons_)
674 {
675 if (boost::geometry::within(point, llt.polygon2d()))
676 {
677 on_route = true;
678 on_route_idx = j;
679 break;
680 }
681 }
682 if (on_route || object_has_zero_speed)
683 break;
684 }
685
686 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"), "[CPU] on_route=" << on_route
687 << " on_route_idx=" << on_route_idx
688 << " speed=" << object_speed
689 << " stride=" << iteration_stride);
690
691 if (!on_route)
692 {
693 RCLCPP_DEBUG(rclcpp::get_logger("yield_plugin"), "[CPU] Object not on route — skipping");
694 return std::nullopt;
695 }
696
697 double smallest_dist = std::numeric_limits<double>::infinity();
698 for (size_t i = 0; i < ego_trajectory.trajectory_points.size() - 1; ++i)
699 {
700 auto ego_seg_start = ego_trajectory.trajectory_points.at(i);
701 auto ego_seg_end = ego_trajectory.trajectory_points.at(i + 1);
702 double previous_distance = std::numeric_limits<double>::infinity();
703 for (size_t j = on_route_idx; j < object_predictions.size() - 1; j += iteration_stride)
704 {
705 auto object_seg_start = object_predictions.at(j);
706 auto object_seg_end = object_predictions.at(j + 1);
707 double ego_seg_start_time = rclcpp::Time(ego_seg_start.target_time).seconds();
708 double ego_seg_end_time = rclcpp::Time(ego_seg_end.target_time).seconds();
709 double object_seg_start_time = rclcpp::Time(object_seg_start.header.stamp).seconds();
710 double object_seg_end_time = rclcpp::Time(object_seg_end.header.stamp).seconds();
711
712 RCLCPP_DEBUG_STREAM(nh_->get_logger(), "ego_seg_start.target_time: " << std::to_string(ego_seg_start_time) << ", ego_seg_end.target_time: " << std::to_string(ego_seg_end_time));
713 RCLCPP_DEBUG_STREAM(nh_->get_logger(), "object_seg_start.target_time: " << std::to_string(object_seg_start_time) << ", object_seg_end.target_time: " << std::to_string(object_seg_end_time));
714 RCLCPP_DEBUG_STREAM(nh_->get_logger(), "ego_seg_start.x: " << ego_seg_start.x << ", ego_seg_start.y: " << ego_seg_start.y);
715 RCLCPP_DEBUG_STREAM(nh_->get_logger(), "ego_seg_end.x: " << ego_seg_end.x << ", ego_seg_end.y: " << ego_seg_end.y);
716
717 RCLCPP_DEBUG_STREAM(nh_->get_logger(), "object_seg_start.x: " << object_seg_start.predicted_position.position.x << ", object_seg_start.y: " << object_seg_start.predicted_position.position.y);
718 RCLCPP_DEBUG_STREAM(nh_->get_logger(), "object_seg_end.x: " << object_seg_end.predicted_position.position.x << ", object_seg_end.y: " << object_seg_end.predicted_position.position.y);
719
720 // Linearly interpolate positions at a common timestamp for both trajectories
721 double interp_ratio = (object_seg_start_time - ego_seg_start_time) / (ego_seg_end_time - ego_seg_start_time);
722 // if negative extrapolation, skip because car wouldn't go backwards
723 if (interp_ratio < 0)
724 {
725 RCLCPP_DEBUG_STREAM(nh_->get_logger(),
726 "Negative extrapolation, skipping this pair of points. object_seg_start_time: "
727 << std::to_string(object_seg_start_time) << ", ego_seg_start_time: "
728 << std::to_string(ego_seg_start_time));
729 continue;
730 }
731 double ego_interp_x = ego_seg_start.x + interp_ratio * (ego_seg_end.x - ego_seg_start.x);
732 double ego_interp_y = ego_seg_start.y + interp_ratio * (ego_seg_end.y - ego_seg_start.y);
733 double object_x = object_seg_start.predicted_position.position.x;
734 double object_y = object_seg_start.predicted_position.position.y;
735
736 // Calculate the distance between the two interpolated points
737 const auto distance{std::hypot(ego_interp_x - object_x, ego_interp_y - object_y)};
738
739 smallest_dist = std::min(distance, smallest_dist);
740
741 // Following "if logic" assumes the object_predictions is a simple cv model, aka, object_predictions point is a straight line over time.
742 // And current ego_trajectory point is fixed in this iteration.
743 // Then once the distance between the two start to increase over object_predictions iteration,
744 // the distance will always increase and it's unnecessary to continue the logic to find the smallest_dist
745 if (previous_distance < distance)
746 {
747 RCLCPP_DEBUG_STREAM(nh_->get_logger(), "Stopping search here because the distance between predictions started to increase");
748 break;
749 }
750 previous_distance = distance;
751
752 if (i == 0 && j == 0 && distance > config_.collision_check_radius_in_m)
753 {
754 RCLCPP_DEBUG(nh_->get_logger(), "Too far away" );
755 return std::nullopt;
756 }
757
758 if (distance > collision_radius)
759 {
760 // continue searching for collision
761 continue;
762 }
763
764 GetCollisionResult collision_result;
765 collision_result.ego_point = lanelet::BasicPoint2d(ego_interp_x, ego_interp_y);
766 collision_result.object_point = lanelet::BasicPoint2d(object_x, object_y);
767 collision_result.collision_time = rclcpp::Time(object_seg_start.header.stamp);
768 return collision_result;
769 }
770 }
771 RCLCPP_DEBUG_STREAM(
772 rclcpp::get_logger("yield_plugin"),
773 "Was not able to find collision: smallest_dist: " << smallest_dist);
774
775 // No collision detected
776 return std::nullopt;
777 }
778
779 //TODO: Revisit this logic. Further investigation required for this logic since it currently seems like it will never return true
780 bool YieldPlugin::is_object_behind_vehicle(uint32_t object_id, const rclcpp::Time& collision_time, double vehicle_downtrack, double object_downtrack)
781 {
782 const auto previous_clearance_count = consecutive_clearance_count_for_obstacles_[object_id];
783 // if the object's location is half a length of the vehicle past its rear-axle, it is considered behind
784 // half a length of the vehicle to conservatively estimate the rear axle to rear bumper length
785 if (object_downtrack < vehicle_downtrack - config_.vehicle_length / 2)
786 {
788 RCLCPP_INFO_STREAM(nh_->get_logger(), "Detected an object nearby might be behind the vehicle at timestamp: " << std::to_string(collision_time.seconds()) <<
789 ", and consecutive_clearance_count_for obstacle: " << object_id << ", is: " << consecutive_clearance_count_for_obstacles_[object_id]);
790 }
791 // confirmed false positive for a collision
793 {
794 return true;
795 }
796 // if the clearance counter didn't increase by this point, true collision was detected
797 // therefore reset the consecutive clearance counter as it is no longer consecutive
798 if (consecutive_clearance_count_for_obstacles_[object_id] == previous_clearance_count)
799 {
801 }
802
803 return false;
804 }
805
806 std::optional<rclcpp::Time> YieldPlugin::get_collision_time(const carma_planning_msgs::msg::TrajectoryPlan& original_tp,
807 const carma_perception_msgs::msg::ExternalObject& curr_obstacle, double original_tp_max_speed)
808 {
809 auto plan_start_time = get_trajectory_start_time(original_tp);
810
811 RCLCPP_DEBUG_STREAM(nh_->get_logger(), "Object's back time: " << std::to_string(rclcpp::Time(curr_obstacle.predictions.back().header.stamp).seconds())
812 << ", plan_start_time: " << std::to_string(plan_start_time));
813
814 // do not process outdated objects
815 if (rclcpp::Time(curr_obstacle.predictions.back().header.stamp).seconds() <= plan_start_time)
816 {
817 return std::nullopt;
818 }
819
820 std::vector<carma_perception_msgs::msg::PredictedState> new_list;
821 carma_perception_msgs::msg::PredictedState curr_state;
822 // artificially include current position as one of the predicted states
823 curr_state.header.stamp = curr_obstacle.header.stamp;
824 curr_state.predicted_position.position.x = curr_obstacle.pose.pose.position.x;
825 curr_state.predicted_position.position.y = curr_obstacle.pose.pose.position.y;
826 // NOTE: predicted_velocity is not used for collision calculation, but timestamps
827 curr_state.predicted_velocity.linear.x = curr_obstacle.velocity.twist.linear.x;
828 curr_state.predicted_velocity.linear.y = curr_obstacle.velocity.twist.linear.y;
829 RCLCPP_DEBUG_STREAM(nh_->get_logger(), "Object: " << curr_obstacle.id <<", type: " << static_cast<int>(curr_obstacle.object_type)
830 << ", speed_x: " << curr_obstacle.velocity.twist.linear.x << ", speed_y: " << curr_obstacle.velocity.twist.linear.y);
831 new_list.push_back(curr_state);
832 new_list.insert(new_list.end(), curr_obstacle.predictions.cbegin(), curr_obstacle.predictions.cend());
833
834 const auto collision_result = get_collision(original_tp, new_list, config_.intervehicle_collision_distance_in_m, original_tp_max_speed);
835
836 if (!collision_result)
837 {
838 // reset the consecutive clearance counter because no collision was detected at this iteration
839 consecutive_clearance_count_for_obstacles_[curr_obstacle.id] = 0;
840 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"), "[CPU] obj=" << curr_obstacle.id << " no collision detected");
841 return std::nullopt;
842 }
843
844 // if within collision radius, it is not a collision if obstacle is behind the vehicle despite being in collision radius
845 const double vehicle_downtrack = wm_->routeTrackPos(collision_result.value().ego_point).downtrack;
846 const double object_downtrack = wm_->routeTrackPos(collision_result.value().object_point).downtrack;
847
848 if (is_object_behind_vehicle(curr_obstacle.id, collision_result.value().collision_time, vehicle_downtrack, object_downtrack))
849 {
850 RCLCPP_INFO_STREAM(nh_->get_logger(), "Confirmed that the object: " << curr_obstacle.id << " is behind the vehicle at timestamp " << std::to_string(collision_result.value().collision_time.seconds()));
851 return std::nullopt;
852 }
853
854 const auto distance{std::hypot(
855 collision_result.value().ego_point.x() - collision_result.value().object_point.x(),
856 collision_result.value().ego_point.y() - collision_result.value().object_point.y()
857 )}; //for debug
858
859 RCLCPP_WARN_STREAM(nh_->get_logger(), "Collision detected for object: " << curr_obstacle.id << ", at timestamp " << std::to_string(collision_result.value().collision_time.seconds()) <<
860 ", x: " << collision_result.value().ego_point.x() << ", y: " << collision_result.value().ego_point.y() <<
861 ", within actual downtrack distance: " << object_downtrack - vehicle_downtrack <<
862 ", and collision distance: " << distance);
863 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"), "[CPU] obj=" << curr_obstacle.id << " collision at t=" << collision_result.value().collision_time.seconds());
864
865 return collision_result.value().collision_time;
866 }
867
868 static lanelet::BasicPoint2d interp_trajectory_pt_at_time(
869 double query_time,
870 const std::vector<CudaPoint>& ego_points,
871 int num_ego_points,
872 const carma_planning_msgs::msg::TrajectoryPlan& trajectory_plan)
873 {
874 lanelet::BasicPoint2d result(ego_points[0].x, ego_points[0].y);
875 for (int i = 0; i < num_ego_points - 1; ++i) {
876 const double seg_start_time = rclcpp::Time(trajectory_plan.trajectory_points[i].target_time).seconds();
877 const double seg_end_time = rclcpp::Time(trajectory_plan.trajectory_points[i + 1].target_time).seconds();
878 if (seg_start_time <= query_time && query_time <= seg_end_time) {
879 const double interp_ratio = (seg_end_time > seg_start_time) ? (query_time - seg_start_time) / (seg_end_time - seg_start_time) : 0.0;
880 result.x() = trajectory_plan.trajectory_points[i].x + interp_ratio * (trajectory_plan.trajectory_points[i+1].x - trajectory_plan.trajectory_points[i].x);
881 result.y() = trajectory_plan.trajectory_points[i].y + interp_ratio * (trajectory_plan.trajectory_points[i+1].y - trajectory_plan.trajectory_points[i].y);
882 break;
883 }
884 }
885 return result;
886 }
887
888 static lanelet::BasicPoint2d interp_predicted_pt_at_time(
889 double query_time,
890 const std::vector<carma_perception_msgs::msg::PredictedState>& predictions,
891 int start_index)
892 {
893 lanelet::BasicPoint2d result(
894 predictions.front().predicted_position.position.x,
895 predictions.front().predicted_position.position.y);
896 for (int j = start_index; j < static_cast<int>(predictions.size()) - 1; ++j) {
897 const double seg_start_time = rclcpp::Time(predictions[j].header.stamp).seconds();
898 const double seg_end_time = rclcpp::Time(predictions[j + 1].header.stamp).seconds();
899 if (seg_start_time <= query_time && query_time <= seg_end_time) {
900 const double interp_ratio = (seg_end_time > seg_start_time) ? (query_time - seg_start_time) / (seg_end_time - seg_start_time) : 0.0;
901 result.x() = predictions[j].predicted_position.position.x +
902 interp_ratio * (predictions[j+1].predicted_position.position.x - predictions[j].predicted_position.position.x);
903 result.y() = predictions[j].predicted_position.position.y +
904 interp_ratio * (predictions[j+1].predicted_position.position.y - predictions[j].predicted_position.position.y);
905 break;
906 }
907 }
908 return result;
909 }
910
912 const std::vector<carma_perception_msgs::msg::PredictedState>& predictions,
913 int stride, bool object_has_zero_speed) const
914 {
915 for (size_t j = 0; j < predictions.size(); j += stride) {
916 const lanelet::BasicPoint2d point(predictions[j].predicted_position.position.x,
917 predictions[j].predicted_position.position.y);
918 for (const auto& llt : route_llt_polygons_) {
919 if (boost::geometry::within(point, llt.polygon2d())) {
920 return {true, static_cast<int>(j)};
921 }
922 }
923 if (object_has_zero_speed) break;
924 }
925 return {false, 0};
926 }
927
928 std::unordered_map<uint32_t, rclcpp::Time> YieldPlugin::get_collision_times_concurrently(
929 const carma_planning_msgs::msg::TrajectoryPlan& original_tp,
930 const std::vector<carma_perception_msgs::msg::ExternalObject>& external_objects,
931 double original_tp_max_speed)
932 {
933 if (!cuda_is_available())
934 return get_collision_times_concurrently_cpu(original_tp, external_objects, original_tp_max_speed);
935 return get_collision_times_concurrently_cuda(original_tp, external_objects, original_tp_max_speed);
936 }
937
938 std::unordered_map<uint32_t, rclcpp::Time> YieldPlugin::get_collision_times_concurrently_cpu(
939 const carma_planning_msgs::msg::TrajectoryPlan& original_tp,
940 const std::vector<carma_perception_msgs::msg::ExternalObject>& external_objects,
941 double original_tp_max_speed)
942 {
943 std::unordered_map<uint32_t, std::future<std::optional<rclcpp::Time>>> futures;
944 std::unordered_map<uint32_t, rclcpp::Time> collision_times;
945 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),
946 "[CPU] Launching " << external_objects.size() << " async get_collision_time tasks");
947 std::vector<std::thread> threads;
948 threads.reserve(external_objects.size());
949 for (const auto& object : external_objects) {
950 std::packaged_task<std::optional<rclcpp::Time>()> task(
951 [this, &original_tp, &object, &original_tp_max_speed] {
952 return get_collision_time(original_tp, object, original_tp_max_speed);
953 });
954 futures[object.id] = task.get_future();
955 threads.emplace_back(std::move(task));
956 }
957 for (auto& t : threads) t.join();
958 for (const auto& object : external_objects) {
959 if (const auto collision_time{futures.at(object.id).get()}) {
960 collision_times[object.id] = collision_time.value();
961 }
962 }
963 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),
964 "[CPU] Done — " << collision_times.size() << " collision(s) confirmed");
965 return collision_times;
966 }
967
968 std::unordered_map<uint32_t, rclcpp::Time> YieldPlugin::get_collision_times_concurrently_cuda(
969 const carma_planning_msgs::msg::TrajectoryPlan& original_tp,
970 const std::vector<carma_perception_msgs::msg::ExternalObject>& external_objects,
971 double original_tp_max_speed)
972 {
973 std::unordered_map<uint32_t, rclcpp::Time> collision_times;
974
975 if (original_tp.trajectory_points.size() < 2) return collision_times;
976
977 const double plan_start_time = get_trajectory_start_time(original_tp);
978
979 // Timestamps as absolute doubles; reference used to normalise into float32.
980 const double ref_time = plan_start_time;
981
982 // Build ego SoA (structure of array) with normalised timestamps.
983 const auto num_ego_points = static_cast<int>(original_tp.trajectory_points.size());
984 std::vector<CudaPoint> ego_pts;
985 ego_pts.reserve(num_ego_points);
986 for (const auto& tp : original_tp.trajectory_points) {
987 ego_pts.push_back({
988 static_cast<float>(tp.x),
989 static_cast<float>(tp.y),
990 static_cast<float>(rclcpp::Time(tp.target_time).seconds() - ref_time)
991 });
992 }
993
994 // Per-object data accumulated for the CUDA batch.
995 struct ActiveObject {
996 uint32_t id;
997 // Prediction list with current position prepended (same as get_collision_time builds).
998 std::vector<carma_perception_msgs::msg::PredictedState> predictions;
999 int on_route_idx; // first prediction index known to be on the route
1000 };
1001
1002 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),
1003 "[GPU] Processing " << external_objects.size() << " external objects");
1004
1005 std::vector<ActiveObject> active;
1006 std::vector<CudaPoint> obs_flat;
1007 std::vector<int> obs_offsets;
1008 std::vector<int> obs_sizes;
1009
1010 for (const auto& obj : external_objects) {
1011 // Skip objects whose entire prediction horizon is before the plan start.
1012 if (rclcpp::Time(obj.predictions.back().header.stamp).seconds() <= plan_start_time) {
1013 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),
1014 "[GPU] obj=" << obj.id << " skipped — predictions expired before plan start");
1015 continue;
1016 }
1017
1018 // Build prediction list: prepend current position (mirrors get_collision_time).
1019 std::vector<carma_perception_msgs::msg::PredictedState> pred_list;
1020 pred_list.reserve(obj.predictions.size() + 1);
1021 {
1022 carma_perception_msgs::msg::PredictedState curr;
1023 curr.header.stamp = obj.header.stamp;
1024 curr.predicted_position.position.x = obj.pose.pose.position.x;
1025 curr.predicted_position.position.y = obj.pose.pose.position.y;
1026 curr.predicted_velocity.linear.x = obj.velocity.twist.linear.x;
1027 curr.predicted_velocity.linear.y = obj.velocity.twist.linear.y;
1028 pred_list.push_back(curr);
1029 }
1030 pred_list.insert(pred_list.end(), obj.predictions.cbegin(), obj.predictions.cend());
1031
1032 if (pred_list.size() < 2) continue;
1033
1034 const double object_prediction_step_duration =
1035 (rclcpp::Time(pred_list.at(1).header.stamp) - rclcpp::Time(pred_list.front().header.stamp)).seconds();
1036 if (object_prediction_step_duration < 0.0) continue;
1037
1038 // Quick spatial pre-filter: if the object starts beyond collision_check_radius_in_m
1039 // it cannot collide with the ego at the start of the trajectory.
1040 {
1041 const double ego_to_object_dx = ego_pts[0].x - obj.pose.pose.position.x;
1042 const double ego_to_object_dy = ego_pts[0].y - obj.pose.pose.position.y;
1043 const double ego_to_object_dist = std::hypot(ego_to_object_dx, ego_to_object_dy);
1044 if (ego_to_object_dist > config_.collision_check_radius_in_m) {
1045 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),
1046 "[GPU] obj=" << obj.id << " skipped — dist_from_ego=" << ego_to_object_dist
1047 << " > radius=" << config_.collision_check_radius_in_m);
1049 continue;
1050 }
1051 }
1052
1053 // On-route check — stride logic as in get_collision, but uses pre-computed
1054 // per-lanelet bounding boxes instead of getLaneletsFromPoint (O(1) vs spatial query).
1055 const double object_speed = std::hypot(
1056 pred_list.front().predicted_velocity.linear.x,
1057 pred_list.front().predicted_velocity.linear.y);
1058 const bool object_has_zero_speed = object_speed < config_.obstacle_zero_speed_threshold_in_ms;
1059
1060 const double stride_max_t = 2.0 * config_.intervehicle_collision_distance_in_m /
1061 std::sqrt(std::pow(object_speed, 2) + std::pow(original_tp_max_speed, 2));
1062 const int iteration_stride = std::max(1, static_cast<int>(stride_max_t / object_prediction_step_duration));
1063
1064 const auto [on_route, on_route_idx] = find_on_route_in_predictions(pred_list, iteration_stride, object_has_zero_speed);
1065
1066 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),
1067 "[GPU] obj=" << obj.id << " on_route=" << on_route
1068 << " on_route_idx=" << on_route_idx
1069 << " speed=" << object_speed
1070 << " stride=" << iteration_stride);
1071
1072 if (!on_route) {
1074 continue;
1075 }
1076
1077 // Pack the on-route portion of the prediction into the flat obstacle buffer.
1078 obs_offsets.push_back(static_cast<int>(obs_flat.size()));
1079 int count = 0;
1080 for (int j = on_route_idx; j < static_cast<int>(pred_list.size()); ++j) {
1081 obs_flat.push_back({
1082 static_cast<float>(pred_list[j].predicted_position.position.x),
1083 static_cast<float>(pred_list[j].predicted_position.position.y),
1084 static_cast<float>(rclcpp::Time(pred_list[j].header.stamp).seconds() - ref_time)
1085 });
1086 ++count;
1087 }
1088 obs_sizes.push_back(count);
1089 active.push_back({obj.id, std::move(pred_list), on_route_idx});
1090 }
1091
1092 if (active.empty()) return collision_times;
1093
1094 // -----------------------------------------------------------------------
1095 // GPU: exact, continuous-time segment-pair collision detection.
1096 // -----------------------------------------------------------------------
1097 auto _t0_cuda = std::chrono::steady_clock::now();
1098 try {
1099 const auto cuda_results = cuda_check_all_collisions(
1100 ego_pts, obs_flat, obs_offsets, obs_sizes,
1101 static_cast<float>(config_.intervehicle_collision_distance_in_m));
1102 const double _cuda_ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - _t0_cuda).count();
1103 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),
1104 "[timing] cuda_check_all_collisions: " << _cuda_ms << " ms");
1105
1106 // -----------------------------------------------------------------------
1107 // Post-process: recover collision positions and run behind-vehicle check.
1108 // -----------------------------------------------------------------------
1109 for (size_t k = 0; k < active.size(); ++k) {
1110 const auto& object_result = cuda_results[k];
1111
1112 if (!object_result.has_collision) {
1113 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),
1114 "[GPU] obj=" << active[k].id << " no collision detected");
1116 continue;
1117 }
1118
1119 const double collision_time_abs = static_cast<double>(object_result.collision_t_norm) + ref_time;
1120 const rclcpp::Time collision_time(static_cast<int64_t>(collision_time_abs * 1e9));
1121
1122 const lanelet::BasicPoint2d ego_collision_point = interp_trajectory_pt_at_time(collision_time_abs, ego_pts, num_ego_points, original_tp);
1123 const lanelet::BasicPoint2d object_collision_point = interp_predicted_pt_at_time(collision_time_abs, active[k].predictions, active[k].on_route_idx);
1124
1125 const double vehicle_downtrack = wm_->routeTrackPos(ego_collision_point).downtrack;
1126 const double object_downtrack = wm_->routeTrackPos(object_collision_point).downtrack;
1127
1128 if (is_object_behind_vehicle(active[k].id, collision_time,
1129 vehicle_downtrack, object_downtrack)) {
1130 RCLCPP_INFO_STREAM(nh_->get_logger(),
1131 "Confirmed that the object: " << active[k].id
1132 << " is behind the vehicle at timestamp "
1133 << std::to_string(collision_time.seconds()));
1134 continue;
1135 }
1136
1137 RCLCPP_WARN_STREAM(nh_->get_logger(),
1138 "Collision detected for object: " << active[k].id
1139 << ", at timestamp " << std::to_string(collision_time.seconds())
1140 << ", x: " << ego_collision_point.x() << ", y: " << ego_collision_point.y()
1141 << ", within actual downtrack distance: "
1142 << object_downtrack - vehicle_downtrack);
1143
1144 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),
1145 "[GPU] obj=" << active[k].id
1146 << " collision at t=" << collision_time_abs
1147 << " ego=(" << ego_collision_point.x() << "," << ego_collision_point.y() << ")"
1148 << " obs=(" << object_collision_point.x() << "," << object_collision_point.y() << ")"
1149 << " downtrack_gap=" << (object_downtrack - vehicle_downtrack));
1150 collision_times[active[k].id] = collision_time;
1151 }
1152
1153 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),
1154 "[GPU] Done — " << collision_times.size() << " collision(s) confirmed");
1155
1156 } catch (const std::runtime_error& e) {
1157 std::string error_msg(e.what());
1158 // Detect CUDA-specific errors
1159 bool is_cuda_error = (error_msg.find("CUDA") != std::string::npos ||
1160 error_msg.find("cuda") != std::string::npos ||
1161 error_msg.find("driver version") != std::string::npos ||
1162 error_msg.find("runtime version") != std::string::npos ||
1163 error_msg.find("GPU") != std::string::npos);
1164
1165 if (is_cuda_error) {
1166 RCLCPP_WARN_STREAM_ONCE(rclcpp::get_logger("yield_plugin"),
1167 "[GPU] CUDA unavailable (" << e.what() << "), please make sure GPU is accessible for this node or container. Using CPU fallback");
1168 } else {
1169 RCLCPP_ERROR_STREAM_ONCE(rclcpp::get_logger("yield_plugin"),
1170 "[GPU] Unexpected error during GPU collision detection: " << e.what() << ", using CPU fallback");
1171 }
1172 return get_collision_times_concurrently_cpu(original_tp, external_objects, original_tp_max_speed);
1173 } catch (const std::exception& e) {
1174 RCLCPP_ERROR_STREAM_ONCE(rclcpp::get_logger("yield_plugin"),
1175 "[GPU] Unexpected exception during GPU collision detection: " << typeid(e).name() << " - " << e.what() << ", using CPU fallback");
1176 return get_collision_times_concurrently_cpu(original_tp, external_objects, original_tp_max_speed);
1177 }
1178 return collision_times;
1179 }
1180
1181 std::optional<std::pair<carma_perception_msgs::msg::ExternalObject, double>> YieldPlugin::get_earliest_collision_object_and_time(const carma_planning_msgs::msg::TrajectoryPlan& original_tp,
1182 const std::vector<carma_perception_msgs::msg::ExternalObject>& external_objects)
1183 {
1184 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"), "ExternalObjects size: " << external_objects.size());
1185
1186 if (!wm_->getRoute())
1187 {
1188 RCLCPP_WARN(nh_->get_logger(), "Yield plugin was not able to analyze collision since route is not available! Please check if route is set");
1189 return std::nullopt;
1190 }
1191
1192 // Populate route_llt_polygons_ if empty but route is actually available.
1193 if (route_llt_polygons_.empty())
1194 {
1196 }
1197
1198 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"External Object List (external_objects) size: " << external_objects.size());
1199 const double original_max_speed = max_trajectory_speed(original_tp.trajectory_points, get_trajectory_end_time(original_tp));
1200 auto _t0_conc = std::chrono::steady_clock::now();
1201 std::unordered_map<uint32_t, rclcpp::Time> collision_times = get_collision_times_concurrently(original_tp,external_objects, original_max_speed);
1202 const double _conc_ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - _t0_conc).count();
1203 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),
1204 "[timing] get_collision_times_concurrently: " << _conc_ms << " ms");
1205
1206 if (collision_times.empty()) { return std::nullopt; }
1207
1208 const auto earliest_colliding_object_id{std::min_element(
1209 std::cbegin(collision_times), std::cend(collision_times),
1210 [](const auto & a, const auto & b){ return a.second < b.second; })->first};
1211
1212 const auto earliest_colliding_object{std::find_if(
1213 std::cbegin(external_objects), std::cend(external_objects),
1214 [&earliest_colliding_object_id](const auto & object) { return object.id == earliest_colliding_object_id; })};
1215
1216 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"earliest object x: " << earliest_colliding_object->velocity.twist.linear.x
1217 << ", y: " << earliest_colliding_object->velocity.twist.linear.y);
1218 return std::make_pair(*earliest_colliding_object, collision_times.at(earliest_colliding_object_id).seconds());
1219
1220 }
1221
1222 double YieldPlugin::get_predicted_velocity_at_time(const geometry_msgs::msg::Twist& object_velocity_in_map_frame,
1223 const carma_planning_msgs::msg::TrajectoryPlan& original_tp, double timestamp_in_sec_to_predict)
1224 {
1225 RCLCPP_DEBUG_STREAM(nh_->get_logger(), "timestamp_in_sec_to_predict: " << std::to_string(timestamp_in_sec_to_predict) <<
1226 ", trajectory_end_time: " << std::to_string(get_trajectory_end_time(original_tp)));
1227
1228 double point_b_time = 0.0;
1229 carma_planning_msgs::msg::TrajectoryPlanPoint point_a;
1230 carma_planning_msgs::msg::TrajectoryPlanPoint point_b;
1231
1232 // trajectory points' time is guaranteed to be increasing
1233 // then find the corresponding point at timestamp_in_sec_to_predict
1234 for (size_t i = 0; i < original_tp.trajectory_points.size() - 1; ++i)
1235 {
1236 point_a = original_tp.trajectory_points.at(i);
1237 point_b = original_tp.trajectory_points.at(i + 1);
1238 point_b_time = rclcpp::Time(point_b.target_time).seconds();
1239 if (point_b_time >= timestamp_in_sec_to_predict)
1240 {
1241 break;
1242 }
1243 }
1244
1245 auto dx = point_b.x - point_a.x;
1246 auto dy = point_b.y - point_a.y;
1247 const tf2::Vector3 trajectory_direction(dx, dy, 0);
1248
1249 RCLCPP_DEBUG_STREAM(nh_->get_logger(), "timestamp_in_sec_to_predict: " << std::to_string(timestamp_in_sec_to_predict)
1250 << ", point_b_time: " << std::to_string(point_b_time)
1251 << ", dx: " << dx << ", dy: " << dy << ", "
1252 << ", object_velocity_in_map_frame.x: " << object_velocity_in_map_frame.linear.x
1253 << ", object_velocity_in_map_frame.y: " << object_velocity_in_map_frame.linear.y);
1254
1255 if (trajectory_direction.length() < 0.001) //EPSILON
1256 {
1257 return 0.0;
1258 }
1259
1260 const tf2::Vector3 object_direction(object_velocity_in_map_frame.linear.x, object_velocity_in_map_frame.linear.y, 0);
1261
1262 return tf2::tf2Dot(object_direction, trajectory_direction) / trajectory_direction.length();
1263 }
1264
1265 carma_planning_msgs::msg::TrajectoryPlan YieldPlugin::update_traj_for_object(const carma_planning_msgs::msg::TrajectoryPlan& original_tp,
1266 const std::vector<carma_perception_msgs::msg::ExternalObject>& external_objects, double initial_velocity)
1267 {
1268 if (original_tp.trajectory_points.size() < 2)
1269 {
1270 RCLCPP_WARN(nh_->get_logger(), "Yield plugin received less than 2 points in update_traj_for_object, returning unchanged...");
1271 return original_tp;
1272 }
1273
1274 // Get earliest collision object
1275 auto _t0_ect = std::chrono::steady_clock::now();
1276 const auto earliest_collision_obj_pair = get_earliest_collision_object_and_time(original_tp, external_objects);
1277 const double _ect_ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - _t0_ect).count();
1278 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),
1279 "[timing] get_earliest_collision_object_and_time: " << _ect_ms << " ms");
1280
1281 if (!earliest_collision_obj_pair)
1282 {
1283 RCLCPP_DEBUG(nh_->get_logger(),"No collision detected, so trajectory not modified.");
1284 return original_tp;
1285 }
1286
1287 carma_perception_msgs::msg::ExternalObject earliest_collision_obj = earliest_collision_obj_pair.value().first;
1288 double earliest_collision_time_in_seconds = earliest_collision_obj_pair.value().second;
1289
1290 // Issue (https://github.com/usdot-fhwa-stol/carma-platform/issues/2155): If the yield_plugin can detect if the roadway object is moving along the route,
1291 // it is able to plan yielding much earlier and smoother using on_route_vehicle_collision_horizon_in_s.
1292
1293 const lanelet::BasicPoint2d vehicle_point(original_tp.trajectory_points[0].x,original_tp.trajectory_points[0].y);
1294 auto _rtp_t0_upd = std::chrono::steady_clock::now();
1295 const double vehicle_downtrack = wm_->routeTrackPos(vehicle_point).downtrack;
1296
1297 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"vehicle_downtrack: " << vehicle_downtrack);
1298
1299 RCLCPP_WARN_STREAM(nh_->get_logger(),"Collision Detected!");
1300
1301 const lanelet::BasicPoint2d object_point(earliest_collision_obj.pose.pose.position.x, earliest_collision_obj.pose.pose.position.y);
1302 const double object_downtrack = wm_->routeTrackPos(object_point).downtrack;
1303
1304 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"object_downtrack: " << object_downtrack);
1305 const double _rtp_ms_upd = std::chrono::duration<double, std::milli>(
1306 std::chrono::steady_clock::now() - _rtp_t0_upd).count();
1307 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),
1308 "[update_traj] routeTrackPos x2: " << _rtp_ms_upd << " ms");
1309
1310 const double object_downtrack_lead = std::max(0.0, object_downtrack - vehicle_downtrack);
1311 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"object_downtrack_lead: " << object_downtrack_lead);
1312
1313 // The vehicle's goal velocity of the yielding behavior is to match the velocity of the object along the trajectory.
1314 double goal_velocity = get_predicted_velocity_at_time(earliest_collision_obj.velocity.twist, original_tp, earliest_collision_time_in_seconds);
1315 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"object's speed along trajectory at collision: " << goal_velocity);
1316
1317 // roadway object position
1318 const double gap_time_until_min_gap_distance = std::max(0.0, object_downtrack_lead - config_.minimum_safety_gap_in_meters)/initial_velocity;
1319
1320 if (goal_velocity <= config_.obstacle_zero_speed_threshold_in_ms){
1321 RCLCPP_WARN_STREAM(nh_->get_logger(),"The obstacle is not moving, goal velocity is set to 0 from: " << goal_velocity);
1322 goal_velocity = 0.0;
1323 }
1324
1325 // determine the safety inter-vehicle gap based on speed
1326 double safety_gap = std::max(goal_velocity * gap_time_until_min_gap_distance, config_.minimum_safety_gap_in_meters);
1327 if (!std::isnormal(safety_gap))
1328 {
1329 RCLCPP_WARN_STREAM(rclcpp::get_logger("yield_plugin"),"Detected non-normal (nan, inf, etc.) safety_gap."
1330 "Making it desired safety gap configured at config_.minimum_safety_gap_in_meters: " << config_.minimum_safety_gap_in_meters);
1332 }
1334 {
1335 // externally_commanded_safety_gap is desired distance gap commanded from external sources
1336 // such as different plugin, map, or infrastructure depending on the use case
1337 auto _t0_gap = std::chrono::steady_clock::now();
1338 double externally_commanded_safety_gap = check_traj_for_digital_min_gap(original_tp);
1339 const double _gap_ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - _t0_gap).count();
1340 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),
1341 "[timing] check_traj_for_digital_min_gap: " << _gap_ms << " ms");
1342 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"externally_commanded_safety_gap: " << externally_commanded_safety_gap);
1343 // if a digital gap is available, it is replaced as safety gap
1344 safety_gap = std::max(safety_gap, externally_commanded_safety_gap);
1345 }
1346
1347 const double goal_pos = std::max(0.0, object_downtrack_lead - safety_gap - config_.vehicle_length);
1348 const double initial_pos = 0.0; //relative initial position (first trajectory point)
1349 const double original_max_speed = max_trajectory_speed(original_tp.trajectory_points, earliest_collision_time_in_seconds);
1350 const double delta_v_max = fabs(goal_velocity - original_max_speed);
1351 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"delta_v_max: " << delta_v_max << ", safety_gap: " << safety_gap);
1352
1353 const double time_required_for_comfortable_decel_in_s = config_.acceleration_adjustment_factor * 2 * goal_pos / delta_v_max;
1354 const double min_time_required_for_comfortable_decel_in_s = delta_v_max / config_.yield_max_deceleration_in_ms2;
1355
1356 // planning time for object avoidance
1357 double planning_time_in_s = std::max({config_.min_obj_avoidance_plan_time_in_s, time_required_for_comfortable_decel_in_s, min_time_required_for_comfortable_decel_in_s});
1358 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"time_required_for_comfortable_decel_in_s: " << time_required_for_comfortable_decel_in_s << ", min_time_required_for_comfortable_decel_in_s: " << min_time_required_for_comfortable_decel_in_s);
1359
1360 RCLCPP_DEBUG_STREAM(nh_->get_logger(),"Object avoidance planning time: " << planning_time_in_s);
1361
1362 auto _t0_jmt = std::chrono::steady_clock::now();
1363 auto jmt_trajectory = generate_JMT_trajectory(original_tp,
1364 initial_pos, goal_pos, initial_velocity, goal_velocity,
1365 planning_time_in_s, original_max_speed);
1366 const double _jmt_ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - _t0_jmt).count();
1367 RCLCPP_DEBUG_STREAM(rclcpp::get_logger("yield_plugin"),
1368 "[timing] generate_JMT_trajectory: " << _jmt_ms << " ms");
1369
1370 // If expected to stop to prevent collision, we should save this trajectory to commit to it
1371 if (!last_traj_plan_committed_to_stopping_.has_value() &&
1372 goal_velocity < EPSILON &&
1373 earliest_collision_time_in_seconds - nh_->now().seconds()
1375 {
1377 }
1378 return jmt_trajectory;
1379 }
1380
1381
1382 std::vector<double> YieldPlugin::get_relative_downtracks(const carma_planning_msgs::msg::TrajectoryPlan& trajectory_plan) const
1383 {
1384 std::vector<double> downtracks;
1385 downtracks.reserve(trajectory_plan.trajectory_points.size());
1386 // relative downtrack distance of the fist Point is 0.0
1387 downtracks.push_back(0.0);
1388 for (size_t i=1; i < trajectory_plan.trajectory_points.size(); i++){
1389
1390 double dx = trajectory_plan.trajectory_points.at(i).x - trajectory_plan.trajectory_points.at(i-1).x;
1391 double dy = trajectory_plan.trajectory_points.at(i).y - trajectory_plan.trajectory_points.at(i-1).y;
1392 downtracks.push_back(sqrt(dx*dx + dy*dy));
1393 }
1394 return downtracks;
1395 }
1396
1397 double YieldPlugin::polynomial_calc(std::vector<double> coeff, double x) const
1398 {
1399 double result = 0;
1400 for (size_t i = 0; i < coeff.size(); i++)
1401 {
1402 double value = coeff.at(i) * pow(x, static_cast<int>(coeff.size() - 1 - i));
1403 result = result + value;
1404 }
1405 return result;
1406 }
1407
1408 double YieldPlugin::polynomial_calc_d(std::vector<double> coeff, double x) const
1409 {
1410 double result = 0;
1411 for (size_t i = 0; i < coeff.size()-1; i++)
1412 {
1413 double value = static_cast<int>(coeff.size() - 1 - i) * coeff.at(i) * pow(x, static_cast<int>(coeff.size() - 2 - i));
1414 result = result + value;
1415 }
1416 return result;
1417 }
1418
1419 double YieldPlugin::max_trajectory_speed(const std::vector<carma_planning_msgs::msg::TrajectoryPlanPoint>& trajectory_points, double timestamp_in_sec_to_search_until) const
1420 {
1421 double max_speed = 0;
1422 for(size_t i = 0; i < trajectory_points.size() - 2; i++ )
1423 {
1424 double dx = trajectory_points.at(i + 1).x - trajectory_points.at(i).x;
1425 double dy = trajectory_points.at(i + 1).y - trajectory_points.at(i).y;
1426 double d = sqrt(dx*dx + dy*dy);
1427 double t = (rclcpp::Time(trajectory_points.at(i + 1).target_time).seconds() - rclcpp::Time(trajectory_points.at(i).target_time).seconds());
1428 double v = d/t;
1429 if(v > max_speed)
1430 {
1431 max_speed = v;
1432 }
1433 if (rclcpp::Time(trajectory_points.at(i + 1).target_time).seconds() >= timestamp_in_sec_to_search_until)
1434 {
1435 break;
1436 }
1437
1438 }
1439 return max_speed;
1440 }
1441
1442 double YieldPlugin::check_traj_for_digital_min_gap(const carma_planning_msgs::msg::TrajectoryPlan& original_tp) const
1443 {
1444 double desired_gap = 0;
1445 if (!wm_->getRoute()) return desired_gap;
1446
1447 const lanelet::BasicPoint2d traj_start(original_tp.trajectory_points.front().x,
1448 original_tp.trajectory_points.front().y);
1449 const lanelet::BasicPoint2d traj_end(original_tp.trajectory_points.back().x,
1450 original_tp.trajectory_points.back().y);
1451
1452 // Fetch up to 4 candidates per endpoint — a boundary point can lie on
1453 // overlapping lanelets and a single result may be the wrong one.
1454 auto start_llts = wm_->getLaneletsFromPoint(traj_start, 4);
1455 auto end_llts = wm_->getLaneletsFromPoint(traj_end, 4);
1456
1457 if (start_llts.empty() || end_llts.empty())
1458 {
1459 // Trajectory generation may place a point off-road in rare edge cases
1460 // (see https://github.com/usdot-fhwa-stol/carma-platform/issues/2503)
1461 RCLCPP_WARN_STREAM(nh_->get_logger(), "check_traj_for_digital_min_gap: trajectory endpoint "
1462 "not on a lanelet, skipping digital gap check.");
1463 return desired_gap;
1464 }
1465
1466 std::unordered_set<lanelet::Id> start_ids;
1467 std::unordered_set<lanelet::Id> end_ids;
1468 for (const auto& llt : start_llts) start_ids.insert(llt.id());
1469 for (const auto& llt : end_llts) end_ids.insert(llt.id());
1470
1471 // One pass: first index matching any start candidate, last index matching any end candidate.
1472 // Taking "last" for end covers all overlapping lanelets at the trajectory's back boundary.
1473 std::optional<size_t> start_pos;
1474 std::optional<size_t> end_pos;
1475 size_t pos = 0;
1476 const auto& path = wm_->getRoute()->shortestPath();
1477 for (const auto& llt : path)
1478 {
1479 if (!start_pos.has_value() && start_ids.count(llt.id())) start_pos = pos;
1480 if (end_ids.count(llt.id())) end_pos = pos;
1481 ++pos;
1482 }
1483
1484 if (!start_pos || !end_pos || *start_pos > *end_pos)
1485 {
1486 RCLCPP_WARN_STREAM(nh_->get_logger(), "check_traj_for_digital_min_gap: trajectory endpoints "
1487 "not found on route shortest path, skipping digital gap check.");
1488 return desired_gap;
1489 }
1490
1491 pos = 0;
1492 for (const auto& llt : path)
1493 {
1494 if (pos > *end_pos) break;
1495 if (pos >= *start_pos)
1496 {
1497 auto digital_min_gap = llt.regulatoryElementsAs<lanelet::DigitalMinimumGap>();
1498 if (!digital_min_gap.empty())
1499 {
1500 double digital_gap = digital_min_gap[0]->getMinimumGap();
1501 RCLCPP_DEBUG_STREAM(nh_->get_logger(), "Digital Gap found with value: " << digital_gap);
1502 desired_gap = std::max(desired_gap, digital_gap);
1503 }
1504 }
1505 ++pos;
1506 }
1507 return desired_gap;
1508 }
1509
1510 void YieldPlugin::set_georeference_string(const std::string& georeference)
1511 {
1512 if (georeference_ != georeference)
1513 {
1514 georeference_ = georeference;
1515 map_projector_ = std::make_shared<lanelet::projection::LocalFrameProjector>(georeference.c_str()); // Build projector from proj string
1516 }
1517 }
1518
1519 void YieldPlugin::set_external_objects(const std::vector<carma_perception_msgs::msg::ExternalObject>& object_list)
1520 {
1521 external_objects_ = object_list;
1522 }
1523
1525 {
1526 if (!wm_->getRoute())
1527 {
1528 RCLCPP_WARN(nh_->get_logger(), "update_route_llt_cache called but route is not available");
1529 return;
1530 }
1531 route_llt_ids_.clear();
1532 route_llt_polygons_.clear();
1533 for (const auto& llt : wm_->getRoute()->shortestPath())
1534 {
1535 route_llt_ids_.insert(llt.id());
1536 route_llt_polygons_.push_back(llt);
1537 }
1538 }
1539
1540} // namespace yield_plugin
std::set< lanelet::Id > route_llt_ids_
void plan_trajectory_callback(carma_planning_msgs::srv::PlanTrajectory::Request::SharedPtr req, carma_planning_msgs::srv::PlanTrajectory::Response::SharedPtr resp)
Service callback for trajectory planning.
std::vector< double > get_relative_downtracks(const carma_planning_msgs::msg::TrajectoryPlan &trajectory_plan) const
calculates distance between trajectory points in a plan
LaneChangeStatusCB lc_status_publisher_
void set_incoming_request_info(std::vector< lanelet::BasicPoint2d > req_trajectory, double req_speed, double req_planning_time, double req_timestamp)
set values for member variables related to cooperative behavior
std::vector< lanelet::BasicPoint2d > req_trajectory_points_
void mobilityrequest_cb(const carma_v2x_msgs::msg::MobilityRequest::UniquePtr msg)
callback for mobility request
double check_traj_for_digital_min_gap(const carma_planning_msgs::msg::TrajectoryPlan &original_tp) const
checks trajectory for minimum gap associated with it from the road
double get_predicted_velocity_at_time(const geometry_msgs::msg::Twist &object_velocity_in_map_frame, const carma_planning_msgs::msg::TrajectoryPlan &original_tp, double timestamp_in_sec_to_predict)
Given the object velocity in map frame with x,y components, this function returns the projected veloc...
std::shared_ptr< lanelet::projection::LocalFrameProjector > map_projector_
std::vector< lanelet::BasicPoint2d > convert_eceftrajectory_to_mappoints(const carma_v2x_msgs::msg::Trajectory &ecef_trajectory) const
convert a carma trajectory from ecef frame to map frame ecef trajectory consists of the point and a s...
std::optional< std::pair< carma_perception_msgs::msg::ExternalObject, double > > get_earliest_collision_object_and_time(const carma_planning_msgs::msg::TrajectoryPlan &original_tp, const std::vector< carma_perception_msgs::msg::ExternalObject > &external_objects)
Return the earliest collision object and time of collision pair from the given trajectory and list of...
std::optional< rclcpp::Time > get_collision_time(const carma_planning_msgs::msg::TrajectoryPlan &original_tp, const carma_perception_msgs::msg::ExternalObject &curr_obstacle, double original_tp_max_speed)
Return collision time given two trajectories with one being external object with predicted steps.
std::optional< rclcpp::Time > last_speed_time_
std::unordered_map< uint32_t, rclcpp::Time > get_collision_times_concurrently_cpu(const carma_planning_msgs::msg::TrajectoryPlan &original_tp, const std::vector< carma_perception_msgs::msg::ExternalObject > &external_objects, double original_tp_max_speed)
CPU implementation of get_collision_times_concurrently. Launches one thread per external object and c...
void update_route_llt_cache()
Rebuild the cached route lanelet polygons and IDs from the current route. Should be called once whene...
void bsm_cb(const carma_v2x_msgs::msg::BSM::UniquePtr msg)
callback for bsm message
MobilityResponseCB mobility_response_publisher_
std::string bsmIDtoString(carma_v2x_msgs::msg::BSMCoreData bsm_core)
void set_external_objects(const std::vector< carma_perception_msgs::msg::ExternalObject > &object_list)
Setter for external objects with predictions in the environment.
YieldPlugin(std::shared_ptr< carma_ros2_utils::CarmaLifecycleNode > nh, carma_wm::WorldModelConstPtr wm, YieldPluginConfig config, MobilityResponseCB mobility_response_publisher, LaneChangeStatusCB lc_status_publisher)
Constructor.
std::shared_ptr< carma_ros2_utils::CarmaLifecycleNode > nh_
void set_georeference_string(const std::string &georeference)
Setter for map projection string to define lat/lon -> map conversion.
std::unordered_map< uint32_t, rclcpp::Time > get_collision_times_concurrently(const carma_planning_msgs::msg::TrajectoryPlan &original_tp, const std::vector< carma_perception_msgs::msg::ExternalObject > &external_objects, double original_tp_max_speed)
Given the list of objects with predicted states, get all collision times concurrently using multi-thr...
YieldPluginConfig config_
std::optional< rclcpp::Time > first_time_stopped_to_prevent_collision_
std::optional< GetCollisionResult > get_collision(const carma_planning_msgs::msg::TrajectoryPlan &ego_trajectory, const std::vector< carma_perception_msgs::msg::PredictedState > &object_predictions, double collision_radius, double ego_max_speed)
Return naive collision time and locations based on collision radius given two trajectories with one b...
std::vector< std::pair< int, lanelet::BasicPoint2d > > detect_trajectories_intersection(std::vector< lanelet::BasicPoint2d > self_trajectory, std::vector< lanelet::BasicPoint2d > incoming_trajectory) const
detect intersection point(s) of two trajectories
std::optional< carma_planning_msgs::msg::TrajectoryPlan > last_traj_plan_committed_to_stopping_
lanelet::BasicPoint2d ecef_to_map_point(const carma_v2x_msgs::msg::LocationECEF &ecef_point) const
convert a point in ecef frame (in cm) into map frame (in meters)
carma_planning_msgs::msg::TrajectoryPlan update_traj_for_object(const carma_planning_msgs::msg::TrajectoryPlan &original_tp, const std::vector< carma_perception_msgs::msg::ExternalObject > &external_objects, double initial_velocity)
trajectory is modified to safely avoid obstacles on the road
carma_planning_msgs::msg::TrajectoryPlan generate_JMT_trajectory(const carma_planning_msgs::msg::TrajectoryPlan &original_tp, double initial_pos, double goal_pos, double initial_velocity, double goal_velocity, double planning_time, double original_max_speed)
generate a Jerk Minimizing Trajectory(JMT) with the provided start and end conditions
carma_v2x_msgs::msg::MobilityResponse compose_mobility_response(const std::string &resp_recipient_id, const std::string &req_plan_id, bool response) const
compose a mobility response message
std::pair< bool, int > find_on_route_in_predictions(const std::vector< carma_perception_msgs::msg::PredictedState > &predictions, int stride, bool zero_speed) const
Check whether any predicted state of an object falls within the route lanelet polygons.
std::vector< lanelet::ConstLanelet > route_llt_polygons_
carma_planning_msgs::msg::TrajectoryPlan update_traj_for_cooperative_behavior(const carma_planning_msgs::msg::TrajectoryPlan &original_tp, double current_speed)
update trajectory for yielding to an incoming cooperative behavior
std::unordered_map< uint32_t, rclcpp::Time > get_collision_times_concurrently_cuda(const carma_planning_msgs::msg::TrajectoryPlan &original_tp, const std::vector< carma_perception_msgs::msg::ExternalObject > &external_objects, double original_tp_max_speed)
CUDA implementation of get_collision_times_concurrently. Filters objects to those with on-route predi...
std::unordered_map< uint32_t, int > consecutive_clearance_count_for_obstacles_
std::vector< carma_perception_msgs::msg::ExternalObject > external_objects_
bool is_object_behind_vehicle(uint32_t object_id, const rclcpp::Time &collision_time, double vehicle_point, double object_downtrack)
Check if object location is behind the vehicle using estimates of the vehicle's length and route down...
double max_trajectory_speed(const std::vector< carma_planning_msgs::msg::TrajectoryPlanPoint > &trajectory_points, double timestamp_in_sec_to_search_until) const
calculates the maximum speed in a set of tajectory points
double polynomial_calc(std::vector< double > coeff, double x) const
calculate quintic polynomial equation for a given x
std::optional< double > last_speed_
double polynomial_calc_d(std::vector< double > coeff, double x) const
calculate derivative of quintic polynomial equation for a given x
carma_wm::WorldModelConstPtr wm_
std::ostringstream oss
std::vector< double > moving_average_filter(const std::vector< double > input, int window_size, bool ignore_first_point=true)
Extremely simplie moving average filter.
Definition: filters.cpp:24
int get_nearest_point_index(const std::vector< lanelet::BasicPoint2d > &points, const carma_planning_msgs::msg::VehicleState &state)
Returns the nearest point (in terms of cartesian 2d distance) to the provided vehicle pose in the pro...
void set_logger(rclcpp::Logger logger)
Replace the module-level logger used by all basic_autonomy functions.
Definition: log.cpp:33
rclcpp::Logger get_logger()
Return the module-level logger used by all basic_autonomy functions.
Definition: log.cpp:32
auto to_string(const UtmZone &zone) -> std::string
Definition: utm_zone.cpp:21
std::shared_ptr< const WorldModel > WorldModelConstPtr
Definition: WorldModel.hpp:454
list first_point
Definition: process_bag.py:52
std::function< void(const carma_planning_msgs::msg::LaneChangeStatus &)> LaneChangeStatusCB
double get_smallest_time_step_of_traj(const carma_planning_msgs::msg::TrajectoryPlan &original_tp)
double get_trajectory_start_time(const carma_planning_msgs::msg::TrajectoryPlan &trajectory)
double get_trajectory_end_time(const carma_planning_msgs::msg::TrajectoryPlan &trajectory)
static lanelet::BasicPoint2d interp_predicted_pt_at_time(double query_time, const std::vector< carma_perception_msgs::msg::PredictedState > &predictions, int start_index)
std::function< void(const carma_v2x_msgs::msg::MobilityResponse &)> MobilityResponseCB
static lanelet::BasicPoint2d interp_trajectory_pt_at_time(double query_time, const std::vector< CudaPoint > &ego_points, int num_ego_points, const carma_planning_msgs::msg::TrajectoryPlan &trajectory_plan)
double get_trajectory_duration(const carma_planning_msgs::msg::TrajectoryPlan &trajectory)
Stuct containing the algorithm configuration values for the YieldPluginConfig.
bool always_accept_mobility_request
double max_stop_speed_in_ms
double collision_check_radius_in_m
std::string vehicle_id
double speed_moving_average_window_size
int consecutive_clearance_count_for_passed_obstacles_threshold
double acceleration_adjustment_factor
double yield_max_deceleration_in_ms2
double time_horizon_until_collision_to_commit_to_stop_in_s
double intervehicle_collision_distance_in_m
double obstacle_zero_speed_threshold_in_ms
bool enable_cooperative_behavior
double min_obj_avoidance_plan_time_in_s
double minimum_safety_gap_in_meters
double safety_collision_time_gap_in_s
Convenience class for saving collision results.
lanelet::BasicPoint2d object_point
lanelet::BasicPoint2d ego_point
constexpr auto EPSILON