Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -15,13 +15,18 @@
#ifndef BELUGA_MOTION_HPP
#define BELUGA_MOTION_HPP

#include <beluga/motion/ackermann_drive_model.hpp>
#include <beluga/motion/differential_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 (AckermannDriveModel)
*/

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

#endif
288 changes: 288 additions & 0 deletions beluga/include/beluga/motion/ackermann_drive_model.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
// Copyright 2022-2023 Ekumen, Inc.

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 this file needs a new name I think.

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.

I was in doubt about the name, so that it is the same as the class it could be called velocity_drive_model.hpp what do you think?

@hidmic hidmic Oct 3, 2025

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.

Hmm, maybe differential velocity drive model is the least surprising name for this. It's not entirely accurate but it's not opaque either.

//
// 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_ACKERMANN_DRIVE_MODEL_HPP
#define BELUGA_MOTION_ACKERMANN_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 AckermannControls {
double v; ///< Linear velocity (m/s)
double phi; ///< Steering angle (rad)
};

/// Parameters to construct a AckermannDriveModel instance.
/**
* See Probabilistic Robotics \cite thrun2005probabilistic Chapter 5.3, particularly table 5.3.

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 💯

*/
struct AckermannDriveModelParam {
/// Steering noise from steering angle
/**
* How much steering noise is generated by the steering angle.
* Also known as `alpha1 in the Ackermann drive model param`.
*/
double steering_noise_from_steering;
/// Steering noise from linear velocity
/**
* How much steering noise is generated by the linear velocity.
* Also known as `alpha2 in the Ackermann drive model param`.
*/
double steering_noise_from_velocity;
/// Velocity noise from linear velocity
/**
* How much velocity noise is generated by the linear velocity.
* Also known as `alpha3 in the Ackermann drive model param`.
*/
double velocity_noise_from_velocity;
/// Velocity noise from steering angle
/**
* How much velocity noise is generated by the steering angle.
* Also known as `alpha4 in the Ackermann drive model param`.
*/
double velocity_noise_from_steering;
/// Additional orientation noise from linear velocity
/**
* How much extra orientation noise is generated by the linear velocity.
* Also known as `alpha6`.
*/
double orientation_noise_from_velocity;
/// Additional orientation noise from steering angle
/**
* How much extra orientation noise is generated by the steering angle.
* Also known as `alpha7`.
*/
double orientation_noise_from_steering;

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 The alpha6 and alpha7 parameters are from probabilistic robotics to calculate additional angular noise in the final orientation. In this hybrid model for the Ackerman, I could eliminate it and not use this additional noise. would it be correct?

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.

As we discussed in the meeting yesterday, let's keep them. From the arguments in Probabilistic Roboticis, they have the same reason to exist in our hybrid model as they do in the diff model.

/// Distance between the rear and front wheel axles (meters).
/**
* See \cite Localization and Mapping in Local Occupancy Grid Maps: Simulation
* in Ackermann model mobile robot by Ronald A. Cardenas , Jasper W. Huanay
* and Ivan Calle
*/
double wheelbase;
};

