Skip to content

Calculate next path point using lookahead for Graceful Controller #5003

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

SakshayMahna
Copy link


Basic Info

Info Please fill out this column
Ticket(s) this addresses #4865
Primary OS tested on Ubuntu
Robotic platform tested on Gazebo Simulation of Turtlebot3
Does this PR contain AI generated software? No

Description of contribution in a few bullet points

  • Added default parameter files for Regulated Pure Pursuit Controller and Graceful Controller
  • Added 2 new parameters for Graceful Controller (lookahead_resolution and interpolate_after_goal)
  • Added lookahead logic to calculate next path point for Graceful Controller

Additional Information

  • Graceful Controller now calculates the next point on path using lookahead logic, it starts from a maximum lookahead value and searches for a path using lookahead distance in intervals of lookahead_resolution.
  • The controller also calculates interpolated point after goal. This can be switched on and off using the parameter interpolate_after_goal.

Description of documentation updates required from your changes

  • Added new parameter, so need to add that to default configs and documentation page.

Description of how this change was tested

  • Same unit tests and tested in simulation.

Future work that may be required in bullet points

  • The interpolation after goal logic seems a bit problematic and may need refinement.

For Maintainers:

  • Check that any new parameters added are updated in docs.nav2.org
  • Check that any significant change is added to the migration guide
  • Check that any new features OR changes to existing behaviors are reflected in the tuning guide
  • Check that any new functions have Doxygen added
  • Check that any new features have test coverage
  • Check that any new plugins is added to the plugins page
  • If BT Node, Additionally: add to BT's XML index of nodes for groot, BT package's readme table, and BT library lists

Copy link
Contributor

mergify bot commented Mar 22, 2025

@SakshayMahna, your PR has failed to build. Please check CI outputs and resolve issues.
You may need to rebase or pull in main due to API changes (or your contribution genuinely fails).

Copy link
Contributor

mergify bot commented Mar 23, 2025

@SakshayMahna, your PR has failed to build. Please check CI outputs and resolve issues.
You may need to rebase or pull in main due to API changes (or your contribution genuinely fails).

1 similar comment
Copy link
Contributor

mergify bot commented Mar 23, 2025

@SakshayMahna, your PR has failed to build. Please check CI outputs and resolve issues.
You may need to rebase or pull in main due to API changes (or your contribution genuinely fails).

Copy link

codecov bot commented Mar 29, 2025

Codecov Report

Attention: Patch coverage is 87.64045% with 11 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...v2_graceful_controller/src/graceful_controller.cpp 57.69% 11 Missing ⚠️
Files with missing lines Coverage Δ
...e/nav2_graceful_controller/graceful_controller.hpp 100.00% <ø> (ø)
...ude/nav2_graceful_controller/parameter_handler.hpp 100.00% <ø> (ø)
nav2_graceful_controller/src/parameter_handler.cpp 100.00% <100.00%> (ø)
...t_controller/regulated_pure_pursuit_controller.hpp 100.00% <ø> (ø)
...ntroller/src/regulated_pure_pursuit_controller.cpp 85.39% <100.00%> (-2.79%) ⬇️
nav2_util/src/controller_utils.cpp 100.00% <100.00%> (ø)
...v2_graceful_controller/src/graceful_controller.cpp 81.60% <57.69%> (-8.09%) ⬇️

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Member

@SteveMacenski SteveMacenski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking aloud a bit:

This bit of code looks like we iterate through the path, try to find the max_lookahead distance away and back up closer and closer to the robot until we find a solution that is collision free.

Is that case, we should attempt to find the interpolated lookahead point for the max_lookahead away to start off with, that gives us the best and smoothest value.

After that point, its a bit arbitrary the distance selected, isn't it? Selecting a lookahead_resolution versus marching through the path points I don't think improves the smoothness, does it? Either way there's a discontinuity now on the distance away to use based on the collision state (unless we set the search resolution to be very fine, which is probably not computationally worthwhile).

For example, the resolution you set was 0.1, but a normal path from a planning plugin probably does it closer to 0.05 for the costmap resolution when that resolution is 5cm. So wouldn't this case actually be more coarse?

Did you test this without the lookahead_resolution feature and generate those charts using the normal path point iteration? I suspect the improvement in smoothness you see is when the max_lookahead is used due to lack of collision requiring backing up the path to finding the first valid trajectory. My intuition says that when we do the backing-up process, we own that discontinuity either way, given we're backing up by 5cm or 10cm, respectively.

@SakshayMahna
Copy link
Author

This bit of code looks like we iterate through the path, try to find the max_lookahead distance away and back up closer and closer to the robot until we find a solution that is collision free.

Yes, this is exactly what is being done. The reasoning behind this was, in case max_lookahead distance has a collision, there is atleast some solution (found by backing up closer to robot) that can be followed.

Is that case, we should attempt to find the interpolated lookahead point for the max_lookahead away to start off with, that gives us the best and smoothest value.

After that point, its a bit arbitrary the distance selected, isn't it? Selecting a lookahead_resolution versus marching through the path points I don't think improves the smoothness, does it? Either way there's a discontinuity now on the distance away to use based on the collision state (unless we set the search resolution to be very fine, which is probably not computationally worthwhile).

For example, the resolution you set was 0.1, but a normal path from a planning plugin probably does it closer to 0.05 for the costmap resolution when that resolution is 5cm. So wouldn't this case actually be more coarse?

Makes sense, the discontinuity is introduced once we move to another lookahead point. That's why thought of putting lookahead_resolution to adjust what may work best for the user.

Did you test this without the lookahead_resolution feature and generate those charts using the normal path point iteration? I suspect the improvement in smoothness you see is when the max_lookahead is used due to lack of collision requiring backing up the path to finding the first valid trajectory. My intuition says that when we do the backing-up process, we own that discontinuity either way, given we're backing up by 5cm or 10cm, respectively.

Velocity plot for using points on path (not using lookahead_resolution) seems almost similar:
choose_point_v

As long as the lookahead distance remains the same, the path is as smooth as it can be. It's only in case of collision we may need a strategy to generate a different solution for the robot. In this case the strategy is to reduce the lookahead distance, which introduces discontinuity. Possibly a different strategy may minimize this discontinuity.

@SteveMacenski
Copy link
Member

That's why thought of putting lookahead_resolution to adjust what may work best for the user.

Do you see an improvement using the lookahead_resolution instead of marching through the path as originally done in the back up procedure?

By default you have it set to 0.1, but the path resolution is ~0.05. If you lower that to 0.05 or even lower like 0.02, is it smoother than before?

If so, I think that's a neat feature and we can keep it, users can tune to their liking and we can set it default to the costmap resolution.

If not, I think maybe we should revert that bit and only use the interpolation when using the full lookahead distance. Else, just use the path points since there's a jump anyway.

@SakshayMahna
Copy link
Author

No, I don't see any improvement while using a smaller lookahead resolution.

The plots for the 3 cases (smaller resolution, larger resolution, using points) all look similar.
I'll just use the path points then.

@@ -195,21 +195,19 @@ geometry_msgs::msg::TwistStamped GracefulController::computeVelocityCommands(
// Else, fall through and see if we should follow control law longer
}

// Precompute distance to candidate poses
std::vector<double> target_distances;
computeDistanceAlongPath(transformed_plan.poses, target_distances);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this will cause a minor regression - the getLookaheadPoint() function simply looks at the distance between the target point and the robot - not the distance ALONG the path. In some instances, this can lead to the controller massively shortcutting the desired path.

I would suggest making getLookaheadPoint use distance along path instead of hypot().

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right!
I can make this change in this PR.
Or another issue/PR would be appropriate for this?

Copy link
Member

@SteveMacenski SteveMacenski Apr 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having it done here is important since that is a regression from the current behavior in this branch 😄

Also, it is a nice feature which RPP would also share in since they share the implementation! I know that's actually been a topic of conversion in other threads / tickets to add that feature, so I think that's wonderful to add in

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SakshayMahna I believe this item is resolved, correct? @mikeferguson can you take another look?

@@ -195,21 +195,19 @@ geometry_msgs::msg::TwistStamped GracefulController::computeVelocityCommands(
// Else, fall through and see if we should follow control law longer
}

// Precompute distance to candidate poses
std::vector<double> target_distances;
computeDistanceAlongPath(transformed_plan.poses, target_distances);
Copy link
Member

@SteveMacenski SteveMacenski Apr 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having it done here is important since that is a regression from the current behavior in this branch 😄

Also, it is a nice feature which RPP would also share in since they share the implementation! I know that's actually been a topic of conversion in other threads / tickets to add that feature, so I think that's wonderful to add in

