Skip to content
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

Publish odometry with timer and allow to predict it #109

Conversation

efernandez
Copy link
Collaborator

  • Add predict_odom parameter that:
    1. Disables publishing the odometry in the notifyCallback
    2. Enables publishing the odometry in the tfPublishTimerCallback, predicting it into the current time.
      The Jacobian of the motion model used in the predict function is computed so the odometry covariance can be propagated.
      The odometry cached in the class is updated with the predicted pose, covariance and time stamp.
  • Add process_noise_diagonal parameter that defines the diagonal of the process noise covariance that is added to the propagated covariance computing after predicting the odometry into the current time.
    The process noise covariance has 8x8 size, so it can represent all the state, although the acceleration is ignored at the moment.
  • Add new predict free functions to compute the Jacobian of the motion model wrt the state. Two versions are provided:
    1. One that takes each single component and a plain array of double to
      the Jacobian.
    2. Another that takes aggregate objects like tf2_2d::Transform and an fuse_core::Matrix8d for the Jacobian.
  • Cache the stamp of the latest covariance successfully computed in the notifyCallback, so it's possible to know if the covariance is valid and up to date in the tfPublishTimerCallback.
    If the covariance is invalid or not up to date, no time is wasted propagating it.

@efernandez efernandez force-pushed the CORE-14776_predict_odom_upstream branch 2 times, most recently from e3388c0 to e982ccd Compare October 16, 2019 07:10
@svwilliams
Copy link
Contributor

svwilliams commented Oct 16, 2019 via email

@efernandez
Copy link
Collaborator Author

nvm, I removed my comment about the CI test failures because I figured out the roslint warnings and I fixed them.

fuse_models/src/odometry_2d_publisher.cpp Outdated Show resolved Hide resolved
* @param[in] acc_linear2_y - Second Y acceleration
* @param[in] jacobians - Jacobians wrt the state
*/
// TODO(efernandez) fuse this with the templated predict function so we can use the analytic Jacobian in the cost
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would really like to get these two functions merged.

This could be done by easily by defaulting the jacobian variable to null.