/// Velocity model for a Ackermann drive.
/**
* Supports 2D and (flattened) 3D state types.
* This class satisfies \ref MotionModelPage.
*
* The model is and adaptation using the single track kinematic model

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.

Suggested change
* The model is and adaptation using the single track kinematic model
* The model is an adaptation using the single track kinematic model

* and the noise models of Probabilistic Robotics.
* The model serves for any drive that can be simplified to a Single Track vehicle:
* ackermann, bicycle, tri-cycle, etc.
* See Probabilistic Robotics \cite thrun2005probabilistic Chapter 5.3.
*
Comment on lines +104 to +105

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.

Explain here that the model is and adaptation using the single track kinematic model and the noise models of Probabilistic Robotics.

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.

Notice that this model serves for any drive that can be simplified to a Single Track vehicle: ackermann, bicycle, tri-cycle, etc.

* \tparam StateType Type for particle's state. Either Sophus::SE2d or Sophus::SE3d.
*/
template <class StateType = Sophus::SE2d>
class AckermannDriveModel {
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 = AckermannDriveModelParam;

/// Constructs a AckermannDriveModel instance.
/**
* \param params Parameters to configure this instance.
* See beluga::AckermannDriveModelParam for details.
*/
explicit AckermannDriveModel(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;

const auto time = timestamped.timestamp;
const auto previous_time = previous_timestamped.timestamp;
const auto delta_time = std::chrono::duration<double>(time - previous_time);
return sampling_fn_2d(pose, previous_pose, delta_time);
}

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

[[nodiscard]] auto sampling_fn_2d(
const Sophus::SE2d& pose,
const Sophus::SE2d& previous_pose,
std::chrono::duration<double> delta_time) const {
// Calculate velocities from poses
const auto controls = calculate_velocities(pose, previous_pose, delta_time);

// Velocity noise parameters (following velocity motion model from Probabilistic Robotics)
// Use temporary distributions to safely extract param_type objects
const auto linear_velocity_distribution = std::normal_distribution<double>{
controls.v, std::sqrt(
params_.velocity_noise_from_velocity * controls.v * controls.v +
params_.velocity_noise_from_steering * controls.phi * controls.phi)};
const auto linear_velocity_params = linear_velocity_distribution.param();

const auto steering_angle_distribution = std::normal_distribution<double>{
controls.phi, std::sqrt(
params_.steering_noise_from_velocity * controls.v * controls.v +
params_.steering_noise_from_steering * controls.phi * controls.phi)};
const auto steering_angle_params = steering_angle_distribution.param();

// Additional orientation noise (gamma_hat) using rotation parameters
const auto gamma_distribution = std::normal_distribution<double>{
0.0, // zero mean
std::sqrt(
params_.orientation_noise_from_velocity * controls.v * controls.v +
params_.orientation_noise_from_steering * controls.phi * controls.phi)};
const auto gamma_params = gamma_distribution.param();

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 phi_hat = distribution(gen, steering_angle_params);
const auto gamma_hat = distribution(gen, gamma_params);

const auto omega_hat = v_hat * std::tan(phi_hat) / params_.wheelbase;
// 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
AckermannControls calculate_velocities(
const Sophus::SE2d& pose,
const Sophus::SE2d& previous_pose,
std::chrono::duration<double> delta_time) const {
const double delta_t_sec = delta_time.count();

// Euclidean distance (chord length between poses)
const auto translation = pose.translation() - previous_pose.translation();
const double chord_distance = translation.norm();

const auto relative_transform = previous_pose.inverse() * pose;
// Angular velocity from orientation change
const auto angular_change = relative_transform.so2();

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.

💯

const double angle_change = angular_change.log();
const double angular_velocity = angle_change / delta_t_sec;

// Determine direction sign (forward/backward motion)
const double dx = relative_transform.translation().x();
const double sign = (dx >= 0.0) ? 1.0 : -1.0;

// Linear velocity calculation
double linear_velocity = 0.0;
double steering_angle = 0.0;
if (std::abs(angle_change) > small_angle_threshold) {
// 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_t_sec;
if (std::abs(linear_velocity) > small_angle_threshold) {
const double ratio = params_.wheelbase * angular_velocity / linear_velocity;
steering_angle = std::atan(ratio);
}
Comment on lines +231 to +235

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 need to think this, the fact that we need to guard this is flagging me that there's an problem with how we are calculating motion.

} else {
// Straight line motion: v = distance / time
linear_velocity = sign * chord_distance / delta_t_sec;
}
return AckermannControls{linear_velocity, steering_angle};
}
/// 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,
std::chrono::duration<double> delta_time) const {
const double delta_t_sec = delta_time.count();

const auto current_theta = state.so2().log();

Sophus::SE2d new_pose;

if (std::abs(omega_hat) < small_angle_threshold) {
// Nearly straight line motion
const auto translation =
Eigen::Vector2d{v_hat * delta_t_sec * std::cos(current_theta), v_hat * delta_t_sec * std::sin(current_theta)};
const auto new_theta = current_theta + gamma_hat * delta_t_sec;
new_pose = Sophus::SE2d{Sophus::SO2d{new_theta}, state.translation() + translation};
} else {
Comment thread
glpuga marked this conversation as resolved.
// 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_t_sec);
const auto dy = (v_hat / omega_hat) * std::cos(current_theta) -
(v_hat / omega_hat) * std::cos(current_theta + omega_hat * delta_t_sec);
const auto translation = Eigen::Vector2d{dx, dy};
const auto new_theta = current_theta + omega_hat * delta_t_sec + gamma_hat * delta_t_sec;
new_pose = Sophus::SE2d{Sophus::SO2d{new_theta}, state.translation() + translation};
}
return new_pose;
}

param_type params_;

/// Threshold for distinguishing between straight-line and circular motion.
/**
* Below this threshold (~0.57 degrees), motion is treated as straight-line to avoid
* numerical instabilities in radius calculations for nearly-zero angular velocities.
*/
static constexpr double small_angle_threshold = 0.01;
};

/// Alias for a 2D Ackermann drive model, for convenience.
using AckermannDriveModel2d = AckermannDriveModel<Sophus::SE2d>;
} // namespace beluga

#endif
9 changes: 6 additions & 3 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 @@ -100,7 +101,9 @@ class OmnidirectionalDriveModel {
*/
template <class Control, typename = common_tuple_type_t<Control, control_type>>
[[nodiscard]] auto operator()(Control&& action) const {
const auto& [pose, previous_pose] = action;
const auto& [pose_stamped, previous_pose_stamped] = action;
const state_type& pose = pose_stamped;
const state_type& previous_pose = previous_pose_stamped;

const auto translation = pose.translation() - previous_pose.translation();
const double distance = translation.norm();
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