Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions beluga/include/beluga/algorithm/amcl_core.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class Amcl {
using measurement_type = typename SensorModel::measurement_type;
using state_type = typename SensorModel::state_type;
using map_type = typename SensorModel::map_type;
using control_type = TimeStamped<state_type>;
using spatial_hasher_type = spatial_hash<state_type>;
using random_state_generator_type = RandomStateGenerator;
using estimation_type = std::invoke_result_t<beluga::detail::estimate_fn, std::vector<state_type>>;
Expand Down Expand Up @@ -162,7 +163,7 @@ class Amcl {
* \return An optional pair containing the estimated pose and covariance after the update,
* or std::nullopt if no update was performed.
*/
auto update(state_type control_action, measurement_type measurement) -> std::optional<estimation_type> {
auto update(control_type control_action, measurement_type measurement) -> std::optional<estimation_type> {
if (particles_.empty()) {
return std::nullopt;
}
Expand Down Expand Up @@ -227,7 +228,7 @@ class Amcl {

random_state_generator_type random_state_generator_;

beluga::RollingWindow<state_type, 2> control_action_window_;
beluga::RollingWindow<control_type, 2> control_action_window_;

bool force_update_{true};
};
Expand Down
6 changes: 6 additions & 0 deletions beluga/include/beluga/motion.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@
#define BELUGA_MOTION_HPP

#include <beluga/motion/differential_drive_model.hpp>
#include <beluga/motion/differential_velocity_drive_model.hpp>
#include <beluga/motion/omnidirectional_drive_model.hpp>
#include <beluga/motion/stationary_model.hpp>

/**
* \file
* \brief Includes all Beluga motion models.
*
* Motion models in Beluga include:
* - Position-based models: Use pose differences (DifferentialDriveModel, OmnidirectionalDriveModel, StationaryModel)
* - Velocity-based models: Use timestamped poses to calculate velocities (DifferentialVelocityDriveModel)
*/

/**
Expand Down Expand Up @@ -61,6 +66,7 @@
* - beluga::DifferentialDriveModel
* - beluga::OmnidirectionalDriveModel
* - beluga::StationaryModel
* - beluga::DifferentialVelocityDriveModel
*/

#endif
274 changes: 274 additions & 0 deletions beluga/include/beluga/motion/differential_velocity_drive_model.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
// Copyright 2022-2023 Ekumen, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#ifndef BELUGA_MOTION_DIFFERENTIAL_VELOCITY_DRIVE_MODEL_HPP
#define BELUGA_MOTION_DIFFERENTIAL_VELOCITY_DRIVE_MODEL_HPP

#include <chrono>
#include <random>
#include <sophus/se3.hpp>
#include <tuple>

#include <beluga/type_traits/tuple_traits.hpp>
#include <beluga/utility/time_stamped.hpp>

#include <beluga/3d_embedding.hpp>
#include <sophus/se2.hpp>
#include <sophus/so2.hpp>
#include <type_traits>

/**
* \file
* \brief Implementation of a velocity motion model.
*/

namespace beluga {

/// Velocity components for differential drive motion model.
struct Velocity {
double v; ///< Linear velocity (m/s)
double w; ///< Angular velocity (rad/s)
};

/// Parameters to construct a DifferentialVelocityDriveModel instance.
/**
* See Probabilistic Robotics \cite thrun2005probabilistic Chapter 5.3, particularly table 5.3.
*/
struct DifferentialVelocityDriveModelParam {
/// Rotational noise from rotational velocity
/**
* How much rotational noise is generated by the rotational velocity.
* Also known as `alpha1 in the differential drive model param`.
*/
double rotation_noise_from_rotation;
/// Rotational noise from translation velocity
/**
* How much rotational noise is generated by the linear velocity.
* Also known as `alpha2 in the differential drive model param`.
*/
double rotation_noise_from_translation;
/// Translational noise from translation velocity
/**
* How much translational noise is generated by the linear velocity.
* Also known as `alpha3 in the differential drive model param`.
*/
double translation_noise_from_translation;
/// Translational noise from rotational velocity
/**
* How much translational noise is generated by the rotational velocity.
* Also known as `alpha4 in the differential drive model param`.
*/
double translation_noise_from_rotation;
/// Additional orientation noise from translational velocity
/**
* How much extra orientation noise is generated by the linear velocity.
* Also known as `alpha6`.
*/
double orientation_noise_from_translation;
/// Additional orientation noise from rotational velocity
/**
* How much extra orientation noise is generated by the rotational velocity.
* Also known as `alpha7`.
*/
double orientation_noise_from_rotation;
};

/// Sampled velocity model for a differential drive.
/**
* Supports 2D and (flattened) 3D state types.
* This class satisfies \ref MotionModelPage.
*
* See Probabilistic Robotics \cite thrun2005probabilistic Chapter 5.3.
*
* \tparam StateType Type for particle's state. Either Sophus::SE2d or Sophus::SE3d.
*/
template <class StateType = Sophus::SE2d>
class DifferentialVelocityDriveModel {
static_assert(
std::is_same_v<StateType, Sophus::SE2d> or std::is_same_v<StateType, Sophus::SE3d>,
"Velocity model only supports SE2 and SE3 state types.");

public:
/// 2D or flattened 3D pose as motion model state (to match that of the particles).
using state_type = StateType;

/// Time point type for motion model control actions.
using timestamped_state_type = TimeStamped<state_type>;

/// Current and previous pose estimates and time points as motion model control action.
using control_type = std::tuple<timestamped_state_type, timestamped_state_type>;

/// Parameter type that the constructor uses to configure the motion model.
using param_type = DifferentialVelocityDriveModelParam;

/// Constructs a DifferentialVelocityDriveModel instance.
/**
* \param params Parameters to configure this instance.
* See beluga::DifferentialVelocityDriveModelParam for details.
*/
explicit DifferentialVelocityDriveModel(const param_type& params) : params_{params} {}

/// Computes a state sampling function conditioned on a given control action.
/**
* \tparam Control A tuple-like container matching the model's `control_type`.
* \param action Control action to condition the motion model with.
* \return a callable satisfying \ref StateSamplingFunctionPage.
*/
template <class Control, typename = common_tuple_type_t<Control, control_type>>
[[nodiscard]] auto operator()(const Control& action) const {
const auto& [timestamped, previous_timestamped] = action;
const auto& pose = timestamped.value;
const auto& previous_pose = previous_timestamped.value;

auto time = timestamped.timestamp;
auto previous_time = previous_timestamped.timestamp;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: both can be const

const auto delta_time = std::chrono::duration<double>(time - previous_time).count();
if constexpr (std::is_same_v<state_type, Sophus::SE2d>) {
return sampling_fn_2d(pose, previous_pose, delta_time);
} else {
return sampling_fn_3d(pose, previous_pose, delta_time);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

is it possible to keep the delta_t in a std::chrono::duration, so that it retains units until we actually need to do some calculation?

}

private:
using control_type_2d = std::tuple<Sophus::SE2d, Sophus::SE2d>;
using control_type_3d = std::tuple<Sophus::SE3d, Sophus::SE3d>;

[[nodiscard]] auto sampling_fn_3d(const Sophus::SE3d& pose, const Sophus::SE3d& previous_pose, double delta_time)
const {
const auto current_pose_2d = To2d(pose);
const auto previous_pose_pose_2d = To2d(previous_pose);
const auto two_d_sampling_fn = sampling_fn_2d(current_pose_2d, previous_pose_pose_2d, delta_time);
return [=](const state_type& state, auto& gen) { return To3d(two_d_sampling_fn(To2d(state), gen)); };
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What's the use of providing support for 3D state (and flattening it) if this model is 2D only? Do we need this to support some other model? they are all 2D AFAIK.


[[nodiscard]] auto sampling_fn_2d(const Sophus::SE2d& pose, const Sophus::SE2d& previous_pose, double delta_time)
const {
// Calculate velocities from poses
const auto velocity = calculate_velocities(pose, previous_pose, delta_time);

using DistributionParam = typename std::normal_distribution<double>::param_type;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't think we can do this. I don't think we can do assumptions regarding the (undefined) normal_distribution::param_type , in particular of what arguments it takes for construction.

The normal_distribution(const param_type& parm) is meant to be used with the param() function member of a preexisting instance, not from an instance of param_type created by us. In particular, I found no definitions of what the constructor of such a type must look like, which makes me think it's implementation dependent.

A normal_distribution object is defined by two parameters: its mean (μ) and its standard deviation (stddev, &sigma). An object of type param_type carries this information, but it is meant to be used only to construct or specify the parameters for a normal_distribution object, not to inspect the individual parameters.

The risk is this not being portable. If we are lucky, this might be a build error, but if we are not it might be an invisible error if instead of storing the mean and the std, an implementation decides to store the mean and the variance instead.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I looked into this because I would have expected the second constructor argument to be the variance, not the standard deviation.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

To solve this, I can avoid building param_type directly. Instead, I can create instances of std::normal_distribution with the correct parameters, but the other models are using this same implementation. If necessary, I can change it in the other models as well after this pr.


// Velocity noise parameters (following velocity motion model from Probabilistic Robotics)
const auto linear_velocity_params = DistributionParam{
velocity.v, std::sqrt(
params_.translation_noise_from_translation * std::abs(velocity.v) +
params_.translation_noise_from_rotation * std::abs(velocity.w))};

const auto angular_velocity_params = DistributionParam{
velocity.w, std::sqrt(
params_.rotation_noise_from_translation * std::abs(velocity.v) +
params_.rotation_noise_from_rotation * std::abs(velocity.w))};

// Additional orientation noise (gamma_hat) using rotation parameters
const auto gamma_params = DistributionParam{
0.0, // zero mean
std::sqrt(
params_.orientation_noise_from_translation * std::abs(velocity.v) +
params_.orientation_noise_from_rotation * std::abs(velocity.w))};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If I'm reading tables 5.1 and 5.3 correctly, everything in std::abs must instead be squared before multiplication. std::abs would be correct only if they were outside of the square root.


return [=](const auto& state, auto& gen) {
static thread_local auto distribution = std::normal_distribution<double>{};

// Sample noisy velocities
const auto v_hat = distribution(gen, linear_velocity_params);
const auto omega_hat = distribution(gen, angular_velocity_params);
const auto gamma_hat = distribution(gen, gamma_params);

// Apply velocity motion model
return apply_velocity_motion(state, v_hat, omega_hat, gamma_hat, delta_time);
};
}

/// Calculate linear and angular velocities from two poses and delta time
Velocity calculate_velocities(const Sophus::SE2d& pose, const Sophus::SE2d& previous_pose, double delta_time) const {
// Euclidean distance (chord length between poses)
const auto translation = pose.translation() - previous_pose.translation();
const double chord_distance = translation.norm();

// Angular velocity from orientation change
const auto angular_change = pose.so2() * previous_pose.so2().inverse();
const double angle_change = angular_change.log();
const double angular_velocity = angle_change / delta_time;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we check if delta_t == 0 somewhere?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

if Δt is zero, the resulting velocity is also zero, so the particle remains exactly in the same initial state.

This makes the filter too rigid, as there is no dispersion in the particle cloud.

In this case, is it advisable to maintain a small amount of noise to allow for some dispersion even when Δt = 0?
In other words, is it advisable to inject noise even if there is no deterministic motion?

Is there a standard way or practice for handling this case in the probabilistic motion model?


// Determine direction sign (forward/backward motion)
const auto forward_direction =
Eigen::Vector2d{std::cos(previous_pose.so2().log()), std::sin(previous_pose.so2().log())};
const double dot_product = translation.dot(forward_direction);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think you can use the unit_complex() method of the pose instead to get the complex equivalente with x() and y() members being the projections youre calculating trigonometrically.

If fact, log() is doing the exact opposite calculation, so this will be expensive if benchmarked: https://github.com/strasdat/Sophus/blob/d0b7315a0d90fc6143defa54596a3a95d9fa10ec/sophus/so2.hpp#L165

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

In any case, an alternative calculation is to calculate the current-pose-in-the-previous-pose-frame (which you're already partly doing in line 201 for the orientation), and check if the A-to-B transform that results has positive X coordinate.

const double sign = (dot_product >= 0) ? 1.0 : -1.0;

// Linear velocity calculation
double linear_velocity = 0.0;

if (std::abs(angle_change) > std::numeric_limits<double>::epsilon()) {
// Circular motion: calculate radius from chord and angle
// For an arc: chord = 2r·sin(θ/2), therefore r = chord / (2·sin(θ/2))
const double radius = chord_distance / (2.0 * std::sin(std::abs(angle_change) / 2.0));

// Arc length: s = r · θ
const double arc_distance = radius * std::abs(angle_change);

// Linear velocity with direction sign
linear_velocity = sign * arc_distance / delta_time;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This calculation is probably not numerically very stable for straight motion. In regular straight motion, the angle change will never be exactly zero (or below epsilon), it will just be a very small number. For the purposes of this code, the sin of a very small number is is the same number (in the limit), so follow me here:

  • we willl calculate radius, a huge number, because we divide a regular number by something that is basically half of angle-change, a very small number.
  • that exploding radius is then multiplied by that small number again to get the arc_distance, which gets something in human scale again, but only if resolution were infinite. You're using doubles (accurate to 15 digits) , so you have a lot of leeway here, but very small angles might give totally unreasonable numbers if the radius cannot be represented accurately.

I think we should take a shortcut here if std::abs(angle_change) < 0.01, and in that case:

arc_distance = chord_distance

the error in this cheap approximation is the error in the 1 / (2 * sin(0.01 / 2)) == 1 / 0.01 for small x, which a quick calculation tells me is about 10e-5 and otherwise problem-free, so totally worth it.

download


} else {
// Straight line motion: v = distance / time
linear_velocity = sign * chord_distance / delta_time;
}

return Velocity{linear_velocity, angular_velocity};
}
/// Apply velocity motion model to get new pose
Sophus::SE2d apply_velocity_motion(
const Sophus::SE2d& state,
double v_hat,
double omega_hat,
double gamma_hat,
double delta_time) const {
const auto current_theta = state.so2().log();

Sophus::SE2d new_pose;

if (std::abs(omega_hat) < std::numeric_limits<double>::epsilon()) {
// Nearly straight line motion
const auto translation =
Eigen::Vector2d{v_hat * delta_time * std::cos(current_theta), v_hat * delta_time * std::sin(current_theta)};
const auto new_theta = current_theta + gamma_hat * delta_time;
new_pose = Sophus::SE2d{Sophus::SO2d{new_theta}, state.translation() + translation};
} else {
// Circular motion (following velocity motion model equations)
const auto dx = -(v_hat / omega_hat) * std::sin(current_theta) +
(v_hat / omega_hat) * std::sin(current_theta + omega_hat * delta_time);
const auto dy = (v_hat / omega_hat) * std::cos(current_theta) -
(v_hat / omega_hat) * std::cos(current_theta + omega_hat * delta_time);
const auto translation = Eigen::Vector2d{dx, dy};
const auto new_theta = current_theta + omega_hat * delta_time + gamma_hat * delta_time;
new_pose = Sophus::SE2d{Sophus::SO2d{new_theta}, state.translation() + translation};
}

return new_pose;
}

param_type params_;
};

/// Alias for a 2D velocity drive model, for convenience.
using VelocityDriveModel2d = DifferentialVelocityDriveModel<Sophus::SE2d>;

/// Alias for a 3D velocity drive model, for convenience.
using VelocityDriveModel3d = DifferentialVelocityDriveModel<Sophus::SE3d>;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't think DifferentialVelocityDriveModel is a good name, and it might be that this is not the best model.
Remember that we aimed at creating a motion model for Ackermann/Bicycle (more generally SingleTrack ). Check the first reference in https://github.com/ekumenlabs/monaco_f1tenth/issues/24 .

A differential can have 0 translation and yet non-zero angular velocity; a Single Track cannot. This extra freedom probably accounts for the extra noise sources.

The models are quite similar, but I think the Single Track is the one we are aiming for.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@glpuga I will schedule a meeting to discuss this part.


} // namespace beluga

#endif
11 changes: 6 additions & 5 deletions beluga/include/beluga/motion/omnidirectional_drive_model.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,12 @@ struct OmnidirectionalDriveModelParam {
*/
class OmnidirectionalDriveModel {
public:
/// Current and previous odometry estimates as motion model control action.
using control_type = std::tuple<Sophus::SE2d, Sophus::SE2d>;
/// 2D pose as motion model state (to match that of the particles).
using state_type = Sophus::SE2d;

/// Current and previous odometry estimates as motion model control action.
using control_type = std::tuple<state_type, state_type>;

/// Parameter type that the constructor uses to configure the motion model.
using param_type = OmnidirectionalDriveModelParam;

Expand All @@ -102,12 +103,12 @@ class OmnidirectionalDriveModel {
[[nodiscard]] auto operator()(Control&& action) const {
const auto& [pose, previous_pose] = action;

const auto translation = pose.translation() - previous_pose.translation();
const auto translation = pose->translation() - previous_pose->translation();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@fbattocchia if we make this change, it won't work when pose is actually of state_type.

You can work around this with:

Suggested change
const auto translation = pose->translation() - previous_pose->translation();
const state_type& pose = std::get<0>(action);
const state_type& previous_pose = std::get<1>(action);
const auto translation = pose.translation() - previous_pose.translation();

We have to sacrifice structured bindings but it'll work in all cases.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It's a good idea. I had considered this solution too, but I wasn't sure if the change would be okay.

const double distance = translation.norm();
const double distance_variance = distance * distance;

const auto& previous_orientation = previous_pose.so2();
const auto& current_orientation = pose.so2();
const auto& previous_orientation = previous_pose->so2();
const auto& current_orientation = pose->so2();
const auto rotation = current_orientation * previous_orientation.inverse();

const auto heading_rotation = Sophus::SO2d{std::atan2(translation.y(), translation.x())};
Expand Down
5 changes: 2 additions & 3 deletions beluga/include/beluga/motion/stationary_model.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,10 @@ namespace beluga {
*/
class StationaryModel {
public:
/// Current and previous odometry estimates as motion model control action.
using control_type = std::tuple<Sophus::SE2d, Sophus::SE2d>;
/// 2D pose as motion model state (to match that of the particles).
using state_type = Sophus::SE2d;

/// Current and previous odometry estimates as motion model control action.
using control_type = std::tuple<state_type, state_type>;
/// Computes a state sampling function conditioned on a given control action.
/**
* The updated state will be centered around `state` with some covariance.
Expand Down
Loading