template<typename T>
inline void predict(
  const T position1_x,
  ...
  T& acc_linear2_y,
  T* jacobian = nullptr
{
  ...
  if (!jacobian) return;
  ...
}

This would completely replace the existing templated function. The other two signatures (that involve tf2_2d objects) would call this function, providing a non-null jacobian when appropriate.

Another option that seems more C++/type-safe would be to use a boost::optional. I really wish std::optional was included in C++14.

template<typename T>
inline void predict(
  const T position1_x,
  ...
  T& acc_linear2_y,
  boost::optional<Eigen::Matrix<T, 8, 8>&> jacobian
{
  ...
  if (!jacobian) return;
  ...
}

where I'm not exactly sure what Eigen type should be passed in. An EigenBase<T, 8, 8> maybe???? I'm just making things up now. In the case of using this function to compute Ceres cost function Jacobians, Ceres will provide a C-style double array. Within the cost function implementation, you could use Eigen::Map<> to convert the double array into an Eigen compatible type. I feel that should be possible, but don't know the exact type required off the top of my head.

Ultimately I'm fine with either approach. I just don't like having the duplicate code that exists now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried that and it didn't work because predict is used inside a cost function and Ceres requires the jacobian to be a T** double pointer: http://ceres-solver.org/analytical_derivatives.html

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. I guess what I was expecting was predict() would be wrapped in a derived ceres::CostFunction. The way Ceres populates jacobians is not directly compatible with the new predict() function anyway. So something vaguely along the lines of:

class Unicycle2DStateCostFunction : public ceres::CostFunction
{
public:
  bool Evaluate(
    double const* const* parameters,
    double* residuals,
    double** jacobians) const override
  {
    boost::optional<Eigen::Matrix8d> jacobian_matrix = boost::none;
    if (jacobians)
    {
      jacobian_matrix = Eigen::Matrix8d();
    }

    predict(
      position1,
      yaw1,
      vel_linear1,
      vel_yaw1,
      acc_linear1,
      dt_,
      position_pred,
      yaw_pred,
      vel_linear_pred,
      vel_yaw_pred,
      acc_linear_pred,
      jacobian_matrix);  // Only computes jacobians if Ceres wants them

    // Populate the Ceres-requested jacobian blocks
    // I'm having to think about what goes here too hard for a code review.
    // The predict() function computes State2 as a function of State1.
    // But the cost function computes (conceptually) "error = State2 - State1"
    // I'm sure there is a way to translate the predict() jacobian into a Ceres
    // jacobian. Regardless, Ceres wants the jacobian in blocks, not as one
    // giant matrix, so some copying will be required.
    if (jacobians)
    {
      if (jacobians[0])
      {
        Eigen::Map<fuse_core::MatrixXd>(jacobians[0], 8, 2) = jacobian_matrix->block(0, 0, 8, 2);
        // Maybe???
      }
      if (jacobians[1])
      {
        ...
      }
      ...
    }
  }

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do that next week. All other changes requested are in. I'm going to validate things still work as intended.

Copy link
Contributor

@ayrton04 ayrton04 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, Stephen's feedback notwithstanding.

fuse_models/src/odometry_2d_publisher.cpp Outdated Show resolved Hide resolved
@efernandez efernandez force-pushed the CORE-14776_predict_odom_upstream branch from eb8cb18 to 7e97427 Compare October 19, 2019 18:34
@efernandez
Copy link
Collaborator Author

I've addressed some of your review comments and replied to the others. I'm going to wait for your feedback before I continue implementing more changes on this PR.

Thanks for your review. This PR was a first iteration on this topic, so I was indeed looking for that kind of feedback. 😃

@efernandez efernandez force-pushed the CORE-14776_predict_odom_upstream branch 2 times, most recently from 44c1185 to 836c7f0 Compare October 25, 2019 10:01
@efernandez efernandez changed the title Allow to predict odometry Publish odometry with timer and allow to predict it Oct 25, 2019
}

tf_broadcaster_.sendTransform(trans);
if (params_.publish_tf)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

publish_tf wasn't used before. That was a bug I just discovered.

Copy link
Contributor

@svwilliams svwilliams left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes look good. All that's left is to merge the predict functions in some usable way.

}

if (std::any_of(process_noise_diagonal.begin(), process_noise_diagonal.end(),
[](const auto& v) { return v < 0.0; })) // NOLINT(whitespace/braces)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[](const auto& v) { return v < 0.0; })) // NOLINT(whitespace/braces)
[](const auto& v) { return v <= 0.0; })) // NOLINT(whitespace/braces)

You're probably duplicating code I wrote elsewhere, 0's really shouldn't be allowed either.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I don't allow 0.0, then the default value for process_noise_diagonal can't be all 0.0s as it's now. Then the question is what value should I use in that case. Maybe something like 1e-6? But the diagonal represents different state components, with different units, so some should be smaller than others, like the velocities or accelerations. 🤔

The reason I allowed 0.0s in this case is because it's the process noise covariance, so if the user doesn't specify it, then it'd be like there's no process noise, that it's not recommended, but the math won't blow up. Maybe I can add a ROS_WARN saying that it's not recommended to have zeros in the process noise covariance diagonal.

Regarding the check, I can only find this in unicycle 2D motion model, but it doesn't perform the check I do here. Maybe it should also have a check similar to mine.

std::vector<double> process_noise_diagonal;
private_node_handle_.param("process_noise_diagonal", process_noise_diagonal, process_noise_diagonal);
if (process_noise_diagonal.size() != 8)
{
throw std::runtime_error("Process noise diagonal must be of length 8!");
}

Copy link
Contributor

@svwilliams svwilliams Nov 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect the math will blow up actually. Inside the motion model cost function, the error is normalized by the square root information matrix. If there are any zero terms in the process noise, the covariance cannot be inverted into an information matrix.

