33#include < gz/plugin/Register.hh>
44#include < tf2/LinearMath/Quaternion.h>
55
6+ #include < cmath>
7+
68GZ_ADD_PLUGIN (
79 custom_plugins::NoisyOdometryPlugin,
810 gz::sim::System,
@@ -15,6 +17,13 @@ namespace custom_plugins
1517{
1618 NoisyOdometryPlugin::NoisyOdometryPlugin () {}
1719
20+ double NoisyOdometryPlugin::NormalizeAngle (double _angle) const
21+ {
22+ while (_angle > M_PI) _angle -= 2.0 * M_PI;
23+ while (_angle < -M_PI) _angle += 2.0 * M_PI;
24+ return _angle;
25+ }
26+
1827 void NoisyOdometryPlugin::Configure (const gz::sim::Entity &_entity,
1928 const std::shared_ptr<const sdf::Element> &_sdf,
2029 gz::sim::EntityComponentManager &_ecm,
@@ -29,11 +38,18 @@ namespace custom_plugins
2938 frame_id_ = _sdf->Get <std::string>(" frame_id" , " odom" ).first ;
3039 child_frame_id_ = _sdf->Get <std::string>(" child_frame_id" , " base_footprint" ).first ;
3140
41+ // If the SDF does not provide the odometry motion-model coefficients
42+ // explicitly, derive reasonable defaults from the legacy noise parameter.
43+ alpha1_ = _sdf->Get <double >(" alpha1" , gaussian_noise_coeff_).first ;
44+ alpha2_ = _sdf->Get <double >(" alpha2" , gaussian_noise_coeff_).first ;
45+ alpha3_ = _sdf->Get <double >(" alpha3" , gaussian_noise_coeff_).first ;
46+ alpha4_ = _sdf->Get <double >(" alpha4" , gaussian_noise_coeff_).first ;
47+
3248 gz_node_.Subscribe (gz_cmd_vel_topic_, &NoisyOdometryPlugin::OnCmdVel, this );
3349
3450 if (!rclcpp::ok ()) rclcpp::init (0 , nullptr );
3551 ros_node_ = rclcpp::Node::make_shared (" noisy_odom_node_" + model_.Name (_ecm));
36-
52+
3753 rclcpp::QoS qos (10 );
3854 qos.transient_local ();
3955 ros_pub_ = ros_node_->create_publisher <nav_msgs::msg::Odometry>(ros_topic_, qos);
@@ -51,43 +67,91 @@ namespace custom_plugins
5167 {
5268 if (_info.paused ) return ;
5369
70+ auto poseComp = _ecm.Component <gz::sim::components::Pose>(model_.Entity ());
71+ if (!poseComp) return ;
72+
73+ const gz::math::Pose3d current_true_pose = poseComp->Data ();
74+
5475 if (!initialized_)
5576 {
56- auto poseComp = _ecm.Component <gz::sim::components::Pose>(model_.Entity ());
57- if (poseComp) {
58- noisy_pose_internal_ = poseComp->Data ();
59- last_update_time_ = _info.simTime ;
60- initialized_ = true ;
61- }
77+ // The noisy odometry starts aligned with the true pose only once.
78+ // After that, it evolves independently by integrating noisy motion
79+ // increments, so drift accumulates naturally over time.
80+ noisy_pose_internal_ = current_true_pose;
81+ last_true_pose_ = current_true_pose;
82+ last_update_time_ = _info.simTime ;
83+ initialized_ = true ;
6284 return ;
6385 }
6486
65- double dt = std::chrono::duration<double >(_info.simTime - last_update_time_).count ();
87+ const double dt =
88+ std::chrono::duration<double >(_info.simTime - last_update_time_).count ();
89+
6690 if (dt <= 0.0 ) return ;
6791 last_update_time_ = _info.simTime ;
6892
69- double v_cmd, w_cmd;
70- {
71- std::lock_guard<std::mutex> lock (mutex_);
72- v_cmd = current_v_;
73- w_cmd = current_w_;
74- }
75-
76- double linear_noise = gaussian_noise_coeff_ * gz::math::Rand::DblNormal (0 , 1 ) * std::abs (v_cmd);
77- double angular_noise = gaussian_noise_coeff_ * gz::math::Rand::DblNormal (0 , 1 ) * std::abs (w_cmd);
78-
79- double v_noisy = v_cmd + linear_noise;
80- double w_noisy = w_cmd + angular_noise;
81-
82- double angle = w_noisy * dt;
83- gz::math::Vector3d axis (0 , 0 , 1 );
84- gz::math::Quaterniond deltaOrientation (std::cos (angle / 2.0 ), 0 , 0 , std::sin (angle / 2.0 ));
85-
86- noisy_pose_internal_.Rot () = noisy_pose_internal_.Rot () * deltaOrientation;
93+ // Compute the true relative motion between the last and current simulator pose.
94+ // This gives a physically meaningful motion increment without publishing the
95+ // simulator pose directly.
96+ const gz::math::Vector3d delta_world =
97+ current_true_pose.Pos () - last_true_pose_.Pos ();
98+
99+ const double previous_true_yaw = last_true_pose_.Rot ().Yaw ();
100+ const double current_true_yaw = current_true_pose.Rot ().Yaw ();
101+
102+ // Express the translation increment in the previous robot frame so the
103+ // odometry model operates in the body frame, as usual in planar robotics.
104+ const gz::math::Vector3d delta_body =
105+ last_true_pose_.Rot ().RotateVectorReverse (delta_world);
106+
107+ const double trans = std::sqrt (
108+ delta_body.X () * delta_body.X () + delta_body.Y () * delta_body.Y ());
109+
110+ const double rot1 =
111+ (trans > 1e-9 ) ? NormalizeAngle (std::atan2 (delta_body.Y (), delta_body.X ())) : 0.0 ;
112+
113+ const double delta_yaw = NormalizeAngle (current_true_yaw - previous_true_yaw);
114+ const double rot2 = NormalizeAngle (delta_yaw - rot1);
115+
116+ // Odometry motion model:
117+ // - rotation uncertainty grows with both rotation and translation
118+ // - translation uncertainty grows with both translation and turning
119+ //
120+ // This is more robust than injecting noise directly on cmd_vel, and still
121+ // preserves the expected long-term drift because only noisy increments are
122+ // integrated into the internal odometric state.
123+ const double sigma_rot1 =
124+ alpha1_ * std::abs (rot1) + alpha2_ * std::abs (trans);
125+ const double sigma_trans =
126+ alpha3_ * std::abs (trans) + alpha4_ * (std::abs (rot1) + std::abs (rot2));
127+ const double sigma_rot2 =
128+ alpha1_ * std::abs (rot2) + alpha2_ * std::abs (trans);
129+
130+ const double rot1_noisy = rot1 + gz::math::Rand::DblNormal (0.0 , sigma_rot1);
131+ const double trans_noisy = trans + gz::math::Rand::DblNormal (0.0 , sigma_trans);
132+ const double rot2_noisy = rot2 + gz::math::Rand::DblNormal (0.0 , sigma_rot2);
133+
134+ // Integrate the noisy relative motion over the internal odometry state.
135+ // This is the key point that makes the estimate drift over time instead of
136+ // snapping back to the true simulator pose.
137+ const double current_noisy_yaw = noisy_pose_internal_.Rot ().Yaw ();
138+ const double heading_after_rot1 = current_noisy_yaw + rot1_noisy;
139+ const double new_noisy_yaw =
140+ NormalizeAngle (current_noisy_yaw + rot1_noisy + rot2_noisy);
141+
142+ noisy_pose_internal_.Pos ().X (
143+ noisy_pose_internal_.Pos ().X () + trans_noisy * std::cos (heading_after_rot1));
144+ noisy_pose_internal_.Pos ().Y (
145+ noisy_pose_internal_.Pos ().Y () + trans_noisy * std::sin (heading_after_rot1));
146+ noisy_pose_internal_.Pos ().Z (current_true_pose.Pos ().Z ());
147+
148+ noisy_pose_internal_.Rot () = gz::math::Quaterniond (0.0 , 0.0 , new_noisy_yaw);
87149 noisy_pose_internal_.Rot ().Normalize ();
88150
89- gz::math::Vector3d deltaPos (v_noisy * dt, 0 , 0 );
90- noisy_pose_internal_.Pos () += noisy_pose_internal_.Rot ().RotateVector (deltaPos);
151+ // The published twist is consistent with the noisy increment that has just
152+ // been integrated. This keeps the message self-consistent.
153+ last_noisy_linear_velocity_ = trans_noisy / dt;
154+ last_noisy_angular_velocity_ = NormalizeAngle (rot1_noisy + rot2_noisy) / dt;
91155
92156 nav_msgs::msg::Odometry odom_msg;
93157 odom_msg.header .stamp = ros_node_->now ();
@@ -103,9 +167,41 @@ namespace custom_plugins
103167 odom_msg.pose .pose .orientation .z = noisy_pose_internal_.Rot ().Z ();
104168 odom_msg.pose .pose .orientation .w = noisy_pose_internal_.Rot ().W ();
105169
106- odom_msg.twist .twist .linear .x = v_noisy;
107- odom_msg.twist .twist .angular .z = w_noisy;
170+ odom_msg.twist .twist .linear .x = last_noisy_linear_velocity_;
171+ odom_msg.twist .twist .linear .y = 0.0 ;
172+ odom_msg.twist .twist .linear .z = 0.0 ;
173+
174+ odom_msg.twist .twist .angular .x = 0.0 ;
175+ odom_msg.twist .twist .angular .y = 0.0 ;
176+ odom_msg.twist .twist .angular .z = last_noisy_angular_velocity_;
177+
178+ // Fill the most relevant covariance entries with values consistent with the
179+ // motion model. The message remains simple, but no longer pretends to have
180+ // perfect certainty.
181+ const double pose_var_xy = sigma_trans * sigma_trans;
182+ const double pose_var_yaw =
183+ (sigma_rot1 * sigma_rot1) + (sigma_rot2 * sigma_rot2);
184+
185+ const double twist_var_v = pose_var_xy / (dt * dt);
186+ const double twist_var_w = pose_var_yaw / (dt * dt);
187+
188+ odom_msg.pose .covariance [0 ] = pose_var_xy;
189+ odom_msg.pose .covariance [7 ] = pose_var_xy;
190+ odom_msg.pose .covariance [14 ] = 1e-9 ;
191+ odom_msg.pose .covariance [21 ] = 1e-9 ;
192+ odom_msg.pose .covariance [28 ] = 1e-9 ;
193+ odom_msg.pose .covariance [35 ] = pose_var_yaw;
194+
195+ odom_msg.twist .covariance [0 ] = twist_var_v;
196+ odom_msg.twist .covariance [7 ] = 1e-9 ;
197+ odom_msg.twist .covariance [14 ] = 1e-9 ;
198+ odom_msg.twist .covariance [21 ] = 1e-9 ;
199+ odom_msg.twist .covariance [28 ] = 1e-9 ;
200+ odom_msg.twist .covariance [35 ] = twist_var_w;
108201
109202 ros_pub_->publish (odom_msg);
203+
204+ // Store the true pose only to extract the next real motion increment.
205+ last_true_pose_ = current_true_pose;
110206 }
111207}
0 commit comments