Skip to content

Commit 025df5c

Browse files
feat: Add timer-based propagation functionality to AMCL
1 parent 01e489b commit 025df5c

5 files changed

Lines changed: 138 additions & 14 deletions

File tree

beluga_amcl/include/beluga_amcl/amcl_node.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ class AmclNode : public BaseAMCLNode {
9191
/// Callback for laser scan updates.
9292
void laser_callback(sensor_msgs::msg::LaserScan::ConstSharedPtr);
9393

94+
/// Callback for increased propagation timer.
95+
void propagation_timer_callback();
96+
97+
/// Helper function to get base pose in odom frame at specific time.
98+
auto get_base_pose_in_odom(const tf2::TimePoint& time) const -> std::optional<Sophus::SE2d>;
99+
94100
/// Callback for pose (re)initialization.
95101
void do_initial_pose_callback(geometry_msgs::msg::PoseWithCovarianceStamped::SharedPtr) override;
96102

@@ -143,6 +149,9 @@ class AmclNode : public BaseAMCLNode {
143149
/// Connection for laser scan updates filter and callback.
144150
::message_filters::Connection laser_scan_connection_;
145151

152+
/// Timer for increased propagation rate.
153+
rclcpp::TimerBase::SharedPtr propagation_timer_;
154+
146155
/// Particle filter instance.
147156
std::unique_ptr<beluga_ros::Amcl> particle_filter_;
148157
/// Last known pose estimate, if any.

beluga_amcl/src/amcl_node.cpp

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,16 @@ AmclNode::AmclNode(const rclcpp::NodeOptions& options) : BaseAMCLNode{"amcl", ""
183183
"and ignore subsequent ones.";
184184
declare_parameter("first_map_only", false, descriptor);
185185
}
186+
187+
{
188+
auto descriptor = rcl_interfaces::msg::ParameterDescriptor();
189+
descriptor.description = "Frequency in Hz for increased propagation rate. Set to 0 to disable.";
190+
descriptor.floating_point_range.resize(1);
191+
descriptor.floating_point_range[0].from_value = 0.0;
192+
descriptor.floating_point_range[0].to_value = 100.0;
193+
descriptor.floating_point_range[0].step = 0.0;
194+
declare_parameter("increase_propagation", rclcpp::ParameterValue(0.0), descriptor);
195+
}
186196
}
187197

188198
AmclNode::~AmclNode() {
@@ -251,6 +261,17 @@ void AmclNode::do_activate(const rclcpp_lifecycle::State&) {
251261
std::placeholders::_3),
252262
common_service_qos, common_callback_group_);
253263
RCLCPP_INFO(get_logger(), "Created request_nomotion_update service");
264+
265+
// Setup increased propagation timer if enabled
266+
{
267+
const double propagation_freq = get_parameter("increase_propagation").as_double();
268+
if (propagation_freq > 0.0) {
269+
auto period = std::chrono::duration<double>(1.0 / propagation_freq);
270+
propagation_timer_ =
271+
create_wall_timer(period, std::bind(&AmclNode::propagation_timer_callback, this), common_callback_group_);
272+
RCLCPP_INFO(get_logger(), "Created propagation timer at %.1f Hz", propagation_freq);
273+
}
274+
}
254275
}
255276

