Add AsymmetricInflationLayer costmap plugin for path-aware lane-keeping#6118
Add AsymmetricInflationLayer costmap plugin for path-aware lane-keeping#6118mbloechli wants to merge 10 commits intoros-navigation:mainfrom
Conversation
Signed-off-by: mbloechli <mbloechlinger@duatic.com>
Signed-off-by: mbloechli <mbloechlinger@duatic.com>
Signed-off-by: mbloechli <mbloechlinger@duatic.com>
Signed-off-by: mbloechli <mbloechlinger@duatic.com>
Signed-off-by: mbloechli <mbloechlinger@duatic.com>
Signed-off-by: mbloechli <mbloechlinger@duatic.com>
669704c to
6265c9c
Compare
Signed-off-by: mbloechli <mbloechlinger@duatic.com>
SteveMacenski
left a comment
There was a problem hiding this comment.
Quick initial architectural review:
Out of curiosity, does this share many of the same methods as the legacy layer? If so, can this derive from that layer to make use of the same implementations of those methods?
Does this do incremental updates like the inflation layer? The needs_reinflation_ implies so but I thought you mentioned it does it clean each iteration instead.
| @@ -0,0 +1,807 @@ | |||
| // Copyright (c) 2026 | |||
There was a problem hiding this comment.
You should add yourself if all novel, else the original inflation layer + you
There was a problem hiding this comment.
Done. I've additionally now changed the license of the asymmetric_inflation_layer from apache 2.0 to BSD, because the legacy_inflation_layer was licensed with BSD
| cost_scaling_factor_ = node->declare_or_get_parameter( | ||
| name_ + "." + "cost_scaling_factor", 10.0); | ||
| asymmetry_factor_ = node->declare_or_get_parameter( | ||
| name_ + "." + "asymmetry_factor", 0.75); |
There was a problem hiding this comment.
Please explain the asymmetry_factor and neutral_threshold parameters
There was a problem hiding this comment.
asymmetry_factor
It's a value between 0 and 1, that influences how quickly the wave in BFS travels. When expanding the BFS the lowest distance cells get expanded first. When calculating distance of a cell to it's nearest lethal_cell the distance is scaled by std::max(0.0, 1.0 - asymmetry_factor_ * path_side), where path side is either 1 (left), -1 (right), 0 (neutral)
In short: with the asymmetry_factor the user can change on which side of the path the robot should be incentivized to drive through:
- asymmetry_factor < 0: drive on the left side of the road
- asymmetry_factor = 0: drive in the middle of the road
- asymmetry_factor > 0: drive on the right side of the road
neutral_threshold
Any lethal_cell further away from the path than the neutal_threshold will not be inflated.
| // Sanity check: bins must be empty at the start of each cycle. | ||
| // On failure, log a single summary (avoid per-cell log flood). | ||
| size_t leftover = 0; | ||
| for (const auto & bin : inflation_cells_) { | ||
| leftover += bin.size(); | ||
| } | ||
| RCLCPP_FATAL_EXPRESSION( | ||
| logger_, leftover != 0, | ||
| "AsymmetricInflationLayer: BFS bins not empty at start of updateCosts() " | ||
| "(%zu leftover cells)", leftover); |
There was a problem hiding this comment.
When could this happen and could we add protections then instead? I'm not sure off hand the size of inflation_cells_ but this "it shouldn't happen" log seems like it may take some computation time. Could we make sure things are correct instead?
There was a problem hiding this comment.
This is a log I copied over from legacy_inflation_layer. This log helped me find bugs while I was still developing the asymmetric layer. I didn't delete the logger after finishing the implementation of it, because the log still exists in the legacy_inflation_layer. Do you know, what this log still exists in the legacy_inflation_layer?
| if (seen_.size() != size_x * size_y) { | ||
| RCLCPP_WARN( | ||
| logger_, "AsymmetricInflationLayer::updateCosts(): seen_ vector size is wrong"); | ||
| seen_ = std::vector<bool>(size_x * size_y, false); | ||
| obstacle_side_grid_ = std::vector<int8_t>(size_x * size_y, 0); | ||
| } |
There was a problem hiding this comment.
Shouldn't match size handle this? This seems like an odd place to handle resizing (or that its a WARNING). Costmaps do change size sometimes and that's expected (matchSize)
There was a problem hiding this comment.
Yes, I kept this here, because it also exists in legacy_inflation_layer
| double bucket_size = std::max(neutral_threshold_, 1.0); | ||
| std::unordered_map<uint64_t, std::vector<size_t>> spatial_hash; | ||
|
|
||
| if (use_asymmetry) { |
There was a problem hiding this comment.
I think here and below is all only if use_asymmetry is true. You could check if its false when its computed and just setCurrent(true); return; at that point to unnest this.
|
|
||
| // Spatial Hash Grid Construction | ||
| double bucket_size = std::max(neutral_threshold_, 1.0); | ||
| std::unordered_map<uint64_t, std::vector<size_t>> spatial_hash; |
There was a problem hiding this comment.
This first for loop could probably use a comment to explain what its doing and the hash map. I'm also not 100% sure if this is really the most efficient way to do this, but I'm also not 100% understanding what this is doing from an initial quick glance (i.e. neutral cost, hash table window size of min/maxs)
| } | ||
| } | ||
|
|
||
| // The standard Nav2 inflation_layer has already populated master_array |
There was a problem hiding this comment.
I'm stopping here because I think this method already above is a bit convoluted and I'm not following it already without more comments / explanations. I'd rather read in detail once I understand what the intent is.
Above I'm seeing a for loop for each path point in some window iteratively, then a full grid window iteration, and finally the actual while loop (which can't be avoided). Populating a window-sized data structure and iterating over it again is not very efficient, I wouldn't think.
Yes, this does indeed share some of the same methods with the legacy layer:
I'm not sure though if the additional coupling outweighs the inherited methods.
When I opened the feature request it didn't do incremental updates yet, because when I tried to implement it a month ago, I was having issues with artifacts in the costmap. Yesterday when I was making performance improvements to the updateCosts() and computeObstacleSide() functions, I reimplemented the incremental updates as well and saw now artifacts this time. Maybe I did an error when I tried to implement the iterative updates a month ago. |
Signed-off-by: mbloechli <mbloechlinger@duatic.com>
…egacy_inflation_layer, of which I copied a lot of Signed-off-by: mbloechli <mbloechlinger@duatic.com>
Signed-off-by: mbloechli <mbloechlinger@duatic.com>
Basic Info
Description of contribution in a few bullet points
This contribution adds an Asymmetric Inflation Layer, that encourages lane-keeping behaviour without requiring routing. This works by subscribing to the plan topic -> classifying each lethal obstacle as left or right of the global path, then using asymmetric effective distances to bias the BFS priority queue.
Description of documentation updates required from your changes
The layer will need to be documented along the other layers on the nav2 wiki.
Description of how this change was tested
Future work that may be required in bullet points
For Maintainers:
backport-*.