@@ -195,21 +195,19 @@ geometry_msgs::msg::TwistStamped GracefulController::computeVelocityCommands(
// Else, fall through and see if we should follow control law longer
}

// Precompute distance to candidate poses
std::vector<double> target_distances;
computeDistanceAlongPath(transformed_plan.poses, target_distances);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SakshayMahna I believe this item is resolved, correct? @mikeferguson can you take another look?

Copy link
Member

@SteveMacenski SteveMacenski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's one superficial refactor that would in my opinion make this easier to read / understand:

  // Calculate target pose through lookahead interpolation
  double dist_to_target = params_->max_lookahead;
  geometry_msgs::msg::PoseStamped target_pose = nav2_util::getLookAheadPoint(
    dist_to_target, transformed_plan, params_->interpolate_after_goal);
  bool valid_target_pose_found = validateTargetPose(target_pose, dist_to_target, dist_to_goal, local_plan, costmap_transform, cmd_vel);

  if (!valid_target_pose_found) {
    ...
    ...

    valid_target_pose_found = validateTargetPose(target_pose, dist_to_target, dist_to_goal, local_plan, costmap_transform, cmd_vel);
    if (valid_target_pose_found) {
      break;
    }
  }

This would remove the else statement and explicitly setting the valid_target_pose_found instead of just capturing the validate method's return.

@SteveMacenski
Copy link
Member

@SakshayMahna can you work on these last couple of updates?

@SakshayMahna
Copy link
Author

Hello Steve,
Yes, missed the last few notifications and emails. Working on it.

Copy link
Contributor

mergify bot commented May 28, 2025

This pull request is in conflict. Could you fix it @SakshayMahna?

@SakshayMahna
Copy link
Author

Hi @SteveMacenski
I did make the requested changes. However, I am not able to think of a way to get the code coverage to target (90.72%), I am stuck at 87.64%.

What I understand it is because of the fallback code that was added, that in case Lookahead Point is not found, fallback to default behaviour of checking the poses one by one.

The only way I think this can be tested is by adding an obstacle in the cost map. That way, the Lookahead Point will become invalid because of collision check by simulate_trajectory, and the execution would go to fallback code. But still I have doubts on how to make this work, because there is a set number of max iterations simulate_trajectory works for.

What do you think about this? Any ideas to improve the code coverage?

Copy link
Member

@SteveMacenski SteveMacenski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some of these changes in refactoring will resolve the testing gap! :-)


