Skip to content

Commit e21b99f

Browse files
authored
Add hysteresis, fix failing tests for kilted and jazzy (#22)
* Hysteresis function * Fix build error for Kilted and Jazzy * Build error and test failure fix
1 parent 2d376c7 commit e21b99f

File tree

8 files changed

+161
-4
lines changed

8 files changed

+161
-4
lines changed

roadmap_explorer/include/roadmap_explorer/FullPathOptimizer.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,14 @@ class FullPathOptimizer
187187
bool add_yaw_to_tsp = false;
188188
bool add_distance_to_robot_to_tsp = false;
189189

190+
// Goal hysteresis variables to prevent frequent goal switching
191+
FrontierPtr current_committed_goal_; // The goal we're currently committed to
192+
double current_goal_path_cost_; // Path cost to the current goal
193+
bool has_committed_goal_ = false; // Whether we have a committed goal
194+
195+
// Hysteresis parameters
196+
double goal_hysteresis_threshold_ = 0.15; // New goal must be 15% better to switch
197+
190198
// Constants for path optimization
191199
static constexpr double PATH_NOT_FOUND_PENALTY_MULTIPLIER = 100000.0;
192200
static constexpr double YAW_COST_WEIGHT = 2.3;

roadmap_explorer/params/exploration_params.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ roadmap_explorer_node:
4040
num_frontiers_in_local_area: 5.0
4141
local_frontier_search_radius: 12.0
4242
add_yaw_to_tsp: false
43-
add_distance_to_robot_to_tsp: false
43+
add_distance_to_robot_to_tsp: false
44+
goal_hysteresis_threshold: 0.15 # New goal must be 15% better to switch (0.0 = always switch, 1.0 = never switch)
4445

4546
explorationBT:
4647
bt_sleep_ms: 70

roadmap_explorer/params/tb3_exploration_params.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ roadmap_explorer_node:
4040
num_frontiers_in_local_area: 5.0
4141
local_frontier_search_radius: 12.0
4242
add_yaw_to_tsp: false
43-
add_distance_to_robot_to_tsp: false
43+
add_distance_to_robot_to_tsp: false
44+
goal_hysteresis_threshold: 0.15 # New goal must be 15% better to switch (0.0 = always switch, 1.0 = never switch)
4445

4546
explorationBT:
4647
bt_sleep_ms: 70

roadmap_explorer/src/ExplorationServer.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,13 @@ nav2_util::CallbackReturn ExplorationServer::on_activate(const rclcpp_lifecycle:
7272
"save_map_and_roadmap",
7373
std::bind(&ExplorationServer::handle_save_map_and_roadmap, this,
7474
std::placeholders::_1, std::placeholders::_2),
75+
#ifdef ROS_DISTRO_HUMBLE
7576
rmw_qos_profile_services_default,
77+
#elif ROS_DISTRO_JAZZY || ROS_DISTRO_KILTED
78+
rclcpp::ServicesQoS(),
79+
#else
80+
#error "Unsupported ROS distro"
81+
#endif
7682
service_cb_group_);
7783

7884
LOG_INFO("Created save_map_and_roadmap service");

roadmap_explorer/src/FullPathOptimizer.cpp

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ FullPathOptimizer::FullPathOptimizer(
3939
add_yaw_to_tsp = parameterInstance.getValue<bool>("fullPathOptimizer.add_yaw_to_tsp");
4040
add_distance_to_robot_to_tsp = parameterInstance.getValue<bool>(
4141
"fullPathOptimizer.add_distance_to_robot_to_tsp");
42+
goal_hysteresis_threshold_ = parameterInstance.getValue<double>(
43+
"fullPathOptimizer.goal_hysteresis_threshold");
4244

4345
explore_costmap_ros_ = explore_costmap_ros;
4446
local_search_area_publisher_ = node_->create_publisher<visualization_msgs::msg::MarkerArray>(
@@ -375,6 +377,26 @@ bool FullPathOptimizer::getNextGoal(
375377
add_yaw_to_tsp = parameterInstance.getValue<bool>("fullPathOptimizer.add_yaw_to_tsp");
376378
add_distance_to_robot_to_tsp = parameterInstance.getValue<bool>(
377379
"fullPathOptimizer.add_distance_to_robot_to_tsp");
380+
goal_hysteresis_threshold_ = parameterInstance.getValue<double>(
381+
"fullPathOptimizer.goal_hysteresis_threshold");
382+
383+
// Check if current committed goal is still valid
384+
bool current_goal_valid = false;
385+
if (has_committed_goal_ && current_committed_goal_) {
386+
// Check if the committed goal still exists in the frontier list and is valid
387+
auto it = std::find_if(frontier_list.begin(), frontier_list.end(),
388+
[this](const FrontierPtr& f) {
389+
return f->getUID() == current_committed_goal_->getUID();
390+
});
391+
392+
if (it != frontier_list.end() && (*it)->isAchievable() && !(*it)->isBlacklisted()) {
393+
current_goal_valid = true;
394+
LOG_INFO("Current committed goal is still valid (UID: " << current_committed_goal_->getUID() << ")");
395+
} else {
396+
LOG_INFO("Current committed goal is no longer valid - will compute new goal");
397+
has_committed_goal_ = false;
398+
}
399+
}
378400

379401
SortedFrontiers sortedFrontiers;
380402
// sort based on path length
@@ -401,17 +423,25 @@ bool FullPathOptimizer::getNextGoal(
401423

402424
rosVisualizerInstance.visualizePath("global_repositioning_path", globalReposPath);
403425

426+
// Update committed goal since we're switching to global repositioning
427+
current_committed_goal_ = nextFrontier;
428+
has_committed_goal_ = true;
429+
current_goal_path_cost_ = nextFrontier->getPathLengthInM();
430+
LOG_INFO("Committing to global repositioning goal (UID: " << nextFrontier->getUID() << ")");
431+
404432
return true;
405433
} else {
406434
LOG_ERROR(
407435
"Could not find local or global frontiers. Returning a zero frontier. The program may crash if goal point is checked...");
408436
nextFrontier = zeroFrontier;
437+
has_committed_goal_ = false;
409438
return false;
410439
}
411440
}
412441

413442
if (!getBestFullPath(sortedFrontiers, bestFrontierWaypoint, robotP)) {
414443
nextFrontier = zeroFrontier;
444+
has_committed_goal_ = false;
415445
return false;
416446
}
417447

@@ -440,8 +470,54 @@ bool FullPathOptimizer::getNextGoal(
440470
rosVisualizerInstance.visualizePath("full_path", bestPathROS);
441471
}
442472
EventLoggerInstance.endEvent("publishPlan", 2);
473+
443474
// 0 is robot pose. Return the first frontier in the path.
444-
nextFrontier = bestFrontierWaypoint[1];
475+
FrontierPtr candidateGoal = bestFrontierWaypoint[1];
476+
477+
// Apply hysteresis logic to prevent frequent goal switching
478+
if (current_goal_valid) {
479+
// Calculate cost to reach the current committed goal
480+
FrontierPtr robotPoseFrontier = std::make_shared<Frontier>();
481+
robotPoseFrontier->setGoalPoint(robotP.pose.position.x, robotP.pose.position.y);
482+
robotPoseFrontier->setUID(generateUID(robotPoseFrontier));
483+
484+
double cost_to_current_goal = calculateLengthRobotToGoal(robotPoseFrontier, current_committed_goal_);
485+
double cost_to_new_goal = calculateLengthRobotToGoal(robotPoseFrontier, candidateGoal);
486+
487+
// Calculate improvement ratio
488+
double improvement = (cost_to_current_goal - cost_to_new_goal) / cost_to_current_goal;
489+
490+
LOG_INFO("Hysteresis check - Current goal cost: " << cost_to_current_goal <<
491+
", New goal cost: " << cost_to_new_goal <<
492+
", Improvement: " << (improvement * 100.0) << "%");
493+
494+
if (improvement >= goal_hysteresis_threshold_) {
495+
// New goal is significantly better - switch to it
496+
LOG_INFO("New goal is " << (improvement * 100.0) << "% better (threshold: " <<
497+
(goal_hysteresis_threshold_ * 100.0) << "%) - switching goals");
498+
LOG_INFO("Switching from goal UID " << current_committed_goal_->getUID() <<
499+
" to " << candidateGoal->getUID());
500+
current_committed_goal_ = candidateGoal;
501+
current_goal_path_cost_ = cost_to_new_goal;
502+
nextFrontier = candidateGoal;
503+
} else {
504+
// Stick with current goal
505+
LOG_INFO("Sticking with current goal (improvement " << (improvement * 100.0) <<
506+
"% below threshold " << (goal_hysteresis_threshold_ * 100.0) << "%)");
507+
nextFrontier = current_committed_goal_;
508+
}
509+
} else {
510+
// No current goal or it became invalid - commit to the new candidate
511+
LOG_INFO("No valid committed goal - committing to new goal (UID: " << candidateGoal->getUID() << ")");
512+
current_committed_goal_ = candidateGoal;
513+
has_committed_goal_ = true;
514+
FrontierPtr robotPoseFrontier = std::make_shared<Frontier>();
515+
robotPoseFrontier->setGoalPoint(robotP.pose.position.x, robotP.pose.position.y);
516+
robotPoseFrontier->setUID(generateUID(robotPoseFrontier));
517+
current_goal_path_cost_ = calculateLengthRobotToGoal(robotPoseFrontier, candidateGoal);
518+
nextFrontier = candidateGoal;
519+
}
520+
445521
return true;
446522
}
447523

roadmap_explorer/src/Parameters.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ void ParameterHandler::makeParameters(std::shared_ptr<nav2_util::LifecycleNode>
103103
node,
104104
"fullPathOptimizer.add_distance_to_robot_to_tsp", rclcpp::ParameterValue(
105105
false));
106+
nav2_util::declare_parameter_if_not_declared(
107+
node,
108+
"fullPathOptimizer.goal_hysteresis_threshold", rclcpp::ParameterValue(
109+
0.15));
106110

107111
parameter_map_["fullPathOptimizer.num_frontiers_in_local_area"] = node->get_parameter(
108112
"fullPathOptimizer.num_frontiers_in_local_area").as_double();
@@ -112,6 +116,8 @@ void ParameterHandler::makeParameters(std::shared_ptr<nav2_util::LifecycleNode>
112116
"fullPathOptimizer.add_yaw_to_tsp").as_bool();
113117
parameter_map_["fullPathOptimizer.add_distance_to_robot_to_tsp"] = node->get_parameter(
114118
"fullPathOptimizer.add_distance_to_robot_to_tsp").as_bool();
119+
parameter_map_["fullPathOptimizer.goal_hysteresis_threshold"] = node->get_parameter(
120+
"fullPathOptimizer.goal_hysteresis_threshold").as_double();
115121

116122
// --- explorationBT ---
117123
nav2_util::declare_parameter_if_not_declared(

roadmap_explorer/tests/bt_plugins/test_bt_plugin_integration.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,13 @@ TEST_F(BTPluginIntegrationTest, DirectInstantiationIntegration)
155155

156156
// Test execution
157157
size_t initial_count = EventLoggerInstance.getPlanningCount();
158+
#ifdef ROS_DISTRO_HUMBLE
158159
BT::NodeStatus status = tree.tickRoot();
160+
#elif ROS_DISTRO_JAZZY || ROS_DISTRO_KILTED
161+
BT::NodeStatus status = tree.tickOnce();
162+
#else
163+
#error "Unsupported ROS distro"
164+
#endif
159165

160166
EXPECT_EQ(status, BT::NodeStatus::SUCCESS);
161167
EXPECT_EQ(EventLoggerInstance.getPlanningCount(), initial_count + 1);
@@ -222,7 +228,13 @@ TEST_F(BTPluginIntegrationTest, BehaviorTreeCreation)
222228

223229
// Test tree execution
224230
size_t initial_count = EventLoggerInstance.getPlanningCount();
231+
#ifdef ROS_DISTRO_HUMBLE
225232
BT::NodeStatus status = tree.tickRoot();
233+
#elif ROS_DISTRO_JAZZY || ROS_DISTRO_KILTED
234+
BT::NodeStatus status = tree.tickOnce();
235+
#else
236+
#error "Unsupported ROS distro"
237+
#endif
226238

227239
EXPECT_EQ(status, BT::NodeStatus::SUCCESS);
228240
EXPECT_EQ(EventLoggerInstance.getPlanningCount(), initial_count + 1);
@@ -258,7 +270,13 @@ TEST_F(BTPluginIntegrationTest, MultipleBTNodes)
258270

259271
// Test tree execution - should increment counter 3 times
260272
size_t initial_count = EventLoggerInstance.getPlanningCount();
273+
#ifdef ROS_DISTRO_HUMBLE
261274
BT::NodeStatus status = tree.tickRoot();
275+
#elif ROS_DISTRO_JAZZY || ROS_DISTRO_KILTED
276+
BT::NodeStatus status = tree.tickOnce();
277+
#else
278+
#error "Unsupported ROS distro"
279+
#endif
262280

263281
EXPECT_EQ(status, BT::NodeStatus::SUCCESS);
264282
EXPECT_EQ(EventLoggerInstance.getPlanningCount(), initial_count + 3);
@@ -316,7 +334,13 @@ TEST_F(BTPluginIntegrationTest, DifferentNodeConfigurations)
316334

317335
// Test execution
318336
size_t initial_count = EventLoggerInstance.getPlanningCount();
337+
#ifdef ROS_DISTRO_HUMBLE
319338
BT::NodeStatus status = tree.tickRoot();
339+
#elif ROS_DISTRO_JAZZY || ROS_DISTRO_KILTED
340+
BT::NodeStatus status = tree.tickOnce();
341+
#else
342+
#error "Unsupported ROS distro"
343+
#endif
320344

321345
EXPECT_EQ(status, BT::NodeStatus::SUCCESS);
322346
EXPECT_EQ(EventLoggerInstance.getPlanningCount(), initial_count + 2);
@@ -387,7 +411,13 @@ TEST_F(BTPluginIntegrationTest, ConcurrentFactoryAccess)
387411
const int num_executions = 5;
388412

389413
for (int i = 0; i < num_executions; ++i) {
414+
#ifdef ROS_DISTRO_HUMBLE
390415
BT::NodeStatus status = tree.tickRoot();
416+
#elif ROS_DISTRO_JAZZY || ROS_DISTRO_KILTED
417+
BT::NodeStatus status = tree.tickOnce();
418+
#else
419+
#error "Unsupported ROS distro"
420+
#endif
391421
EXPECT_EQ(status, BT::NodeStatus::SUCCESS);
392422
}
393423

roadmap_explorer/tests/bt_plugins/test_log_iteration_plugin.cpp

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,13 @@ TEST_F(LogIterationPluginTest, DifferentNodeNames)
233233

234234
// Test tree execution
235235
size_t initial_count = EventLoggerInstance.getPlanningCount();
236+
#ifdef ROS_DISTRO_HUMBLE
236237
BT::NodeStatus status = tree.tickRoot();
238+
#elif ROS_DISTRO_JAZZY || ROS_DISTRO_KILTED
239+
BT::NodeStatus status = tree.tickOnce();
240+
#else
241+
#error "Unsupported ROS distro"
242+
#endif
237243

238244
EXPECT_EQ(status, BT::NodeStatus::SUCCESS);
239245
// Should have executed both nodes, so count should increase by 2
@@ -316,7 +322,8 @@ TEST_F(LogIterationPluginTest, BasicThreadSafety)
316322
context_ = createContext();
317323
plugin_->registerNodes(*factory_, context_);
318324

319-
// Create a simple behavior tree XML with multiple LogIterationBT nodes
325+
#ifdef ROS_DISTRO_HUMBLE
326+
// For Humble (BT.CPP 3.x): Use Parallel node
320327
std::string bt_xml = R"(
321328
<root BTCPP_format="4">
322329
<BehaviorTree ID="ThreadSafetyTest">
@@ -327,6 +334,22 @@ TEST_F(LogIterationPluginTest, BasicThreadSafety)
327334
</BehaviorTree>
328335
</root>
329336
)";
337+
#elif ROS_DISTRO_JAZZY || ROS_DISTRO_KILTED
338+
// For Jazzy/Kilted (BT.CPP 4.x): Use Sequence instead
339+
// Parallel nodes behave differently with tickOnce() in BT.CPP 4.x
340+
std::string bt_xml = R"(
341+
<root BTCPP_format="4">
342+
<BehaviorTree ID="ThreadSafetyTest">
343+
<Sequence>
344+
<LogIterationBT name="thread_test_1" />
345+
<LogIterationBT name="thread_test_2" />
346+
</Sequence>
347+
</BehaviorTree>
348+
</root>
349+
)";
350+
#else
351+
#error "Unsupported ROS distro"
352+
#endif
330353

331354
// Create blackboard
332355
auto blackboard = BT::Blackboard::create();
@@ -339,7 +362,13 @@ TEST_F(LogIterationPluginTest, BasicThreadSafety)
339362
size_t initial_count = EventLoggerInstance.getPlanningCount();
340363

341364
// Execute the tree (which runs both nodes in parallel)
365+
#ifdef ROS_DISTRO_HUMBLE
342366
BT::NodeStatus status = tree.tickRoot();
367+
#elif ROS_DISTRO_JAZZY || ROS_DISTRO_KILTED
368+
BT::NodeStatus status = tree.tickOnce();
369+
#else
370+
#error "Unsupported ROS distro"
371+
#endif
343372

344373
EXPECT_EQ(status, BT::NodeStatus::SUCCESS);
345374

0 commit comments

Comments
 (0)