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