d += std::hypot(curr_pose.x - prev_pose.x, curr_pose.y - prev_pose.y);
if (d >= lookahead_dist) {
goal_pose_it = poses.begin() + i;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplify: I think you can just set goal_pose_it = poses.begin() + i; each iteration and when d >= lookahead_dist only do the break. That would let you remove the pose_found logic and the later goal_pose_it = poses.end();.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I gave the suggested implementation quite some tries. But for the suggested one, the robot gets assigned a goal pose beyond the end point automatically (even with interpolate_after_goal=false). And the behaviour just doesn't feel right.

However, with the current implementation everything seems perfectly fine.

I don't understand the reason, but I'm sure I maybe missing something.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make a branch off this one and send a link here? I'll take a look

geometry_msgs::msg::PoseStamped target_pose = nav2_util::getLookAheadPoint(
dist_to_target, transformed_plan, params_->interpolate_after_goal);

valid_target_pose_found = validateTargetPose(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if getLookAheadPoint returns transformed_plan.poses.back() because !pose_found? Then dist_to_target may not be params_->max_lookahead for the input

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loop in getLookAheadPoint iterates from each pose calculating distance along path. The loop for it only breaks when distance found is greater than max_lookahead. The only case, we get !pose_found is when the distance is not able to add up to max_lookahead.

This is fine, because the logic inside validateTargetPose only rejects the point if it is at a distance greater than max_lookahead or less than min_lookahead. Otherwise, the code logic continues to run the same.

Copy link
Member

@SteveMacenski SteveMacenski Jun 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm saying that you ahve this block of code:


      dist_to_target = params_->max_lookahead;
      target_pose = nav2_util::getLookAheadPoint(
        dist_to_target, transformed_plan, params_->interpolate_after_goal);

If the target_pose is not exactly max_lookahead away because !pose_found, then the inputs to validateTargetPose aren't consistent. The target_pose is not actually dist_to_target away. dist_to_target is not updated based on the actual return of getLookAheadPoint. dist_to_target == params_->max_lookahead always in this case even when target_pose is the last pose instead.

I think something like the following is necessary:

if (target_pose == transformed_plan.poses.back() {
  // update dist_to_target
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, understood now. It makes sense to change the lookahead distance if the path is clipped to end at the last pose.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SakshayMahna OK! Let me know when its implemented :-)

Copy link
Contributor

mergify bot commented Jun 8, 2025

This pull request is in conflict. Could you fix it @SakshayMahna?

Copy link
Contributor

mergify bot commented Jun 8, 2025

@SakshayMahna, your PR has failed to build. Please check CI outputs and resolve issues.
You may need to rebase or pull in main due to API changes (or your contribution genuinely fails).

Copy link
Contributor

mergify bot commented Jun 8, 2025

This pull request is in conflict. Could you fix it @SakshayMahna?

Signed-off-by: Sakshay Mahna <sakshum19@gmail.com>
@SakshayMahna SakshayMahna reopened this Jun 8, 2025
@SakshayMahna
Copy link
Author

SakshayMahna commented Jun 8, 2025

Hi @SteveMacenski
I made a mistake while syncing the main branch. And managed to get all my commits deleted.
However, I had a backup, and was able to recover the relevant changes.
And I tested the changes running on simulation as well.

Sorry for the mess.

Signed-off-by: Sakshay Mahna <sakshum19@gmail.com>
Signed-off-by: Sakshay Mahna <sakshum19@gmail.com>
Copy link
Member

@SteveMacenski SteveMacenski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have a few open comments above, but overall doesn't this look simpler and cleaner! Great work 😄


d += std::hypot(curr_pose.x - prev_pose.x, curr_pose.y - prev_pose.y);
if (d >= lookahead_dist) {
goal_pose_it = poses.begin() + i;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

} else if (dist_to_target < params_->min_lookahead) {
// Make sure target is far enough away to avoid instability
break;
for (int i = transformed_plan.poses.size(); i >= 0; --i) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you make this an unsigned int instead of an int, then you don't need the static_cast below and nothing else needs to change

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On making it unsigned int, i>=0 condition will always remain valid.
And I am getting a compiler error for the same.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SakshayMahna unsigned int i = transformed_plan.poses.size(); i >= 0; --i then if (i == transformed_plan.poses.size())

Signed-off-by: Sakshay Mahna <sakshum19@gmail.com>
Signed-off-by: Sakshay Mahna <sakshum19@gmail.com>
Signed-off-by: Sakshay Mahna <sakshum19@gmail.com>
Copy link
Contributor

mergify bot commented Jun 16, 2025

@SakshayMahna, your PR has failed to build. Please check CI outputs and resolve issues.
You may need to rebase or pull in main due to API changes (or your contribution genuinely fails).

@SteveMacenski
Copy link
Member

@SakshayMahna please rebase on main to make CI turn over. There was a message filter API change that we resolved in main

Copy link
Member

@SteveMacenski SteveMacenski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super close now! Take a look at the remaining open items from previous reviews (should be 3x) :-) Thanks for the iterations - this is much improved!

@@ -35,6 +35,7 @@ struct Parameters
double transform_tolerance;
double min_lookahead;
double max_lookahead;
bool interpolate_after_goal;
Copy link
Member

@SteveMacenski SteveMacenski Jun 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove since now unused :-)

} else if (dist_to_target < params_->min_lookahead) {
// Make sure target is far enough away to avoid instability
break;
for (int i = transformed_plan.poses.size(); i >= 0; --i) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SakshayMahna unsigned int i = transformed_plan.poses.size(); i >= 0; --i then if (i == transformed_plan.poses.size())


d += std::hypot(curr_pose.x - prev_pose.x, curr_pose.y - prev_pose.y);
if (d >= lookahead_dist) {
goal_pose_it = poses.begin() + i;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make a branch off this one and send a link here? I'll take a look

geometry_msgs::msg::PoseStamped target_pose = nav2_util::getLookAheadPoint(
dist_to_target, transformed_plan, params_->interpolate_after_goal);

valid_target_pose_found = validateTargetPose(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SakshayMahna OK! Let me know when its implemented :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants