-
Notifications
You must be signed in to change notification settings - Fork 123
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
unicycle motion model: add option to rotate the process noise covariance to the current state orientation #325
base: devel
Are you sure you want to change the base?
Conversation
…nce to the current state orientation using its yaw angle
…ariance to state orientation
IMHO if this is the tright thing to do, we should always do it instead of having a parameter. I'm not sure if that's the case though. I'd have to look into the math, but I don't have time atm. |
…y to x and y position as the other state variables are already along the current state orientation
fuse_models/src/unicycle_2d.cpp
Outdated
@@ -348,8 +352,20 @@ void Unicycle2D::generateMotionModel( | |||
state_history_.emplace(beginning_stamp, std::move(state1)); | |||
state_history_.emplace(ending_stamp, std::move(state2)); | |||
|
|||
// Scale process noise covariance pose by the norm of the current state twist | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove extra empty line:
fuse_models/src/unicycle_2d.cpp
Outdated
// Rotate the process noise covariance with the yaw angle of the current state orientation | ||
if (rotate_process_noise_covariance_to_state_orientation_) | ||
{ | ||
auto rotation_matrix = Eigen::Rotation2Dd(state1.pose.yaw()).toRotationMatrix(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be const
:
auto rotation_matrix = Eigen::Rotation2Dd(state1.pose.yaw()).toRotationMatrix(); | |
const auto rotation_matrix = Eigen::Rotation2Dd(state1.pose.yaw()).toRotationMatrix(); |
fuse_models/src/unicycle_2d.cpp
Outdated
rotate_process_noise_covariance_to_state_orientation_, | ||
rotate_process_noise_covariance_to_state_orientation_); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indentation seems to be missing one space for each line here:
rotate_process_noise_covariance_to_state_orientation_, | |
rotate_process_noise_covariance_to_state_orientation_); | |
rotate_process_noise_covariance_to_state_orientation_, | |
rotate_process_noise_covariance_to_state_orientation_); |
@@ -189,6 +189,9 @@ class Unicycle2D : public fuse_core::AsyncMotionModel | |||
fuse_core::Matrix8d process_noise_covariance_; //!< Process noise covariance matrix | |||
bool scale_process_noise_{ false }; //!< Whether to scale the process noise covariance pose by the norm | |||
//!< of the current state twist | |||
bool rotate_process_noise_covariance_to_state_orientation_{ false }; //!< Whether to rotate the process noise |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I said before, I wonder if this should always be done. I sounds like it's probably the right thing to do. In that case, I think we should always do it, and we wouldn't need a ROS param at all. 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also believe that this should always be done and would enable it for all our robots. That's why I now also removed the ROS parameter with the latest commit.
The only reason why I created a ROS parameter before and set it to false by default was that existing applications using fuse are not changed and remain backwards compatibility. If you agree that this is no issue, then I would be happy to have it always enabled. Otherwise I will just revert the last commit and I'm fine with a ROS parameter too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the math is wrong, then I would consider it a bug. I see no need for an option to retain the wrong math either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How can we confirm it was a bug?
Is it something we can clearly see from the math?
@fabianhirmann Would you be able to provide a unit test that only passes with your changes but didn't before? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just pushed a unit test for it that does not pass before my changes but only with my changes. There I also wrote an explanation why I expect that the final process noise covariance should be rotated. Summarized my perspective is that the process noise covariance is defined in the robot frame so when the position is defined in the world frame, we need to rotate the process noise covariance to the world frame (using the yaw angle that defines the orientation relation between the robot frame and world frame).
An example, imagine we are defining the process noise covariance with cov_robot_xx = 2.0
and cov_robot_yy = 1.0
(assuming that the process noise covariance is in the robot frame there is then less process noise covariance in the lateral direction than the longitudinal direction). Then, when having yaw angle = 90°
, I would expect that the final process noise covariance applied to the position in the world frame would have cov_world_xx = 1.0
and cov_world_yy = 2.0
because the orientation of the robot is upwards and the covariance must then get 90° rotated.
…r better coding style
…entation: remove of option to enable/disable it by a ROS parameter and instead always rotate it
…ecessary rewrite in code to make creation of the unit test easier and separated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some minor comments, but LGTM.
Thanks for adding a unit test.
I didn't have time to give this a try or do the math myself, but what you're saying and doing makes perfect sense to me.
I'll try to give this a try offline with some bagged data to sanity check the changes.
* @param[out] constraints One or more motion model constraints between the requested timestamps. | ||
* @param[out] variables One or more variables at both the \p beginning_stamp and \p ending_stamp. The | ||
* variables should include initial values for the optimizer. | ||
*/ | ||
void generateMotionModel( | ||
static void generateMotionModel( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this breaks existing code because it might break the API for children classes. 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you have to make this static
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made this function static because I am honestly not that familiar with gtest and setting the private variables process_noise_covariance_
, state_history_
, etc. seemed complicated otherwise. Also Unicycle2D::onInit()
gets some ROS parameters that again seemed more complicated to handle. Defining that function as static which I can then directly use seemed easier for me.
However, I just thought about it again and with another way I came around this issue and there is no need to set this static and make many changes in the class itself (and also many comments from you are no more relevant then). If you feel like having a static function is still better, I can just revert my changes again. I look forward to your feedback on it.
@@ -123,6 +123,26 @@ class Unicycle2D : public fuse_core::AsyncMotionModel | |||
*/ | |||
bool applyCallback(fuse_core::Transaction& transaction) override; | |||
|
|||
/** | |||
* @brief Generator function for providing to the \c TimestampManager to create a single motion model segment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this needs to be reworded, maybe something like:
* @brief Generator function for providing to the \c TimestampManager to create a single motion model segment | |
* @brief Generator function for providing to the \c TimestampManager a way to create a single motion model segment |
fuse_models/src/unicycle_2d.cpp
Outdated
@@ -204,6 +204,7 @@ void Unicycle2D::onInit() | |||
process_noise_covariance_ = fuse_core::Vector8d(process_noise_diagonal.data()).asDiagonal(); | |||
|
|||
private_node_handle_.param("scale_process_noise", scale_process_noise_, scale_process_noise_); | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the extra lines for the others are only because they need extra work other than just calling param
, so I wouldn't add a new line here:
fuse_models/src/unicycle_2d.cpp
Outdated
const std::string name, | ||
fuse_core::Matrix8d process_noise_covariance, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you really need to copy here? Why don't you do the following?:
const std::string name, | |
fuse_core::Matrix8d process_noise_covariance, | |
const std::string& name, | |
const fuse_core::Matrix8d& process_noise_covariance, |
It looks like you're relying on making a copy of the process noise covariance, so the input doesn't change. I believe it's cleaner to just create a local copy below because there are cases where we return even before using the process nosie covariance, so there's no need to copy always it.
|
||
using fuse_models::Unicycle2D::generateMotionModel; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
using fuse_models::Unicycle2D::generateMotionModel; | |
using fuse_models::Unicycle2D::generateMotionModel; |
#include <string> | ||
#include <vector> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure, but I think more general headers are usually at the end in the fuse
repo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some minor comments, but LGTM.
Thanks for adding a unit test.
I didn't have time to give this a try or do the math myself, but what you're saying and doing makes perfect sense to me.
I'll try to give this a try offline with some bagged data to sanity check the changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some minor comments, but LGTM.
Thanks for adding a unit test.
I didn't have time to give this a try or do the math myself, but what you're saying and doing makes perfect sense to me.
I'll try to give this a try offline with some bagged data to sanity check the changes.
…te static function Unicycle2D::generateMotionModel and make changes to unit test to test that now again non-static protected member function which needs setting private member variables before
0c2c81e
to
ecb11a0
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks a lot better IMO. Thanks for doing it!
Just one more comment about using ROS node. Not sure why that is required. 🤔
Please remove if not needed.
int main(int argc, char **argv) | ||
{ | ||
testing::InitGoogleTest(&argc, argv); | ||
return RUN_ALL_TESTS(); | ||
ros::init(argc, argv, "unicycle_2d_test"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need ROS?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need ROS because the Unicycle2D
class is derived from the fuse_core::AsyncMotionModel
which contains variables of ros::NodeHandle
and ros::AsyncSpinner
and if we don't initialize ROS, there are the following errors shown:
[FATAL ${node} ros.roscpp 1684306444.156373179]: You must call ros::init() before creating the first NodeHandle
[ERROR ${node} ros.roscpp 1684306444.167166713]: [registerPublisher] Failed to contact master at [:0]. Retrying...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. Before the unit tests didn't create a Unicycle2DTest
object, but now you create one.
I don't think there's any workaround to avoid this, so I think it's fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
But you'd need @svwilliams or @ayrton04 , or someone else to review and merge this.
int main(int argc, char **argv) | ||
{ | ||
testing::InitGoogleTest(&argc, argv); | ||
return RUN_ALL_TESTS(); | ||
ros::init(argc, argv, "unicycle_2d_test"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. Before the unit tests didn't create a Unicycle2DTest
object, but now you create one.
I don't think there's any workaround to avoid this, so I think it's fine.
https://build.ros.org/job/Mpr__fuse__ubuntu_bionic_amd64/70/ and https://build.ros.org/job/Npr__fuse__ubuntu_focal_amd64/72/ seem to have failed due to CI infrastructure issues. @ayrton04 @svwilliams I wonder if there's a way to make those checks run again. Either way, the other one is also failing. I don't have access to that one. |
I think we can just close/open the PR to kick the job off again. |
Closing and re-opening to rerun CI tests |
@svwilliams Based on https://build.ros.org/job/Mpr__fuse__ubuntu_bionic_amd64/71/console 09:46:37 Build timed out (after 120 minutes). Marking the build as failed.
11:07:30 Build was aborted the test build timeout should be increased. |
I was looking at this over the weekend. Now that this test requires a roscore running, it needs to be changed from a gtest to a rostest. The test case is waiting for a rosmaster to be available. |
…score is required to run it
@svwilliams Thank you very much for that hint! I changed it now to a rostest and the CI tests indeed run successful. |
// Rotate the process noise covariance with the yaw angle of the current state orientation | ||
const auto rotation_matrix = Eigen::Rotation2Dd(state1.pose.yaw()).toRotationMatrix(); | ||
// Apply only to x and y position as the other state variables are already along the | ||
// current state orientation | ||
process_noise_covariance.topLeftCorner<2, 2>() = | ||
rotation_matrix * process_noise_covariance.topLeftCorner<2, 2>() * rotation_matrix.transpose(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little confused with this. The actual residual that is computed for the unicycle constraints is just the position difference. That difference is expressed in the world/map frame here, so rotating the process noise into the frame doesn't make sense to me. This would make sense if that residual is also expressed in the first poses frame.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@svwilliams Has your team had a chance to validate or test this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked at some point, but haven't had time to actually fix it.
From what I remember, I agree with the assessment that the current covariance is not represented in the same frame as the error. However, I believe the correct thing to do is change the error calculation to be in the frame of Pose1 rather than to spin the covariance to be in the global frame.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that makes more sense to me. I can work on a PR that does that
This PR adds an option to rotate the process noise covariance of the unicycle motion model to the current state orientation using its yaw angle.
I noticed that this does not exist yet when I set the process noise covariance to a non-symmetric value for x- and y-coordinates which then had the unexpected effect that the resulted covariance was indeed having (in this case) less covariance in the y-values (y-position, y-velocity, y-acceleration) but always in the world frame (in this case map frame) which was not intuitive for me because I would have expected that the unicycle motion model applies the process noise covariance according to the current state orientation meaning that process noise covariance's x-values are along the longitudinal forward direction and y-values along lateral left direction (in the same way as the base link is mostly defined).
It would be great if @svwilliams, @efernandez, @ayrton04 or @carlos-m159 could have a look on it and provide feedback such that this can be integrated in the fuse main repository.
Thank you,
Fabian