Although this is not that process noise, I guess.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly, the process_noise_diagonal set here is NOT used in: https://github.com/locusrobotics/fuse/blob/devel/fuse_models/src/unicycle_2d.cpp#L246 and https://github.com/locusrobotics/fuse/blob/devel/fuse_models/src/unicycle_2d_state_kinematic_constraint.cpp#L81, where a covariance with zeros will certainly blow up.

This one is only used in https://github.com/locusrobotics/fuse/pull/109/files#diff-ec05e4b7290a6166c67ff2570f66529fR320, that will simply make that addition a nop.

That being said, if we agree on some initial values, or some WARN messages, I could add that, so the user is aware of a potentially bad configuration.

Ideally, the publisher should be using the same process noise covariance as the unicycle_2d motion model. Indeed, I think the publisher should allow the user to specify the motion model to use for prediction, but that would be another story, for another PR.


return;
}
odom_pub_.publish(odom_output_);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@svwilliams @ayrton04 I just fixed this. I have this instruction inside the if (params_.predict_to_current_time) block, so if that parameter was false, the odometry was never published. Now it's always published, either with the optimization or predicted value. Let me know if you think something is wrong with this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote the comment before pushing the changes, but you can simply search for publish( and se e that now is outside that if block.

refs CORE-14776
refs CORE-14776

* Don't publish the odometry in the notifyCallback method, and publish
  it in the publishTimerCallback (formerly tfPublishTimerCallback)
  method instead.
* Add 'process_noise_diagonal' parameter that defines the diagonal of
  the process noise covariance that is added to the propagated
  covariance computing after predicting the odometry into the current
  time.
  The process noise covariance has 8x8 size, so it can represent all
  the state, although the acceleration is ignored at the moment.
* Add new 'predict' free functions to compute the Jacobian of the motion
  model wrt the state. Two versions are provided:
  1. One that takes each single component and a plain array of double to
     the Jacobian.
  2. Another that takes aggregate objects like tf2_2d::Transform and an
     fuse_core::Matrix8d for the Jacobian.
* Cache the stamp of the latest covariance successfully computed in the
  notifyCallback, so it's possible to know if the covariance is valid
  and up to date in the publishTimerCallback method.
  If the covariance is invalid or not up to date, no time is wasted
  propagating it.
refs CORE-14776

Check all entries are positive.
refs CORE-14776

Now the publish_frequency ROS parameter (formerly tf_publish_frequency)
controls both the filtered state publication and the TF broadcasting.
refs CORE-14776

When we lookup the base_to_odom transform we should use the same time
stamp as trans, which is the same as the odom_output_, which can either
be last_stamp_ if predict_to_current_time is false, or
event.current_real if it's true.
@efernandez efernandez force-pushed the CORE-14776_predict_odom_upstream branch from 836c7f0 to cceb457 Compare November 1, 2019 18:42
auto base_to_odom = tf_buffer_->lookupTransform(
params_.base_link_frame_id,
params_.odom_frame_id,
trans.header.stamp,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@svwilliams @ayrton04 This was a bug! Before the time stamp passed to lookupTransform was latest_stamp_, but if predict_to_current_time is true, this should be event.current_real instead. Using trans.header.stamp we get the correct stamp in both cases.

Because of this, with a robot turning in place the pose estimation overshoots. This is easy to see looking at a LIDAR scan alignment wrt the map. After this fix, the scans aligned perfectly. 😃

Let me know if you want this on another PR, only with this change, as it looks like a big issue.

@efernandez
Copy link
Collaborator Author

@svwilliams I still need to merge the predict functions, but I also found a couple of issues. See my comments in the code. I've thoroughly tested this brunch in sim and real data. I'll continue doing so, but now I'm more confident on the code contributed here.

I'll try to get the predict functions merged asap, hopefully over the weekend. But maybe it's better that this PR gets merged and I create another one after, so it's easier to review.

@svwilliams
Copy link
Contributor

Merging this PR, and expecting a second "predict" PR at some point in the future. :)

@svwilliams svwilliams merged commit eaaf1f2 into locusrobotics:devel Nov 5, 2019
@efernandez efernandez deleted the CORE-14776_predict_odom_upstream branch March 11, 2020 09:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants