Nav2 Navigation Stack - lyrical  lyrical
ROS 2 Navigation Stack
adaptive_tolerance_goal_checker.cpp
1 // Copyright (c) 2026, David Grbac
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 <memory>
16 #include <string>
17 #include <limits>
18 #include <vector>
19 #include <cmath>
20 
21 #include "nav2_controller/plugins/adaptive_tolerance_goal_checker.hpp"
22 #include "pluginlib/class_list_macros.hpp"
23 #include "angles/angles.h"
24 #include "nav2_ros_common/node_utils.hpp"
25 #include "nav2_util/geometry_utils.hpp"
26 #include "tf2/utils.hpp"
27 
28 using rcl_interfaces::msg::ParameterType;
29 using std::placeholders::_1;
30 
31 namespace nav2_controller
32 {
33 
35 : fine_xy_goal_tolerance_(0.10),
36  fine_xy_goal_tolerance_sq_(0.01),
37  coarse_xy_goal_tolerance_(0.25),
38  coarse_xy_goal_tolerance_sq_(0.0625),
39  xy_goal_tolerance_buffer_(0.0),
40  fine_xy_goal_tolerance_reset_sq_(0.01),
41  coarse_xy_goal_tolerance_reset_sq_(0.0625),
42  yaw_goal_tolerance_(0.25),
43  path_length_tolerance_(1.0),
44  stateful_(true),
45  symmetric_yaw_tolerance_(false),
46  trans_stopped_velocity_(0.10),
47  rot_stopped_velocity_(0.10),
48  required_stagnation_cycles_(15),
49  check_xy_(true),
50  in_tolerance_zone_(false),
51  stopped_stagnation_count_(0),
52  distance_stagnation_count_(0),
53  best_distance_sq_(std::numeric_limits<double>::max()),
54  approach_dx_(0.0),
55  approach_dy_(0.0),
56  xy_acceptance_reason_(XyAcceptanceReason::NONE)
57 {
58 }
59 
61 {
62  auto node = node_.lock();
63  if (post_set_params_handler_ && node) {
64  node->remove_post_set_parameters_callback(post_set_params_handler_.get());
65  }
66  post_set_params_handler_.reset();
67  if (on_set_params_handler_ && node) {
68  node->remove_on_set_parameters_callback(on_set_params_handler_.get());
69  }
70  on_set_params_handler_.reset();
71 }
72 
74  const nav2::LifecycleNode::WeakPtr & parent,
75  const std::string & plugin_name,
76  const std::shared_ptr<nav2_costmap_2d::Costmap2DROS>/*costmap_ros*/)
77 {
78  plugin_name_ = plugin_name;
79  node_ = parent;
80  auto node = node_.lock();
81  logger_ = node->get_logger();
82 
83  fine_xy_goal_tolerance_ = node->declare_or_get_parameter(
84  plugin_name + ".fine_xy_goal_tolerance", 0.10);
85  coarse_xy_goal_tolerance_ = node->declare_or_get_parameter(
86  plugin_name + ".coarse_xy_goal_tolerance", 0.25);
87  xy_goal_tolerance_buffer_ = node->declare_or_get_parameter(
88  plugin_name + ".xy_goal_tolerance_buffer", 0.0);
89  yaw_goal_tolerance_ = node->declare_or_get_parameter(
90  plugin_name + ".yaw_goal_tolerance", 0.25);
91  path_length_tolerance_ = node->declare_or_get_parameter(
92  plugin_name + ".path_length_tolerance", 1.0);
93  stateful_ = node->declare_or_get_parameter(plugin_name + ".stateful", true);
94  symmetric_yaw_tolerance_ = node->declare_or_get_parameter(
95  plugin_name + ".symmetric_yaw_tolerance", false);
96  trans_stopped_velocity_ = node->declare_or_get_parameter(
97  plugin_name + ".trans_stopped_velocity", 0.10);
98  rot_stopped_velocity_ = node->declare_or_get_parameter(
99  plugin_name + ".rot_stopped_velocity", 0.10);
100  required_stagnation_cycles_ = node->declare_or_get_parameter(
101  plugin_name + ".required_stagnation_cycles", 15);
102 
103  fine_xy_goal_tolerance_sq_ = fine_xy_goal_tolerance_ * fine_xy_goal_tolerance_;
104  coarse_xy_goal_tolerance_sq_ = coarse_xy_goal_tolerance_ * coarse_xy_goal_tolerance_;
105  fine_xy_goal_tolerance_reset_sq_ = (fine_xy_goal_tolerance_ + xy_goal_tolerance_buffer_) *
106  (fine_xy_goal_tolerance_ + xy_goal_tolerance_buffer_);
107  coarse_xy_goal_tolerance_reset_sq_ = (coarse_xy_goal_tolerance_ + xy_goal_tolerance_buffer_) *
108  (coarse_xy_goal_tolerance_ + xy_goal_tolerance_buffer_);
109 
110  if (fine_xy_goal_tolerance_ >= coarse_xy_goal_tolerance_) {
111  RCLCPP_WARN(
112  logger_, "Fine XY goal tolerance (%.3f) is greater or equal to coarse XY goal "
113  "tolerance (%.3f). This may lead to unintended behavior (when fine >= coarse the "
114  "checker will act as a simple goal checker). Consider setting "
115  "fine_xy_goal_tolerance < coarse_xy_goal_tolerance.",
116  fine_xy_goal_tolerance_, coarse_xy_goal_tolerance_);
117  }
118 
119  post_set_params_handler_ = node->add_post_set_parameters_callback(
120  std::bind(
122  this, std::placeholders::_1));
123  on_set_params_handler_ = node->add_on_set_parameters_callback(
124  std::bind(
126  this, std::placeholders::_1));
127 }
128 
130 {
131  check_xy_ = true;
132  in_tolerance_zone_ = false;
133  stopped_stagnation_count_ = 0;
134  distance_stagnation_count_ = 0;
135  best_distance_sq_ = std::numeric_limits<double>::max();
136  approach_dx_ = 0.0;
137  approach_dy_ = 0.0;
138  xy_acceptance_reason_ = XyAcceptanceReason::NONE;
139 }
140 
142  const geometry_msgs::msg::Pose & query_pose,
143  const geometry_msgs::msg::Pose & goal_pose,
144  const geometry_msgs::msg::Twist & velocity,
145  const nav_msgs::msg::Path & transformed_global_plan)
146 {
147  if (!isGoalXYReached(query_pose, goal_pose, velocity, transformed_global_plan)) {
148  return false;
149  }
150 
151  std::lock_guard<std::mutex> lock_reinit(mutex_);
152 
153  // XY is satisfied — check yaw
154  const double query_yaw = tf2::getYaw(query_pose.orientation);
155  const double goal_yaw = tf2::getYaw(goal_pose.orientation);
156  bool yaw_reached = false;
157 
158  if (symmetric_yaw_tolerance_) {
159  const double dyaw_forward = angles::shortest_angular_distance(query_yaw, goal_yaw);
160  const double dyaw_backward = angles::shortest_angular_distance(
161  query_yaw, angles::normalize_angle(goal_yaw + M_PI));
162  yaw_reached = std::fabs(dyaw_forward) <= yaw_goal_tolerance_ ||
163  std::fabs(dyaw_backward) <= yaw_goal_tolerance_;
164  } else {
165  const double dyaw = angles::shortest_angular_distance(query_yaw, goal_yaw);
166  yaw_reached = std::fabs(dyaw) <= yaw_goal_tolerance_;
167  }
168 
169  std::string xy_acceptance_reason = toString(xy_acceptance_reason_);
170  if (yaw_reached) {
171  RCLCPP_INFO(
172  logger_,
173  "AdaptiveToleranceGoalChecker: goal reached via %s "
174  "(fine: %.3f m, coarse: %.3f m)",
175  xy_acceptance_reason.c_str(),
176  fine_xy_goal_tolerance_, coarse_xy_goal_tolerance_);
177  }
178 
179  return yaw_reached;
180 }
181 
183  const geometry_msgs::msg::Pose & query_pose,
184  const geometry_msgs::msg::Pose & goal_pose,
185  const geometry_msgs::msg::Twist & velocity,
186  const nav_msgs::msg::Path & transformed_global_plan)
187 {
188  std::lock_guard<std::mutex> lock_reinit(mutex_);
189 
190  // Skip check if local plan is still long (robot is far from goal region)
191  if (nav2_util::geometry_utils::calculate_path_length(transformed_global_plan) >
192  path_length_tolerance_)
193  {
194  return false;
195  }
196 
197  if (check_xy_) {
198  const double dx = query_pose.position.x - goal_pose.position.x;
199  const double dy = query_pose.position.y - goal_pose.position.y;
200  const double dist_sq = dx * dx + dy * dy;
201 
202  // Tier 1: Tight (desired) tolerance — immediate acceptance
203  if (dist_sq <= fine_xy_goal_tolerance_sq_) {
204  xy_acceptance_reason_ = XyAcceptanceReason::FINE_TOLERANCE;
205  if (stateful_) {
206  check_xy_ = false;
207  }
208  // Fall through to yaw check
209 
210  // Tier 2: Within the coarse tolerance zone — check velocity stagnation
211  } else if (dist_sq <= coarse_xy_goal_tolerance_sq_) {
212  // Just entered the zone: initialize tracking
213  if (!in_tolerance_zone_) {
214  in_tolerance_zone_ = true;
215  stopped_stagnation_count_ = 0;
216  distance_stagnation_count_ = 0;
217  best_distance_sq_ = dist_sq;
218  approach_dx_ = -dx;
219  approach_dy_ = -dy;
220  return false;
221  }
222 
223  // Check if best distance has been improved
224  if (dist_sq < best_distance_sq_) {
225  best_distance_sq_ = dist_sq;
226  distance_stagnation_count_ = 0;
227  } else {
228  distance_stagnation_count_++;
229  }
230 
231  // Check if the robot is stopped or not making progress toward goal
232  if (
233  std::hypot(velocity.linear.x, velocity.linear.y) <= trans_stopped_velocity_ &&
234  std::fabs(velocity.angular.z) <= rot_stopped_velocity_)
235  {
236  stopped_stagnation_count_++;
237  } else {
238  stopped_stagnation_count_ = 0;
239  }
240 
241  // Finish line: robot crossed from approaching (dot<0) to passed (dot>=0) the
242  // virtual line orthogonal to the approach pose and the goal pose, representing
243  // that the robot has passed the goal longitudinally while approaching.
244  const bool crossed_finish_line = dx * approach_dx_ + dy * approach_dy_ >= 0.0;
245 
246  if (!crossed_finish_line &&
247  stopped_stagnation_count_ < required_stagnation_cycles_ &&
248  distance_stagnation_count_ < required_stagnation_cycles_)
249  {
250  return false;
251  }
252 
253  // Accepted at coarse: record which trigger fired
254  if (crossed_finish_line) {
255  xy_acceptance_reason_ = XyAcceptanceReason::COARSE_TOLERANCE_FINISH_LINE;
256  } else if (stopped_stagnation_count_ >= required_stagnation_cycles_) {
257  xy_acceptance_reason_ = XyAcceptanceReason::COARSE_TOLERANCE_STOPPED_STAGNATION;
258  } else {
259  xy_acceptance_reason_ = XyAcceptanceReason::COARSE_TOLERANCE_DISTANCE_STAGNATION;
260  }
261 
262  if (stateful_) {
263  check_xy_ = false;
264  }
265  } else {
266  // Outside both tolerances: reset tracking state
267  in_tolerance_zone_ = false;
268  stopped_stagnation_count_ = 0;
269  distance_stagnation_count_ = 0;
270  return false;
271  }
272  } else if (stateful_ && xy_goal_tolerance_buffer_ > 0.0) {
273  const double dx = query_pose.position.x - goal_pose.position.x;
274  const double dy = query_pose.position.y - goal_pose.position.y;
275  const double dist_sq = dx * dx + dy * dy;
276  // If stateful and using xy_goal_tolerance_buffer_,
277  // reset check_xy_ and tracking state when drifting outside the buffer region.
278  const double reset_threshold_sq =
279  xy_acceptance_reason_ == XyAcceptanceReason::FINE_TOLERANCE ?
280  fine_xy_goal_tolerance_reset_sq_ : coarse_xy_goal_tolerance_reset_sq_;
281 
282  if (dist_sq > reset_threshold_sq) {
283  check_xy_ = true;
284  in_tolerance_zone_ = false;
285  stopped_stagnation_count_ = 0;
286  distance_stagnation_count_ = 0;
287  return false;
288  }
289  }
290 
291  return true;
292 }
293 
295  geometry_msgs::msg::Pose & pose_tolerance,
296  geometry_msgs::msg::Twist & vel_tolerance,
297  double & path_length_tolerance)
298 {
299  std::lock_guard<std::mutex> lock_reinit(mutex_);
300  const double invalid_field = std::numeric_limits<double>::lowest();
301 
302  // Report max tolerance as the worst-case bound
303  pose_tolerance.position.x = coarse_xy_goal_tolerance_;
304  pose_tolerance.position.y = coarse_xy_goal_tolerance_;
305  pose_tolerance.position.z = invalid_field;
306  pose_tolerance.orientation =
307  nav2_util::geometry_utils::orientationAroundZAxis(yaw_goal_tolerance_);
308 
309  vel_tolerance.linear.x = trans_stopped_velocity_;
310  vel_tolerance.linear.y = trans_stopped_velocity_;
311  vel_tolerance.linear.z = invalid_field;
312 
313  vel_tolerance.angular.x = invalid_field;
314  vel_tolerance.angular.y = invalid_field;
315  vel_tolerance.angular.z = rot_stopped_velocity_;
316 
317  path_length_tolerance = path_length_tolerance_;
318 
319  return true;
320 }
321 
323 {
324  switch (reason) {
325  case XyAcceptanceReason::FINE_TOLERANCE:
326  return "fine tolerance";
327  case XyAcceptanceReason::COARSE_TOLERANCE_FINISH_LINE:
328  return "coarse tolerance / finish line";
329  case XyAcceptanceReason::COARSE_TOLERANCE_STOPPED_STAGNATION:
330  return "coarse tolerance / stopped stagnation";
331  case XyAcceptanceReason::COARSE_TOLERANCE_DISTANCE_STAGNATION:
332  return "coarse tolerance / distance stagnation";
333  case XyAcceptanceReason::NONE:
334  default:
335  return "unknown";
336  }
337 }
338 
339 rcl_interfaces::msg::SetParametersResult
341  const std::vector<rclcpp::Parameter> & parameters)
342 {
343  rcl_interfaces::msg::SetParametersResult result;
344  result.successful = true;
345  for (const auto & parameter : parameters) {
346  const auto & param_type = parameter.get_type();
347  const auto & param_name = parameter.get_name();
348  if (param_name.find(plugin_name_ + ".") != 0) {
349  continue;
350  }
351  if (param_type == ParameterType::PARAMETER_DOUBLE) {
352  if (parameter.as_double() < 0.0) {
353  RCLCPP_WARN(
354  logger_, "The value of parameter '%s' is incorrectly set to %f, "
355  "it should be >=0. Ignoring parameter update.",
356  param_name.c_str(), parameter.as_double());
357  result.successful = false;
358  }
359  } else if (param_type == ParameterType::PARAMETER_INTEGER) {
360  if (param_name == plugin_name_ + ".required_stagnation_cycles" &&
361  parameter.as_int() < 1)
362  {
363  RCLCPP_WARN(
364  logger_, "The value of parameter '%s' is incorrectly set to %ld, "
365  "it should be >= 1. Ignoring parameter update.",
366  param_name.c_str(), parameter.as_int());
367  result.successful = false;
368  }
369  }
370  }
371  return result;
372 }
373 
374 void
376  const std::vector<rclcpp::Parameter> & parameters)
377 {
378  std::lock_guard<std::mutex> lock_reinit(mutex_);
379  for (const auto & parameter : parameters) {
380  const auto & param_type = parameter.get_type();
381  const auto & param_name = parameter.get_name();
382  if (param_name.find(plugin_name_ + ".") != 0) {
383  continue;
384  }
385  if (param_type == ParameterType::PARAMETER_DOUBLE) {
386  if (param_name == plugin_name_ + ".fine_xy_goal_tolerance") {
387  fine_xy_goal_tolerance_ = parameter.as_double();
388  fine_xy_goal_tolerance_sq_ = fine_xy_goal_tolerance_ * fine_xy_goal_tolerance_;
389  fine_xy_goal_tolerance_reset_sq_ =
390  (fine_xy_goal_tolerance_ + xy_goal_tolerance_buffer_) *
391  (fine_xy_goal_tolerance_ + xy_goal_tolerance_buffer_);
392  if (fine_xy_goal_tolerance_ >= coarse_xy_goal_tolerance_) {
393  RCLCPP_WARN(
394  logger_, "Fine XY goal tolerance (%.3f) is greater or equal to coarse XY goal "
395  "tolerance (%.3f). This may lead to unintended behavior (when fine >= coarse the "
396  "checker will act as a simple goal checker). Consider setting "
397  "fine_xy_goal_tolerance < coarse_xy_goal_tolerance.",
398  fine_xy_goal_tolerance_, coarse_xy_goal_tolerance_);
399  }
400  } else if (param_name == plugin_name_ + ".coarse_xy_goal_tolerance") {
401  coarse_xy_goal_tolerance_ = parameter.as_double();
402  coarse_xy_goal_tolerance_sq_ = coarse_xy_goal_tolerance_ * coarse_xy_goal_tolerance_;
403  coarse_xy_goal_tolerance_reset_sq_ =
404  (coarse_xy_goal_tolerance_ + xy_goal_tolerance_buffer_) *
405  (coarse_xy_goal_tolerance_ + xy_goal_tolerance_buffer_);
406  if (fine_xy_goal_tolerance_ >= coarse_xy_goal_tolerance_) {
407  RCLCPP_WARN(
408  logger_, "Fine XY goal tolerance (%.3f) is greater or equal to coarse XY goal "
409  "tolerance (%.3f). This may lead to unintended behavior (when fine >= coarse the "
410  "checker will act as a simple goal checker). Consider setting "
411  "fine_xy_goal_tolerance < coarse_xy_goal_tolerance.",
412  fine_xy_goal_tolerance_, coarse_xy_goal_tolerance_);
413  }
414  } else if (param_name == plugin_name_ + ".xy_goal_tolerance_buffer") {
415  xy_goal_tolerance_buffer_ = parameter.as_double();
416  fine_xy_goal_tolerance_reset_sq_ =
417  (fine_xy_goal_tolerance_ + xy_goal_tolerance_buffer_) *
418  (fine_xy_goal_tolerance_ + xy_goal_tolerance_buffer_);
419  coarse_xy_goal_tolerance_reset_sq_ =
420  (coarse_xy_goal_tolerance_ + xy_goal_tolerance_buffer_) *
421  (coarse_xy_goal_tolerance_ + xy_goal_tolerance_buffer_);
422  } else if (param_name == plugin_name_ + ".yaw_goal_tolerance") {
423  yaw_goal_tolerance_ = parameter.as_double();
424  } else if (param_name == plugin_name_ + ".path_length_tolerance") {
425  path_length_tolerance_ = parameter.as_double();
426  } else if (param_name == plugin_name_ + ".trans_stopped_velocity") {
427  trans_stopped_velocity_ = parameter.as_double();
428  } else if (param_name == plugin_name_ + ".rot_stopped_velocity") {
429  rot_stopped_velocity_ = parameter.as_double();
430  }
431  } else if (param_type == ParameterType::PARAMETER_BOOL) {
432  if (param_name == plugin_name_ + ".stateful") {
433  stateful_ = parameter.as_bool();
434  } else if (param_name == plugin_name_ + ".symmetric_yaw_tolerance") {
435  symmetric_yaw_tolerance_ = parameter.as_bool();
436  }
437  } else if (param_type == ParameterType::PARAMETER_INTEGER) {
438  if (param_name == plugin_name_ + ".required_stagnation_cycles") {
439  required_stagnation_cycles_ = static_cast<int>(parameter.as_int());
440  }
441  }
442  }
443 }
444 
445 } // namespace nav2_controller
446 
Goal Checker plugin with two tolerance tiers: a tight desired tolerance and a looser coarse tolerance...
rcl_interfaces::msg::SetParametersResult validateParameterUpdatesCallback(const std::vector< rclcpp::Parameter > &parameters)
Validate incoming parameter updates before applying them. This callback is triggered when one or more...
bool isGoalXYReached(const geometry_msgs::msg::Pose &query_pose, const geometry_msgs::msg::Pose &goal_pose, const geometry_msgs::msg::Twist &velocity, const nav_msgs::msg::Path &transformed_global_plan) override
Check if XY goal position has been reached (without considering yaw)
XyAcceptanceReason
Reason that the XY component of the goal was accepted.
bool getTolerances(geometry_msgs::msg::Pose &pose_tolerance, geometry_msgs::msg::Twist &vel_tolerance, double &path_length_tolerance) override
Get the position and velocity tolerances.
void updateParametersCallback(const std::vector< rclcpp::Parameter > &parameters)
Apply parameter updates after validation This callback is executed when parameters have been successf...
void initialize(const nav2::LifecycleNode::WeakPtr &parent, const std::string &plugin_name, const std::shared_ptr< nav2_costmap_2d::Costmap2DROS > costmap_ros) override
Initialize the goal checker.
~AdaptiveToleranceGoalChecker()
Destroy the Progress Goal Checker object.
void reset() override
Reset the goal checker state.
AdaptiveToleranceGoalChecker()
Construct a new Progress Goal Checker object.
static std::string toString(XyAcceptanceReason reason)
Convert XY acceptance reason to string.
bool isGoalReached(const geometry_msgs::msg::Pose &query_pose, const geometry_msgs::msg::Pose &goal_pose, const geometry_msgs::msg::Twist &velocity, const nav_msgs::msg::Path &transformed_global_plan) override
Check if the goal is reached.
Function-object for checking whether a goal has been reached.