Nav2 Navigation Stack - rolling  main
ROS 2 Navigation Stack
asymmetric_inflation_layer.cpp
1 // Copyright (c) 2026, Marc Blöchlinger
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 #include "nav2_costmap_2d/asymmetric_inflation_layer.hpp"
16 
17 #include <limits>
18 #include <vector>
19 #include <algorithm>
20 #include <utility>
21 #include <unordered_map>
22 #include <cstdint>
23 #include <cmath>
24 
25 #ifdef _OPENMP
26 #include <omp.h>
27 #endif
28 
29 #include "nav2_ros_common/node_utils.hpp"
30 #include "nav2_util/robot_utils.hpp"
31 #include "pluginlib/class_list_macros.hpp"
32 #include "tf2_geometry_msgs/tf2_geometry_msgs.hpp"
33 #include "geometry_msgs/msg/pose_stamped.hpp"
34 #include "geometry_msgs/msg/transform_stamped.hpp"
35 
37 
38 using nav2_costmap_2d::LETHAL_OBSTACLE;
39 using nav2_costmap_2d::NO_INFORMATION;
40 using rcl_interfaces::msg::ParameterType;
41 
42 namespace nav2_costmap_2d
43 {
44 
45 AsymmetricInflationLayer::AsymmetricInflationLayer()
46 : cost_scaling_factor_left_(0),
47  cost_scaling_factor_right_(0),
48  goal_distance_threshold_(0)
49 {
50 }
51 
52 void
53 AsymmetricInflationLayer::onInitialize()
54 {
55  {
56  double temp_tf_tol = 0.0;
57 
58  auto node = node_.lock();
59  if (!node) {
60  throw std::runtime_error{"Failed to lock node"};
61  }
62 
63  enabled_ = node->declare_or_get_parameter(name_ + "." + "enabled", true);
64  inflation_radius_ = node->declare_or_get_parameter(
65  name_ + "." + "inflation_radius", 2.0);
66  inflate_unknown_ = node->declare_or_get_parameter(name_ + "." + "inflate_unknown", false);
67  inflate_around_unknown_ = node->declare_or_get_parameter(
68  name_ + "." + "inflate_around_unknown", false);
69  num_threads_ = node->declare_or_get_parameter(
70  name_ + "." + "num_threads", -1);
71  cost_scaling_factor_left_ = node->declare_or_get_parameter(
72  name_ + "." + "cost_scaling_factor_left", 4.0);
73  cost_scaling_factor_right_ = node->declare_or_get_parameter(
74  name_ + "." + "cost_scaling_factor_right", 4.0);
75  plan_topic_ = node->declare_or_get_parameter<std::string>(
76  name_ + "." + "plan_topic", "plan");
77  goal_distance_threshold_ = node->declare_or_get_parameter(
78  name_ + "." + "goal_distance_threshold", 1.5);
79  transform_staleness_threshold_ = node->declare_or_get_parameter(
80  name_ + "." + "transform_staleness_threshold", 0.0);
81 
82  // Get costmap2d-level parameter
83  node->get_parameter("transform_tolerance", temp_tf_tol);
84  transform_tolerance_ = tf2::durationFromSec(temp_tf_tol);
85 
86  if (inflation_radius_ < 0.0) {
87  throw std::runtime_error(
88  "AsymmetricInflationLayer: inflation_radius must be >= 0");
89  }
90  if (cost_scaling_factor_left_ < 0.0) {
91  throw std::runtime_error(
92  "AsymmetricInflationLayer: cost_scaling_factor_left must be >= 0");
93  }
94  if (cost_scaling_factor_right_ < 0.0) {
95  throw std::runtime_error(
96  "AsymmetricInflationLayer: cost_scaling_factor_right must be >= 0");
97  }
98  if (goal_distance_threshold_ < 0.0) {
99  throw std::runtime_error(
100  "AsymmetricInflationLayer: goal_distance_threshold must be >= 0");
101  }
102  if (num_threads_ < -1) {
103  throw std::runtime_error(
104  "AsymmetricInflationLayer: num_threads must be -1 (auto) or > 0");
105  }
106  if (temp_tf_tol < 0.0) {
107  throw std::runtime_error(
108  "AsymmetricInflationLayer: transform_tolerance must be >= 0");
109  }
110 
111  cost_scaling_factor_ =
112  std::max(cost_scaling_factor_left_, cost_scaling_factor_right_);
113 
114  plan_topic_ = joinWithParentNamespace(plan_topic_);
115  path_sub_ = node->create_subscription<nav_msgs::msg::Path>(
116  plan_topic_,
117  std::bind(
118  &AsymmetricInflationLayer::globalPathCallback,
119  this, std::placeholders::_1),
121  }
122 
123  setCurrent(true);
124  need_reinflation_ = false;
125  asymmetry_active_ = false;
126  cell_inflation_radius_ = cellDistance(inflation_radius_);
127  matchSize();
128 }
129 
130 void
131 AsymmetricInflationLayer::activate()
132 {
133  auto node = node_.lock();
134  if (!node) {
135  throw std::runtime_error{"Failed to lock node"};
136  }
137  on_set_params_handler_ = node->add_on_set_parameters_callback(
138  std::bind(
139  &AsymmetricInflationLayer::validateParameterUpdatesCallback,
140  this, std::placeholders::_1));
141  post_set_params_handler_ = node->add_post_set_parameters_callback(
142  std::bind(
143  &AsymmetricInflationLayer::updateParametersCallback,
144  this, std::placeholders::_1));
145 }
146 
147 void
148 AsymmetricInflationLayer::deactivate()
149 {
150  auto node = node_.lock();
151  if (on_set_params_handler_ && node) {
152  node->remove_on_set_parameters_callback(on_set_params_handler_.get());
153  }
154  on_set_params_handler_.reset();
155  if (post_set_params_handler_ && node) {
156  node->remove_post_set_parameters_callback(post_set_params_handler_.get());
157  }
158  post_set_params_handler_.reset();
159 }
160 
161 void
162 AsymmetricInflationLayer::globalPathCallback(const nav_msgs::msg::Path::ConstSharedPtr msg)
163 {
164  if (latest_global_path_ && *latest_global_path_ == *msg) {return;}
165 
166  // Cache the path
167  std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
168  {
169  std::lock_guard<std::mutex> lock(path_mutex_);
170  latest_global_path_ = msg;
171  }
172  // Force a full-map reinflation on the next update cycle.
173  need_reinflation_ = true;
174  setCurrent(false);
175 }
176 
177 void
178 AsymmetricInflationLayer::matchSize()
179 {
180  std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
181  InflationLayer::matchSize();
182 
183  computeAsymmetricCaches();
184 }
185 
186 void
187 AsymmetricInflationLayer::updateBounds(
188  double robot_x, double robot_y, double robot_yaw, double * min_x,
189  double * min_y, double * max_x, double * max_y)
190 {
191  std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
192 
193  // Track robot pose for the goal-proximity fallback check
194  current_robot_x_ = robot_x;
195  current_robot_y_ = robot_y;
196 
197  InflationLayer::updateBounds(robot_x, robot_y, robot_yaw, min_x, min_y, max_x, max_y);
198 }
199 
200 void
201 AsymmetricInflationLayer::onFootprintChanged()
202 {
203  std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
204  InflationLayer::onFootprintChanged();
205  computeAsymmetricCaches();
206 }
207 
208 std::vector<AsymmetricPathSegment>
209 AsymmetricInflationLayer::extractLocalPathSegments(
210  nav2_costmap_2d::Costmap2D & master_grid)
211 {
212  std::vector<AsymmetricPathSegment> local_path_segments;
213  nav_msgs::msg::Path current_path;
214  {
215  std::lock_guard<std::mutex> lock(path_mutex_);
216  if (!latest_global_path_ || latest_global_path_->poses.size() < 2) {
217  return local_path_segments;
218  }
219  current_path = *latest_global_path_;
220  }
221 
222  // Check if the path is already in costmap frame
223  std::string global_frame = layered_costmap_->getGlobalFrameID();
224  std::string path_frame = current_path.header.frame_id;
225  bool need_transform = (global_frame != path_frame && !path_frame.empty());
226 
227  // Look up the current path frame -> costmap frame transform
228  geometry_msgs::msg::TransformStamped transform;
229  if (need_transform) {
230  if (!nav2_util::lookupTransformWithStalenessCheck(
231  *tf_, global_frame, path_frame, clock_->now(),
232  transform_staleness_threshold_, transform))
233  {
234  RCLCPP_WARN_THROTTLE(
235  logger_, *clock_, 1000,
236  "AsymmetricInflationLayer: TF lookup failed or returned stale data (%s -> %s). "
237  "Falling back to symmetric inflation.",
238  path_frame.c_str(), global_frame.c_str());
239  return local_path_segments;
240  }
241  }
242 
243  // Transform the goal pose from the path frame to the costmap frame (e.g., map -> odom)
244  geometry_msgs::msg::PoseStamped goal_pose = current_path.poses.back();
245  if (need_transform) {
246  tf2::doTransform(goal_pose, goal_pose, transform);
247  }
248 
249  // Disable asymmetry near the goal to prevent target oscillations
250  double dist_to_goal = std::hypot(
251  goal_pose.pose.position.x - current_robot_x_,
252  goal_pose.pose.position.y - current_robot_y_);
253 
254  if (dist_to_goal <= goal_distance_threshold_) {
255  return local_path_segments;
256  }
257 
258  // Get local window edge coordinates
259  const double map_min_x = master_grid.getOriginX();
260  const double map_min_y = master_grid.getOriginY();
261  const double map_max_x = map_min_x +
262  static_cast<double>(master_grid.getSizeInCellsX()) * master_grid.getResolution();
263  const double map_max_y = map_min_y +
264  static_cast<double>(master_grid.getSizeInCellsY()) * master_grid.getResolution();
265 
266  // Calculate the AABB of the local window in path frame
267  double win_min_x = map_min_x, win_max_x = map_max_x;
268  double win_min_y = map_min_y, win_max_y = map_max_y;
269  if (need_transform) {
270  tf2::Transform tf;
271  tf2::fromMsg(transform.transform, tf); // path_frame -> global_frame
272  const tf2::Transform inv = tf.inverse(); // global_frame -> path_frame
273 
274  win_min_x = win_min_y = std::numeric_limits<double>::max();
275  win_max_x = win_max_y = std::numeric_limits<double>::lowest();
276 
277  const double cxs[4] = {map_min_x, map_max_x, map_max_x, map_min_x};
278  const double cys[4] = {map_min_y, map_min_y, map_max_y, map_max_y};
279  for (int c = 0; c < 4; ++c) {
280  const tf2::Vector3 p = inv * tf2::Vector3(cxs[c], cys[c], 0.0);
281  win_min_x = std::min(win_min_x, p.x());
282  win_max_x = std::max(win_max_x, p.x());
283  win_min_y = std::min(win_min_y, p.y());
284  win_max_y = std::max(win_max_y, p.y());
285  }
286  }
287 
288  // Extract path segments from local window.
289  for (size_t i = 1; i < current_path.poses.size(); ++i) {
290  const auto & pa = current_path.poses[i - 1].pose.position;
291  const auto & pb = current_path.poses[i].pose.position;
292 
293  // Cull in the path frame, before transforming.
294  const double seg_min_x = std::min(pa.x, pb.x) - inflation_radius_;
295  const double seg_max_x = std::max(pa.x, pb.x) + inflation_radius_;
296  const double seg_min_y = std::min(pa.y, pb.y) - inflation_radius_;
297  const double seg_max_y = std::max(pa.y, pb.y) + inflation_radius_;
298  if (seg_max_x < win_min_x || seg_min_x > win_max_x ||
299  seg_max_y < win_min_y || seg_min_y > win_max_y)
300  {
301  continue;
302  }
303 
304  // Calculate transform for path segments that are within the transformed local window.
305  double ax = pa.x, ay = pa.y, bx = pb.x, by = pb.y;
306  if (need_transform) {
307  geometry_msgs::msg::PoseStamped transformed_start;
308  geometry_msgs::msg::PoseStamped transformed_end;
309  tf2::doTransform(current_path.poses[i - 1], transformed_start, transform);
310  tf2::doTransform(current_path.poses[i], transformed_end, transform);
311  ax = transformed_start.pose.position.x;
312  ay = transformed_start.pose.position.y;
313  bx = transformed_end.pose.position.x;
314  by = transformed_end.pose.position.y;
315 
316  // Re-cull in the costmap frame to ensure that only segments that touch the costmap are kept.
317  const double min_x = std::min(ax, bx) - inflation_radius_;
318  const double max_x = std::max(ax, bx) + inflation_radius_;
319  const double min_y = std::min(ay, by) - inflation_radius_;
320  const double max_y = std::max(ay, by) + inflation_radius_;
321  if (max_x < map_min_x || min_x > map_max_x || max_y < map_min_y || min_y > map_max_y) {
322  continue;
323  }
324  }
325 
326  local_path_segments.push_back({{ax, ay}, {bx, by}});
327  }
328  return local_path_segments;
329 }
330 
331 Side
332 AsymmetricInflationLayer::computeObstacleSide(
333  double cx, double cy,
334  const std::vector<size_t> & candidates,
335  const std::vector<AsymmetricPathSegment> & local_path_segments)
336 {
337  const double inflation_radius_sq = inflation_radius_ * inflation_radius_;
338 
339  double min_dist_sq = std::numeric_limits<double>::max();
340  double best_cross = 0.0;
341 
342  // Evaluate candidate segments provided by the spatial hash.
343  for (size_t p : candidates) {
344  // Define segment endpoints A (start) and B (end).
345  const auto & segment = local_path_segments[p];
346  double ax = segment.start.first;
347  double ay = segment.start.second;
348  double bx = segment.end.first;
349  double by = segment.end.second;
350 
351  // Skip cells outside of the segment bounding box expanded by the inflation radius.
352  double min_x = std::min(ax, bx) - inflation_radius_;
353  double max_x = std::max(ax, bx) + inflation_radius_;
354  if (cx < min_x || cx > max_x) {continue;}
355 
356  double min_y = std::min(ay, by) - inflation_radius_;
357  double max_y = std::max(ay, by) + inflation_radius_;
358  if (cy < min_y || cy > max_y) {continue;}
359 
360  // Calculate distance to path segment and orientation via cross product.
361  // Vectors: AB (path segment) and AC (path to cell)
362  double abx = bx - ax;
363  double aby = by - ay;
364  double len_sq = abx * abx + aby * aby;
365 
366  double acx = cx - ax;
367  double acy = cy - ay;
368 
369  double dist_sq;
370  double cross;
371 
372  // Smallest expected squared segment length
373  double min_segment_length_sq = 1e-6;
374 
375  // Prevent division by zero for zero-length segments
376  if (len_sq < min_segment_length_sq) {
377  dist_sq = acx * acx + acy * acy;
378  cross = 0.0;
379  } else {
380  // 't': Scalar projection of C onto AB, clamped to segment bounds [0, 1]
381  double t = std::clamp((acx * abx + acy * aby) / len_sq, 0.0, 1.0);
382 
383  // Vector from the projected point on AB to C
384  double proj_dx = acx - t * abx;
385  double proj_dy = acy - t * aby;
386  dist_sq = proj_dx * proj_dx + proj_dy * proj_dy;
387 
388  // 2D cross product for orientation (Positive = Left, Negative = Right)
389  cross = abx * acy - aby * acx;
390  }
391 
392  // Update if shortest distance so far
393  if (dist_sq < min_dist_sq) {
394  min_dist_sq = dist_sq;
395  best_cross = cross;
396  }
397  }
398 
399  // Check if the cell is outside the inflation radius.
400  if (min_dist_sq > inflation_radius_sq) {
401  return Side::Neutral;
402  }
403 
404  // Return the orientation based on the cross product of the closest segment.
405  if (best_cross > 0.0) {return Side::Left;}
406  if (best_cross < 0.0) {return Side::Right;}
407 
408  return Side::Neutral;
409 }
410 
411 void
412 AsymmetricInflationLayer::updateCosts(
413  nav2_costmap_2d::Costmap2D & master_grid, int min_i, int min_j,
414  int max_i, int max_j)
415 {
416  std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
417 
418  if (!enabled_ || cell_inflation_radius_ == 0) {
419  return;
420  }
421 
422  // Pass 1: symmetric baseline via inherited distance-transform inflation
423  InflationLayer::updateCosts(master_grid, min_i, min_j, max_i, max_j);
424 
425  std::vector<AsymmetricPathSegment> local_path_segments = extractLocalPathSegments(master_grid);
426 
427  // Whether the disfavored-side overlay applies this cycle.
428  const bool asymmetry_active =
429  !local_path_segments.empty() && cost_scaling_factor_left_ != cost_scaling_factor_right_;
430 
431  // Force full-map reinflation if the asymmetry state has changed since the last cycle.
432  if (asymmetry_active != asymmetry_active_) {
433  asymmetry_active_ = asymmetry_active;
434  need_reinflation_ = true;
435  setCurrent(false);
436  }
437 
438  // Abort if we don't have a valid path or if the scaling rates are equal (no asymmetry).
439  if (!asymmetry_active) {
440  setCurrent(true);
441  return;
442  }
443 
444  // Pass 2: disfavored-side asymmetric overlay via distance transform
445  unsigned char * master_array = master_grid.getCharMap();
446  const unsigned int size_x = master_grid.getSizeInCellsX();
447  const unsigned int size_y = master_grid.getSizeInCellsY();
448 
449  // Clamp update window (mirrors InflationLayer::updateCosts)
450  const int cmin_i = std::max(0, min_i);
451  const int cmin_j = std::max(0, min_j);
452  const int cmax_i = std::min(static_cast<int>(size_x), max_i);
453  const int cmax_j = std::min(static_cast<int>(size_y), max_j);
454 
455  // Padded ROI — same formula as InflationLayer::updateCosts
456  const int padding = static_cast<int>(cell_inflation_radius_);
457  const int roi_min_i = std::max(0, cmin_i - padding);
458  const int roi_min_j = std::max(0, cmin_j - padding);
459  const int roi_max_i = std::min(static_cast<int>(size_x), cmax_i + padding);
460  const int roi_max_j = std::min(static_cast<int>(size_y), cmax_j + padding);
461  const int roi_width = roi_max_i - roi_min_i;
462  const int roi_height = roi_max_j - roi_min_j;
463 
464  auto spatial_hash = buildPathSpatialHash(local_path_segments);
465 
466  MatrixXfRM dist_map = seedDistanceMap(
467  master_grid, roi_min_i, roi_min_j, roi_width, roi_height,
468  spatial_hash, local_path_segments);
469 
470  DistanceTransform::distanceTransform2D(dist_map, roi_height, roi_width);
471 
472  applyInflation(
473  master_array, dist_map,
474  cmin_i, cmin_j, cmax_i, cmax_j,
475  roi_min_i, roi_min_j, size_x);
476 
477  setCurrent(true);
478 }
479 
480 std::unordered_map<uint64_t, std::vector<size_t>>
481 AsymmetricInflationLayer::buildPathSpatialHash(
482  const std::vector<AsymmetricPathSegment> & local_path_segments)
483 {
484  std::unordered_map<uint64_t, std::vector<size_t>> spatial_hash;
485 
486  for (size_t p = 0; p < local_path_segments.size(); ++p) {
487  // Create segment AB from an original consecutive path pose pair.
488  const auto & segment = local_path_segments[p];
489  double ax = segment.start.first;
490  double ay = segment.start.second;
491  double bx = segment.end.first;
492  double by = segment.end.second;
493 
494  // Pad the segment's bounding box by the inflation radius.
495  double min_x = std::min(ax, bx) - inflation_radius_;
496  double max_x = std::max(ax, bx) + inflation_radius_;
497  double min_y = std::min(ay, by) - inflation_radius_;
498  double max_y = std::max(ay, by) + inflation_radius_;
499 
500  // Find which buckets this padded segment touches
501  int64_t min_bx = static_cast<int64_t>(std::floor(min_x / inflation_radius_));
502  int64_t max_bx = static_cast<int64_t>(std::floor(max_x / inflation_radius_));
503  int64_t min_by = static_cast<int64_t>(std::floor(min_y / inflation_radius_));
504  int64_t max_by = static_cast<int64_t>(std::floor(max_y / inflation_radius_));
505 
506  for (int64_t b_x = min_bx; b_x <= max_bx; ++b_x) {
507  for (int64_t b_y = min_by; b_y <= max_by; ++b_y) {
508  // Bitwise magic to safely map 2D signed coordinates into a 1D unsigned 64-bit key
509  uint64_t key = (static_cast<uint64_t>(static_cast<uint32_t>(b_x)) << 32) |
510  (static_cast<uint32_t>(b_y));
511 
512  spatial_hash[key].push_back(p);
513  }
514  }
515  }
516 
517  return spatial_hash;
518 }
519 
521 AsymmetricInflationLayer::seedDistanceMap(
522  nav2_costmap_2d::Costmap2D & master_grid,
523  int roi_min_i, int roi_min_j, int roi_width, int roi_height,
524  const std::unordered_map<uint64_t, std::vector<size_t>> & spatial_hash,
525  const std::vector<AsymmetricPathSegment> & local_path_segments)
526 {
527  unsigned char * master_array = master_grid.getCharMap();
528  const unsigned int size_x = master_grid.getSizeInCellsX();
529  const unsigned int size_y = master_grid.getSizeInCellsY();
530 
531  MatrixXfRM dist_map(roi_height, roi_width);
532  dist_map.setConstant(DistanceTransform::DT_INF);
533 
534  Side disfavored_side = (cost_scaling_factor_left_ < cost_scaling_factor_right_) ?
535  Side::Left : Side::Right;
536  const int roi_max_i = roi_min_i + roi_width;
537  const int roi_max_j = roi_min_j + roi_height;
538 
539  // Helper function to check if a neighbor is "traversable" (i.e., open space)
540  auto is_traversable = [&](int nx, int ny) {
541  unsigned char c = master_array[master_grid.getIndex(nx, ny)];
542  return inflate_around_unknown_ ?
543  (c != LETHAL_OBSTACLE && c != NO_INFORMATION) : (c != LETHAL_OBSTACLE);
544  };
545 
546  // Seed all obstacle boundary cells, that are nearby a path segment
547  for (int j = roi_min_j; j < roi_max_j; ++j) {
548  for (int i = roi_min_i; i < roi_max_i; ++i) {
549  unsigned char cost = master_array[master_grid.getIndex(i, j)];
550 
551  // Early exit 1: Skip cells that aren't lethal/unknown obstacles
552  if (cost != LETHAL_OBSTACLE && !(inflate_around_unknown_ && cost == NO_INFORMATION)) {
553  continue;
554  }
555 
556  // Check if the cell touches the absolute edges of the costmap
557  bool is_on_map_edge = (i == 0 || i == static_cast<int>(size_x) - 1 ||
558  j == 0 || j == static_cast<int>(size_y) - 1);
559 
560  // An obstacle cell is a boundary if it's on the map edge OR touches free space.
561  bool is_boundary = is_on_map_edge ||
562  is_traversable(i - 1, j) || is_traversable(i + 1, j) ||
563  is_traversable(i, j - 1) || is_traversable(i, j + 1);
564 
565  // Early exit 2: Skip interior obstacle cells
566  if (!is_boundary) {
567  continue;
568  }
569 
570  // Find segments that are nearby this cell using the spatial hash
571  double cx, cy;
572  master_grid.mapToWorld(i, j, cx, cy);
573  int64_t b_x = static_cast<int64_t>(std::floor(cx / inflation_radius_));
574  int64_t b_y = static_cast<int64_t>(std::floor(cy / inflation_radius_));
575  uint64_t key = (static_cast<uint64_t>(static_cast<uint32_t>(b_x)) << 32) |
576  static_cast<uint32_t>(b_y);
577 
578  // Only enqueue boundary cells on the disfavored side of the path.
579  // Cells on favored side already got correctly inflated during the symmetric inflation pass.
580  auto it = spatial_hash.find(key);
581  if (it == spatial_hash.end()) {
582  continue;
583  }
584 
585  // Determine which side of the path this cell is on
586  Side side = computeObstacleSide(cx, cy, it->second, local_path_segments);
587  if (side != Side::Neutral && side == disfavored_side) {
588  dist_map(j - roi_min_j, i - roi_min_i) = 0.0f;
589  }
590  }
591  }
592 
593  return dist_map;
594 }
595 
596 void
597 AsymmetricInflationLayer::applyInflation(
598  unsigned char * master_array,
599  const MatrixXfRM & distance_map,
600  int min_i, int min_j, int max_i, int max_j,
601  int roi_min_i, int roi_min_j,
602  unsigned int size_x)
603 {
604  if (cost_lut_disfavored_.empty()) {
605  return;
606  }
607 
608  const float cell_inflation_radius_f = static_cast<float>(cell_inflation_radius_);
609  const int lut_max = static_cast<int>(cost_lut_disfavored_.size() - 1);
610  const unsigned char * lut_data = cost_lut_disfavored_.data();
611  const int lut_precision = COST_LUT_PRECISION;
612 
613 #ifdef _OPENMP
614  const int num_threads = getOptimalThreadCount();
615  #pragma omp parallel for num_threads(num_threads) schedule(dynamic, 16)
616 #endif
617  for (int j = min_j; j < max_j; ++j) {
618  const int row_offset = j * static_cast<int>(size_x);
619  const int dist_row = j - roi_min_j;
620 
621  for (int i = min_i; i < max_i; ++i) {
622  const float distance_cells = distance_map(dist_row, i - roi_min_i);
623  if (distance_cells > cell_inflation_radius_f) {
624  continue;
625  }
626 
627  const unsigned int index = row_offset + i;
628  const unsigned char old_cost = master_array[index];
629  const unsigned int d_scaled = std::min(
630  static_cast<unsigned int>(lut_max),
631  static_cast<unsigned int>(distance_cells * lut_precision + 0.5f));
632  const unsigned char new_cost = lut_data[d_scaled];
633 
634  if (new_cost > old_cost) {
635  master_array[index] = new_cost;
636  }
637  }
638  }
639 }
640 
641 void
642 AsymmetricInflationLayer::computeAsymmetricCaches()
643 {
644  std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
645 
646  if (cell_inflation_radius_ == 0) {
647  return;
648  }
649 
650  // Build cost LUT for the disfavored side using c_side (the smaller scaling factor).
651  // computeCost() always uses cost_scaling_factor_ (c_max), so we inline the formula with c_side.
652  const double c_side = std::min(cost_scaling_factor_left_, cost_scaling_factor_right_);
653  const unsigned int max_dist_scaled = cell_inflation_radius_ * COST_LUT_PRECISION + 1;
654 
655  cost_lut_disfavored_.resize(max_dist_scaled + 1);
656  for (unsigned int d_scaled = 0; d_scaled <= max_dist_scaled; ++d_scaled) {
657  const double distance = static_cast<double>(d_scaled) / COST_LUT_PRECISION;
658  unsigned char cost = 0;
659  if (distance == 0.0) {
660  cost = LETHAL_OBSTACLE;
661  } else if (distance * resolution_ <= inscribed_radius_) {
662  cost = INSCRIBED_INFLATED_OBSTACLE;
663  } else {
664  double factor = exp(-c_side * (distance * resolution_ - inscribed_radius_));
665  cost = static_cast<unsigned char>((INSCRIBED_INFLATED_OBSTACLE - 1) * factor);
666  }
667  cost_lut_disfavored_[d_scaled] = cost;
668  }
669 }
670 
671 rcl_interfaces::msg::SetParametersResult
672 AsymmetricInflationLayer::validateParameterUpdatesCallback(
673  const std::vector<rclcpp::Parameter> & parameters)
674 {
675  // The parent callback is name-agnostic: it rejects any negative double.
676  // Since every parameter this layer adds requires >= 0, delegating validates them all.
677  return InflationLayer::validateParameterUpdatesCallback(parameters);
678 }
679 
680 void
681 AsymmetricInflationLayer::updateParametersCallback(
682  const std::vector<rclcpp::Parameter> & parameters)
683 {
684  std::vector<rclcpp::Parameter> base_parameters;
685  base_parameters.reserve(parameters.size());
686  for (const auto & parameter : parameters) {
687  if (parameter.get_name() != name_ + ".cost_scaling_factor") {
688  base_parameters.push_back(parameter);
689  }
690  }
691  InflationLayer::updateParametersCallback(base_parameters);
692 
693  std::lock_guard<Costmap2D::mutex_t> guard(*getMutex());
694  bool side_scaling_changed = false;
695 
696  for (const auto & parameter : parameters) {
697  const auto & param_type = parameter.get_type();
698  const auto & param_name = parameter.get_name();
699  if (param_name.find(name_ + ".") != 0) {
700  continue;
701  }
702 
703  if (param_type == ParameterType::PARAMETER_DOUBLE) {
704  if (param_name == name_ + ".cost_scaling_factor_left" && // NOLINT
705  cost_scaling_factor_left_ != parameter.as_double())
706  {
707  cost_scaling_factor_left_ = parameter.as_double();
708  side_scaling_changed = true;
709  } else if (param_name == name_ + ".cost_scaling_factor_right" && // NOLINT
710  cost_scaling_factor_right_ != parameter.as_double())
711  {
712  cost_scaling_factor_right_ = parameter.as_double();
713  side_scaling_changed = true;
714  } else if (param_name == name_ + ".goal_distance_threshold" && // NOLINT
715  goal_distance_threshold_ != parameter.as_double())
716  {
717  goal_distance_threshold_ = parameter.as_double();
718  need_reinflation_ = true;
719  setCurrent(false);
720  } else if (param_name == name_ + ".transform_staleness_threshold") { // NOLINT
721  transform_staleness_threshold_ = parameter.as_double();
722  need_reinflation_ = true;
723  setCurrent(false);
724  }
725  }
726  }
727 
728  if (side_scaling_changed) {
729  cost_scaling_factor_ =
730  std::max(cost_scaling_factor_left_, cost_scaling_factor_right_);
731  need_reinflation_ = true;
732  setCurrent(false);
733  matchSize();
734  }
735 }
736 
737 } // namespace nav2_costmap_2d
A QoS profile for standard reliable topics with a history of 10 messages.
Costmap layer that inflates obstacles asymmetrically relative to the global path, biasing the navigab...
A 2D costmap provides a mapping between points in the world and their associated "costs".
Definition: costmap_2d.hpp:69
void mapToWorld(unsigned int mx, unsigned int my, double &wx, double &wy) const
Convert from map coordinates to world coordinates.
Definition: costmap_2d.cpp:280
unsigned int getIndex(unsigned int mx, unsigned int my) const
Given two map coordinates... compute the associated index.
Definition: costmap_2d.hpp:231
unsigned char * getCharMap() const
Will return a pointer to the underlying unsigned char array used as the costmap.
Definition: costmap_2d.cpp:260
double getResolution() const
Accessor for the resolution of the costmap.
Definition: costmap_2d.cpp:578
unsigned int getSizeInCellsX() const
Accessor for the x size of the costmap in cells.
Definition: costmap_2d.cpp:548
double getOriginY() const
Accessor for the y origin of the costmap.
Definition: costmap_2d.cpp:573
unsigned int getSizeInCellsY() const
Accessor for the y size of the costmap in cells.
Definition: costmap_2d.cpp:553
double getOriginX() const
Accessor for the x origin of the costmap.
Definition: costmap_2d.cpp:568
Abstract class for layered costmap plugin implementations.
Definition: layer.hpp:62
Eigen::Matrix< float, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor > MatrixXfRM
Row-major float matrix type for efficient row-wise access.