256277
void AmclNode::do_deactivate(const rclcpp_lifecycle::State&) {
@@ -259,6 +280,7 @@ void AmclNode::do_deactivate(const rclcpp_lifecycle::State&) {
259280
laser_scan_filter_.reset();
260281
laser_scan_sub_.reset();
261282
global_localization_server_.reset();
283+
propagation_timer_.reset();
262284
if (likelihood_field_pub_) {
263285
likelihood_field_pub_->on_deactivate();
264286
}
@@ -466,26 +488,51 @@ void AmclNode::do_periodic_timer_callback() {
466488
}
467489
}
468490

469-
void AmclNode::laser_callback(sensor_msgs::msg::LaserScan::ConstSharedPtr laser_scan) {
470-
if (!particle_filter_) {
471-
RCLCPP_WARN_THROTTLE(
472-
get_logger(), *get_clock(), 2000, "Ignoring laser data because the particle filter has not been initialized");
473-
return;
474-
}
475-
491+
auto AmclNode::get_base_pose_in_odom(const tf2::TimePoint& time) const -> std::optional<Sophus::SE2d> {
476492
auto base_pose_in_odom = Sophus::SE2d{};
477493
try {
478-
// Use the lookupTransform overload with no timeout since we're not using a dedicated
479-
// tf thread. The message filter we are using avoids the need for it.
480494
tf2::convert(
481495
tf_buffer_
482496
->lookupTransform(
483-
get_parameter("odom_frame_id").as_string(), get_parameter("base_frame_id").as_string(),
484-
tf2_ros::fromMsg(laser_scan->header.stamp))
497+
get_parameter("odom_frame_id").as_string(), get_parameter("base_frame_id").as_string(), time)
485498
.transform,
486499
base_pose_in_odom);
500+
return base_pose_in_odom;
487501
} catch (const tf2::TransformException& error) {
488-
RCLCPP_ERROR(get_logger(), "Could not transform from odom to base: %s", error.what());
502+
RCLCPP_WARN(get_logger(), "Could not get base pose in odom: %s", error.what());
503+
return std::nullopt;
504+
}
505+
}
506+
507+
void AmclNode::propagation_timer_callback() {
508+
if (!particle_filter_) {
509+
RCLCPP_WARN_THROTTLE(
510+
get_logger(), *get_clock(), 2000, "Ignoring propagation because the particle filter has not been initialized");
511+
return;
512+
}
513+
514+
// Get current base pose in odom frame (latest available)
515+
auto base_pose_in_odom = get_base_pose_in_odom(tf2::TimePointZero);
516+
if (!base_pose_in_odom.has_value()) {
517+
return;
518+
}
519+
520+
// Force a propagation-only update (without sensor data)
521+
particle_filter_->update_propagation(base_pose_in_odom.value());
522+
523+
RCLCPP_INFO(get_logger(), "Forced propagation update executed");
524+
}
525+
526+
void AmclNode::laser_callback(sensor_msgs::msg::LaserScan::ConstSharedPtr laser_scan) {
527+
if (!particle_filter_) {
528+
RCLCPP_WARN_THROTTLE(
529+
get_logger(), *get_clock(), 2000, "Ignoring laser data because the particle filter has not been initialized");
530+
return;
531+
}
532+
533+
// Get base pose in odom frame at laser scan timestamp
534+
auto base_pose_in_odom = get_base_pose_in_odom(tf2_ros::fromMsg(laser_scan->header.stamp));
535+
if (!base_pose_in_odom.has_value()) {
489536
return;
490537
}
491538

@@ -505,7 +552,7 @@ void AmclNode::laser_callback(sensor_msgs::msg::LaserScan::ConstSharedPtr laser_
505552

506553
const auto update_start_time = std::chrono::high_resolution_clock::now();
507554
const auto new_estimate = particle_filter_->update(
508-
base_pose_in_odom, //
555+
base_pose_in_odom.value(), //
509556
beluga_ros::LaserScan{
510557
laser_scan,
511558
laser_pose_in_base,
@@ -518,7 +565,7 @@ void AmclNode::laser_callback(sensor_msgs::msg::LaserScan::ConstSharedPtr laser_
518565

519566
if (new_estimate.has_value()) {
520567
const auto& [base_pose_in_map, _] = new_estimate.value();
521-
last_known_odom_transform_in_map_ = base_pose_in_map * base_pose_in_odom.inverse();
568+
last_known_odom_transform_in_map_ = base_pose_in_map * base_pose_in_odom.value().inverse();
522569
last_known_estimate_ = new_estimate;
523570

524571
RCLCPP_INFO(

beluga_amcl/test/test_amcl_node.cpp

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ class AmclNodeUnderTest : public beluga_amcl::AmclNode {
4545

4646
/// Return the last known estimate. Throws if there is no estimate.
4747
const auto& estimate() { return last_known_estimate_.value(); }
48+
49+
/// Check if propagation timer is created
50+
bool has_propagation_timer() const { return propagation_timer_ != nullptr; }
4851
};
4952

5053
/// Base node fixture class with common utilities.
@@ -698,6 +701,47 @@ TEST_F(TestNode, TransformValue) {
698701
EXPECT_NEAR(transform.so2().log(), 0.0, 0.01);
699702
}
700703

704+
TEST_F(TestNode, PropagationTimerNotCreatedWhenDisabled) {
705+
amcl_node_->set_parameter(rclcpp::Parameter{"increase_propagation", 0.0});
706+
amcl_node_->configure();
707+
amcl_node_->activate();
708+
tester_node_->publish_map();
709+
ASSERT_TRUE(wait_for_initialization());
710+
711+
// Timer should not be created when frequency is 0
712+
EXPECT_FALSE(amcl_node_->has_propagation_timer());
713+
}
714+
715+
TEST_F(TestNode, PropagationTimerCreatedWhenEnabled) {
716+
amcl_node_->set_parameter(rclcpp::Parameter{"increase_propagation", 10.0});
717+
amcl_node_->configure();
718+
amcl_node_->activate();
719+
tester_node_->publish_map();
720+
ASSERT_TRUE(wait_for_initialization());
721+
722+
// Timer should be created when frequency > 0
723+
EXPECT_TRUE(amcl_node_->has_propagation_timer());
724+
}
725+
726+
TEST_F(TestNode, PropagationTimerIntegrationTest) {
727+
amcl_node_->set_parameter(rclcpp::Parameter{"increase_propagation", 5.0});
728+
amcl_node_->set_parameter(rclcpp::Parameter{"set_initial_pose", true});
729+
amcl_node_->configure();
730+
amcl_node_->activate();
731+
tester_node_->publish_map();
732+
ASSERT_TRUE(wait_for_initialization());
733+
734+
// Configure TF so get_base_pose_in_odom works by publishing a laser scan with transform
735+
tester_node_->publish_laser_scan_with_odom_to_base(Sophus::SE2d{});
736+
737+
// Wait for several timer executions
738+
spin_for(500ms, amcl_node_, tester_node_);
739+
740+
// Verify particle filter still exists and has particles
741+
EXPECT_TRUE(amcl_node_->particle_filter() != nullptr);
742+
EXPECT_GT(amcl_node_->particle_filter()->particles().size(), 0UL);
743+
}
744+
701745
class TestParameterValue : public ::testing::TestWithParam<rclcpp::Parameter> {};
702746

703747
INSTANTIATE_TEST_SUITE_P(

beluga_ros/include/beluga_ros/amcl.hpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,16 @@ class Amcl {
210210
/// Update the map used for localization.
211211
void update_map(beluga_ros::OccupancyGrid map);
212212

213+
/// Update particles based on motion only (propagation only).
214+
/**
215+
* This method only performs the propagation step of the particle filter update,
216+
* applying the motion model without any sensor correction. Useful for forced
217+
* propagation at regular intervals without waiting for sensor data.
218+
*
219+
* \param base_pose_in_odom Base pose in the odometry frame.
220+
*/
221+
void update_propagation(Sophus::SE2d base_pose_in_odom);
222+
213223
/// Update particles based on motion and sensor information.
214224
/**
215225
* This method performs a particle filter update step using motion and sensor data. It evaluates whether

beluga_ros/src/amcl.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@ void Amcl::update_map(beluga_ros::OccupancyGrid map) {
5050
std::visit([&](auto& sensor_model) { sensor_model.update_map(std::move(map)); }, sensor_model_);
5151
}
5252

53+
void Amcl::update_propagation(Sophus::SE2d base_pose_in_odom) {
54+
if (particles_.empty()) {
55+
return;
56+
}
57+
58+
// Force propagation without checking update_policy_
59+
std::visit(
60+
[&, this](auto& policy, auto& motion_model) {
61+
particles_ |= beluga::actions::propagate(policy, motion_model(control_action_window_ << base_pose_in_odom)) |
62+
beluga::actions::normalize(policy);
63+
},
64+
execution_policy_, motion_model_);
65+
}
66+
5367
auto Amcl::update(Sophus::SE2d base_pose_in_odom, beluga_ros::LaserScan laser_scan)
5468
-> std::optional<std::pair<Sophus::SE2d, Sophus::Matrix3d>> {
5569
if (particles_.empty()) {

0 commit comments

Comments
 (0)