add humble-navigation2

This commit is contained in:
X-lanni
2025-05-27 19:03:40 +08:00
parent 974abb5e1e
commit e74ec539c2
1280 changed files with 204114 additions and 0 deletions
@@ -0,0 +1,129 @@
cmake_minimum_required(VERSION 3.5)
project(nav2_mppi_controller)
add_definitions(-DXTENSOR_ENABLE_XSIMD)
add_definitions(-DXTENSOR_USE_XSIMD)
set(XTENSOR_USE_TBB 0)
set(XTENSOR_USE_OPENMP 0)
set(XTENSOR_USE_XSIMD 1)
# set(XTENSOR_DEFAULT_LAYOUT column_major) # row_major, column_major
# set(XTENSOR_DEFAULT_TRAVERSAL row_major) # row_major, column_major
find_package(ament_cmake REQUIRED)
find_package(xtensor REQUIRED)
find_package(xsimd REQUIRED)
include_directories(
include
)
set(dependencies_pkgs
rclcpp
nav2_common
pluginlib
tf2
geometry_msgs
visualization_msgs
nav_msgs
nav2_core
nav2_costmap_2d
nav2_util
tf2_geometry_msgs
tf2_eigen
tf2_ros
)
foreach(pkg IN LISTS dependencies_pkgs)
find_package(${pkg} REQUIRED)
endforeach()
nav2_package()
include(CheckCXXCompilerFlag)
check_cxx_compiler_flag("-mno-avx512f" COMPILER_SUPPORTS_AVX512)
check_cxx_compiler_flag("-msse4.2" COMPILER_SUPPORTS_SSE4)
check_cxx_compiler_flag("-mavx2" COMPILER_SUPPORTS_AVX2)
check_cxx_compiler_flag("-mfma" COMPILER_SUPPORTS_FMA)
if(COMPILER_SUPPORTS_AVX512)
add_compile_options(-mno-avx512f)
endif()
if(COMPILER_SUPPORTS_SSE4)
add_compile_options(-msse4.2)
endif()
if(COMPILER_SUPPORTS_AVX2)
add_compile_options(-mavx2)
endif()
if(COMPILER_SUPPORTS_FMA)
add_compile_options(-mfma)
endif()
# If building one the same hardware to be deployed on, try `-march=native`!
add_compile_options(-O3 -finline-limit=10000000 -ffp-contract=fast -ffast-math -mtune=generic)
add_library(mppi_controller SHARED
src/controller.cpp
src/optimizer.cpp
src/critic_manager.cpp
src/trajectory_visualizer.cpp
src/path_handler.cpp
src/parameters_handler.cpp
src/noise_generator.cpp
)
add_library(mppi_critics SHARED
src/critics/obstacles_critic.cpp
src/critics/cost_critic.cpp
src/critics/goal_critic.cpp
src/critics/goal_angle_critic.cpp
src/critics/path_align_critic.cpp
src/critics/path_align_legacy_critic.cpp
src/critics/path_follow_critic.cpp
src/critics/path_angle_critic.cpp
src/critics/prefer_forward_critic.cpp
src/critics/twirling_critic.cpp
src/critics/constraint_critic.cpp
src/critics/velocity_deadband_critic.cpp
)
set(libraries mppi_controller mppi_critics)
foreach(lib IN LISTS libraries)
target_compile_options(${lib} PUBLIC -fconcepts)
target_include_directories(${lib} PUBLIC ${xsimd_INCLUDE_DIRS}) # ${OpenMP_INCLUDE_DIRS}
target_link_libraries(${lib} xtensor xtensor::optimize xtensor::use_xsimd)
ament_target_dependencies(${lib} ${dependencies_pkgs})
endforeach()
install(TARGETS mppi_controller mppi_critics
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
install(DIRECTORY include/
DESTINATION include/
)
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
find_package(ament_cmake_gtest REQUIRED)
set(ament_cmake_copyright_FOUND TRUE)
ament_lint_auto_find_test_dependencies()
add_subdirectory(test)
# add_subdirectory(benchmark)
endif()
ament_export_libraries(${libraries})
ament_export_dependencies(${dependencies_pkgs})
ament_export_include_directories(include)
pluginlib_export_plugin_description_file(nav2_core mppic.xml)
pluginlib_export_plugin_description_file(nav2_mppi_controller critics.xml)
ament_package()
@@ -0,0 +1,23 @@
MIT License
Copyright (c) 2021-2022 Fast Sense Studio
Copyright (c) 2022-2023 Samsung Research America
Copyright (c) 2023 Open Navigation LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+323
View File
@@ -0,0 +1,323 @@
# Model Predictive Path Integral Controller
![](media/demo.gif)
## Overview
This is a predictive controller (local trajectory planner) that implements the [Model Predictive Path Integral (MPPI)](https://ieeexplore.ieee.org/document/7487277) algorithm to track a path with adaptive collision avoidance. It contains plugin-based critic functions to impact the behavior of the algorithm. It was created by [Aleksei Budyakov](https://www.linkedin.com/in/aleksei-budyakov-334889224/) and adapted & developed for Nav2 by [Steve Macenski](https://www.linkedin.com/in/steve-macenski-41a985101/).
This plugin implements the ``nav2_core::Controller`` interface allowing it to be used across the navigation stack as a local trajectory planner in the controller server's action server (``controller_server``).
This controller is measured to run at 50+ Hz on a modest Intel processor (4th gen i5). See its Configuration Guide Page for additional parameter descriptions.
It works currently with Differential, Omnidirectional, and Ackermann robots.
## MPPI Description
The MPPI algorithm is an MPC variant that finds a control velocity for the robot using an iterative approach. Using the previous time step's best control solution and the robot's current state, a set of randomly sampled perturbations from a Gaussian distribution are applied. These noised controls are forward simulated to generate a set of trajectories within the robot's motion model.
Next, these trajectories are scored using a set of plugin-based critic functions to find the best trajectory in the batch. The output scores are used to set the best control with a soft max function.
This process is then repeated a number of times and returns a converged solution. This solution is then used as the basis of the next time step's initial control.
## Features
- Predictive MPC trajectory planner
- Utilizes plugin-based critics which can be swapped out, tuned, or replaced easily by the user
- Highly optimized CPU-only performance using vectorization and tensor operations
- Supports a number of common motion models, including Ackermann, Differential-Drive, and Omni-directional
- Includes fallback mechanisms to handle soft-failures before escalating to recovery behaviors
- High-quality code implementation with Doxygen, high unit test coverage, documentation, and parameter guide
- Easily extensible to support modern research variants of MPPI
- Comes pre-tuned for good out-of-the-box behavior
## Configuration
### Controller
| Parameter | Type | Definition |
| --------------------- | ------ | -------------------------------------------------------------------------------------------------------- |
| motion_model | string | Default: DiffDrive. Type of model [DiffDrive, Omni, Ackermann]. |
| critics | string | Default: None. Critics (plugins) names |
| iteration_count | int | Default 1. Iteration count in MPPI algorithm. Recommend to keep as 1 and prefer more batches. |
| batch_size | int | Default 1000. Count of randomly sampled candidate trajectories |
| time_steps | int | Default 56. Number of time steps (points) in each sampled trajectory |
| model_dt | double | Default: 0.05. Time interval (s) between two sampled points in trajectories. |
| vx_std | double | Default 0.2. Sampling standard deviation for VX |
| vy_std | double | Default 0.2. Sampling standard deviation for VY |
| wz_std | double | Default 0.4. Sampling standard deviation for Wz |
| vx_max | double | Default 0.5. Max VX (m/s) |
| vy_max | double | Default 0.5. Max VY in either direction, if holonomic. (m/s) |
| vx_min | double | Default -0.35. Min VX (m/s) |
| wz_max | double | Default 1.9. Max WZ (rad/s) |
| temperature | double | Default: 0.3. Selectiveness of trajectories by their costs (The closer this value to 0, the "more" we take in consideration controls with less cost), 0 mean use control with best cost, huge value will lead to just taking mean of all trajectories without cost consideration |
| gamma | double | Default: 0.015. A trade-off between smoothness (high) and low energy (low). This is a complex parameter that likely won't need to be changed from the default of `0.1` which works well for a broad range of cases. See Section 3D-2 in "Information Theoretic Model Predictive Control: Theory and Applications to Autonomous Driving" for detailed information. |
| visualize | bool | Default: false. Publish visualization of trajectories, which can slow down the controller significantly. Use only for debugging. |
| retry_attempt_limit | int | Default 1. Number of attempts to find feasible trajectory on failure for soft-resets before reporting failure. |
| reset_period | double | Default 1.0. required time of inactivity to reset optimizer (only in Humble due to backport ABI policies) |
| regenerate_noises | bool | Default false. Whether to regenerate noises each iteration or use single noise distribution computed on initialization and reset. Practically, this is found to work fine since the trajectories are being sampled stochastically from a normal distribution and reduces compute jittering at run-time due to thread wake-ups to resample normal distribution. |
#### Trajectory Visualizer
| Parameter | Type | Definition |
| --------------- | ------ | ----------------------------------------------------------------------------------------------------------- |
| trajectory_step | int | Default: 5. The step between trajectories to visualize to downsample candidate trajectory pool. |
| time_step | int | Default: 3. The step between points on trajectories to visualize to downsample trajectory density. |
#### Path Handler
| Parameter | Type | Definition |
| --------------- | ------ | ----------------------------------------------------------------------------------------------------------- |
| max_robot_pose_search_dist | double | Default: Costmap half-size. Max integrated distance ahead of robot pose to search for nearest path point in case of path looping. |
| prune_distance | double | Default: 1.5. Distance ahead of nearest point on path to robot to prune path to. |
| transform_tolerance | double | Default: 0.1. Time tolerance for data transformations with TF. |
| enforce_path_inversion | double | Default: False. If true, it will prune paths containing cusping points for segments changing directions (e.g. path inversions) such that the controller will be forced to change directions at or very near the planner's requested inversion point. This is targeting Smac Planner users with feasible paths who need their robots to switch directions where specifically requested. |
| inversion_xy_tolerance | double | Default: 0.2. Cartesian proximity (m) to path inversion point to be considered "achieved" to pass on the rest of the path after path inversion. |
| inversion_yaw_tolerance | double | Default: 0.4. Angular proximity (radians) to path inversion point to be considered "achieved" to pass on the rest of the path after path inversion. 0.4 rad = 23 deg. |
#### Ackermann Motion Model
| Parameter | Type | Definition |
| -------------------- | ------ | ----------------------------------------------------------------------------------------------------------- |
| min_turning_r | double | minimum turning radius for ackermann motion model |
#### Constraint Critic
| Parameter | Type | Definition |
| --------------- | ------ | ----------------------------------------------------------------------------------------------------------- |
| cost_weight | double | Default 4.0. Weight to apply to critic term. |
| cost_power | int | Default 1. Power order to apply to term.
#### Goal Angle Critic
| Parameter | Type | Definition |
| --------------- | ------ | ----------------------------------------------------------------------------------------------------------- |
| cost_weight | double | Default 3.0. Weight to apply to critic term. |
| cost_power | int | Default 1. Power order to apply to term. |
| threshold_to_consider | double | Default 0.5. Minimal distance between robot and goal above which angle goal cost considered. |
#### Goal Critic
| Parameter | Type | Definition |
| -------------------- | ------ | ----------------------------------------------------------------------------------------------------------- |
| cost_weight | double | Default 5.0. Weight to apply to critic term. |
| cost_power | int | Default 1. Power order to apply to term. |
| threshold_to_consider | double | Default 1.4. Distance between robot and goal above which goal cost starts being considered |
#### Obstacles Critic
Uses estimated distances from obstacles using cost and inflation parameters to avoid obstacles
| Parameter | Type | Definition |
| --------------- | ------ | ----------------------------------------------------------------------------------------------------------- |
| consider_footprint | bool | Default: False. Whether to use point cost (if robot is circular or low compute power) or compute SE2 footprint cost. |
| critical_weight | double | Default 20.0. Weight to apply to critic for near collisions closer than `collision_margin_distance` to prevent near collisions **only** as a method of virtually inflating the footprint. This should not be used to generally influence obstacle avoidance away from critical collisions. |
| repulsion_weight | double | Default 1.5. Weight to apply to critic for generally preferring routes in lower cost space. This is separated from the critical term to allow for fine tuning of obstacle behaviors with path alignment for dynamic scenes without impacting actions which may directly lead to near-collisions. This is applied within the `inflation_radius` distance from obstacles. |
| cost_power | int | Default 1. Power order to apply to term. |
| collision_cost | double | Default 10000.0. Cost to apply to a true collision in a trajectory. |
| collision_margin_distance | double | Default 0.10. Margin distance from collision to apply severe penalty, similar to footprint inflation. Between 0.05-0.2 is reasonable. |
| near_goal_distance | double | Default 0.5. Distance near goal to stop applying preferential obstacle term to allow robot to smoothly converge to goal pose in close proximity to obstacles.
| cost_scaling_factor | double | Default 10.0. Exponential decay factor across inflation radius. This should be the same as for your inflation layer (Humble only)
| inflation_radius | double | Default 0.55. Radius to inflate costmap around lethal obstacles. This should be the same as for your inflation layer (Humble only)
#### Cost Critic
Uses inflated costmap cost directly to avoid obstacles
| Parameter | Type | Definition |
| --------------- | ------ | ----------------------------------------------------------------------------------------------------------- |
| consider_footprint | bool | Default: False. Whether to use point cost (if robot is circular or low compute power) or compute SE2 footprint cost. |
| cost_weight | double | Default 3.81. Wight to apply to critic to avoid obstacles. |
| cost_power | int | Default 1. Power order to apply to term. |
| collision_cost | double | Default 1000000.0. Cost to apply to a true collision in a trajectory. |
| critical_cost | double | Default 300.0. Cost to apply to a pose with any point in in inflated space to prefer distance from obstacles. |
| near_goal_distance | double | Default 0.5. Distance near goal to stop applying preferential obstacle term to allow robot to smoothly converge to goal pose in close proximity to obstacles.
| inflation_layer_name | string | Default "". Name of the inflation layer. If empty, it uses the last inflation layer in the costmap. If you have multiple inflation layers, you may want to specify the name of the layer to use. |
#### Path Align Critic
| Parameter | Type | Definition |
| --------------- | ------ | ----------------------------------------------------------------------------------------------------------- |
| cost_weight | double | Default 10.0. Weight to apply to critic term. |
| cost_power | int | Default 1. Power order to apply to term. |
| threshold_to_consider | double | Default 0.5. Distance between robot and goal above which path align cost stops being considered |
| offset_from_furthest | double | Default 20. Checks that the candidate trajectories are sufficiently far along their way tracking the path to apply the alignment critic. This ensures that path alignment is only considered when actually tracking the path, preventing awkward initialization motions preventing the robot from leaving the path to achieve the appropriate heading. |
| trajectory_point_step | double | Default 4. Step of trajectory points to evaluate for path distance to reduce compute time. Between 1-10 is typically reasonable. |
| max_path_occupancy_ratio | double | Default 0.07 (7%). Maximum proportion of the path that can be occupied before this critic is not considered to allow the obstacle and path follow critics to avoid obstacles while following the path's intent in presence of dynamic objects in the scene. |
| use_path_orientations | bool | Default false. Whether to consider path's orientations in path alignment, which can be useful when paired with feasible smac planners to incentivize directional changes only where/when the smac planner requests them. If you want the robot to deviate and invert directions where the controller sees fit, keep as false. If your plans do not contain orientation information (e.g. navfn), keep as false. |
Note: There is a "Legacy" version of this critic also available with the same parameters of an old formulation pre-October 2023.
#### Path Angle Critic
| Parameter | Type | Definition |
| --------------- | ------ | ----------------------------------------------------------------------------------------------------------- |
| cost_weight | double | Default 2.0. Weight to apply to critic term. |
| cost_power | int | Default 1. Power order to apply to term. |
| threshold_to_consider | double | Default 0.5. Distance between robot and goal above which path angle cost stops being considered |
| offset_from_furthest | int | Default 4. Number of path points after furthest one any trajectory achieves to compute path angle relative to. |
| max_angle_to_furthest | double | Default 1.2. Angular distance between robot and goal above which path angle cost starts being considered |
| forward_preference | bool | Default true. Whether or not your robot has a preference for which way is forward in motion. Different from if reversing is generally allowed, but if you robot contains *no* particular preference one way or another. |
#### Path Follow Critic
| Parameter | Type | Definition |
| --------------- | ------ | ----------------------------------------------------------------------------------------------------------- |
| cost_weight | double | Default 5.0. Weight to apply to critic term. |
| cost_power | int | Default 1. Power order to apply to term. |
| offset_from_furthest | int | Default 6. Number of path points after furthest one any trajectory achieves to drive path tracking relative to. |
| threshold_to_consider | float | Default 1.4. Distance between robot and goal above which path follow cost stops being considered |
#### Prefer Forward Critic
| Parameter | Type | Definition |
| --------------- | ------ | ----------------------------------------------------------------------------------------------------------- |
| cost_weight | double | Default 5.0. Weight to apply to critic term. |
| cost_power | int | Default 1. Power order to apply to term. |
| threshold_to_consider | double | Default 0.5. Distance between robot and goal above which prefer forward cost stops being considered |
#### Twirling Critic
| Parameter | Type | Definition |
| --------------- | ------ | ----------------------------------------------------------------------------------------------------------- |
| cost_weight | double | Default 10.0. Weight to apply to critic term. |
| cost_power | int | Default 1. Power order to apply to term. |
#### Velocity Deadband Critic
| Parameter | Type | Definition |
| --------------- | ------ | ----------------------------------------------------------------------------------------------------------- |
| cost_weight | double | Default 35.0. Weight to apply to critic term. |
| cost_power | int | Default 1. Power order to apply to term. |
| deadband_velocities | double[] | Default [0.0, 0.0, 0.0]. The array of deadband velocities [vx, vz, wz]. A zero array indicates that the critic will take no action. |
### XML configuration example
```
controller_server:
ros__parameters:
controller_frequency: 30.0
FollowPath:
plugin: "nav2_mppi_controller::MPPIController"
time_steps: 56
model_dt: 0.05
batch_size: 2000
vx_std: 0.2
vy_std: 0.2
wz_std: 0.4
vx_max: 0.5
vx_min: -0.35
vy_max: 0.5
wz_max: 1.9
iteration_count: 1
prune_distance: 1.7
transform_tolerance: 0.1
temperature: 0.3
gamma: 0.015
motion_model: "DiffDrive"
visualize: false
TrajectoryVisualizer:
trajectory_step: 5
time_step: 3
AckermannConstraints:
min_turning_r: 0.2
critics: ["ConstraintCritic", "CostCritic", "GoalCritic", "GoalAngleCritic", "PathAlignCritic", "PathFollowCritic", "PathAngleCritic", "PreferForwardCritic"]
ConstraintCritic:
enabled: true
cost_power: 1
cost_weight: 4.0
GoalCritic:
enabled: true
cost_power: 1
cost_weight: 5.0
threshold_to_consider: 1.4
GoalAngleCritic:
enabled: true
cost_power: 1
cost_weight: 3.0
threshold_to_consider: 0.5
PreferForwardCritic:
enabled: true
cost_power: 1
cost_weight: 5.0
threshold_to_consider: 0.5
# ObstaclesCritic:
# enabled: true
# cost_power: 1
# repulsion_weight: 1.5
# critical_weight: 20.0
# consider_footprint: false
# collision_cost: 10000.0
# collision_margin_distance: 0.1
# near_goal_distance: 0.5
CostCritic:
enabled: true
cost_power: 1
cost_weight: 3.81
critical_cost: 300.0
consider_footprint: true
collision_cost: 1000000.0
near_goal_distance: 1.0
PathAlignCritic:
PathAlignCritic:
enabled: true
cost_power: 1
cost_weight: 14.0
max_path_occupancy_ratio: 0.05
trajectory_point_step: 3
threshold_to_consider: 0.5
offset_from_furthest: 20
use_path_orientations: false
PathFollowCritic:
enabled: true
cost_power: 1
cost_weight: 5.0
offset_from_furthest: 5
threshold_to_consider: 1.4
PathAngleCritic:
enabled: true
cost_power: 1
cost_weight: 2.0
offset_from_furthest: 4
threshold_to_consider: 0.5
max_angle_to_furthest: 1.0
forward_preference: true
# VelocityDeadbandCritic:
# enabled: true
# cost_power: 1
# cost_weight: 35.0
# deadband_velocities: [0.05, 0.05, 0.05]
# TwirlingCritic:
# enabled: true
# twirling_cost_power: 1
# twirling_cost_weight: 10.0
```
## Topics
| Topic | Type | Description |
|---------------------------|----------------------------------|-----------------------------------------------------------------------|
| `trajectories` | `visualization_msgs/MarkerArray` | Randomly generated trajectories, including resulting control sequence |
| `transformed_global_plan` | `nav_msgs/Path` | Part of global plan considered by local planner |
## Notes to Users
### General Words of Wisdom
The `model_dt` parameter generally should be set to the duration of your control frequency. So if your control frequency is 20hz, this should be `0.05`. However, you may also set it lower **but not larger**.
Visualization of the trajectories using `visualize` uses compute resources to back out trajectories for visualization and therefore slows compute time. It is not suggested that this parameter is set to `true` during a deployed use, but is a useful debug instrument while tuning the system, but use sparingly. Visualizing 2000 batches @ 56 points at 30 hz is _a lot_.
The most common parameters you might want to start off changing are the velocity profiles (`vx_max`, `vx_min`, `wz_max`, and `vy_max` if holonomic) and the `motion_model` to correspond to your vehicle. Its wise to consider the `prune_distance` of the path plan in proportion to your maximum velocity and prediction horizon. The only deeper parameter that will likely need to be adjusted for your particular settings is the Obstacle critics' `repulsion_weight` since the tuning of this is proportional to your inflation layer's radius. Higher radii should correspond to reduced `repulsion_weight` due to the penalty formation (e.g. `inflation_radius - min_dist_to_obstacle`). If this penalty is too high, the robot will slow significantly when entering cost-space from non-cost space or jitter in narrow corridors. It is noteworthy, but likely not necessary to be changed, that the Obstacle critic may use the full footprint information if `consider_footprint = true`, though comes at an increased compute cost.
If you don't require path following behavior (e.g. just want to follow a goal pose and let the model predictive elements decide the best way to accomplish that), you may easily remove the PathAlign, PathFollow and PathAngle critics.
By default, the controller is tuned and has the capabilities established in the PathAlign/Obstacle critics to generally follow the path closely when no obstacles prevent it, but able to deviate from the path when blocked. See `PathAlignCritic::score()` for details, but it is disabled when the local path is blocked so the obstacle critic takes over in that state.
### Prediction Horizon, Costmap Sizing, and Offsets
As this is a predictive planner, there is some relationship between maximum speed, prediction times, and costmap size that users should keep in mind while tuning for their application. If a controller server costmap is set to 3.0m in size, that means that with the robot in the center, there is 1.5m of information on either side of the robot. When your prediction horizon (time_steps * model_dt) at maximum speed (vx_max) is larger than this, then your robot will be artificially limited in its maximum speeds and behavior by the costmap limitation. For example, if you predict forward 3 seconds (60 steps @ 0.05s per step) at 0.5m/s maximum speed, the **minimum** required costmap radius is 1.5m - or 3m total width.
The same applies to the Path Follow and Align offsets from furthest. In the same example if the furthest point we can consider is already at the edge of the costmap, then further offsets are thresholded because they're unusable. So its important while selecting these parameters to make sure that the theoretical offsets can exist on the costmap settings selected with the maximum prediction horizon and velocities desired. Setting the threshold for consideration in the path follower + goal critics as the same as your prediction horizon can make sure you have clean hand-offs between them, as the path follower will otherwise attempt to slow slightly once it reaches the final goal pose as its marker.
The Path Follow critic cannot drive velocities greater than the projectable distance of that velocity on the available path on the rolling costmap. The Path Align critic `offset_from_furthest` represents the number of path points a trajectory passes through while tracking the path. If this is set either absurdly low (e.g. 5) it can trigger when a robot is simply trying to start path tracking causing some suboptimal behaviors and local minima while starting a task. If it is set absurdly high (e.g. 50) relative to the path resolution and costmap size, then the critic may never trigger or only do so when at full-speed. A balance here is wise. A selection of this value to be ~30% of the maximum velocity distance projected is good (e.g. if a planner produces points every 2.5cm, 60 can fit on the 1.5m local costmap radius. If the max speed is 0.5m/s with a 3s prediction time, then 20 points represents 33% of the maximum speed projected over the prediction horizon onto the path). When in doubt, `prediction_horizon_s * max_speed / path_resolution / 3.0` is a good baseline.
### Obstacle, Inflation Layer, and Path Following
There also exists a relationship between the costmap configurations and the Obstacle critic configurations. If the Obstacle critic is not well tuned with the costmap parameters (inflation radius, scale) it can cause the robot to wobble significantly as it attempts to take finitely lower-cost trajectories with a slightly lower cost in exchange for jerky motion. The default behavior was tuned for small AMRs (e.g. turtlebots or similar), so if using a larger robot, you may want to reduce the `repulsion_weight` in kind. It may also perform awkward maneuvers when in free-space to try to maximize time in a small pocket of 0-cost over a more natural motion which involves moving into some low-costed region. Finally, it may generally refuse to go into costed space at all when starting in a free 0-cost space if the gain is set disproportionately higher than the Path Follow scoring to encourage the robot to move along the path. This is due to the critic cost of staying in free space becoming more attractive than entering even lightly costed space in exchange for progression along the task.
Thus, care should be taken to select weights of the obstacle critic in conjunction with the costmap inflation radius and scale so that a robot does not have such issues. How I (Steve, your friendly neighborhood navigator) tuned this was to first create the appropriate obstacle critic behavior desirable in conjunction with the inflation layer parameters. Its worth noting that the Obstacle critic converts the cost into a distance from obstacles, so the nature of the distribution of costs in the inflation isn't overly significant. However, the inflation radius and the scale will define the cost at the end of the distribution where free-space meets the lowest cost value within the radius. So testing for quality behavior when going over that threshold should be considered.
As you increase or decrease your weights on the Obstacle, you may notice the aforementioned behaviors (e.g. won't overcome free to non-free threshold). To overcome them, increase the FollowPath critic cost to increase the desire for the trajectory planner to continue moving towards the goal. Make sure to not overshoot this though, keep them balanced. A desirable outcome is smooth motion roughly in the center of spaces without significant close interactions with obstacles. It shouldn't be perfectly following a path yet nor should the output velocity be wobbling jaggedly.
Once you have your obstacle avoidance behavior tuned and matched with an appropriate path following penalty, tune the Path Align critic to align with the path. If you design exact-path-alignment behavior, its possible to skip the obstacle critic step as highly tuning the system to follow the path will give it less ability to deviate to avoid obstacles (though it'll slow and stop). Tuning the critic weight for the Obstacle critic high will do the job to avoid near-collisions but the repulsion weight is largely unnecessary to you. For others wanting more dynamic behavior, it _can_ be beneficial to slowly lower the weight on the obstacle critic to give the path alignment critic some more room to work. If your path was generated with a cost-aware planner (like all provided by Nav2) and providing paths sufficiently far from obstacles for your satisfaction, the impact of a slightly reduced Obstacle critic with a Path Alignment critic will do you well. Not over-weighting the path align critic will allow the robot to deviate from the path to get around dynamic obstacles in the scene or other obstacles not previous considered during path planning. It is subjective as to the best behavior for your application, but it has been shown that MPPI can be an exact path tracker and/or avoid dynamic obstacles very fluidly and everywhere in between. The defaults provided are in the generally right regime for a balanced initial trade-off.
@@ -0,0 +1,22 @@
find_package(benchmark REQUIRED)
set(BENCHMARK_NAMES
optimizer_benchmark
controller_benchmark
)
foreach(name IN LISTS BENCHMARK_NAMES)
add_executable(${name}
${name}.cpp
)
ament_target_dependencies(${name}
${dependencies_pkgs}
)
target_link_libraries(${name}
mppi_controller mppi_critics benchmark
)
target_include_directories(${name} PRIVATE
${PROJECT_SOURCE_DIR}/test/utils
)
endforeach()
@@ -0,0 +1,238 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include <benchmark/benchmark.h>
#include <string>
#include <geometry_msgs/msg/pose_stamped.hpp>
#include <geometry_msgs/msg/twist.hpp>
#include <nav_msgs/msg/path.hpp>
#include <nav2_costmap_2d/cost_values.hpp>
#include <nav2_costmap_2d/costmap_2d.hpp>
#include <nav2_costmap_2d/costmap_2d_ros.hpp>
#include <nav2_core/goal_checker.hpp>
#include <xtensor/xarray.hpp>
#include <xtensor/xio.hpp>
#include <xtensor/xview.hpp>
#include "nav2_mppi_controller/motion_models.hpp"
#include "nav2_mppi_controller/controller.hpp"
#include "utils.hpp"
class RosLockGuard
{
public:
RosLockGuard() {rclcpp::init(0, nullptr);}
~RosLockGuard() {rclcpp::shutdown();}
};
RosLockGuard g_rclcpp;
void prepareAndRunBenchmark(
bool consider_footprint, std::string motion_model,
std::vector<std::string> critics, benchmark::State & state)
{
bool visualize = false;
int batch_size = 300;
int time_steps = 12;
unsigned int path_points = 50u;
int iteration_count = 2;
double lookahead_distance = 10.0;
TestCostmapSettings costmap_settings{};
auto costmap_ros = getDummyCostmapRos(costmap_settings);
auto costmap = costmap_ros->getCostmap();
TestPose start_pose = costmap_settings.getCenterPose();
double path_step = costmap_settings.resolution;
TestPathSettings path_settings{start_pose, path_points, path_step, path_step};
TestOptimizerSettings optimizer_settings{batch_size, time_steps, iteration_count,
lookahead_distance, motion_model, consider_footprint};
unsigned int offset = 4;
unsigned int obstacle_size = offset * 2;
unsigned char obstacle_cost = 250;
auto [obst_x, obst_y] = costmap_settings.getCenterIJ();
obst_x = obst_x - offset;
obst_y = obst_y - offset;
addObstacle(costmap, {obst_x, obst_y, obstacle_size, obstacle_cost});
printInfo(optimizer_settings, path_settings, critics);
rclcpp::NodeOptions options;
std::vector<rclcpp::Parameter> params;
setUpControllerParams(visualize, params);
setUpOptimizerParams(optimizer_settings, critics, params);
options.parameter_overrides(params);
auto node = getDummyNode(options);
auto tf_buffer = std::make_shared<tf2_ros::Buffer>(node->get_clock());
tf_buffer->setUsingDedicatedThread(true); // One-thread broadcasting-listening model
auto broadcaster =
std::make_shared<tf2_ros::TransformBroadcaster>(node);
auto tf_listener = std::make_shared<tf2_ros::TransformListener>(*tf_buffer);
auto map_odom_broadcaster = std::async(
std::launch::async, sendTf, "map", "odom", broadcaster, node,
20);
auto odom_base_link_broadcaster = std::async(
std::launch::async, sendTf, "odom", "base_link", broadcaster, node,
20);
auto controller = getDummyController(node, tf_buffer, costmap_ros);
// evalControl args
auto pose = getDummyPointStamped(node, start_pose);
auto velocity = getDummyTwist();
auto path = getIncrementalDummyPath(node, path_settings);
controller->setPlan(path);
nav2_core::GoalChecker * dummy_goal_checker{nullptr};
for (auto _ : state) {
controller->computeVelocityCommands(pose, velocity, dummy_goal_checker);
}
map_odom_broadcaster.wait();
odom_base_link_broadcaster.wait();
}
static void BM_DiffDrivePointFootprint(benchmark::State & state)
{
bool consider_footprint = true;
std::string motion_model = "DiffDrive";
std::vector<std::string> critics = {{"GoalCritic"}, {"GoalAngleCritic"}, {"ObstaclesCritic"},
{"PathAngleCritic"}, {"PathFollowCritic"}, {"PreferForwardCritic"}};
prepareAndRunBenchmark(consider_footprint, motion_model, critics, state);
}
static void BM_DiffDrive(benchmark::State & state)
{
bool consider_footprint = true;
std::string motion_model = "DiffDrive";
std::vector<std::string> critics = {{"GoalCritic"}, {"GoalAngleCritic"}, {"ObstaclesCritic"},
{"PathAngleCritic"}, {"PathFollowCritic"}, {"PreferForwardCritic"}};
prepareAndRunBenchmark(consider_footprint, motion_model, critics, state);
}
static void BM_Omni(benchmark::State & state)
{
bool consider_footprint = true;
std::string motion_model = "Omni";
std::vector<std::string> critics = {{"GoalCritic"}, {"GoalAngleCritic"}, {"ObstaclesCritic"},
{"TwirlingCritic"}, {"PathFollowCritic"}, {"PreferForwardCritic"}};
prepareAndRunBenchmark(consider_footprint, motion_model, critics, state);
}
static void BM_Ackermann(benchmark::State & state)
{
bool consider_footprint = true;
std::string motion_model = "Ackermann";
std::vector<std::string> critics = {{"GoalCritic"}, {"GoalAngleCritic"}, {"ObstaclesCritic"},
{"PathAngleCritic"}, {"PathFollowCritic"}, {"PreferForwardCritic"}};
prepareAndRunBenchmark(consider_footprint, motion_model, critics, state);
}
static void BM_GoalCritic(benchmark::State & state)
{
bool consider_footprint = true;
std::string motion_model = "Ackermann";
std::vector<std::string> critics = {{"GoalCritic"}};
prepareAndRunBenchmark(consider_footprint, motion_model, critics, state);
}
static void BM_GoalAngleCritic(benchmark::State & state)
{
bool consider_footprint = true;
std::string motion_model = "Ackermann";
std::vector<std::string> critics = {{"GoalAngleCritic"}};
prepareAndRunBenchmark(consider_footprint, motion_model, critics, state);
}
static void BM_ObstaclesCritic(benchmark::State & state)
{
bool consider_footprint = true;
std::string motion_model = "Ackermann";
std::vector<std::string> critics = {{"ObstaclesCritic"}};
prepareAndRunBenchmark(consider_footprint, motion_model, critics, state);
}
static void BM_ObstaclesCriticPointFootprint(benchmark::State & state)
{
bool consider_footprint = false;
std::string motion_model = "Ackermann";
std::vector<std::string> critics = {{"ObstaclesCritic"}};
prepareAndRunBenchmark(consider_footprint, motion_model, critics, state);
}
static void BM_TwilringCritic(benchmark::State & state)
{
bool consider_footprint = true;
std::string motion_model = "Ackermann";
std::vector<std::string> critics = {{"TwirlingCritic"}};
prepareAndRunBenchmark(consider_footprint, motion_model, critics, state);
}
static void BM_PathFollowCritic(benchmark::State & state)
{
bool consider_footprint = true;
std::string motion_model = "Ackermann";
std::vector<std::string> critics = {{"PathFollowCritic"}};
prepareAndRunBenchmark(consider_footprint, motion_model, critics, state);
}
static void BM_PathAngleCritic(benchmark::State & state)
{
bool consider_footprint = true;
std::string motion_model = "Ackermann";
std::vector<std::string> critics = {{"PathAngleCritic"}};
prepareAndRunBenchmark(consider_footprint, motion_model, critics, state);
}
BENCHMARK(BM_DiffDrivePointFootprint)->Unit(benchmark::kMillisecond);
BENCHMARK(BM_DiffDrive)->Unit(benchmark::kMillisecond);
BENCHMARK(BM_Omni)->Unit(benchmark::kMillisecond);
BENCHMARK(BM_Ackermann)->Unit(benchmark::kMillisecond);
BENCHMARK(BM_GoalCritic)->Unit(benchmark::kMillisecond);
BENCHMARK(BM_GoalAngleCritic)->Unit(benchmark::kMillisecond);
BENCHMARK(BM_PathAngleCritic)->Unit(benchmark::kMillisecond);
BENCHMARK(BM_PathFollowCritic)->Unit(benchmark::kMillisecond);
BENCHMARK(BM_ObstaclesCritic)->Unit(benchmark::kMillisecond);
BENCHMARK(BM_ObstaclesCriticPointFootprint)->Unit(benchmark::kMillisecond);
BENCHMARK(BM_TwilringCritic)->Unit(benchmark::kMillisecond);
BENCHMARK_MAIN();
@@ -0,0 +1,213 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include <benchmark/benchmark.h>
#include <string>
#include "gtest/gtest.h"
#include <geometry_msgs/msg/pose_stamped.hpp>
#include <geometry_msgs/msg/twist.hpp>
#include <nav_msgs/msg/path.hpp>
#include <nav2_costmap_2d/cost_values.hpp>
#include <nav2_costmap_2d/costmap_2d.hpp>
#include <nav2_costmap_2d/costmap_2d_ros.hpp>
#include <nav2_core/goal_checker.hpp>
#include <xtensor/xarray.hpp>
#include <xtensor/xio.hpp>
#include <xtensor/xview.hpp>
#include "nav2_mppi_controller/optimizer.hpp"
#include "nav2_mppi_controller/motion_models.hpp"
#include "nav2_mppi_controller/tools/parameters_handler.hpp"
#include "utils.hpp"
class RosLockGuard
{
public:
RosLockGuard() {rclcpp::init(0, nullptr);}
~RosLockGuard() {rclcpp::shutdown();}
};
RosLockGuard g_rclcpp;
void prepareAndRunBenchmark(
bool consider_footprint, std::string motion_model,
std::vector<std::string> critics, benchmark::State & state)
{
int batch_size = 300;
int time_steps = 12;
unsigned int path_points = 50u;
int iteration_count = 2;
double lookahead_distance = 10.0;
TestCostmapSettings costmap_settings{};
auto costmap_ros = getDummyCostmapRos(costmap_settings);
auto costmap = costmap_ros->getCostmap();
TestPose start_pose = costmap_settings.getCenterPose();
double path_step = costmap_settings.resolution;
TestPathSettings path_settings{start_pose, path_points, path_step, path_step};
TestOptimizerSettings optimizer_settings{batch_size, time_steps, iteration_count,
lookahead_distance, motion_model, consider_footprint};
unsigned int offset = 4;
unsigned int obstacle_size = offset * 2;
unsigned char obstacle_cost = 250;
auto [obst_x, obst_y] = costmap_settings.getCenterIJ();
obst_x = obst_x - offset;
obst_y = obst_y - offset;
addObstacle(costmap, {obst_x, obst_y, obstacle_size, obstacle_cost});
printInfo(optimizer_settings, path_settings, critics);
auto node = getDummyNode(optimizer_settings, critics);
auto parameters_handler = std::make_unique<mppi::ParametersHandler>(node);
auto optimizer = getDummyOptimizer(node, costmap_ros, parameters_handler.get());
// evalControl args
auto pose = getDummyPointStamped(node, start_pose);
auto velocity = getDummyTwist();
auto path = getIncrementalDummyPath(node, path_settings);
nav2_core::GoalChecker * dummy_goal_checker{nullptr};
for (auto _ : state) {
optimizer->evalControl(pose, velocity, path, dummy_goal_checker);
}
}
static void BM_DiffDrivePointFootprint(benchmark::State & state)
{
bool consider_footprint = true;
std::string motion_model = "DiffDrive";
std::vector<std::string> critics = {{"GoalCritic"}, {"GoalAngleCritic"}, {"ObstaclesCritic"},
{"PathAngleCritic"}, {"PathFollowCritic"}, {"PreferForwardCritic"}};
prepareAndRunBenchmark(consider_footprint, motion_model, critics, state);
}
static void BM_DiffDrive(benchmark::State & state)
{
bool consider_footprint = true;
std::string motion_model = "DiffDrive";
std::vector<std::string> critics = {{"GoalCritic"}, {"GoalAngleCritic"}, {"ObstaclesCritic"},
{"PathAngleCritic"}, {"PathFollowCritic"}, {"PreferForwardCritic"}};
prepareAndRunBenchmark(consider_footprint, motion_model, critics, state);
}
static void BM_Omni(benchmark::State & state)
{
bool consider_footprint = true;
std::string motion_model = "Omni";
std::vector<std::string> critics = {{"GoalCritic"}, {"GoalAngleCritic"}, {"ObstaclesCritic"},
{"TwirlingCritic"}, {"PathFollowCritic"}, {"PreferForwardCritic"}};
prepareAndRunBenchmark(consider_footprint, motion_model, critics, state);
}
static void BM_Ackermann(benchmark::State & state)
{
bool consider_footprint = true;
std::string motion_model = "Ackermann";
std::vector<std::string> critics = {{"GoalCritic"}, {"GoalAngleCritic"}, {"ObstaclesCritic"},
{"PathAngleCritic"}, {"PathFollowCritic"}, {"PreferForwardCritic"}};
prepareAndRunBenchmark(consider_footprint, motion_model, critics, state);
}
static void BM_GoalCritic(benchmark::State & state)
{
bool consider_footprint = true;
std::string motion_model = "Ackermann";
std::vector<std::string> critics = {{"GoalCritic"}};
prepareAndRunBenchmark(consider_footprint, motion_model, critics, state);
}
static void BM_GoalAngleCritic(benchmark::State & state)
{
bool consider_footprint = true;
std::string motion_model = "Ackermann";
std::vector<std::string> critics = {{"GoalAngleCritic"}};
prepareAndRunBenchmark(consider_footprint, motion_model, critics, state);
}
static void BM_ObstaclesCritic(benchmark::State & state)
{
bool consider_footprint = true;
std::string motion_model = "Ackermann";
std::vector<std::string> critics = {{"ObstaclesCritic"}};
prepareAndRunBenchmark(consider_footprint, motion_model, critics, state);
}
static void BM_ObstaclesCriticPointFootprint(benchmark::State & state)
{
bool consider_footprint = false;
std::string motion_model = "Ackermann";
std::vector<std::string> critics = {{"ObstaclesCritic"}};
prepareAndRunBenchmark(consider_footprint, motion_model, critics, state);
}
static void BM_TwilringCritic(benchmark::State & state)
{
bool consider_footprint = true;
std::string motion_model = "Ackermann";
std::vector<std::string> critics = {{"TwirlingCritic"}};
prepareAndRunBenchmark(consider_footprint, motion_model, critics, state);
}
static void BM_PathFollowCritic(benchmark::State & state)
{
bool consider_footprint = true;
std::string motion_model = "Ackermann";
std::vector<std::string> critics = {{"PathFollowCritic"}};
prepareAndRunBenchmark(consider_footprint, motion_model, critics, state);
}
static void BM_PathAngleCritic(benchmark::State & state)
{
bool consider_footprint = true;
std::string motion_model = "Ackermann";
std::vector<std::string> critics = {{"PathAngleCritic"}};
prepareAndRunBenchmark(consider_footprint, motion_model, critics, state);
}
BENCHMARK(BM_DiffDrivePointFootprint)->Unit(benchmark::kMillisecond);
BENCHMARK(BM_DiffDrive)->Unit(benchmark::kMillisecond);
BENCHMARK(BM_Omni)->Unit(benchmark::kMillisecond);
BENCHMARK(BM_Ackermann)->Unit(benchmark::kMillisecond);
BENCHMARK(BM_GoalCritic)->Unit(benchmark::kMillisecond);
BENCHMARK(BM_GoalAngleCritic)->Unit(benchmark::kMillisecond);
BENCHMARK(BM_PathAngleCritic)->Unit(benchmark::kMillisecond);
BENCHMARK(BM_PathFollowCritic)->Unit(benchmark::kMillisecond);
BENCHMARK(BM_ObstaclesCritic)->Unit(benchmark::kMillisecond);
BENCHMARK(BM_ObstaclesCriticPointFootprint)->Unit(benchmark::kMillisecond);
BENCHMARK(BM_TwilringCritic)->Unit(benchmark::kMillisecond);
BENCHMARK_MAIN();
@@ -0,0 +1,53 @@
<class_libraries>
<library path="mppi_critics">
<class type="mppi::critics::ObstaclesCritic" base_class_type="mppi::critics::CriticFunction">
<description>mppi critic for obstacle avoidance</description>
</class>
<class type="mppi::critics::CostCritic" base_class_type="mppi::critics::CriticFunction">
<description>mppi critic for obstacle avoidance using costmap score</description>
</class>
<class type="mppi::critics::GoalCritic" base_class_type="mppi::critics::CriticFunction">
<description>mppi critic for driving towards the goal</description>
</class>
<class type="mppi::critics::GoalAngleCritic" base_class_type="mppi::critics::CriticFunction">
<description>mppi critic for achieving the goal heading angle</description>
</class>
<class type="mppi::critics::PathAlignCritic" base_class_type="mppi::critics::CriticFunction">
<description>mppi critic for aligning to path</description>
</class>
<class type="mppi::critics::PathAlignLegacyCritic" base_class_type="mppi::critics::CriticFunction">
<description>mppi critic for aligning to path (legacy)</description>
</class>
<class type="mppi::critics::PathAngleCritic" base_class_type="mppi::critics::CriticFunction">
<description>mppi critic for tracking the path in the correct heading</description>
</class>
<class type="mppi::critics::PathFollowCritic" base_class_type="mppi::critics::CriticFunction">
<description>mppi critic for driving towards the goal that is furthest among trajectories nearest path points</description>
</class>
<class type="mppi::critics::PreferForwardCritic" base_class_type="mppi::critics::CriticFunction">
<description></description>
</class>
<class type="mppi::critics::TwirlingCritic" base_class_type="mppi::critics::CriticFunction">
<description>mppi critic for preventing twirling behavior when using omnidirectional models</description>
</class>
<class type="mppi::critics::ConstraintCritic" base_class_type="mppi::critics::CriticFunction">
<description>mppi critic for incentivizing moving within kinematic and dynamic bounds</description>
</class>
<class type="mppi::critics::VelocityDeadbandCritic" base_class_type="mppi::critics::CriticFunction">
<description>mppi critic for restricting command velocities in deadband range</description>
</class>
</library>
</class_libraries>
@@ -0,0 +1,132 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__CONTROLLER_HPP_
#define NAV2_MPPI_CONTROLLER__CONTROLLER_HPP_
#include <string>
#include <memory>
#include "nav2_mppi_controller/tools/path_handler.hpp"
#include "nav2_mppi_controller/optimizer.hpp"
#include "nav2_mppi_controller/tools/trajectory_visualizer.hpp"
#include "nav2_mppi_controller/models/constraints.hpp"
#include "nav2_mppi_controller/tools/utils.hpp"
#include "nav2_core/controller.hpp"
#include "nav2_core/goal_checker.hpp"
#include "rclcpp/rclcpp.hpp"
namespace nav2_mppi_controller
{
using namespace mppi; // NOLINT
/**
* @class mppi::MPPIController
* @brief Main plugin controller for MPPI Controller
*/
class MPPIController : public nav2_core::Controller
{
public:
/**
* @brief Constructor for mppi::MPPIController
*/
MPPIController() = default;
/**
* @brief Configure controller on bringup
* @param parent WeakPtr to node
* @param name Name of plugin
* @param tf TF buffer to use
* @param costmap_ros Costmap2DROS object of environment
*/
void configure(
const rclcpp_lifecycle::LifecycleNode::WeakPtr & parent,
std::string name, const std::shared_ptr<tf2_ros::Buffer> tf,
const std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros) override;
/**
* @brief Cleanup resources
*/
void cleanup() override;
/**
* @brief Activate controller
*/
void activate() override;
/**
* @brief Deactivate controller
*/
void deactivate() override;
/**
* @brief Reset the controller state between tasks
*/
void reset();
/**
* @brief Main method to compute velocities using the optimizer
* @param robot_pose Robot pose
* @param robot_speed Robot speed
* @param goal_checker Pointer to the goal checker for awareness if completed task
*/
geometry_msgs::msg::TwistStamped computeVelocityCommands(
const geometry_msgs::msg::PoseStamped & robot_pose,
const geometry_msgs::msg::Twist & robot_speed,
nav2_core::GoalChecker * goal_checker) override;
/**
* @brief Set new reference path to track
* @param path Path to track
*/
void setPlan(const nav_msgs::msg::Path & path) override;
/**
* @brief Set new speed limit from callback
* @param speed_limit Speed limit to use
* @param percentage Bool if the speed limit is absolute or relative
*/
void setSpeedLimit(const double & speed_limit, const bool & percentage) override;
protected:
/**
* @brief Visualize trajectories
* @param transformed_plan Transformed input plan
*/
void visualize(nav_msgs::msg::Path transformed_plan);
std::string name_;
rclcpp_lifecycle::LifecycleNode::WeakPtr parent_;
rclcpp::Clock::SharedPtr clock_;
rclcpp::Logger logger_{rclcpp::get_logger("MPPIController")};
std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros_;
std::shared_ptr<tf2_ros::Buffer> tf_buffer_;
std::unique_ptr<ParametersHandler> parameters_handler_;
Optimizer optimizer_;
PathHandler path_handler_;
TrajectoryVisualizer trajectory_visualizer_;
bool visualize_;
double reset_period_;
// Last time computeVelocityCommands was called
rclcpp::Time last_time_called_;
};
} // namespace nav2_mppi_controller
#endif // NAV2_MPPI_CONTROLLER__CONTROLLER_HPP_
@@ -0,0 +1,57 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__CRITIC_DATA_HPP_
#define NAV2_MPPI_CONTROLLER__CRITIC_DATA_HPP_
#include <memory>
#include <vector>
#include <xtensor/xtensor.hpp>
#include "geometry_msgs/msg/pose_stamped.hpp"
#include "nav2_core/goal_checker.hpp"
#include "nav2_mppi_controller/models/state.hpp"
#include "nav2_mppi_controller/models/trajectories.hpp"
#include "nav2_mppi_controller/models/path.hpp"
#include "nav2_mppi_controller/motion_models.hpp"
namespace mppi
{
/**
* @struct mppi::CriticData
* @brief Data to pass to critics for scoring, including state, trajectories,
* pruned path, global goal, costs, and important parameters to share
*/
struct CriticData
{
const models::State & state;
const models::Trajectories & trajectories;
const models::Path & path;
const geometry_msgs::msg::Pose & goal;
xt::xtensor<float, 1> & costs;
float & model_dt;
bool fail_flag;
nav2_core::GoalChecker * goal_checker;
std::shared_ptr<MotionModel> motion_model;
std::optional<std::vector<bool>> path_pts_valid;
std::optional<size_t> furthest_reached_path_point;
};
} // namespace mppi
#endif // NAV2_MPPI_CONTROLLER__CRITIC_DATA_HPP_
@@ -0,0 +1,118 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__CRITIC_FUNCTION_HPP_
#define NAV2_MPPI_CONTROLLER__CRITIC_FUNCTION_HPP_
#include <string>
#include <memory>
#include "rclcpp_lifecycle/lifecycle_node.hpp"
#include "nav2_costmap_2d/costmap_2d_ros.hpp"
#include "nav2_mppi_controller/tools/parameters_handler.hpp"
#include "nav2_mppi_controller/critic_data.hpp"
namespace mppi::critics
{
/**
* @class mppi::critics::CollisionCost
* @brief Utility for storing cost information
*/
struct CollisionCost
{
float cost{0};
bool using_footprint{false};
};
/**
* @class mppi::critics::CriticFunction
* @brief Abstract critic objective function to score trajectories
*/
class CriticFunction
{
public:
/**
* @brief Constructor for mppi::critics::CriticFunction
*/
CriticFunction() = default;
/**
* @brief Destructor for mppi::critics::CriticFunction
*/
virtual ~CriticFunction() = default;
/**
* @brief Configure critic on bringup
* @param parent WeakPtr to node
* @param parent_name name of the controller
* @param name Name of plugin
* @param costmap_ros Costmap2DROS object of environment
* @param dynamic_parameter_handler Parameter handler object
*/
void on_configure(
rclcpp_lifecycle::LifecycleNode::WeakPtr parent,
const std::string & parent_name,
const std::string & name,
std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros,
ParametersHandler * param_handler)
{
parent_ = parent;
logger_ = parent_.lock()->get_logger();
name_ = name;
parent_name_ = parent_name;
costmap_ros_ = costmap_ros;
costmap_ = costmap_ros_->getCostmap();
parameters_handler_ = param_handler;
auto getParam = parameters_handler_->getParamGetter(name_);
getParam(enabled_, "enabled", true);
initialize();
}
/**
* @brief Main function to score trajectory
* @param data Critic data to use in scoring
*/
virtual void score(CriticData & data) = 0;
/**
* @brief Initialize critic
*/
virtual void initialize() = 0;
/**
* @brief Get name of critic
*/
std::string getName()
{
return name_;
}
protected:
bool enabled_;
std::string name_, parent_name_;
rclcpp_lifecycle::LifecycleNode::WeakPtr parent_;
std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros_;
nav2_costmap_2d::Costmap2D * costmap_{nullptr};
ParametersHandler * parameters_handler_;
rclcpp::Logger logger_{rclcpp::get_logger("MPPIController")};
};
} // namespace mppi::critics
#endif // NAV2_MPPI_CONTROLLER__CRITIC_FUNCTION_HPP_
@@ -0,0 +1,103 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__CRITIC_MANAGER_HPP_
#define NAV2_MPPI_CONTROLLER__CRITIC_MANAGER_HPP_
#include <memory>
#include <string>
#include <vector>
#include <pluginlib/class_loader.hpp>
#include <xtensor/xtensor.hpp>
#include "geometry_msgs/msg/twist.hpp"
#include "geometry_msgs/msg/twist_stamped.hpp"
#include "nav2_costmap_2d/costmap_2d_ros.hpp"
#include "rclcpp_lifecycle/lifecycle_node.hpp"
#include "nav2_mppi_controller/tools/parameters_handler.hpp"
#include "nav2_mppi_controller/tools/utils.hpp"
#include "nav2_mppi_controller/critic_data.hpp"
#include "nav2_mppi_controller/critic_function.hpp"
namespace mppi
{
/**
* @class mppi::CriticManager
* @brief Manager of objective function plugins for scoring trajectories
*/
class CriticManager
{
public:
/**
* @brief Constructor for mppi::CriticManager
*/
CriticManager() = default;
/**
* @brief Virtual Destructor for mppi::CriticManager
*/
virtual ~CriticManager() = default;
/**
* @brief Configure critic manager on bringup and load plugins
* @param parent WeakPtr to node
* @param name Name of plugin
* @param costmap_ros Costmap2DROS object of environment
* @param dynamic_parameter_handler Parameter handler object
*/
void on_configure(
rclcpp_lifecycle::LifecycleNode::WeakPtr parent, const std::string & name,
std::shared_ptr<nav2_costmap_2d::Costmap2DROS>, ParametersHandler *);
/**
* @brief Score trajectories by the set of loaded critic functions
* @param CriticData Struct of necessary information to pass to the critic functions
*/
void evalTrajectoriesScores(CriticData & data) const;
protected:
/**
* @brief Get parameters (critics to load)
*/
void getParams();
/**
* @brief Load the critic plugins
*/
virtual void loadCritics();
/**
* @brief Get full-name namespaced critic IDs
*/
std::string getFullName(const std::string & name);
protected:
rclcpp_lifecycle::LifecycleNode::WeakPtr parent_;
std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros_;
std::string name_;
ParametersHandler * parameters_handler_;
std::vector<std::string> critic_names_;
std::unique_ptr<pluginlib::ClassLoader<critics::CriticFunction>> loader_;
std::vector<std::unique_ptr<critics::CriticFunction>> critics_;
rclcpp::Logger logger_{rclcpp::get_logger("MPPIController")};
};
} // namespace mppi
#endif // NAV2_MPPI_CONTROLLER__CRITIC_MANAGER_HPP_
@@ -0,0 +1,56 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__CRITICS__CONSTRAINT_CRITIC_HPP_
#define NAV2_MPPI_CONTROLLER__CRITICS__CONSTRAINT_CRITIC_HPP_
#include "nav2_mppi_controller/critic_function.hpp"
#include "nav2_mppi_controller/models/state.hpp"
#include "nav2_mppi_controller/tools/utils.hpp"
namespace mppi::critics
{
/**
* @class mppi::critics::ConstraintCritic
* @brief Critic objective function for enforcing feasible constraints
*/
class ConstraintCritic : public CriticFunction
{
public:
/**
* @brief Initialize critic
*/
void initialize() override;
/**
* @brief Evaluate cost related to goal following
*
* @param costs [out] add reference cost values to this tensor
*/
void score(CriticData & data) override;
float getMaxVelConstraint() {return max_vel_;}
float getMinVelConstraint() {return min_vel_;}
protected:
unsigned int power_{0};
float weight_{0};
float min_vel_;
float max_vel_;
};
} // namespace mppi::critics
#endif // NAV2_MPPI_CONTROLLER__CRITICS__CONSTRAINT_CRITIC_HPP_
@@ -0,0 +1,98 @@
// Copyright (c) 2023 Robocc Brice Renaudeau
//
// 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 NAV2_MPPI_CONTROLLER__CRITICS__COST_CRITIC_HPP_
#define NAV2_MPPI_CONTROLLER__CRITICS__COST_CRITIC_HPP_
#include <memory>
#include <string>
#include "nav2_costmap_2d/footprint_collision_checker.hpp"
#include "nav2_costmap_2d/inflation_layer.hpp"
#include "nav2_mppi_controller/critic_function.hpp"
#include "nav2_mppi_controller/models/state.hpp"
#include "nav2_mppi_controller/tools/utils.hpp"
namespace mppi::critics
{
/**
* @class mppi::critics::CostCritic
* @brief Critic objective function for avoiding obstacles using costmap's inflated cost
*/
class CostCritic : public CriticFunction
{
public:
/**
* @brief Initialize critic
*/
void initialize() override;
/**
* @brief Evaluate cost related to obstacle avoidance
*
* @param costs [out] add obstacle cost values to this tensor
*/
void score(CriticData & data) override;
protected:
/**
* @brief Checks if cost represents a collision
* @param cost Point cost at pose center
* @param x X of pose
* @param y Y of pose
* @param theta theta of pose
* @return bool if in collision
*/
bool inCollision(float cost, float x, float y, float theta);
/**
* @brief cost at a robot pose
* @param x X of pose
* @param y Y of pose
* @return Collision information at pose
*/
float costAtPose(float x, float y);
/**
* @brief Find the min cost of the inflation decay function for which the robot MAY be
* in collision in any orientation
* @param costmap Costmap2DROS to get minimum inscribed cost (e.g. 128 in inflation layer documentation)
* @return double circumscribed cost, any higher than this and need to do full footprint collision checking
* since some element of the robot could be in collision
*/
float findCircumscribedCost(std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap);
protected:
nav2_costmap_2d::FootprintCollisionChecker<nav2_costmap_2d::Costmap2D *>
collision_checker_{nullptr};
float possibly_inscribed_cost_;
bool consider_footprint_{true};
float circumscribed_radius_{0};
float circumscribed_cost_{0};
float collision_cost_{0};
float critical_cost_{0};
float weight_{0};
float near_goal_distance_;
std::string inflation_layer_name_;
unsigned int power_{0};
};
} // namespace mppi::critics
#endif // NAV2_MPPI_CONTROLLER__CRITICS__COST_CRITIC_HPP_
@@ -0,0 +1,53 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__CRITICS__GOAL_ANGLE_CRITIC_HPP_
#define NAV2_MPPI_CONTROLLER__CRITICS__GOAL_ANGLE_CRITIC_HPP_
#include "nav2_mppi_controller/critic_function.hpp"
#include "nav2_mppi_controller/models/state.hpp"
#include "nav2_mppi_controller/tools/utils.hpp"
namespace mppi::critics
{
/**
* @class mppi::critics::ConstraintCritic
* @brief Critic objective function for driving towards goal orientation
*/
class GoalAngleCritic : public CriticFunction
{
public:
/**
* @brief Initialize critic
*/
void initialize() override;
/**
* @brief Evaluate cost related to robot orientation at goal pose
* (considered only if robot near last goal in current plan)
*
* @param costs [out] add goal angle cost values to this tensor
*/
void score(CriticData & data) override;
protected:
float threshold_to_consider_{0};
unsigned int power_{0};
float weight_{0};
};
} // namespace mppi::critics
#endif // NAV2_MPPI_CONTROLLER__CRITICS__GOAL_ANGLE_CRITIC_HPP_
@@ -0,0 +1,53 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
// Copyright (c) 2023 Open Navigation LLC
//
// 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 NAV2_MPPI_CONTROLLER__CRITICS__GOAL_CRITIC_HPP_
#define NAV2_MPPI_CONTROLLER__CRITICS__GOAL_CRITIC_HPP_
#include "nav2_mppi_controller/critic_function.hpp"
#include "nav2_mppi_controller/models/state.hpp"
#include "nav2_mppi_controller/tools/utils.hpp"
namespace mppi::critics
{
/**
* @class mppi::critics::ConstraintCritic
* @brief Critic objective function for driving towards goal
*/
class GoalCritic : public CriticFunction
{
public:
/**
* @brief Initialize critic
*/
void initialize() override;
/**
* @brief Evaluate cost related to goal following
*
* @param costs [out] add reference cost values to this tensor
*/
void score(CriticData & data) override;
protected:
unsigned int power_{0};
float weight_{0};
float threshold_to_consider_{0};
};
} // namespace mppi::critics
#endif // NAV2_MPPI_CONTROLLER__CRITICS__GOAL_CRITIC_HPP_
@@ -0,0 +1,103 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__CRITICS__OBSTACLES_CRITIC_HPP_
#define NAV2_MPPI_CONTROLLER__CRITICS__OBSTACLES_CRITIC_HPP_
#include <memory>
#include "nav2_costmap_2d/footprint_collision_checker.hpp"
#include "nav2_costmap_2d/inflation_layer.hpp"
#include "nav2_mppi_controller/critic_function.hpp"
#include "nav2_mppi_controller/models/state.hpp"
#include "nav2_mppi_controller/tools/utils.hpp"
namespace mppi::critics
{
/**
* @class mppi::critics::ConstraintCritic
* @brief Critic objective function for avoiding obstacles, allowing it to deviate off
* the planned path. This is important to tune in tandem with PathAlign to make a balance
* between path-tracking and dynamic obstacle avoidance capabilities as desirable for a
* particular application
*/
class ObstaclesCritic : public CriticFunction
{
public:
/**
* @brief Initialize critic
*/
void initialize() override;
/**
* @brief Evaluate cost related to obstacle avoidance
*
* @param costs [out] add obstacle cost values to this tensor
*/
void score(CriticData & data) override;
protected:
/**
* @brief Checks if cost represents a collision
* @param cost Costmap cost
* @return bool if in collision
*/
inline bool inCollision(float cost) const;
/**
* @brief cost at a robot pose
* @param x X of pose
* @param y Y of pose
* @param theta theta of pose
* @return Collision information at pose
*/
inline CollisionCost costAtPose(float x, float y, float theta);
/**
* @brief Distance to obstacle from cost
* @param cost Costmap cost
* @return float Distance to the obstacle represented by cost
*/
inline float distanceToObstacle(const CollisionCost & cost);
/**
* @brief Find the min cost of the inflation decay function for which the robot MAY be
* in collision in any orientation
* @param costmap Costmap2DROS to get minimum inscribed cost (e.g. 128 in inflation layer documentation)
* @return double circumscribed cost, any higher than this and need to do full footprint collision checking
* since some element of the robot could be in collision
*/
float findCircumscribedCost(std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap);
protected:
nav2_costmap_2d::FootprintCollisionChecker<nav2_costmap_2d::Costmap2D *>
collision_checker_{nullptr};
bool consider_footprint_{true};
float collision_cost_{0};
float inflation_scale_factor_{0}, inflation_radius_{0};
float possibly_inscribed_cost_;
float collision_margin_distance_;
float near_goal_distance_;
float circumscribed_cost_{0}, circumscribed_radius_{0};
unsigned int power_{0};
float repulsion_weight_, critical_weight_{0};
};
} // namespace mppi::critics
#endif // NAV2_MPPI_CONTROLLER__CRITICS__OBSTACLES_CRITIC_HPP_
@@ -0,0 +1,59 @@
// Copyright (c) 2023 Open Navigation LLC
//
// 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 NAV2_MPPI_CONTROLLER__CRITICS__PATH_ALIGN_CRITIC_HPP_
#define NAV2_MPPI_CONTROLLER__CRITICS__PATH_ALIGN_CRITIC_HPP_
#include "nav2_mppi_controller/critic_function.hpp"
#include "nav2_mppi_controller/models/state.hpp"
#include "nav2_mppi_controller/tools/utils.hpp"
namespace mppi::critics
{
/**
* @class mppi::critics::ConstraintCritic
* @brief Critic objective function for aligning to the path. Note:
* High settings of this will follow the path more precisely, but also makes it
* difficult (or impossible) to deviate in the presence of dynamic obstacles.
* This is an important critic to tune and consider in tandem with Obstacle.
*/
class PathAlignCritic : public CriticFunction
{
public:
/**
* @brief Initialize critic
*/
void initialize() override;
/**
* @brief Evaluate cost related to trajectories path alignment
*
* @param costs [out] add reference cost values to this tensor
*/
void score(CriticData & data) override;
protected:
size_t offset_from_furthest_{0};
int trajectory_point_step_{0};
float threshold_to_consider_{0};
float max_path_occupancy_ratio_{0};
bool use_path_orientations_{false};
unsigned int power_{0};
float weight_{0};
};
} // namespace mppi::critics
#endif // NAV2_MPPI_CONTROLLER__CRITICS__PATH_ALIGN_CRITIC_HPP_
@@ -0,0 +1,60 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__CRITICS__PATH_ALIGN_LEGACY_CRITIC_HPP_
#define NAV2_MPPI_CONTROLLER__CRITICS__PATH_ALIGN_LEGACY_CRITIC_HPP_
#include "nav2_mppi_controller/critic_function.hpp"
#include "nav2_mppi_controller/models/state.hpp"
#include "nav2_mppi_controller/tools/utils.hpp"
namespace mppi::critics
{
/**
* @class mppi::critics::PathAlignLegacyCritic
* @brief Critic objective function for aligning to the path. Note:
* High settings of this will follow the path more precisely, but also makes it
* difficult (or impossible) to deviate in the presence of dynamic obstacles.
* This is an important critic to tune and consider in tandem with Obstacle.
* This is the initial 'Legacy' implementation before replacement Oct 2023.
*/
class PathAlignLegacyCritic : public CriticFunction
{
public:
/**
* @brief Initialize critic
*/
void initialize() override;
/**
* @brief Evaluate cost related to trajectories path alignment
*
* @param costs [out] add reference cost values to this tensor
*/
void score(CriticData & data) override;
protected:
size_t offset_from_furthest_{0};
int trajectory_point_step_{0};
float threshold_to_consider_{0};
float max_path_occupancy_ratio_{0};
bool use_path_orientations_{false};
unsigned int power_{0};
float weight_{0};
};
} // namespace mppi::critics
#endif // NAV2_MPPI_CONTROLLER__CRITICS__PATH_ALIGN_LEGACY_CRITIC_HPP_
@@ -0,0 +1,60 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__CRITICS__PATH_ANGLE_CRITIC_HPP_
#define NAV2_MPPI_CONTROLLER__CRITICS__PATH_ANGLE_CRITIC_HPP_
#include "nav2_mppi_controller/critic_function.hpp"
#include "nav2_mppi_controller/models/state.hpp"
#include "nav2_mppi_controller/tools/utils.hpp"
namespace mppi::critics
{
/**
* @class mppi::critics::ConstraintCritic
* @brief Critic objective function for aligning to path in cases of extreme misalignment
* or turning
*/
class PathAngleCritic : public CriticFunction
{
public:
/**
* @brief Initialize critic
*/
void initialize() override;
/**
* @brief Evaluate cost related to robot orientation at goal pose
* (considered only if robot near last goal in current plan)
*
* @param costs [out] add goal angle cost values to this tensor
*/
void score(CriticData & data) override;
protected:
float max_angle_to_furthest_{0};
float threshold_to_consider_{0};
size_t offset_from_furthest_{0};
bool reversing_allowed_{true};
bool forward_preference_{true};
unsigned int power_{0};
float weight_{0};
};
} // namespace mppi::critics
#endif // NAV2_MPPI_CONTROLLER__CRITICS__PATH_ANGLE_CRITIC_HPP_
@@ -0,0 +1,60 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
// Copyright (c) 2023 Open Navigation LLC
//
// 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 NAV2_MPPI_CONTROLLER__CRITICS__PATH_FOLLOW_CRITIC_HPP_
#define NAV2_MPPI_CONTROLLER__CRITICS__PATH_FOLLOW_CRITIC_HPP_
#include "nav2_mppi_controller/critic_function.hpp"
#include "nav2_mppi_controller/models/state.hpp"
#include "nav2_mppi_controller/tools/utils.hpp"
namespace mppi::critics
{
/**
* @class mppi::critics::ConstraintCritic
* @brief Critic objective function for following the path approximately
* To allow for deviation from path in case of dynamic obstacles. Path Align
* is what aligns the trajectories to the path more or less precisely, if desireable.
* A higher weight here with an offset > 1 will accelerate the samples to full speed
* faster and push the follow point further ahead, creating some shortcutting.
*/
class PathFollowCritic : public CriticFunction
{
public:
/**
* @brief Initialize critic
*/
void initialize() override;
/**
* @brief Evaluate cost related to robot orientation at goal pose
* (considered only if robot near last goal in current plan)
*
* @param costs [out] add goal angle cost values to this tensor
*/
void score(CriticData & data) override;
protected:
float threshold_to_consider_{0};
size_t offset_from_furthest_{0};
unsigned int power_{0};
float weight_{0};
};
} // namespace mppi::critics
#endif // NAV2_MPPI_CONTROLLER__CRITICS__PATH_FOLLOW_CRITIC_HPP_
@@ -0,0 +1,52 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__CRITICS__PREFER_FORWARD_CRITIC_HPP_
#define NAV2_MPPI_CONTROLLER__CRITICS__PREFER_FORWARD_CRITIC_HPP_
#include "nav2_mppi_controller/critic_function.hpp"
#include "nav2_mppi_controller/tools/utils.hpp"
namespace mppi::critics
{
/**
* @class mppi::critics::ConstraintCritic
* @brief Critic objective function for preferring forward motion
*/
class PreferForwardCritic : public CriticFunction
{
public:
/**
* @brief Initialize critic
*/
void initialize() override;
/**
* @brief Evaluate cost related to robot orientation at goal pose
* (considered only if robot near last goal in current plan)
*
* @param costs [out] add goal angle cost values to this tensor
*/
void score(CriticData & data) override;
protected:
unsigned int power_{0};
float weight_{0};
float threshold_to_consider_{0};
};
} // namespace mppi::critics
#endif // NAV2_MPPI_CONTROLLER__CRITICS__PREFER_FORWARD_CRITIC_HPP_
@@ -0,0 +1,51 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__CRITICS__TWIRLING_CRITIC_HPP_
#define NAV2_MPPI_CONTROLLER__CRITICS__TWIRLING_CRITIC_HPP_
#include "nav2_mppi_controller/critic_function.hpp"
#include "nav2_mppi_controller/tools/utils.hpp"
namespace mppi::critics
{
/**
* @class mppi::critics::ConstraintCritic
* @brief Critic objective function for penalizing wiggling/twirling
*/
class TwirlingCritic : public CriticFunction
{
public:
/**
* @brief Initialize critic
*/
void initialize() override;
/**
* @brief Evaluate cost related to robot orientation at goal pose
* (considered only if robot near last goal in current plan)
*
* @param costs [out] add goal angle cost values to this tensor
*/
void score(CriticData & data) override;
protected:
unsigned int power_{0};
float weight_{0};
};
} // namespace mppi::critics
#endif // NAV2_MPPI_CONTROLLER__CRITICS__TWIRLING_CRITIC_HPP_
@@ -0,0 +1,54 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__CRITICS__VELOCITY_DEADBAND_CRITIC_HPP_
#define NAV2_MPPI_CONTROLLER__CRITICS__VELOCITY_DEADBAND_CRITIC_HPP_
#include <vector>
#include "nav2_mppi_controller/critic_function.hpp"
#include "nav2_mppi_controller/models/state.hpp"
#include "nav2_mppi_controller/tools/utils.hpp"
namespace mppi::critics
{
/**
* @class mppi::critics::VelocityDeadbandCritic
* @brief Critic objective function for enforcing feasible constraints
*/
class VelocityDeadbandCritic : public CriticFunction
{
public:
/**
* @brief Initialize critic
*/
void initialize() override;
/**
* @brief Evaluate cost related to goal following
*
* @param costs [out] add reference cost values to this tensor
*/
void score(CriticData & data) override;
protected:
unsigned int power_{0};
float weight_{0};
std::vector<float> deadband_velocities_{0.0f, 0.0f, 0.0f};
};
} // namespace mppi::critics
#endif // NAV2_MPPI_CONTROLLER__CRITICS__VELOCITY_DEADBAND_CRITIC_HPP_
@@ -0,0 +1,46 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__MODELS__CONSTRAINTS_HPP_
#define NAV2_MPPI_CONTROLLER__MODELS__CONSTRAINTS_HPP_
namespace mppi::models
{
/**
* @struct mppi::models::ControlConstraints
* @brief Constraints on control
*/
struct ControlConstraints
{
float vx_max;
float vx_min;
float vy;
float wz;
};
/**
* @struct mppi::models::SamplingStd
* @brief Noise parameters for sampling trajectories
*/
struct SamplingStd
{
float vx;
float vy;
float wz;
};
} // namespace mppi::models
#endif // NAV2_MPPI_CONTROLLER__MODELS__CONSTRAINTS_HPP_
@@ -0,0 +1,52 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__MODELS__CONTROL_SEQUENCE_HPP_
#define NAV2_MPPI_CONTROLLER__MODELS__CONTROL_SEQUENCE_HPP_
#include <xtensor/xtensor.hpp>
namespace mppi::models
{
/**
* @struct mppi::models::Control
* @brief A set of controls
*/
struct Control
{
float vx, vy, wz;
};
/**
* @struct mppi::models::ControlSequence
* @brief A control sequence over time (e.g. trajectory)
*/
struct ControlSequence
{
xt::xtensor<float, 1> vx;
xt::xtensor<float, 1> vy;
xt::xtensor<float, 1> wz;
void reset(unsigned int time_steps)
{
vx = xt::zeros<float>({time_steps});
vy = xt::zeros<float>({time_steps});
wz = xt::zeros<float>({time_steps});
}
};
} // namespace mppi::models
#endif // NAV2_MPPI_CONTROLLER__MODELS__CONTROL_SEQUENCE_HPP_
@@ -0,0 +1,45 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__MODELS__OPTIMIZER_SETTINGS_HPP_
#define NAV2_MPPI_CONTROLLER__MODELS__OPTIMIZER_SETTINGS_HPP_
#include <cstddef>
#include "nav2_mppi_controller/models/constraints.hpp"
namespace mppi::models
{
/**
* @struct mppi::models::OptimizerSettings
* @brief Settings for the optimizer to use
*/
struct OptimizerSettings
{
models::ControlConstraints base_constraints{0, 0, 0, 0};
models::ControlConstraints constraints{0, 0, 0, 0};
models::SamplingStd sampling_std{0, 0, 0};
float model_dt{0};
float temperature{0};
float gamma{0};
unsigned int batch_size{0};
unsigned int time_steps{0};
unsigned int iteration_count{0};
bool shift_control_sequence{false};
size_t retry_attempt_limit{0};
};
} // namespace mppi::models
#endif // NAV2_MPPI_CONTROLLER__MODELS__OPTIMIZER_SETTINGS_HPP_
@@ -0,0 +1,46 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__MODELS__PATH_HPP_
#define NAV2_MPPI_CONTROLLER__MODELS__PATH_HPP_
#include <xtensor/xtensor.hpp>
namespace mppi::models
{
/**
* @struct mppi::models::Path
* @brief Path represented as a tensor
*/
struct Path
{
xt::xtensor<float, 1> x;
xt::xtensor<float, 1> y;
xt::xtensor<float, 1> yaws;
/**
* @brief Reset path data
*/
void reset(unsigned int size)
{
x = xt::zeros<float>({size});
y = xt::zeros<float>({size});
yaws = xt::zeros<float>({size});
}
};
} // namespace mppi::models
#endif // NAV2_MPPI_CONTROLLER__MODELS__PATH_HPP_
@@ -0,0 +1,59 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__MODELS__STATE_HPP_
#define NAV2_MPPI_CONTROLLER__MODELS__STATE_HPP_
#include <xtensor/xtensor.hpp>
#include <geometry_msgs/msg/pose_stamped.hpp>
#include <geometry_msgs/msg/twist.hpp>
namespace mppi::models
{
/**
* @struct mppi::models::State
* @brief State information: velocities, controls, poses, speed
*/
struct State
{
xt::xtensor<float, 2> vx;
xt::xtensor<float, 2> vy;
xt::xtensor<float, 2> wz;
xt::xtensor<float, 2> cvx;
xt::xtensor<float, 2> cvy;
xt::xtensor<float, 2> cwz;
geometry_msgs::msg::PoseStamped pose;
geometry_msgs::msg::Twist speed;
/**
* @brief Reset state data
*/
void reset(unsigned int batch_size, unsigned int time_steps)
{
vx = xt::zeros<float>({batch_size, time_steps});
vy = xt::zeros<float>({batch_size, time_steps});
wz = xt::zeros<float>({batch_size, time_steps});
cvx = xt::zeros<float>({batch_size, time_steps});
cvy = xt::zeros<float>({batch_size, time_steps});
cwz = xt::zeros<float>({batch_size, time_steps});
}
};
} // namespace mppi::models
#endif // NAV2_MPPI_CONTROLLER__MODELS__STATE_HPP_
@@ -0,0 +1,47 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__MODELS__TRAJECTORIES_HPP_
#define NAV2_MPPI_CONTROLLER__MODELS__TRAJECTORIES_HPP_
#include <xtensor/xtensor.hpp>
#include <xtensor/xview.hpp>
namespace mppi::models
{
/**
* @class mppi::models::Trajectories
* @brief Candidate Trajectories
*/
struct Trajectories
{
xt::xtensor<float, 2> x;
xt::xtensor<float, 2> y;
xt::xtensor<float, 2> yaws;
/**
* @brief Reset state data
*/
void reset(unsigned int batch_size, unsigned int time_steps)
{
x = xt::zeros<float>({batch_size, time_steps});
y = xt::zeros<float>({batch_size, time_steps});
yaws = xt::zeros<float>({batch_size, time_steps});
}
};
} // namespace mppi::models
#endif // NAV2_MPPI_CONTROLLER__MODELS__TRAJECTORIES_HPP_
@@ -0,0 +1,175 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__MOTION_MODELS_HPP_
#define NAV2_MPPI_CONTROLLER__MOTION_MODELS_HPP_
#include <cstdint>
#include "nav2_mppi_controller/models/control_sequence.hpp"
#include "nav2_mppi_controller/models/state.hpp"
#include <xtensor/xmath.hpp>
#include <xtensor/xmasked_view.hpp>
#include <xtensor/xview.hpp>
#include <xtensor/xnoalias.hpp>
#include "nav2_mppi_controller/tools/parameters_handler.hpp"
namespace mppi
{
/**
* @class mppi::MotionModel
* @brief Abstract motion model for modeling a vehicle
*/
class MotionModel
{
public:
/**
* @brief Constructor for mppi::MotionModel
*/
MotionModel() = default;
/**
* @brief Destructor for mppi::MotionModel
*/
virtual ~MotionModel() = default;
/**
* @brief With input velocities, find the vehicle's output velocities
* @param state Contains control velocities to use to populate vehicle velocities
*/
virtual void predict(models::State & state)
{
using namespace xt::placeholders; // NOLINT
xt::noalias(xt::view(state.vx, xt::all(), xt::range(1, _))) =
xt::view(state.cvx, xt::all(), xt::range(0, -1));
xt::noalias(xt::view(state.wz, xt::all(), xt::range(1, _))) =
xt::view(state.cwz, xt::all(), xt::range(0, -1));
if (isHolonomic()) {
xt::noalias(xt::view(state.vy, xt::all(), xt::range(1, _))) =
xt::view(state.cvy, xt::all(), xt::range(0, -1));
}
}
/**
* @brief Whether the motion model is holonomic, using Y axis
* @return Bool If holonomic
*/
virtual bool isHolonomic() = 0;
/**
* @brief Apply hard vehicle constraints to a control sequence
* @param control_sequence Control sequence to apply constraints to
*/
virtual void applyConstraints(models::ControlSequence & /*control_sequence*/) {}
};
/**
* @class mppi::AckermannMotionModel
* @brief Ackermann motion model
*/
class AckermannMotionModel : public MotionModel
{
public:
/**
* @brief Constructor for mppi::AckermannMotionModel
*/
explicit AckermannMotionModel(ParametersHandler * param_handler, const std::string & name)
{
auto getParam = param_handler->getParamGetter(name + ".AckermannConstraints");
getParam(min_turning_r_, "min_turning_r", 0.2);
}
/**
* @brief Whether the motion model is holonomic, using Y axis
* @return Bool If holonomic
*/
bool isHolonomic() override
{
return false;
}
/**
* @brief Apply hard vehicle constraints to a control sequence
* @param control_sequence Control sequence to apply constraints to
*/
void applyConstraints(models::ControlSequence & control_sequence) override
{
auto & vx = control_sequence.vx;
auto & wz = control_sequence.wz;
auto view = xt::masked_view(wz, (xt::fabs(vx) / xt::fabs(wz)) < min_turning_r_);
view = xt::sign(wz) * xt::fabs(vx) / min_turning_r_;
}
/**
* @brief Get minimum turning radius of ackermann drive
* @return Minimum turning radius
*/
float getMinTurningRadius() {return min_turning_r_;}
private:
float min_turning_r_{0};
};
/**
* @class mppi::DiffDriveMotionModel
* @brief Differential drive motion model
*/
class DiffDriveMotionModel : public MotionModel
{
public:
/**
* @brief Constructor for mppi::DiffDriveMotionModel
*/
DiffDriveMotionModel() = default;
/**
* @brief Whether the motion model is holonomic, using Y axis
* @return Bool If holonomic
*/
bool isHolonomic() override
{
return false;
}
};
/**
* @class mppi::OmniMotionModel
* @brief Omnidirectional motion model
*/
class OmniMotionModel : public MotionModel
{
public:
/**
* @brief Constructor for mppi::OmniMotionModel
*/
OmniMotionModel() = default;
/**
* @brief Whether the motion model is holonomic, using Y axis
* @return Bool If holonomic
*/
bool isHolonomic() override
{
return true;
}
};
} // namespace mppi
#endif // NAV2_MPPI_CONTROLLER__MOTION_MODELS_HPP_
@@ -0,0 +1,267 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__OPTIMIZER_HPP_
#define NAV2_MPPI_CONTROLLER__OPTIMIZER_HPP_
#include <string>
#include <memory>
#include <xtensor/xtensor.hpp>
#include <xtensor/xview.hpp>
#include "rclcpp_lifecycle/lifecycle_node.hpp"
#include "nav2_costmap_2d/costmap_2d_ros.hpp"
#include "nav2_core/goal_checker.hpp"
#include "geometry_msgs/msg/twist.hpp"
#include "geometry_msgs/msg/pose_stamped.hpp"
#include "geometry_msgs/msg/twist_stamped.hpp"
#include "nav_msgs/msg/path.hpp"
#include "nav2_mppi_controller/models/optimizer_settings.hpp"
#include "nav2_mppi_controller/motion_models.hpp"
#include "nav2_mppi_controller/critic_manager.hpp"
#include "nav2_mppi_controller/models/state.hpp"
#include "nav2_mppi_controller/models/trajectories.hpp"
#include "nav2_mppi_controller/models/path.hpp"
#include "nav2_mppi_controller/tools/noise_generator.hpp"
#include "nav2_mppi_controller/tools/parameters_handler.hpp"
#include "nav2_mppi_controller/tools/utils.hpp"
namespace mppi
{
/**
* @class mppi::Optimizer
* @brief Main algorithm optimizer of the MPPI Controller
*/
class Optimizer
{
public:
/**
* @brief Constructor for mppi::Optimizer
*/
Optimizer() = default;
/**
* @brief Destructor for mppi::Optimizer
*/
~Optimizer() {shutdown();}
/**
* @brief Initializes optimizer on startup
* @param parent WeakPtr to node
* @param name Name of plugin
* @param costmap_ros Costmap2DROS object of environment
* @param dynamic_parameter_handler Parameter handler object
*/
void initialize(
rclcpp_lifecycle::LifecycleNode::WeakPtr parent, const std::string & name,
std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros,
ParametersHandler * dynamic_parameters_handler);
/**
* @brief Shutdown for optimizer at process end
*/
void shutdown();
/**
* @brief Compute control using MPPI algorithm
* @param robot_pose Pose of the robot at given time
* @param robot_speed Speed of the robot at given time
* @param plan Path plan to track
* @param goal_checker Object to check if goal is completed
* @return TwistStamped of the MPPI control
*/
geometry_msgs::msg::TwistStamped evalControl(
const geometry_msgs::msg::PoseStamped & robot_pose,
const geometry_msgs::msg::Twist & robot_speed, const nav_msgs::msg::Path & plan,
const geometry_msgs::msg::Pose & goal, nav2_core::GoalChecker * goal_checker);
/**
* @brief Get the trajectories generated in a cycle for visualization
* @return Set of trajectories evaluated in cycle
*/
models::Trajectories & getGeneratedTrajectories();
/**
* @brief Get the optimal trajectory for a cycle for visualization
* @return Optimal trajectory
*/
xt::xtensor<float, 2> getOptimizedTrajectory();
/**
* @brief Set the maximum speed based on the speed limits callback
* @param speed_limit Limit of the speed for use
* @param percentage Whether the speed limit is absolute or relative
*/
void setSpeedLimit(double speed_limit, bool percentage);
/**
* @brief Reset the optimization problem to initial conditions
*/
void reset();
protected:
/**
* @brief Main function to generate, score, and return trajectories
*/
void optimize();
/**
* @brief Prepare state information on new request for trajectory rollouts
* @param robot_pose Pose of the robot at given time
* @param robot_speed Speed of the robot at given time
* @param plan Path plan to track
* @param goal_checker Object to check if goal is completed
*/
void prepare(
const geometry_msgs::msg::PoseStamped & robot_pose,
const geometry_msgs::msg::Twist & robot_speed,
const nav_msgs::msg::Path & plan,
const geometry_msgs::msg::Pose & goal, nav2_core::GoalChecker * goal_checker);
/**
* @brief Obtain the main controller's parameters
*/
void getParams();
/**
* @brief Set the motion model of the vehicle platform
* @param model Model string to use
*/
void setMotionModel(const std::string & model);
/**
* @brief Shift the optimal control sequence after processing for
* next iterations initial conditions after execution
*/
void shiftControlSequence();
/**
* @brief updates generated trajectories with noised trajectories
* from the last cycle's optimal control
*/
void generateNoisedTrajectories();
/**
* @brief Apply hard vehicle constraints on control sequence
*/
void applyControlSequenceConstraints();
/**
* @brief Update velocities in state
* @param state fill state with velocities on each step
*/
void updateStateVelocities(models::State & state) const;
/**
* @brief Update initial velocity in state
* @param state fill state
*/
void updateInitialStateVelocities(models::State & state) const;
/**
* @brief predict velocities in state using model
* for time horizon equal to timesteps
* @param state fill state
*/
void propagateStateVelocitiesFromInitials(models::State & state) const;
/**
* @brief Rollout velocities in state to poses
* @param trajectories to rollout
* @param state fill state
*/
void integrateStateVelocities(
models::Trajectories & trajectories,
const models::State & state) const;
/**
* @brief Rollout velocities in state to poses
* @param trajectories to rollout
* @param state fill state
*/
void integrateStateVelocities(
xt::xtensor<float, 2> & trajectories,
const xt::xtensor<float, 2> & state) const;
/**
* @brief Update control sequence with state controls weighted by costs
* using softmax function
*/
void updateControlSequence();
/**
* @brief Convert control sequence to a twist commant
* @param stamp Timestamp to use
* @return TwistStamped of command to send to robot base
*/
geometry_msgs::msg::TwistStamped
getControlFromSequenceAsTwist(const builtin_interfaces::msg::Time & stamp);
/**
* @brief Whether the motion model is holonomic
* @return Bool if holonomic to populate `y` axis of state
*/
bool isHolonomic() const;
/**
* @brief Using control frequence and time step size, determine if trajectory
* offset should be used to populate initial state of the next cycle
*/
void setOffset(double controller_frequency);
/**
* @brief Perform fallback behavior to try to recover from a set of trajectories in collision
* @param fail Whether the system failed to recover from
*/
bool fallback(bool fail);
protected:
rclcpp_lifecycle::LifecycleNode::WeakPtr parent_;
std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros_;
nav2_costmap_2d::Costmap2D * costmap_;
std::string name_;
std::shared_ptr<MotionModel> motion_model_;
ParametersHandler * parameters_handler_;
CriticManager critic_manager_;
NoiseGenerator noise_generator_;
models::OptimizerSettings settings_;
models::State state_;
models::ControlSequence control_sequence_;
std::array<mppi::models::Control, 4> control_history_;
models::Trajectories generated_trajectories_;
models::Path path_;
geometry_msgs::msg::Pose goal_;
xt::xtensor<float, 1> costs_;
CriticData critics_data_ = {
state_, generated_trajectories_, path_, goal_,
costs_, settings_.model_dt, false, nullptr, nullptr,
std::nullopt, std::nullopt}; /// Caution, keep references
rclcpp::Logger logger_{rclcpp::get_logger("MPPIController")};
};
} // namespace mppi
#endif // NAV2_MPPI_CONTROLLER__OPTIMIZER_HPP_
@@ -0,0 +1,112 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__TOOLS__NOISE_GENERATOR_HPP_
#define NAV2_MPPI_CONTROLLER__TOOLS__NOISE_GENERATOR_HPP_
#include <string>
#include <memory>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <xtensor/xtensor.hpp>
#include <xtensor/xview.hpp>
#include "nav2_mppi_controller/models/optimizer_settings.hpp"
#include "nav2_mppi_controller/tools/parameters_handler.hpp"
#include "nav2_mppi_controller/models/control_sequence.hpp"
#include "nav2_mppi_controller/models/state.hpp"
namespace mppi
{
/**
* @class mppi::NoiseGenerator
* @brief Generates noise trajectories from optimal trajectory
*/
class NoiseGenerator
{
public:
/**
* @brief Constructor for mppi::NoiseGenerator
*/
NoiseGenerator() = default;
/**
* @brief Initialize noise generator with settings and model types
* @param settings Settings of controller
* @param is_holonomic If base is holonomic
* @param name Namespace for configs
* @param param_handler Get parameters util
*/
void initialize(
mppi::models::OptimizerSettings & settings,
bool is_holonomic, const std::string & name, ParametersHandler * param_handler);
/**
* @brief Shutdown noise generator thread
*/
void shutdown();
/**
* @brief Signal to the noise thread the controller is ready to generate a new
* noised control for the next iteration
*/
void generateNextNoises();
/**
* @brief set noised control_sequence to state controls
* @return noises vx, vy, wz
*/
void setNoisedControls(models::State & state, const models::ControlSequence & control_sequence);
/**
* @brief Reset noise generator with settings and model types
* @param settings Settings of controller
* @param is_holonomic If base is holonomic
*/
void reset(mppi::models::OptimizerSettings & settings, bool is_holonomic);
protected:
/**
* @brief Thread to execute noise generation process
*/
void noiseThread();
/**
* @brief Generate random controls by gaussian noise with mean in
* control_sequence_
*
* @return tensor of shape [ batch_size_, time_steps_, 2]
* where 2 stands for v, w
*/
void generateNoisedControls();
xt::xtensor<float, 2> noises_vx_;
xt::xtensor<float, 2> noises_vy_;
xt::xtensor<float, 2> noises_wz_;
mppi::models::OptimizerSettings settings_;
bool is_holonomic_;
std::thread noise_thread_;
std::condition_variable noise_cond_;
std::mutex noise_lock_;
bool active_{false}, ready_{false}, regenerate_noises_{false};
};
} // namespace mppi
#endif // NAV2_MPPI_CONTROLLER__TOOLS__NOISE_GENERATOR_HPP_
@@ -0,0 +1,267 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__TOOLS__PARAMETERS_HANDLER_HPP_
#define NAV2_MPPI_CONTROLLER__TOOLS__PARAMETERS_HANDLER_HPP_
#include <functional>
#include <string>
#include <type_traits>
#include <unordered_map>
#include <utility>
#include <vector>
#include "nav2_util/node_utils.hpp"
#include "rclcpp/rclcpp.hpp"
#include "rclcpp/parameter_value.hpp"
#include "rclcpp_lifecycle/lifecycle_node.hpp"
namespace mppi
{
/**
* @class Parameter Type enum
*/
enum class ParameterType { Dynamic, Static };
/**
* @class mppi::ParametersHandler
* @brief Handles getting parameters and dynamic parmaeter changes
*/
class ParametersHandler
{
public:
using get_param_func_t = void (const rclcpp::Parameter & param);
using post_callback_t = void ();
using pre_callback_t = void ();
/**
* @brief Constructor for mppi::ParametersHandler
*/
ParametersHandler() = default;
/**
* @brief Constructor for mppi::ParametersHandler
* @param parent Weak ptr to node
*/
explicit ParametersHandler(
const rclcpp_lifecycle::LifecycleNode::WeakPtr & parent);
/**
* @brief Starts processing dynamic parameter changes
*/
void start();
/**
* @brief Dynamic parameter callback
* @param parameter Parameter changes to process
* @return Set Parameter Result
*/
rcl_interfaces::msg::SetParametersResult dynamicParamsCallback(
std::vector<rclcpp::Parameter> parameters);
/**
* @brief Get an object to retreive parameters
* @param ns Namespace to get parameters within
* @return Parameter getter object
*/
inline auto getParamGetter(const std::string & ns);
/**
* @brief Set a callback to process after parameter changes
* @param callback Callback function
*/
template<typename T>
void addPostCallback(T && callback);
/**
* @brief Set a callback to process before parameter changes
* @param callback Callback function
*/
template<typename T>
void addPreCallback(T && callback);
/**
* @brief Set a parameter to a dynamic parameter callback
* @param setting Parameter
* @param name Name of parameter
*/
template<typename T>
void setDynamicParamCallback(T & setting, const std::string & name);
/**
* @brief Get mutex lock for changing parameters
* @return Pointer to mutex
*/
std::mutex * getLock()
{
return &parameters_change_mutex_;
}
/**
* @brief Set a parameter to a dynamic parameter callback
* @param name Name of parameter
* @param callback Parameter callback
*/
template<typename T>
void addDynamicParamCallback(const std::string & name, T && callback);
protected:
/**
* @brief Gets parameter
* @param setting Return Parameter type
* @param name Parameter name
* @param default_value Default parameter value
* @param param_type Type of parameter (dynamic or static)
*/
template<typename SettingT, typename ParamT>
void getParam(
SettingT & setting, const std::string & name, ParamT default_value,
ParameterType param_type = ParameterType::Dynamic);
/**
* @brief Set a parameter
* @param setting Return Parameter type
* @param name Parameter name
* @param node Node to set parameter via
*/
template<typename ParamT, typename SettingT, typename NodeT>
void setParam(SettingT & setting, const std::string & name, NodeT node) const;
/**
* @brief Converts parameter type to real types
* @param parameter Parameter to convert into real type
* @return parameter as a functional type
*/
template<typename T>
static auto as(const rclcpp::Parameter & parameter);
std::mutex parameters_change_mutex_;
rclcpp::Logger logger_{rclcpp::get_logger("MPPIController")};
rclcpp::node_interfaces::OnSetParametersCallbackHandle::SharedPtr
on_set_param_handler_;
rclcpp_lifecycle::LifecycleNode::WeakPtr node_;
std::string node_name_;
bool verbose_{false};
std::unordered_map<std::string, std::function<get_param_func_t>>
get_param_callbacks_;
std::vector<std::function<pre_callback_t>> pre_callbacks_;
std::vector<std::function<post_callback_t>> post_callbacks_;
};
inline auto ParametersHandler::getParamGetter(const std::string & ns)
{
return [this, ns](
auto & setting, const std::string & name, auto default_value,
ParameterType param_type = ParameterType::Dynamic) {
getParam(
setting, ns.empty() ? name : ns + "." + name,
std::move(default_value), param_type);
};
}
template<typename T>
void ParametersHandler::addDynamicParamCallback(const std::string & name, T && callback)
{
get_param_callbacks_[name] = callback;
}
template<typename T>
void ParametersHandler::addPostCallback(T && callback)
{
post_callbacks_.push_back(callback);
}
template<typename T>
void ParametersHandler::addPreCallback(T && callback)
{
pre_callbacks_.push_back(callback);
}
template<typename SettingT, typename ParamT>
void ParametersHandler::getParam(
SettingT & setting, const std::string & name,
ParamT default_value,
ParameterType param_type)
{
auto node = node_.lock();
nav2_util::declare_parameter_if_not_declared(
node, name, rclcpp::ParameterValue(default_value));
setParam<ParamT>(setting, name, node);
if (param_type == ParameterType::Dynamic) {
setDynamicParamCallback(setting, name);
}
}
template<typename ParamT, typename SettingT, typename NodeT>
void ParametersHandler::setParam(
SettingT & setting, const std::string & name, NodeT node) const
{
ParamT param_in{};
node->get_parameter(name, param_in);
setting = static_cast<SettingT>(param_in);
}
template<typename T>
void ParametersHandler::setDynamicParamCallback(T & setting, const std::string & name)
{
if (get_param_callbacks_.find(name) != get_param_callbacks_.end()) {
return;
}
auto callback = [this, &setting, name](const rclcpp::Parameter & param) {
setting = as<T>(param);
if (verbose_) {
RCLCPP_INFO(logger_, "Dynamic parameter changed: %s", std::to_string(param).c_str());
}
};
addDynamicParamCallback(name, callback);
if (verbose_) {
RCLCPP_INFO(logger_, "Dynamic Parameter added %s", name.c_str());
}
}
template<typename T>
auto ParametersHandler::as(const rclcpp::Parameter & parameter)
{
if constexpr (std::is_same_v<T, bool>) {
return parameter.as_bool();
} else if constexpr (std::is_integral_v<T>) {
return parameter.as_int();
} else if constexpr (std::is_floating_point_v<T>) {
return parameter.as_double();
} else if constexpr (std::is_same_v<T, std::string>) {
return parameter.as_string();
} else if constexpr (std::is_same_v<T, std::vector<int64_t>>) {
return parameter.as_integer_array();
} else if constexpr (std::is_same_v<T, std::vector<double>>) {
return parameter.as_double_array();
} else if constexpr (std::is_same_v<T, std::vector<std::string>>) {
return parameter.as_string_array();
} else if constexpr (std::is_same_v<T, std::vector<bool>>) {
return parameter.as_bool_array();
}
}
} // namespace mppi
#endif // NAV2_MPPI_CONTROLLER__TOOLS__PARAMETERS_HANDLER_HPP_
@@ -0,0 +1,167 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
// Copyright (c) 2023 Dexory
// Copyright (c) 2023 Open Navigation LLC
//
// 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 NAV2_MPPI_CONTROLLER__TOOLS__PATH_HANDLER_HPP_
#define NAV2_MPPI_CONTROLLER__TOOLS__PATH_HANDLER_HPP_
#include <vector>
#include <utility>
#include <string>
#include <memory>
#include "rclcpp_lifecycle/lifecycle_node.hpp"
#include "tf2_ros/buffer.h"
#include "geometry_msgs/msg/pose_stamped.hpp"
#include "nav_msgs/msg/path.hpp"
#include "builtin_interfaces/msg/time.hpp"
#include "nav2_costmap_2d/costmap_2d_ros.hpp"
#include "nav2_util/geometry_utils.hpp"
#include "nav2_mppi_controller/tools/parameters_handler.hpp"
namespace mppi
{
using PathIterator = std::vector<geometry_msgs::msg::PoseStamped>::iterator;
using PathRange = std::pair<PathIterator, PathIterator>;
/**
* @class mppi::PathHandler
* @brief Manager of incoming reference paths for transformation and processing
*/
class PathHandler
{
public:
/**
* @brief Constructor for mppi::PathHandler
*/
PathHandler() = default;
/**
* @brief Destructor for mppi::PathHandler
*/
~PathHandler() = default;
/**
* @brief Initialize path handler on bringup
* @param parent WeakPtr to node
* @param name Name of plugin
* @param costmap_ros Costmap2DROS object of environment
* @param tf TF buffer for transformations
* @param dynamic_parameter_handler Parameter handler object
*/
void initialize(
rclcpp_lifecycle::LifecycleNode::WeakPtr parent, const std::string & name,
std::shared_ptr<nav2_costmap_2d::Costmap2DROS>,
std::shared_ptr<tf2_ros::Buffer>, ParametersHandler *);
/**
* @brief Set new reference path
* @param Plan Path to use
*/
void setPath(const nav_msgs::msg::Path & plan);
/**
* @brief Get reference path
* @return Path
*/
nav_msgs::msg::Path & getPath();
/**
* @brief transform global plan to local applying constraints,
* then prune global plan
* @param robot_pose Pose of robot
* @return global plan in local frame
*/
nav_msgs::msg::Path transformPath(const geometry_msgs::msg::PoseStamped & robot_pose);
/**
* @brief Get the global goal pose transformed to the local frame
* @param stamp Time to get the goal pose at
* @return Transformed goal pose
*/
geometry_msgs::msg::PoseStamped getTransformedGoal(const builtin_interfaces::msg::Time & stamp);
protected:
/**
* @brief Transform a pose to another frame
* @param frame Frame to transform to
* @param in_pose Input pose
* @param out_pose Output pose
* @return Bool if successful
*/
bool transformPose(
const std::string & frame, const geometry_msgs::msg::PoseStamped & in_pose,
geometry_msgs::msg::PoseStamped & out_pose) const;
/**
* @brief Get largest dimension of costmap (radially)
* @return Max distance from center of costmap to edge
*/
double getMaxCostmapDist();
/**
* @brief Transform a pose to the global reference frame
* @param pose Current pose
* @return output poose in global reference frame
*/
geometry_msgs::msg::PoseStamped
transformToGlobalPlanFrame(const geometry_msgs::msg::PoseStamped & pose);
/**
* @brief Get global plan within window of the local costmap size
* @param global_pose Robot pose
* @return plan transformed in the costmap frame and iterator to the first pose of the global
* plan (for pruning)
*/
std::pair<nav_msgs::msg::Path, PathIterator> getGlobalPlanConsideringBoundsInCostmapFrame(
const geometry_msgs::msg::PoseStamped & global_pose);
/**
* @brief Prune a path to only interesting portions
* @param plan Plan to prune
* @param end Final path iterator
*/
void prunePlan(nav_msgs::msg::Path & plan, const PathIterator end);
/**
* @brief Check if the robot pose is within the set inversion tolerances
* @param robot_pose Robot's current pose to check
* @return bool If the robot pose is within the set inversion tolerances
*/
bool isWithinInversionTolerances(const geometry_msgs::msg::PoseStamped & robot_pose);
std::string name_;
std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_;
std::shared_ptr<tf2_ros::Buffer> tf_buffer_;
ParametersHandler * parameters_handler_;
nav_msgs::msg::Path global_plan_;
nav_msgs::msg::Path global_plan_up_to_inversion_;
rclcpp::Logger logger_{rclcpp::get_logger("MPPIController")};
double max_robot_pose_search_dist_{0};
double prune_distance_{0};
double transform_tolerance_{0};
float inversion_xy_tolerance_{0.2};
float inversion_yaw_tolerance{0.4};
bool enforce_path_inversion_{false};
unsigned int inversion_locale_{0u};
};
} // namespace mppi
#endif // NAV2_MPPI_CONTROLLER__TOOLS__PATH_HANDLER_HPP_
@@ -0,0 +1,114 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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 NAV2_MPPI_CONTROLLER__TOOLS__TRAJECTORY_VISUALIZER_HPP_
#define NAV2_MPPI_CONTROLLER__TOOLS__TRAJECTORY_VISUALIZER_HPP_
#include <memory>
#include <string>
#include <xtensor/xtensor.hpp>
#include "nav_msgs/msg/path.hpp"
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_lifecycle/lifecycle_node.hpp"
#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp"
#include "nav2_mppi_controller/tools/parameters_handler.hpp"
#include "nav2_mppi_controller/tools/utils.hpp"
#include "nav2_mppi_controller/models/trajectories.hpp"
namespace mppi
{
/**
* @class mppi::TrajectoryVisualizer
* @brief Visualizes trajectories for debugging
*/
class TrajectoryVisualizer
{
public:
/**
* @brief Constructor for mppi::TrajectoryVisualizer
*/
TrajectoryVisualizer() = default;
/**
* @brief Configure trajectory visualizer
* @param parent WeakPtr to node
* @param name Name of plugin
* @param frame_id Frame to publish trajectories in
* @param dynamic_parameter_handler Parameter handler object
*/
void on_configure(
rclcpp_lifecycle::LifecycleNode::WeakPtr parent, const std::string & name,
const std::string & frame_id, ParametersHandler * parameters_handler);
/**
* @brief Cleanup object on shutdown
*/
void on_cleanup();
/**
* @brief Activate object
*/
void on_activate();
/**
* @brief Deactivate object
*/
void on_deactivate();
/**
* @brief Add an optimal trajectory to visualize
* @param trajectory Optimal trajectory
*/
void add(const xt::xtensor<float, 2> & trajectory, const std::string & marker_namespace);
/**
* @brief Add candidate trajectories to visualize
* @param trajectories Candidate trajectories
*/
void add(const models::Trajectories & trajectories, const std::string & marker_namespace);
/**
* @brief Visualize the plan
* @param plan Plan to visualize
*/
void visualize(const nav_msgs::msg::Path & plan);
/**
* @brief Reset object
*/
void reset();
protected:
std::string frame_id_;
std::shared_ptr<rclcpp_lifecycle::LifecyclePublisher<visualization_msgs::msg::MarkerArray>>
trajectories_publisher_;
std::shared_ptr<rclcpp_lifecycle::LifecyclePublisher<nav_msgs::msg::Path>> transformed_path_pub_;
std::unique_ptr<visualization_msgs::msg::MarkerArray> points_;
int marker_id_ = 0;
ParametersHandler * parameters_handler_;
size_t trajectory_step_{0};
size_t time_step_{0};
rclcpp::Logger logger_{rclcpp::get_logger("MPPIController")};
};
} // namespace mppi
#endif // NAV2_MPPI_CONTROLLER__TOOLS__TRAJECTORY_VISUALIZER_HPP_
@@ -0,0 +1,679 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
// Copyright (c) 2023 Open Navigation LLC
//
// 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 NAV2_MPPI_CONTROLLER__TOOLS__UTILS_HPP_
#define NAV2_MPPI_CONTROLLER__TOOLS__UTILS_HPP_
#include <algorithm>
#include <chrono>
#include <string>
#include <limits>
#include <memory>
#include <vector>
#include <xtensor/xarray.hpp>
#include <xtensor/xnorm.hpp>
#include <xtensor/xmath.hpp>
#include <xtensor/xview.hpp>
#include "angles/angles.h"
#include "tf2/utils.h"
#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp"
#include "geometry_msgs/msg/twist_stamped.hpp"
#include "nav_msgs/msg/path.hpp"
#include "visualization_msgs/msg/marker_array.hpp"
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_lifecycle/lifecycle_node.hpp"
#include "nav2_util/node_utils.hpp"
#include "nav2_core/goal_checker.hpp"
#include "nav2_mppi_controller/models/optimizer_settings.hpp"
#include "nav2_mppi_controller/models/control_sequence.hpp"
#include "nav2_mppi_controller/models/path.hpp"
#include "builtin_interfaces/msg/time.hpp"
#include "nav2_mppi_controller/critic_data.hpp"
namespace mppi::utils
{
using xt::evaluation_strategy::immediate;
/**
* @brief Convert data into pose
* @param x X position
* @param y Y position
* @param z Z position
* @return Pose object
*/
inline geometry_msgs::msg::Pose createPose(double x, double y, double z)
{
geometry_msgs::msg::Pose pose;
pose.position.x = x;
pose.position.y = y;
pose.position.z = z;
pose.orientation.w = 1;
pose.orientation.x = 0;
pose.orientation.y = 0;
pose.orientation.z = 0;
return pose;
}
/**
* @brief Convert data into scale
* @param x X scale
* @param y Y scale
* @param z Z scale
* @return Scale object
*/
inline geometry_msgs::msg::Vector3 createScale(double x, double y, double z)
{
geometry_msgs::msg::Vector3 scale;
scale.x = x;
scale.y = y;
scale.z = z;
return scale;
}
/**
* @brief Convert data into color
* @param r Red component
* @param g Green component
* @param b Blue component
* @param a Alpha component (transparency)
* @return Color object
*/
inline std_msgs::msg::ColorRGBA createColor(float r, float g, float b, float a)
{
std_msgs::msg::ColorRGBA color;
color.r = r;
color.g = g;
color.b = b;
color.a = a;
return color;
}
/**
* @brief Convert data into a Maarker
* @param id Marker ID
* @param pose Marker pose
* @param scale Marker scale
* @param color Marker color
* @param frame Reference frame to use
* @return Visualization Marker
*/
inline visualization_msgs::msg::Marker createMarker(
int id, const geometry_msgs::msg::Pose & pose, const geometry_msgs::msg::Vector3 & scale,
const std_msgs::msg::ColorRGBA & color, const std::string & frame_id, const std::string & ns)
{
using visualization_msgs::msg::Marker;
Marker marker;
marker.header.frame_id = frame_id;
marker.header.stamp = rclcpp::Time(0, 0);
marker.ns = ns;
marker.id = id;
marker.type = Marker::SPHERE;
marker.action = Marker::ADD;
marker.pose = pose;
marker.scale = scale;
marker.color = color;
return marker;
}
/**
* @brief Convert data into TwistStamped
* @param vx X velocity
* @param wz Angular velocity
* @param stamp Timestamp
* @param frame Reference frame to use
*/
inline geometry_msgs::msg::TwistStamped toTwistStamped(
float vx, float wz, const builtin_interfaces::msg::Time & stamp, const std::string & frame)
{
geometry_msgs::msg::TwistStamped twist;
twist.header.frame_id = frame;
twist.header.stamp = stamp;
twist.twist.linear.x = vx;
twist.twist.angular.z = wz;
return twist;
}
/**
* @brief Convert data into TwistStamped
* @param vx X velocity
* @param vy Y velocity
* @param wz Angular velocity
* @param stamp Timestamp
* @param frame Reference frame to use
*/
inline geometry_msgs::msg::TwistStamped toTwistStamped(
float vx, float vy, float wz, const builtin_interfaces::msg::Time & stamp,
const std::string & frame)
{
auto twist = toTwistStamped(vx, wz, stamp, frame);
twist.twist.linear.y = vy;
return twist;
}
/**
* @brief Convert path to a tensor
* @param path Path to convert
* @return Path tensor
*/
inline models::Path toTensor(const nav_msgs::msg::Path & path)
{
auto result = models::Path{};
result.reset(path.poses.size());
for (size_t i = 0; i < path.poses.size(); ++i) {
result.x(i) = path.poses[i].pose.position.x;
result.y(i) = path.poses[i].pose.position.y;
result.yaws(i) = tf2::getYaw(path.poses[i].pose.orientation);
}
return result;
}
/**
* @brief Check if the robot pose is within the Goal Checker's tolerances to goal
* @param global_checker Pointer to the goal checker
* @param robot Pose of robot
* @param goal Goal pose
* @return bool If robot is within goal checker tolerances to the goal
*/
inline bool withinPositionGoalTolerance(
nav2_core::GoalChecker * goal_checker,
const geometry_msgs::msg::Pose & robot,
const geometry_msgs::msg::Pose & goal)
{
if (goal_checker) {
geometry_msgs::msg::Pose pose_tolerance;
geometry_msgs::msg::Twist velocity_tolerance;
goal_checker->getTolerances(pose_tolerance, velocity_tolerance);
const auto pose_tolerance_sq = pose_tolerance.position.x * pose_tolerance.position.x;
auto dx = robot.position.x - goal.position.x;
auto dy = robot.position.y - goal.position.y;
auto dist_sq = dx * dx + dy * dy;
if (dist_sq < pose_tolerance_sq) {
return true;
}
}
return false;
}
/**
* @brief Check if the robot pose is within tolerance to the goal
* @param pose_tolerance Pose tolerance to use
* @param robot Pose of robot
* @param goal Goal pose
* @return bool If robot is within tolerance to the goal
*/
inline bool withinPositionGoalTolerance(
float pose_tolerance,
const geometry_msgs::msg::Pose & robot,
const geometry_msgs::msg::Pose & goal)
{
const double & dist_sq =
std::pow(goal.position.x - robot.position.x, 2) +
std::pow(goal.position.y - robot.position.y, 2);
const float pose_tolerance_sq = pose_tolerance * pose_tolerance;
if (dist_sq < pose_tolerance_sq) {
return true;
}
return false;
}
/**
* @brief normalize
* Normalizes the angle to be -M_PI circle to +M_PI circle
* It takes and returns radians.
* @param angles Angles to normalize
* @return normalized angles
*/
template<typename T>
auto normalize_angles(const T & angles)
{
auto && theta = xt::eval(xt::fmod(angles + M_PI, 2.0 * M_PI));
return xt::eval(xt::where(theta <= 0.0, theta + M_PI, theta - M_PI));
}
/**
* @brief shortest_angular_distance
*
* Given 2 angles, this returns the shortest angular
* difference. The inputs and ouputs are of course radians.
*
* The result
* would always be -pi <= result <= pi. Adding the result
* to "from" will always get you an equivelent angle to "to".
* @param from Start angle
* @param to End angle
* @return Shortest distance between angles
*/
template<typename F, typename T>
auto shortest_angular_distance(
const F & from,
const T & to)
{
return normalize_angles(to - from);
}
/**
* @brief Evaluate furthest point idx of data.path which is
* nearset to some trajectory in data.trajectories
* @param data Data to use
* @return Idx of furthest path point reached by a set of trajectories
*/
inline size_t findPathFurthestReachedPoint(const CriticData & data)
{
const auto traj_x = xt::view(data.trajectories.x, xt::all(), -1, xt::newaxis());
const auto traj_y = xt::view(data.trajectories.y, xt::all(), -1, xt::newaxis());
const auto dx = data.path.x - traj_x;
const auto dy = data.path.y - traj_y;
const auto dists = dx * dx + dy * dy;
size_t max_id_by_trajectories = 0, min_id_by_path = 0;
float min_distance_by_path = std::numeric_limits<float>::max();
float cur_dist = 0.0f;
for (size_t i = 0; i < dists.shape(0); i++) {
min_id_by_path = 0;
min_distance_by_path = std::numeric_limits<float>::max();
for (size_t j = 0; j < dists.shape(1); j++) {
cur_dist = dists(i, j);
if (cur_dist < min_distance_by_path) {
min_distance_by_path = cur_dist;
min_id_by_path = j;
}
}
max_id_by_trajectories = std::max(max_id_by_trajectories, min_id_by_path);
}
return max_id_by_trajectories;
}
/**
* @brief Evaluate closest point idx of data.path which is
* nearset to the start of the trajectory in data.trajectories
* @param data Data to use
* @return Idx of closest path point at start of the trajectories
*/
inline size_t findPathTrajectoryInitialPoint(const CriticData & data)
{
// First point should be the same for all trajectories from initial conditions
const auto dx = data.path.x - data.trajectories.x(0, 0);
const auto dy = data.path.y - data.trajectories.y(0, 0);
const auto dists = dx * dx + dy * dy;
float min_distance_by_path = std::numeric_limits<float>::max();
size_t min_id = 0;
for (size_t j = 0; j < dists.shape(0); j++) {
if (dists(j) < min_distance_by_path) {
min_distance_by_path = dists(j);
min_id = j;
}
}
return min_id;
}
/**
* @brief evaluate path furthest point if it is not set
* @param data Data to use
*/
inline void setPathFurthestPointIfNotSet(CriticData & data)
{
if (!data.furthest_reached_path_point) {
data.furthest_reached_path_point = findPathFurthestReachedPoint(data);
}
}
/**
* @brief evaluate path costs
* @param data Data to use
*/
inline void findPathCosts(
CriticData & data,
std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros)
{
auto * costmap = costmap_ros->getCostmap();
unsigned int map_x, map_y;
const size_t path_segments_count = data.path.x.shape(0) - 1;
data.path_pts_valid = std::vector<bool>(path_segments_count, false);
for (unsigned int idx = 0; idx < path_segments_count; idx++) {
const auto path_x = data.path.x(idx);
const auto path_y = data.path.y(idx);
if (!costmap->worldToMap(path_x, path_y, map_x, map_y)) {
(*data.path_pts_valid)[idx] = false;
continue;
}
switch (costmap->getCost(map_x, map_y)) {
using namespace nav2_costmap_2d; // NOLINT
case (LETHAL_OBSTACLE):
(*data.path_pts_valid)[idx] = false;
continue;
case (INSCRIBED_INFLATED_OBSTACLE):
(*data.path_pts_valid)[idx] = false;
continue;
case (NO_INFORMATION):
const bool is_tracking_unknown =
costmap_ros->getLayeredCostmap()->isTrackingUnknown();
(*data.path_pts_valid)[idx] = is_tracking_unknown ? true : false;
continue;
}
(*data.path_pts_valid)[idx] = true;
}
}
/**
* @brief evaluate path costs if it is not set
* @param data Data to use
*/
inline void setPathCostsIfNotSet(
CriticData & data,
std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros)
{
if (!data.path_pts_valid) {
findPathCosts(data, costmap_ros);
}
}
/**
* @brief evaluate angle from pose (have angle) to point (no angle)
* @param pose pose
* @param point_x Point to find angle relative to X axis
* @param point_y Point to find angle relative to Y axis
* @param forward_preference If reversing direction is valid
* @return Angle between two points
*/
inline float posePointAngle(
const geometry_msgs::msg::Pose & pose, double point_x, double point_y, bool forward_preference)
{
float pose_x = pose.position.x;
float pose_y = pose.position.y;
float pose_yaw = tf2::getYaw(pose.orientation);
float yaw = atan2f(point_y - pose_y, point_x - pose_x);
// If no preference for forward, return smallest angle either in heading or 180 of heading
if (!forward_preference) {
return std::min(
fabs(angles::shortest_angular_distance(yaw, pose_yaw)),
fabs(angles::shortest_angular_distance(yaw, angles::normalize_angle(pose_yaw + M_PI))));
}
return fabs(angles::shortest_angular_distance(yaw, pose_yaw));
}
/**
* @brief Apply Savisky-Golay filter to optimal trajectory
* @param control_sequence Sequence to apply filter to
* @param control_history Recent set of controls for edge-case handling
* @param Settings Settings to use
*/
inline void savitskyGolayFilter(
models::ControlSequence & control_sequence,
std::array<mppi::models::Control, 4> & control_history,
const models::OptimizerSettings & settings)
{
// Savitzky-Golay Quadratic, 9-point Coefficients
xt::xarray<float> filter = {-21.0, 14.0, 39.0, 54.0, 59.0, 54.0, 39.0, 14.0, -21.0};
filter /= 231.0;
const unsigned int num_sequences = control_sequence.vx.shape(0) - 1;
// Too short to smooth meaningfully
if (num_sequences < 20) {
return;
}
auto applyFilter = [&](const xt::xarray<float> & data) -> float {
return xt::sum(data * filter, {0}, immediate)();
};
auto applyFilterOverAxis =
[&](xt::xtensor<float, 1> & sequence,
const float hist_0, const float hist_1, const float hist_2, const float hist_3) -> void
{
unsigned int idx = 0;
sequence(idx) = applyFilter(
{
hist_0,
hist_1,
hist_2,
hist_3,
sequence(idx),
sequence(idx + 1),
sequence(idx + 2),
sequence(idx + 3),
sequence(idx + 4)});
idx++;
sequence(idx) = applyFilter(
{
hist_1,
hist_2,
hist_3,
sequence(idx - 1),
sequence(idx),
sequence(idx + 1),
sequence(idx + 2),
sequence(idx + 3),
sequence(idx + 4)});
idx++;
sequence(idx) = applyFilter(
{
hist_2,
hist_3,
sequence(idx - 2),
sequence(idx - 1),
sequence(idx),
sequence(idx + 1),
sequence(idx + 2),
sequence(idx + 3),
sequence(idx + 4)});
idx++;
sequence(idx) = applyFilter(
{
hist_3,
sequence(idx - 3),
sequence(idx - 2),
sequence(idx - 1),
sequence(idx),
sequence(idx + 1),
sequence(idx + 2),
sequence(idx + 3),
sequence(idx + 4)});
for (idx = 4; idx != num_sequences - 4; idx++) {
sequence(idx) = applyFilter(
{
sequence(idx - 4),
sequence(idx - 3),
sequence(idx - 2),
sequence(idx - 1),
sequence(idx),
sequence(idx + 1),
sequence(idx + 2),
sequence(idx + 3),
sequence(idx + 4)});
}
idx++;
sequence(idx) = applyFilter(
{
sequence(idx - 4),
sequence(idx - 3),
sequence(idx - 2),
sequence(idx - 1),
sequence(idx),
sequence(idx + 1),
sequence(idx + 2),
sequence(idx + 3),
sequence(idx + 3)});
idx++;
sequence(idx) = applyFilter(
{
sequence(idx - 4),
sequence(idx - 3),
sequence(idx - 2),
sequence(idx - 1),
sequence(idx),
sequence(idx + 1),
sequence(idx + 2),
sequence(idx + 2),
sequence(idx + 2)});
idx++;
sequence(idx) = applyFilter(
{
sequence(idx - 4),
sequence(idx - 3),
sequence(idx - 2),
sequence(idx - 1),
sequence(idx),
sequence(idx + 1),
sequence(idx + 1),
sequence(idx + 1),
sequence(idx + 1)});
idx++;
sequence(idx) = applyFilter(
{
sequence(idx - 4),
sequence(idx - 3),
sequence(idx - 2),
sequence(idx - 1),
sequence(idx),
sequence(idx),
sequence(idx),
sequence(idx),
sequence(idx)});
};
// Filter trajectories
applyFilterOverAxis(
control_sequence.vx, control_history[0].vx,
control_history[1].vx, control_history[2].vx, control_history[3].vx);
applyFilterOverAxis(
control_sequence.vy, control_history[0].vy,
control_history[1].vy, control_history[2].vy, control_history[3].vy);
applyFilterOverAxis(
control_sequence.wz, control_history[0].wz,
control_history[1].wz, control_history[2].wz, control_history[3].wz);
// Update control history
unsigned int offset = settings.shift_control_sequence ? 1 : 0;
control_history[0] = control_history[1];
control_history[1] = control_history[2];
control_history[2] = control_history[3];
control_history[3] = {
control_sequence.vx(offset),
control_sequence.vy(offset),
control_sequence.wz(offset)};
}
/**
* @brief Find the iterator of the first pose at which there is an inversion on the path,
* @param path to check for inversion
* @return the first point after the inversion found in the path
*/
inline unsigned int findFirstPathInversion(nav_msgs::msg::Path & path)
{
// At least 3 poses for a possible inversion
if (path.poses.size() < 3) {
return path.poses.size();
}
// Iterating through the path to determine the position of the path inversion
for (unsigned int idx = 1; idx < path.poses.size() - 1; ++idx) {
// We have two vectors for the dot product OA and AB. Determining the vectors.
float oa_x = path.poses[idx].pose.position.x -
path.poses[idx - 1].pose.position.x;
float oa_y = path.poses[idx].pose.position.y -
path.poses[idx - 1].pose.position.y;
float ab_x = path.poses[idx + 1].pose.position.x -
path.poses[idx].pose.position.x;
float ab_y = path.poses[idx + 1].pose.position.y -
path.poses[idx].pose.position.y;
// Checking for the existance of cusp, in the path, using the dot product.
float dot_product = (oa_x * ab_x) + (oa_y * ab_y);
if (dot_product < 0.0) {
return idx + 1;
}
}
return path.poses.size();
}
/**
* @brief Find and remove poses after the first inversion in the path
* @param path to check for inversion
* @return The location of the inversion, return 0 if none exist
*/
inline unsigned int removePosesAfterFirstInversion(nav_msgs::msg::Path & path)
{
nav_msgs::msg::Path cropped_path = path;
const unsigned int first_after_inversion = findFirstPathInversion(cropped_path);
if (first_after_inversion == path.poses.size()) {
return 0u;
}
cropped_path.poses.erase(
cropped_path.poses.begin() + first_after_inversion, cropped_path.poses.end());
path = cropped_path;
return first_after_inversion;
}
/**
* @brief Compare to trajectory points to find closest path point along integrated distances
* @param vec Vect to check
* @return dist Distance to look for
*/
inline size_t findClosestPathPt(const std::vector<float> & vec, float dist, size_t init = 0)
{
auto iter = std::lower_bound(vec.begin() + init, vec.end(), dist);
if (iter == vec.begin() + init) {
return 0;
}
if (dist - *(iter - 1) < *iter - dist) {
return iter - 1 - vec.begin();
}
return iter - vec.begin();
}
} // namespace mppi::utils
#endif // NAV2_MPPI_CONTROLLER__TOOLS__UTILS_HPP_
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 MiB

@@ -0,0 +1,7 @@
<class_libraries>
<library path="mppi_controller">
<class type="nav2_mppi_controller::MPPIController" base_class_type="nav2_core::Controller">
<description>MPPI Controller for Nav2</description>
</class>
</library>
</class_libraries>
@@ -0,0 +1,42 @@
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>nav2_mppi_controller</name>
<version>1.1.18</version>
<description>nav2_mppi_controller</description>
<maintainer email="stevenmacenski@gmail.com">Steve Macenski</maintainer>
<maintainer email="budyakov.aleksei@gmail.com">Aleksei Budyakov</maintainer>
<license>MIT</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<buildtool_depend>ament_cmake_ros</buildtool_depend>
<depend>rclcpp</depend>
<depend>nav2_common</depend>
<depend>nav2_core</depend>
<depend>nav2_util</depend>
<depend>nav2_costmap_2d</depend>
<depend>geometry_msgs</depend>
<depend>visualization_msgs</depend>
<depend>nav2_msgs</depend>
<depend>pluginlib</depend>
<depend>tf2_geometry_msgs</depend>
<depend>tf2</depend>
<depend>tf2_eigen</depend>
<depend>tf2_ros</depend>
<depend>std_msgs</depend>
<depend>xtensor</depend>
<depend>libomp-dev</depend>
<depend>benchmark</depend>
<depend>xsimd</depend>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<test_depend>ament_cmake_gtest</test_depend>
<export>
<build_type>ament_cmake</build_type>
<nav2_core plugin="${prefix}/mppic.xml" />
<nav2_mppi_controller plugin="${prefix}/critics.xml" />
</export>
</package>
@@ -0,0 +1,138 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include <stdint.h>
#include <chrono>
#include "nav2_mppi_controller/controller.hpp"
#include "nav2_mppi_controller/tools/utils.hpp"
// #define BENCHMARK_TESTING
namespace nav2_mppi_controller
{
void MPPIController::configure(
const rclcpp_lifecycle::LifecycleNode::WeakPtr & parent,
std::string name, const std::shared_ptr<tf2_ros::Buffer> tf,
const std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros)
{
parent_ = parent;
costmap_ros_ = costmap_ros;
tf_buffer_ = tf;
name_ = name;
parameters_handler_ = std::make_unique<ParametersHandler>(parent);
auto node = parent_.lock();
clock_ = node->get_clock();
last_time_called_ = clock_->now();
// Get high-level controller parameters
auto getParam = parameters_handler_->getParamGetter(name_);
getParam(visualize_, "visualize", false);
getParam(reset_period_, "reset_period", 1.0);
// Configure composed objects
optimizer_.initialize(parent_, name_, costmap_ros_, parameters_handler_.get());
path_handler_.initialize(parent_, name_, costmap_ros_, tf_buffer_, parameters_handler_.get());
trajectory_visualizer_.on_configure(
parent_, name_,
costmap_ros_->getGlobalFrameID(), parameters_handler_.get());
RCLCPP_INFO(logger_, "Configured MPPI Controller: %s", name_.c_str());
}
void MPPIController::cleanup()
{
optimizer_.shutdown();
trajectory_visualizer_.on_cleanup();
parameters_handler_.reset();
RCLCPP_INFO(logger_, "Cleaned up MPPI Controller: %s", name_.c_str());
}
void MPPIController::activate()
{
trajectory_visualizer_.on_activate();
parameters_handler_->start();
RCLCPP_INFO(logger_, "Activated MPPI Controller: %s", name_.c_str());
}
void MPPIController::deactivate()
{
trajectory_visualizer_.on_deactivate();
RCLCPP_INFO(logger_, "Deactivated MPPI Controller: %s", name_.c_str());
}
void MPPIController::reset()
{
optimizer_.reset();
}
geometry_msgs::msg::TwistStamped MPPIController::computeVelocityCommands(
const geometry_msgs::msg::PoseStamped & robot_pose,
const geometry_msgs::msg::Twist & robot_speed,
nav2_core::GoalChecker * goal_checker)
{
#ifdef BENCHMARK_TESTING
auto start = std::chrono::system_clock::now();
#endif
if (clock_->now() - last_time_called_ > rclcpp::Duration::from_seconds(reset_period_)) {
reset();
}
last_time_called_ = clock_->now();
std::lock_guard<std::mutex> param_lock(*parameters_handler_->getLock());
geometry_msgs::msg::Pose goal = path_handler_.getTransformedGoal(robot_pose.header.stamp).pose;
nav_msgs::msg::Path transformed_plan = path_handler_.transformPath(robot_pose);
nav2_costmap_2d::Costmap2D * costmap = costmap_ros_->getCostmap();
std::unique_lock<nav2_costmap_2d::Costmap2D::mutex_t> costmap_lock(*(costmap->getMutex()));
geometry_msgs::msg::TwistStamped cmd =
optimizer_.evalControl(robot_pose, robot_speed, transformed_plan, goal, goal_checker);
#ifdef BENCHMARK_TESTING
auto end = std::chrono::system_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
RCLCPP_INFO(logger_, "Control loop execution time: %ld [ms]", duration);
#endif
if (visualize_) {
visualize(std::move(transformed_plan));
}
return cmd;
}
void MPPIController::visualize(nav_msgs::msg::Path transformed_plan)
{
trajectory_visualizer_.add(optimizer_.getGeneratedTrajectories(), "Candidate Trajectories");
trajectory_visualizer_.add(optimizer_.getOptimizedTrajectory(), "Optimal Trajectory");
trajectory_visualizer_.visualize(std::move(transformed_plan));
}
void MPPIController::setPlan(const nav_msgs::msg::Path & path)
{
path_handler_.setPath(path);
}
void MPPIController::setSpeedLimit(const double & speed_limit, const bool & percentage)
{
optimizer_.setSpeedLimit(speed_limit, percentage);
}
} // namespace nav2_mppi_controller
#include "pluginlib/class_list_macros.hpp"
PLUGINLIB_EXPORT_CLASS(nav2_mppi_controller::MPPIController, nav2_core::Controller)
@@ -0,0 +1,78 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include "nav2_mppi_controller/critic_manager.hpp"
namespace mppi
{
void CriticManager::on_configure(
rclcpp_lifecycle::LifecycleNode::WeakPtr parent, const std::string & name,
std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros, ParametersHandler * param_handler)
{
parent_ = parent;
costmap_ros_ = costmap_ros;
name_ = name;
auto node = parent_.lock();
logger_ = node->get_logger();
parameters_handler_ = param_handler;
getParams();
loadCritics();
}
void CriticManager::getParams()
{
auto node = parent_.lock();
auto getParam = parameters_handler_->getParamGetter(name_);
getParam(critic_names_, "critics", std::vector<std::string>{}, ParameterType::Static);
}
void CriticManager::loadCritics()
{
if (!loader_) {
loader_ = std::make_unique<pluginlib::ClassLoader<critics::CriticFunction>>(
"nav2_mppi_controller", "mppi::critics::CriticFunction");
}
critics_.clear();
for (auto name : critic_names_) {
std::string fullname = getFullName(name);
auto instance = std::unique_ptr<critics::CriticFunction>(
loader_->createUnmanagedInstance(fullname));
critics_.push_back(std::move(instance));
critics_.back()->on_configure(
parent_, name_, name_ + "." + name, costmap_ros_,
parameters_handler_);
RCLCPP_INFO(logger_, "Critic loaded : %s", fullname.c_str());
}
}
std::string CriticManager::getFullName(const std::string & name)
{
return "mppi::critics::" + name;
}
void CriticManager::evalTrajectoriesScores(
CriticData & data) const
{
for (size_t q = 0; q < critics_.size(); q++) {
if (data.fail_flag) {
break;
}
critics_[q]->score(data);
}
}
} // namespace mppi
@@ -0,0 +1,81 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include "nav2_mppi_controller/critics/constraint_critic.hpp"
namespace mppi::critics
{
void ConstraintCritic::initialize()
{
auto getParam = parameters_handler_->getParamGetter(name_);
auto getParentParam = parameters_handler_->getParamGetter(parent_name_);
getParam(power_, "cost_power", 1);
getParam(weight_, "cost_weight", 4.0);
RCLCPP_INFO(
logger_, "ConstraintCritic instantiated with %d power and %f weight.",
power_, weight_);
float vx_max, vy_max, vx_min;
getParentParam(vx_max, "vx_max", 0.5);
getParentParam(vy_max, "vy_max", 0.0);
getParentParam(vx_min, "vx_min", -0.35);
const float min_sgn = vx_min > 0.0 ? 1.0 : -1.0;
max_vel_ = sqrtf(vx_max * vx_max + vy_max * vy_max);
min_vel_ = min_sgn * sqrtf(vx_min * vx_min + vy_max * vy_max);
}
void ConstraintCritic::score(CriticData & data)
{
using xt::evaluation_strategy::immediate;
if (!enabled_) {
return;
}
auto sgn = xt::where(data.state.vx > 0.0, 1.0, -1.0);
auto vel_total = sgn * xt::sqrt(data.state.vx * data.state.vx + data.state.vy * data.state.vy);
auto out_of_max_bounds_motion = xt::maximum(vel_total - max_vel_, 0);
auto out_of_min_bounds_motion = xt::maximum(min_vel_ - vel_total, 0);
auto acker = dynamic_cast<AckermannMotionModel *>(data.motion_model.get());
if (acker != nullptr) {
auto & vx = data.state.vx;
auto & wz = data.state.wz;
auto out_of_turning_rad_motion = xt::maximum(
acker->getMinTurningRadius() - (xt::fabs(vx) / xt::fabs(wz)), 0.0);
data.costs += xt::pow(
xt::sum(
(std::move(out_of_max_bounds_motion) +
std::move(out_of_min_bounds_motion) +
std::move(out_of_turning_rad_motion)) *
data.model_dt, {1}, immediate) * weight_, power_);
return;
}
data.costs += xt::pow(
xt::sum(
(std::move(out_of_max_bounds_motion) +
std::move(out_of_min_bounds_motion)) *
data.model_dt, {1}, immediate) * weight_, power_);
}
} // namespace mppi::critics
#include <pluginlib/class_list_macros.hpp>
PLUGINLIB_EXPORT_CLASS(mppi::critics::ConstraintCritic, mppi::critics::CriticFunction)
@@ -0,0 +1,231 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
// Copyright (c) 2023 Open Navigation LLC
//
// 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.
#include <cmath>
#include "nav2_mppi_controller/critics/cost_critic.hpp"
#include "nav2_core/exceptions.hpp"
namespace mppi::critics
{
void CostCritic::initialize()
{
auto getParam = parameters_handler_->getParamGetter(name_);
getParam(consider_footprint_, "consider_footprint", false);
getParam(power_, "cost_power", 1);
getParam(weight_, "cost_weight", 3.81);
getParam(critical_cost_, "critical_cost", 300.0);
getParam(collision_cost_, "collision_cost", 1000000.0);
getParam(near_goal_distance_, "near_goal_distance", 0.5);
getParam(inflation_layer_name_, "inflation_layer_name", std::string(""));
// Normalized by cost value to put in same regime as other weights
weight_ /= 254.0f;
// Normalize weight when parameter is changed dynamically as well
auto weightDynamicCb = [&](const rclcpp::Parameter & weight) {
weight_ = weight.as_double() / 254.0f;
};
parameters_handler_->addDynamicParamCallback(name_ + ".cost_weight", weightDynamicCb);
collision_checker_.setCostmap(costmap_);
possibly_inscribed_cost_ = findCircumscribedCost(costmap_ros_);
if (possibly_inscribed_cost_ < 1.0f) {
RCLCPP_ERROR(
logger_,
"Inflation layer either not found or inflation is not set sufficiently for "
"optimized non-circular collision checking capabilities. It is HIGHLY recommended to set"
" the inflation radius to be at MINIMUM half of the robot's largest cross-section. See "
"github.com/ros-planning/navigation2/tree/main/nav2_smac_planner#potential-fields"
" for full instructions. This will substantially impact run-time performance.");
}
if (costmap_ros_->getUseRadius() == consider_footprint_) {
RCLCPP_WARN(
logger_,
"Inconsistent configuration in collision checking. Please verify the robot's shape settings "
"in both the costmap and the cost critic.");
if (costmap_ros_->getUseRadius()) {
throw nav2_core::PlannerException(
"Considering footprint in collision checking but no robot footprint provided in the "
"costmap.");
}
}
RCLCPP_INFO(
logger_,
"InflationCostCritic instantiated with %d power and %f / %f weights. "
"Critic will collision check based on %s cost.",
power_, critical_cost_, weight_, consider_footprint_ ?
"footprint" : "circular");
}
float CostCritic::findCircumscribedCost(
std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap)
{
bool inflation_layer_found = false;
double result = -1.0;
const double circum_radius = costmap->getLayeredCostmap()->getCircumscribedRadius();
if (static_cast<float>(circum_radius) == circumscribed_radius_) {
// early return if footprint size is unchanged
return circumscribed_cost_;
}
// check if the costmap has an inflation layer
for (auto layer = costmap->getLayeredCostmap()->getPlugins()->begin();
layer != costmap->getLayeredCostmap()->getPlugins()->end();
++layer)
{
auto inflation_layer = std::dynamic_pointer_cast<nav2_costmap_2d::InflationLayer>(*layer);
if (!inflation_layer ||
(!inflation_layer_name_.empty() &&
inflation_layer->getName() != inflation_layer_name_))
{
continue;
}
inflation_layer_found = true;
const double resolution = costmap->getCostmap()->getResolution();
result = inflation_layer->computeCost(circum_radius / resolution);
}
if (!inflation_layer_found) {
RCLCPP_WARN(
logger_,
"No inflation layer found in costmap configuration. "
"If this is an SE2-collision checking plugin, it cannot use costmap potential "
"field to speed up collision checking by only checking the full footprint "
"when robot is within possibly-inscribed radius of an obstacle. This may "
"significantly slow down planning times and not avoid anything but absolute collisions!");
}
circumscribed_radius_ = static_cast<float>(circum_radius);
circumscribed_cost_ = static_cast<float>(result);
return circumscribed_cost_;
}
void CostCritic::score(CriticData & data)
{
using xt::evaluation_strategy::immediate;
if (!enabled_) {
return;
}
if (consider_footprint_) {
// footprint may have changed since initialization if user has dynamic footprints
possibly_inscribed_cost_ = findCircumscribedCost(costmap_ros_);
}
// If near the goal, don't apply the preferential term since the goal is near obstacles
bool near_goal = false;
if (utils::withinPositionGoalTolerance(near_goal_distance_, data.state.pose.pose, data.goal)) {
near_goal = true;
}
auto && repulsive_cost = xt::xtensor<float, 1>::from_shape({data.costs.shape(0)});
repulsive_cost.fill(0.0);
const size_t traj_len = data.trajectories.x.shape(1);
bool all_trajectories_collide = true;
for (size_t i = 0; i < data.trajectories.x.shape(0); ++i) {
bool trajectory_collide = false;
const auto & traj = data.trajectories;
float pose_cost;
for (size_t j = 0; j < traj_len; j++) {
// The costAtPose doesn't use orientation
// The footprintCostAtPose will always return "INSCRIBED" if footprint is over it
// So the center point has more information than the footprint
pose_cost = costAtPose(traj.x(i, j), traj.y(i, j));
if (pose_cost < 1.0f) {continue;} // In free space
if (inCollision(pose_cost, traj.x(i, j), traj.y(i, j), traj.yaws(i, j))) {
trajectory_collide = true;
break;
}
// Let near-collision trajectory points be punished severely
// Note that we collision check based on the footprint actual,
// but score based on the center-point cost regardless
using namespace nav2_costmap_2d; // NOLINT
if (pose_cost >= INSCRIBED_INFLATED_OBSTACLE) {
repulsive_cost[i] += critical_cost_;
} else if (!near_goal) { // Generally prefer trajectories further from obstacles
repulsive_cost[i] += pose_cost;
}
}
if (!trajectory_collide) {
all_trajectories_collide = false;
} else {
repulsive_cost[i] = collision_cost_;
}
}
data.costs += xt::pow((weight_ * repulsive_cost / traj_len), power_);
data.fail_flag = all_trajectories_collide;
}
/**
* @brief Checks if cost represents a collision
* @param cost Costmap cost
* @return bool if in collision
*/
bool CostCritic::inCollision(float cost, float x, float y, float theta)
{
bool is_tracking_unknown =
costmap_ros_->getLayeredCostmap()->isTrackingUnknown();
// If consider_footprint_ check footprint scort for collision
if (consider_footprint_ &&
(cost >= possibly_inscribed_cost_ || possibly_inscribed_cost_ < 1.0f))
{
cost = static_cast<float>(collision_checker_.footprintCostAtPose(
x, y, theta, costmap_ros_->getRobotFootprint()));
}
switch (static_cast<unsigned char>(cost)) {
using namespace nav2_costmap_2d; // NOLINT
case (LETHAL_OBSTACLE):
return true;
case (INSCRIBED_INFLATED_OBSTACLE):
return consider_footprint_ ? false : true;
case (NO_INFORMATION):
return is_tracking_unknown ? false : true;
}
return false;
}
float CostCritic::costAtPose(float x, float y)
{
using namespace nav2_costmap_2d; // NOLINT
unsigned int x_i, y_i;
if (!collision_checker_.worldToMap(x, y, x_i, y_i)) {
return nav2_costmap_2d::NO_INFORMATION;
}
return collision_checker_.pointCost(x_i, y_i);
}
} // namespace mppi::critics
#include <pluginlib/class_list_macros.hpp>
PLUGINLIB_EXPORT_CLASS(
mppi::critics::CostCritic,
mppi::critics::CriticFunction)
@@ -0,0 +1,58 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include "nav2_mppi_controller/critics/goal_angle_critic.hpp"
namespace mppi::critics
{
void GoalAngleCritic::initialize()
{
auto getParam = parameters_handler_->getParamGetter(name_);
getParam(power_, "cost_power", 1);
getParam(weight_, "cost_weight", 3.0);
getParam(threshold_to_consider_, "threshold_to_consider", 0.5);
RCLCPP_INFO(
logger_,
"GoalAngleCritic instantiated with %d power, %f weight, and %f "
"angular threshold.",
power_, weight_, threshold_to_consider_);
}
void GoalAngleCritic::score(CriticData & data)
{
if (!enabled_ || !utils::withinPositionGoalTolerance(
threshold_to_consider_, data.state.pose.pose, data.goal))
{
return;
}
const auto goal_idx = data.path.x.shape(0) - 1;
const float goal_yaw = data.path.yaws(goal_idx);
data.costs += xt::pow(
xt::mean(xt::abs(utils::shortest_angular_distance(data.trajectories.yaws, goal_yaw)), {1}) *
weight_, power_);
}
} // namespace mppi::critics
#include <pluginlib/class_list_macros.hpp>
PLUGINLIB_EXPORT_CLASS(
mppi::critics::GoalAngleCritic,
mppi::critics::CriticFunction)
@@ -0,0 +1,61 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
// Copyright (c) 2023 Open Navigation LLC
//
// 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.
#include "nav2_mppi_controller/critics/goal_critic.hpp"
namespace mppi::critics
{
using xt::evaluation_strategy::immediate;
void GoalCritic::initialize()
{
auto getParam = parameters_handler_->getParamGetter(name_);
getParam(power_, "cost_power", 1);
getParam(weight_, "cost_weight", 5.0);
getParam(threshold_to_consider_, "threshold_to_consider", 1.4);
RCLCPP_INFO(
logger_, "GoalCritic instantiated with %d power and %f weight.",
power_, weight_);
}
void GoalCritic::score(CriticData & data)
{
if (!enabled_ || !utils::withinPositionGoalTolerance(
threshold_to_consider_, data.state.pose.pose, data.goal))
{
return;
}
const auto & goal_x = data.goal.position.x;
const auto & goal_y = data.goal.position.y;
const auto traj_x = xt::view(data.trajectories.x, xt::all(), xt::all());
const auto traj_y = xt::view(data.trajectories.y, xt::all(), xt::all());
auto dists = xt::sqrt(
xt::pow(traj_x - goal_x, 2) +
xt::pow(traj_y - goal_y, 2));
data.costs += xt::pow(xt::mean(dists, {1}, immediate) * weight_, power_);
}
} // namespace mppi::critics
#include <pluginlib/class_list_macros.hpp>
PLUGINLIB_EXPORT_CLASS(mppi::critics::GoalCritic, mppi::critics::CriticFunction)
@@ -0,0 +1,245 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include <cmath>
#include "nav2_mppi_controller/critics/obstacles_critic.hpp"
#include "nav2_core/exceptions.hpp"
namespace mppi::critics
{
void ObstaclesCritic::initialize()
{
auto getParam = parameters_handler_->getParamGetter(name_);
getParam(consider_footprint_, "consider_footprint", false);
getParam(power_, "cost_power", 1);
getParam(repulsion_weight_, "repulsion_weight", 1.5);
getParam(critical_weight_, "critical_weight", 20.0);
getParam(collision_cost_, "collision_cost", 10000.0);
getParam(collision_margin_distance_, "collision_margin_distance", 0.10);
getParam(near_goal_distance_, "near_goal_distance", 0.5);
collision_checker_.setCostmap(costmap_);
possibly_inscribed_cost_ = findCircumscribedCost(costmap_ros_);
if (possibly_inscribed_cost_ < 1.0f) {
RCLCPP_ERROR(
logger_,
"Inflation layer either not found or inflation is not set sufficiently for "
"optimized non-circular collision checking capabilities. It is HIGHLY recommended to set"
" the inflation radius to be at MINIMUM half of the robot's largest cross-section. See "
"github.com/ros-planning/navigation2/tree/main/nav2_smac_planner#potential-fields"
" for full instructions. This will substantially impact run-time performance.");
}
if (costmap_ros_->getUseRadius() == consider_footprint_) {
RCLCPP_WARN(
logger_,
"Inconsistent configuration in collision checking. Please verify the robot's shape settings "
"in both the costmap and the obstacle critic.");
if (costmap_ros_->getUseRadius()) {
throw nav2_core::PlannerException(
"Considering footprint in collision checking but no robot footprint provided in the "
"costmap.");
}
}
RCLCPP_INFO(
logger_,
"ObstaclesCritic instantiated with %d power and %f / %f weights. "
"Critic will collision check based on %s cost.",
power_, critical_weight_, repulsion_weight_, consider_footprint_ ?
"footprint" : "circular");
}
float ObstaclesCritic::findCircumscribedCost(
std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap)
{
double result = -1.0;
bool inflation_layer_found = false;
const double circum_radius = costmap->getLayeredCostmap()->getCircumscribedRadius();
if (static_cast<float>(circum_radius) == circumscribed_radius_) {
// early return if footprint size is unchanged
return circumscribed_cost_;
}
// check if the costmap has an inflation layer
for (auto layer = costmap->getLayeredCostmap()->getPlugins()->begin();
layer != costmap->getLayeredCostmap()->getPlugins()->end();
++layer)
{
auto inflation_layer = std::dynamic_pointer_cast<nav2_costmap_2d::InflationLayer>(*layer);
if (!inflation_layer) {
continue;
}
inflation_layer_found = true;
const double resolution = costmap->getCostmap()->getResolution();
result = inflation_layer->computeCost(circum_radius / resolution);
auto getParam = parameters_handler_->getParamGetter(name_);
getParam(inflation_scale_factor_, "cost_scaling_factor", 10.0);
getParam(inflation_radius_, "inflation_radius", 0.55);
}
if (!inflation_layer_found) {
RCLCPP_WARN(
logger_,
"No inflation layer found in costmap configuration. "
"If this is an SE2-collision checking plugin, it cannot use costmap potential "
"field to speed up collision checking by only checking the full footprint "
"when robot is within possibly-inscribed radius of an obstacle. This may "
"significantly slow down planning times and not avoid anything but absolute collisions!");
}
circumscribed_radius_ = static_cast<float>(circum_radius);
circumscribed_cost_ = static_cast<float>(result);
return circumscribed_cost_;
}
float ObstaclesCritic::distanceToObstacle(const CollisionCost & cost)
{
const float scale_factor = inflation_scale_factor_;
const float min_radius = costmap_ros_->getLayeredCostmap()->getInscribedRadius();
float dist_to_obj = (scale_factor * min_radius - log(cost.cost) + log(253.0f)) / scale_factor;
// If not footprint collision checking, the cost is using the center point cost and
// needs the radius subtracted to obtain the closest distance to the object
if (!cost.using_footprint) {
dist_to_obj -= min_radius;
}
return dist_to_obj;
}
void ObstaclesCritic::score(CriticData & data)
{
using xt::evaluation_strategy::immediate;
if (!enabled_) {
return;
}
if (consider_footprint_) {
// footprint may have changed since initialization if user has dynamic footprints
possibly_inscribed_cost_ = findCircumscribedCost(costmap_ros_);
}
// If near the goal, don't apply the preferential term since the goal is near obstacles
bool near_goal = false;
if (utils::withinPositionGoalTolerance(near_goal_distance_, data.state.pose.pose, data.goal)) {
near_goal = true;
}
auto && raw_cost = xt::xtensor<float, 1>::from_shape({data.costs.shape(0)});
raw_cost.fill(0.0f);
auto && repulsive_cost = xt::xtensor<float, 1>::from_shape({data.costs.shape(0)});
repulsive_cost.fill(0.0f);
const size_t traj_len = data.trajectories.x.shape(1);
bool all_trajectories_collide = true;
for (size_t i = 0; i < data.trajectories.x.shape(0); ++i) {
bool trajectory_collide = false;
float traj_cost = 0.0f;
const auto & traj = data.trajectories;
CollisionCost pose_cost;
for (size_t j = 0; j < traj_len; j++) {
pose_cost = costAtPose(traj.x(i, j), traj.y(i, j), traj.yaws(i, j));
if (pose_cost.cost < 1.0f) {continue;} // In free space
if (inCollision(pose_cost.cost)) {
trajectory_collide = true;
break;
}
// Cannot process repulsion if inflation layer does not exist
if (inflation_radius_ == 0.0f || inflation_scale_factor_ == 0.0f) {
continue;
}
const float dist_to_obj = distanceToObstacle(pose_cost);
// Let near-collision trajectory points be punished severely
if (dist_to_obj < collision_margin_distance_) {
traj_cost += (collision_margin_distance_ - dist_to_obj);
} else if (!near_goal) { // Generally prefer trajectories further from obstacles
repulsive_cost[i] += (inflation_radius_ - dist_to_obj);
}
}
if (!trajectory_collide) {all_trajectories_collide = false;}
raw_cost[i] = trajectory_collide ? collision_cost_ : traj_cost;
}
data.costs += xt::pow(
(critical_weight_ * raw_cost) +
(repulsion_weight_ * repulsive_cost / traj_len),
power_);
data.fail_flag = all_trajectories_collide;
}
/**
* @brief Checks if cost represents a collision
* @param cost Costmap cost
* @return bool if in collision
*/
bool ObstaclesCritic::inCollision(float cost) const
{
bool is_tracking_unknown =
costmap_ros_->getLayeredCostmap()->isTrackingUnknown();
switch (static_cast<unsigned char>(cost)) {
using namespace nav2_costmap_2d; // NOLINT
case (LETHAL_OBSTACLE):
return true;
case (INSCRIBED_INFLATED_OBSTACLE):
return consider_footprint_ ? false : true;
case (NO_INFORMATION):
return is_tracking_unknown ? false : true;
}
return false;
}
CollisionCost ObstaclesCritic::costAtPose(float x, float y, float theta)
{
CollisionCost collision_cost;
float & cost = collision_cost.cost;
collision_cost.using_footprint = false;
unsigned int x_i, y_i;
if (!collision_checker_.worldToMap(x, y, x_i, y_i)) {
cost = nav2_costmap_2d::NO_INFORMATION;
return collision_cost;
}
cost = collision_checker_.pointCost(x_i, y_i);
if (consider_footprint_ &&
(cost >= possibly_inscribed_cost_ || possibly_inscribed_cost_ < 1.0f))
{
cost = static_cast<float>(collision_checker_.footprintCostAtPose(
x, y, theta, costmap_ros_->getRobotFootprint()));
collision_cost.using_footprint = true;
}
return collision_cost;
}
} // namespace mppi::critics
#include <pluginlib/class_list_macros.hpp>
PLUGINLIB_EXPORT_CLASS(
mppi::critics::ObstaclesCritic,
mppi::critics::CriticFunction)
@@ -0,0 +1,144 @@
// Copyright (c) 2023 Open Navigation LLC
//
// 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.
#include "nav2_mppi_controller/critics/path_align_critic.hpp"
#include <xtensor/xfixed.hpp>
#include <xtensor/xmath.hpp>
namespace mppi::critics
{
using namespace xt::placeholders; // NOLINT
using xt::evaluation_strategy::immediate;
void PathAlignCritic::initialize()
{
auto getParam = parameters_handler_->getParamGetter(name_);
getParam(power_, "cost_power", 1);
getParam(weight_, "cost_weight", 10.0);
getParam(max_path_occupancy_ratio_, "max_path_occupancy_ratio", 0.07);
getParam(offset_from_furthest_, "offset_from_furthest", 20);
getParam(trajectory_point_step_, "trajectory_point_step", 4);
getParam(
threshold_to_consider_,
"threshold_to_consider", 0.5);
getParam(use_path_orientations_, "use_path_orientations", false);
RCLCPP_INFO(
logger_,
"ReferenceTrajectoryCritic instantiated with %d power and %f weight",
power_, weight_);
}
void PathAlignCritic::score(CriticData & data)
{
// Don't apply close to goal, let the goal critics take over
if (!enabled_ || utils::withinPositionGoalTolerance(
threshold_to_consider_, data.state.pose.pose, data.goal))
{
return;
}
// Don't apply when first getting bearing w.r.t. the path
utils::setPathFurthestPointIfNotSet(data);
const size_t path_segments_count = *data.furthest_reached_path_point; // up to furthest only
if (path_segments_count < offset_from_furthest_) {
return;
}
// Don't apply when dynamic obstacles are blocking significant proportions of the local path
utils::setPathCostsIfNotSet(data, costmap_ros_);
const size_t closest_initial_path_point = utils::findPathTrajectoryInitialPoint(data);
unsigned int invalid_ctr = 0;
const float range = *data.furthest_reached_path_point - closest_initial_path_point;
for (size_t i = closest_initial_path_point; i < *data.furthest_reached_path_point; i++) {
if (!(*data.path_pts_valid)[i]) {invalid_ctr++;}
if (static_cast<float>(invalid_ctr) / range > max_path_occupancy_ratio_ && invalid_ctr > 2) {
return;
}
}
const auto P_x = xt::view(data.path.x, xt::range(_, -1)); // path points
const auto P_y = xt::view(data.path.y, xt::range(_, -1)); // path points
const auto P_yaw = xt::view(data.path.yaws, xt::range(_, -1)); // path points
const size_t batch_size = data.trajectories.x.shape(0);
const size_t time_steps = data.trajectories.x.shape(1);
auto && cost = xt::xtensor<float, 1>::from_shape({data.costs.shape(0)});
// Find integrated distance in the path
std::vector<float> path_integrated_distances(path_segments_count, 0.0f);
float dx = 0.0f, dy = 0.0f;
for (unsigned int i = 1; i != path_segments_count; i++) {
dx = P_x(i) - P_x(i - 1);
dy = P_y(i) - P_y(i - 1);
float curr_dist = sqrtf(dx * dx + dy * dy);
path_integrated_distances[i] = path_integrated_distances[i - 1] + curr_dist;
}
float traj_integrated_distance = 0.0f;
float summed_path_dist = 0.0f, dyaw = 0.0f;
float num_samples = 0.0f;
float Tx = 0.0f, Ty = 0.0f;
size_t path_pt = 0;
for (size_t t = 0; t < batch_size; ++t) {
traj_integrated_distance = 0.0f;
summed_path_dist = 0.0f;
num_samples = 0.0f;
path_pt = 0u;
const auto T_x = xt::view(data.trajectories.x, t, xt::all());
const auto T_y = xt::view(data.trajectories.y, t, xt::all());
for (size_t p = trajectory_point_step_; p < time_steps; p += trajectory_point_step_) {
Tx = T_x(p);
Ty = T_y(p);
dx = Tx - T_x(p - trajectory_point_step_);
dy = Ty - T_y(p - trajectory_point_step_);
traj_integrated_distance += sqrtf(dx * dx + dy * dy);
path_pt = utils::findClosestPathPt(
path_integrated_distances, traj_integrated_distance, path_pt);
// The nearest path point to align to needs to be not in collision, else
// let the obstacle critic take over in this region due to dynamic obstacles
if ((*data.path_pts_valid)[path_pt]) {
dx = P_x(path_pt) - Tx;
dy = P_y(path_pt) - Ty;
num_samples += 1.0f;
if (use_path_orientations_) {
const auto T_yaw = xt::view(data.trajectories.yaws, t, xt::all());
dyaw = angles::shortest_angular_distance(P_yaw(path_pt), T_yaw(p));
summed_path_dist += sqrtf(dx * dx + dy * dy + dyaw * dyaw);
} else {
summed_path_dist += sqrtf(dx * dx + dy * dy);
}
}
}
if (num_samples > 0) {
cost[t] = summed_path_dist / num_samples;
} else {
cost[t] = 0.0f;
}
}
data.costs += xt::pow(std::move(cost) * weight_, power_);
}
} // namespace mppi::critics
#include <pluginlib/class_list_macros.hpp>
PLUGINLIB_EXPORT_CLASS(
mppi::critics::PathAlignCritic,
mppi::critics::CriticFunction)
@@ -0,0 +1,137 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include "nav2_mppi_controller/critics/path_align_legacy_critic.hpp"
#include <xtensor/xfixed.hpp>
#include <xtensor/xmath.hpp>
namespace mppi::critics
{
using namespace xt::placeholders; // NOLINT
using xt::evaluation_strategy::immediate;
void PathAlignLegacyCritic::initialize()
{
auto getParam = parameters_handler_->getParamGetter(name_);
getParam(power_, "cost_power", 1);
getParam(weight_, "cost_weight", 10.0);
getParam(max_path_occupancy_ratio_, "max_path_occupancy_ratio", 0.07);
getParam(offset_from_furthest_, "offset_from_furthest", 20);
getParam(trajectory_point_step_, "trajectory_point_step", 4);
getParam(
threshold_to_consider_,
"threshold_to_consider", 0.5);
getParam(use_path_orientations_, "use_path_orientations", false);
RCLCPP_INFO(
logger_,
"ReferenceTrajectoryCritic instantiated with %d power and %f weight",
power_, weight_);
}
void PathAlignLegacyCritic::score(CriticData & data)
{
// Don't apply close to goal, let the goal critics take over
if (!enabled_ || utils::withinPositionGoalTolerance(
threshold_to_consider_, data.state.pose.pose, data.goal))
{
return;
}
// Don't apply when first getting bearing w.r.t. the path
utils::setPathFurthestPointIfNotSet(data);
if (*data.furthest_reached_path_point < offset_from_furthest_) {
return;
}
// Don't apply when dynamic obstacles are blocking significant proportions of the local path
utils::setPathCostsIfNotSet(data, costmap_ros_);
const size_t closest_initial_path_point = utils::findPathTrajectoryInitialPoint(data);
unsigned int invalid_ctr = 0;
const float range = *data.furthest_reached_path_point - closest_initial_path_point;
for (size_t i = closest_initial_path_point; i < *data.furthest_reached_path_point; i++) {
if (!(*data.path_pts_valid)[i]) {invalid_ctr++;}
if (static_cast<float>(invalid_ctr) / range > max_path_occupancy_ratio_ && invalid_ctr > 2) {
return;
}
}
const auto & T_x = data.trajectories.x;
const auto & T_y = data.trajectories.y;
const auto & T_yaw = data.trajectories.yaws;
const auto P_x = xt::view(data.path.x, xt::range(_, -1)); // path points
const auto P_y = xt::view(data.path.y, xt::range(_, -1)); // path points
const auto P_yaw = xt::view(data.path.yaws, xt::range(_, -1)); // path points
const size_t batch_size = T_x.shape(0);
const size_t time_steps = T_x.shape(1);
const size_t traj_pts_eval = floor(time_steps / trajectory_point_step_);
const size_t path_segments_count = data.path.x.shape(0) - 1;
auto && cost = xt::xtensor<float, 1>::from_shape({data.costs.shape(0)});
if (path_segments_count < 1) {
return;
}
float dist_sq = 0.0f, dx = 0.0f, dy = 0.0f, dyaw = 0.0f, summed_dist = 0.0f;
float min_dist_sq = std::numeric_limits<float>::max();
size_t min_s = 0;
for (size_t t = 0; t < batch_size; ++t) {
summed_dist = 0.0f;
for (size_t p = trajectory_point_step_; p < time_steps; p += trajectory_point_step_) {
min_dist_sq = std::numeric_limits<float>::max();
min_s = 0;
// Find closest path segment to the trajectory point
for (size_t s = 0; s < path_segments_count - 1; s++) {
xt::xtensor_fixed<float, xt::xshape<2>> P;
dx = P_x(s) - T_x(t, p);
dy = P_y(s) - T_y(t, p);
if (use_path_orientations_) {
dyaw = angles::shortest_angular_distance(P_yaw(s), T_yaw(t, p));
dist_sq = dx * dx + dy * dy + dyaw * dyaw;
} else {
dist_sq = dx * dx + dy * dy;
}
if (dist_sq < min_dist_sq) {
min_dist_sq = dist_sq;
min_s = s;
}
}
// The nearest path point to align to needs to be not in collision, else
// let the obstacle critic take over in this region due to dynamic obstacles
if (min_s != 0 && (*data.path_pts_valid)[min_s]) {
summed_dist += sqrtf(min_dist_sq);
}
}
cost[t] = summed_dist / traj_pts_eval;
}
data.costs += xt::pow(std::move(cost) * weight_, power_);
}
} // namespace mppi::critics
#include <pluginlib/class_list_macros.hpp>
PLUGINLIB_EXPORT_CLASS(
mppi::critics::PathAlignLegacyCritic,
mppi::critics::CriticFunction)
@@ -0,0 +1,109 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
// Copyright (c) 2023 Open Navigation LLC
//
// 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.
#include "nav2_mppi_controller/critics/path_angle_critic.hpp"
#include <math.h>
namespace mppi::critics
{
void PathAngleCritic::initialize()
{
auto getParentParam = parameters_handler_->getParamGetter(parent_name_);
float vx_min;
getParentParam(vx_min, "vx_min", -0.35);
if (fabs(vx_min) < 1e-6) { // zero
reversing_allowed_ = false;
} else if (vx_min < 0.0) { // reversing possible
reversing_allowed_ = true;
}
auto getParam = parameters_handler_->getParamGetter(name_);
getParam(offset_from_furthest_, "offset_from_furthest", 4);
getParam(power_, "cost_power", 1);
getParam(weight_, "cost_weight", 2.0);
getParam(
threshold_to_consider_,
"threshold_to_consider", 0.5);
getParam(
max_angle_to_furthest_,
"max_angle_to_furthest", 1.2);
getParam(
forward_preference_,
"forward_preference", true);
if (!reversing_allowed_) {
forward_preference_ = true;
}
RCLCPP_INFO(
logger_,
"PathAngleCritic instantiated with %d power and %f weight. Reversing %s",
power_, weight_, reversing_allowed_ ? "allowed." : "not allowed.");
}
void PathAngleCritic::score(CriticData & data)
{
using xt::evaluation_strategy::immediate;
if (!enabled_) {
return;
}
if (utils::withinPositionGoalTolerance(
threshold_to_consider_, data.state.pose.pose, data.goal))
{
return;
}
utils::setPathFurthestPointIfNotSet(data);
auto offseted_idx = std::min(
*data.furthest_reached_path_point + offset_from_furthest_, data.path.x.shape(0) - 1);
const float goal_x = xt::view(data.path.x, offseted_idx);
const float goal_y = xt::view(data.path.y, offseted_idx);
if (utils::posePointAngle(
data.state.pose.pose, goal_x, goal_y, forward_preference_) < max_angle_to_furthest_)
{
return;
}
auto yaws_between_points = xt::atan2(
goal_y - data.trajectories.y,
goal_x - data.trajectories.x);
auto yaws =
xt::abs(utils::shortest_angular_distance(data.trajectories.yaws, yaws_between_points));
if (reversing_allowed_ && !forward_preference_) {
const auto yaws_between_points_corrected = xt::where(
yaws < M_PI_2, yaws_between_points, utils::normalize_angles(yaws_between_points + M_PI));
const auto corrected_yaws = xt::abs(
utils::shortest_angular_distance(data.trajectories.yaws, yaws_between_points_corrected));
data.costs += xt::pow(xt::mean(corrected_yaws, {1}, immediate) * weight_, power_);
} else {
data.costs += xt::pow(xt::mean(yaws, {1}, immediate) * weight_, power_);
}
}
} // namespace mppi::critics
#include <pluginlib/class_list_macros.hpp>
PLUGINLIB_EXPORT_CLASS(
mppi::critics::PathAngleCritic,
mppi::critics::CriticFunction)
@@ -0,0 +1,79 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include "nav2_mppi_controller/critics/path_follow_critic.hpp"
#include <xtensor/xmath.hpp>
#include <xtensor/xsort.hpp>
namespace mppi::critics
{
void PathFollowCritic::initialize()
{
auto getParam = parameters_handler_->getParamGetter(name_);
getParam(
threshold_to_consider_,
"threshold_to_consider", 1.4);
getParam(offset_from_furthest_, "offset_from_furthest", 6);
getParam(power_, "cost_power", 1);
getParam(weight_, "cost_weight", 5.0);
}
void PathFollowCritic::score(CriticData & data)
{
if (!enabled_ || data.path.x.shape(0) < 2 ||
utils::withinPositionGoalTolerance(threshold_to_consider_, data.state.pose.pose, data.goal))
{
return;
}
utils::setPathFurthestPointIfNotSet(data);
utils::setPathCostsIfNotSet(data, costmap_ros_);
const size_t path_size = data.path.x.shape(0) - 1;
auto offseted_idx = std::min(
*data.furthest_reached_path_point + offset_from_furthest_, path_size);
// Drive to the first valid path point, in case of dynamic obstacles on path
// we want to drive past it, not through it
bool valid = false;
while (!valid && offseted_idx < path_size - 1) {
valid = (*data.path_pts_valid)[offseted_idx];
if (!valid) {
offseted_idx++;
}
}
const auto path_x = data.path.x(offseted_idx);
const auto path_y = data.path.y(offseted_idx);
const auto last_x = xt::view(data.trajectories.x, xt::all(), -1);
const auto last_y = xt::view(data.trajectories.y, xt::all(), -1);
auto dists = xt::sqrt(
xt::pow(last_x - path_x, 2) +
xt::pow(last_y - path_y, 2));
data.costs += xt::pow(weight_ * std::move(dists), power_);
}
} // namespace mppi::critics
#include <pluginlib/class_list_macros.hpp>
PLUGINLIB_EXPORT_CLASS(
mppi::critics::PathFollowCritic,
mppi::critics::CriticFunction)
@@ -0,0 +1,55 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include "nav2_mppi_controller/critics/prefer_forward_critic.hpp"
namespace mppi::critics
{
void PreferForwardCritic::initialize()
{
auto getParam = parameters_handler_->getParamGetter(name_);
getParam(power_, "cost_power", 1);
getParam(weight_, "cost_weight", 5.0);
getParam(
threshold_to_consider_,
"threshold_to_consider", 0.5);
RCLCPP_INFO(
logger_, "PreferForwardCritic instantiated with %d power and %f weight.", power_, weight_);
}
void PreferForwardCritic::score(CriticData & data)
{
using xt::evaluation_strategy::immediate;
if (!enabled_ || utils::withinPositionGoalTolerance(
threshold_to_consider_, data.state.pose.pose, data.goal))
{
return;
}
auto backward_motion = xt::maximum(-data.state.vx, 0);
data.costs += xt::pow(
xt::sum(
std::move(
backward_motion) * data.model_dt, {1}, immediate) * weight_, power_);
}
} // namespace mppi::critics
#include <pluginlib/class_list_macros.hpp>
PLUGINLIB_EXPORT_CLASS(
mppi::critics::PreferForwardCritic,
mppi::critics::CriticFunction)
@@ -0,0 +1,50 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include "nav2_mppi_controller/critics/twirling_critic.hpp"
namespace mppi::critics
{
void TwirlingCritic::initialize()
{
auto getParam = parameters_handler_->getParamGetter(name_);
getParam(power_, "cost_power", 1);
getParam(weight_, "cost_weight", 10.0);
RCLCPP_INFO(
logger_, "TwirlingCritic instantiated with %d power and %f weight.", power_, weight_);
}
void TwirlingCritic::score(CriticData & data)
{
using xt::evaluation_strategy::immediate;
if (!enabled_ ||
utils::withinPositionGoalTolerance(data.goal_checker, data.state.pose.pose, data.goal))
{
return;
}
const auto wz = xt::abs(data.state.wz);
data.costs += xt::pow(xt::mean(wz, {1}, immediate) * weight_, power_);
}
} // namespace mppi::critics
#include <pluginlib/class_list_macros.hpp>
PLUGINLIB_EXPORT_CLASS(
mppi::critics::TwirlingCritic,
mppi::critics::CriticFunction)
@@ -0,0 +1,104 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include "nav2_mppi_controller/critics/velocity_deadband_critic.hpp"
namespace mppi::critics
{
void VelocityDeadbandCritic::initialize()
{
auto getParam = parameters_handler_->getParamGetter(name_);
getParam(power_, "cost_power", 1);
getParam(weight_, "cost_weight", 35.0);
// Recast double to float
std::vector<double> deadband_velocities{0.0, 0.0, 0.0};
getParam(deadband_velocities, "deadband_velocities", std::vector<double>{0.0, 0.0, 0.0});
std::transform(
deadband_velocities.begin(), deadband_velocities.end(), deadband_velocities_.begin(),
[](double d) {return static_cast<float>(d);});
RCLCPP_INFO_STREAM(
logger_, "VelocityDeadbandCritic instantiated with "
<< power_ << " power, " << weight_ << " weight, deadband_velocity ["
<< deadband_velocities_.at(0) << "," << deadband_velocities_.at(1) << ","
<< deadband_velocities_.at(2) << "]");
}
void VelocityDeadbandCritic::score(CriticData & data)
{
using xt::evaluation_strategy::immediate;
if (!enabled_) {
return;
}
auto & vx = data.state.vx;
auto & wz = data.state.wz;
if (data.motion_model->isHolonomic()) {
auto & vy = data.state.vy;
if (power_ > 1u) {
data.costs += xt::pow(
xt::sum(
std::move(
xt::maximum(fabs(deadband_velocities_.at(0)) - xt::fabs(vx), 0) +
xt::maximum(fabs(deadband_velocities_.at(1)) - xt::fabs(vy), 0) +
xt::maximum(fabs(deadband_velocities_.at(2)) - xt::fabs(wz), 0)) *
data.model_dt,
{1}, immediate) *
weight_,
power_);
} else {
data.costs += xt::sum(
(std::move(
xt::maximum(fabs(deadband_velocities_.at(0)) - xt::fabs(vx), 0) +
xt::maximum(fabs(deadband_velocities_.at(1)) - xt::fabs(vy), 0) +
xt::maximum(fabs(deadband_velocities_.at(2)) - xt::fabs(wz), 0))) *
data.model_dt,
{1}, immediate) *
weight_;
}
return;
}
if (power_ > 1u) {
data.costs += xt::pow(
xt::sum(
std::move(
xt::maximum(fabs(deadband_velocities_.at(0)) - xt::fabs(vx), 0) +
xt::maximum(fabs(deadband_velocities_.at(2)) - xt::fabs(wz), 0)) *
data.model_dt,
{1}, immediate) *
weight_,
power_);
} else {
data.costs += xt::sum(
(std::move(
xt::maximum(fabs(deadband_velocities_.at(0)) - xt::fabs(vx), 0) +
xt::maximum(fabs(deadband_velocities_.at(2)) - xt::fabs(wz), 0))) *
data.model_dt,
{1}, immediate) *
weight_;
}
return;
}
} // namespace mppi::critics
#include <pluginlib/class_list_macros.hpp>
PLUGINLIB_EXPORT_CLASS(mppi::critics::VelocityDeadbandCritic, mppi::critics::CriticFunction)
@@ -0,0 +1,124 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include "nav2_mppi_controller/tools/noise_generator.hpp"
#include <memory>
#include <mutex>
#include <xtensor/xmath.hpp>
#include <xtensor/xrandom.hpp>
#include <xtensor/xnoalias.hpp>
namespace mppi
{
void NoiseGenerator::initialize(
mppi::models::OptimizerSettings & settings, bool is_holonomic,
const std::string & name, ParametersHandler * param_handler)
{
settings_ = settings;
is_holonomic_ = is_holonomic;
active_ = true;
auto getParam = param_handler->getParamGetter(name);
getParam(regenerate_noises_, "regenerate_noises", false);
if (regenerate_noises_) {
noise_thread_ = std::thread(std::bind(&NoiseGenerator::noiseThread, this));
} else {
generateNoisedControls();
}
}
void NoiseGenerator::shutdown()
{
active_ = false;
ready_ = true;
noise_cond_.notify_all();
if (noise_thread_.joinable()) {
noise_thread_.join();
}
}
void NoiseGenerator::generateNextNoises()
{
// Trigger the thread to run in parallel to this iteration
// to generate the next iteration's noises (if applicable).
{
std::unique_lock<std::mutex> guard(noise_lock_);
ready_ = true;
}
noise_cond_.notify_all();
}
void NoiseGenerator::setNoisedControls(
models::State & state,
const models::ControlSequence & control_sequence)
{
std::unique_lock<std::mutex> guard(noise_lock_);
xt::noalias(state.cvx) = control_sequence.vx + noises_vx_;
xt::noalias(state.cvy) = control_sequence.vy + noises_vy_;
xt::noalias(state.cwz) = control_sequence.wz + noises_wz_;
}
void NoiseGenerator::reset(mppi::models::OptimizerSettings & settings, bool is_holonomic)
{
settings_ = settings;
is_holonomic_ = is_holonomic;
// Recompute the noises on reset, initialization, and fallback
{
std::unique_lock<std::mutex> guard(noise_lock_);
xt::noalias(noises_vx_) = xt::zeros<float>({settings_.batch_size, settings_.time_steps});
xt::noalias(noises_vy_) = xt::zeros<float>({settings_.batch_size, settings_.time_steps});
xt::noalias(noises_wz_) = xt::zeros<float>({settings_.batch_size, settings_.time_steps});
ready_ = true;
}
if (regenerate_noises_) {
noise_cond_.notify_all();
} else {
generateNoisedControls();
}
}
void NoiseGenerator::noiseThread()
{
do {
std::unique_lock<std::mutex> guard(noise_lock_);
noise_cond_.wait(guard, [this]() {return ready_;});
ready_ = false;
generateNoisedControls();
} while (active_);
}
void NoiseGenerator::generateNoisedControls()
{
auto & s = settings_;
xt::noalias(noises_vx_) = xt::random::randn<float>(
{s.batch_size, s.time_steps}, 0.0f,
s.sampling_std.vx);
xt::noalias(noises_wz_) = xt::random::randn<float>(
{s.batch_size, s.time_steps}, 0.0f,
s.sampling_std.wz);
if (is_holonomic_) {
xt::noalias(noises_vy_) = xt::random::randn<float>(
{s.batch_size, s.time_steps}, 0.0f,
s.sampling_std.vy);
}
}
} // namespace mppi
@@ -0,0 +1,460 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include "nav2_mppi_controller/optimizer.hpp"
#include <limits>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
#include <cmath>
#include <xtensor/xmath.hpp>
#include <xtensor/xrandom.hpp>
#include <xtensor/xnoalias.hpp>
#include "nav2_costmap_2d/costmap_filters/filter_values.hpp"
namespace mppi
{
using namespace xt::placeholders; // NOLINT
using xt::evaluation_strategy::immediate;
void Optimizer::initialize(
rclcpp_lifecycle::LifecycleNode::WeakPtr parent, const std::string & name,
std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros,
ParametersHandler * param_handler)
{
parent_ = parent;
name_ = name;
costmap_ros_ = costmap_ros;
costmap_ = costmap_ros_->getCostmap();
parameters_handler_ = param_handler;
auto node = parent_.lock();
logger_ = node->get_logger();
getParams();
critic_manager_.on_configure(parent_, name_, costmap_ros_, parameters_handler_);
noise_generator_.initialize(settings_, isHolonomic(), name_, parameters_handler_);
reset();
}
void Optimizer::shutdown()
{
noise_generator_.shutdown();
}
void Optimizer::getParams()
{
std::string motion_model_name;
auto & s = settings_;
auto getParam = parameters_handler_->getParamGetter(name_);
auto getParentParam = parameters_handler_->getParamGetter("");
getParam(s.model_dt, "model_dt", 0.05f);
getParam(s.time_steps, "time_steps", 56);
getParam(s.batch_size, "batch_size", 1000);
getParam(s.iteration_count, "iteration_count", 1);
getParam(s.temperature, "temperature", 0.3f);
getParam(s.gamma, "gamma", 0.015f);
getParam(s.base_constraints.vx_max, "vx_max", 0.5);
getParam(s.base_constraints.vx_min, "vx_min", -0.35);
getParam(s.base_constraints.vy, "vy_max", 0.5);
getParam(s.base_constraints.wz, "wz_max", 1.9);
getParam(s.sampling_std.vx, "vx_std", 0.2);
getParam(s.sampling_std.vy, "vy_std", 0.2);
getParam(s.sampling_std.wz, "wz_std", 0.4);
getParam(s.retry_attempt_limit, "retry_attempt_limit", 1);
getParam(motion_model_name, "motion_model", std::string("DiffDrive"));
s.constraints = s.base_constraints;
setMotionModel(motion_model_name);
parameters_handler_->addPostCallback([this]() {reset();});
double controller_frequency;
getParentParam(controller_frequency, "controller_frequency", 0.0, ParameterType::Static);
setOffset(controller_frequency);
}
void Optimizer::setOffset(double controller_frequency)
{
const double controller_period = 1.0 / controller_frequency;
constexpr double eps = 1e-6;
if ((controller_period + eps) < settings_.model_dt) {
RCLCPP_WARN(
logger_,
"Controller period is less then model dt, consider setting it equal");
} else if (abs(controller_period - settings_.model_dt) < eps) {
RCLCPP_INFO(
logger_,
"Controller period is equal to model dt. Control sequence "
"shifting is ON");
settings_.shift_control_sequence = true;
} else {
throw std::runtime_error(
"Controller period more then model dt, set it equal to model dt");
}
}
void Optimizer::reset()
{
state_.reset(settings_.batch_size, settings_.time_steps);
control_sequence_.reset(settings_.time_steps);
control_history_[0] = {0.0, 0.0, 0.0};
control_history_[1] = {0.0, 0.0, 0.0};
control_history_[2] = {0.0, 0.0, 0.0};
control_history_[3] = {0.0, 0.0, 0.0};
settings_.constraints = settings_.base_constraints;
costs_ = xt::zeros<float>({settings_.batch_size});
generated_trajectories_.reset(settings_.batch_size, settings_.time_steps);
noise_generator_.reset(settings_, isHolonomic());
RCLCPP_INFO(logger_, "Optimizer reset");
}
geometry_msgs::msg::TwistStamped Optimizer::evalControl(
const geometry_msgs::msg::PoseStamped & robot_pose,
const geometry_msgs::msg::Twist & robot_speed,
const nav_msgs::msg::Path & plan,
const geometry_msgs::msg::Pose & goal,
nav2_core::GoalChecker * goal_checker)
{
prepare(robot_pose, robot_speed, plan, goal, goal_checker);
do {
optimize();
} while (fallback(critics_data_.fail_flag));
utils::savitskyGolayFilter(control_sequence_, control_history_, settings_);
auto control = getControlFromSequenceAsTwist(plan.header.stamp);
if (settings_.shift_control_sequence) {
shiftControlSequence();
}
return control;
}
void Optimizer::optimize()
{
for (size_t i = 0; i < settings_.iteration_count; ++i) {
generateNoisedTrajectories();
critic_manager_.evalTrajectoriesScores(critics_data_);
updateControlSequence();
}
}
bool Optimizer::fallback(bool fail)
{
static size_t counter = 0;
if (!fail) {
counter = 0;
return false;
}
reset();
if (++counter > settings_.retry_attempt_limit) {
counter = 0;
throw std::runtime_error("Optimizer fail to compute path");
}
return true;
}
void Optimizer::prepare(
const geometry_msgs::msg::PoseStamped & robot_pose,
const geometry_msgs::msg::Twist & robot_speed,
const nav_msgs::msg::Path & plan,
const geometry_msgs::msg::Pose & goal,
nav2_core::GoalChecker * goal_checker)
{
state_.pose = robot_pose;
state_.speed = robot_speed;
path_ = utils::toTensor(plan);
goal_ = goal;
costs_.fill(0);
critics_data_.fail_flag = false;
critics_data_.goal_checker = goal_checker;
critics_data_.motion_model = motion_model_;
critics_data_.furthest_reached_path_point.reset();
critics_data_.path_pts_valid.reset();
}
void Optimizer::shiftControlSequence()
{
using namespace xt::placeholders; // NOLINT
control_sequence_.vx = xt::roll(control_sequence_.vx, -1);
control_sequence_.wz = xt::roll(control_sequence_.wz, -1);
xt::view(control_sequence_.vx, -1) =
xt::view(control_sequence_.vx, -2);
xt::view(control_sequence_.wz, -1) =
xt::view(control_sequence_.wz, -2);
if (isHolonomic()) {
control_sequence_.vy = xt::roll(control_sequence_.vy, -1);
xt::view(control_sequence_.vy, -1) =
xt::view(control_sequence_.vy, -2);
}
}
void Optimizer::generateNoisedTrajectories()
{
noise_generator_.setNoisedControls(state_, control_sequence_);
noise_generator_.generateNextNoises();
updateStateVelocities(state_);
integrateStateVelocities(generated_trajectories_, state_);
}
bool Optimizer::isHolonomic() const {return motion_model_->isHolonomic();}
void Optimizer::applyControlSequenceConstraints()
{
auto & s = settings_;
if (isHolonomic()) {
control_sequence_.vy = xt::clip(control_sequence_.vy, -s.constraints.vy, s.constraints.vy);
}
control_sequence_.vx = xt::clip(control_sequence_.vx, s.constraints.vx_min, s.constraints.vx_max);
control_sequence_.wz = xt::clip(control_sequence_.wz, -s.constraints.wz, s.constraints.wz);
motion_model_->applyConstraints(control_sequence_);
}
void Optimizer::updateStateVelocities(
models::State & state) const
{
updateInitialStateVelocities(state);
propagateStateVelocitiesFromInitials(state);
}
void Optimizer::updateInitialStateVelocities(
models::State & state) const
{
xt::noalias(xt::view(state.vx, xt::all(), 0)) = state.speed.linear.x;
xt::noalias(xt::view(state.wz, xt::all(), 0)) = state.speed.angular.z;
if (isHolonomic()) {
xt::noalias(xt::view(state.vy, xt::all(), 0)) = state.speed.linear.y;
}
}
void Optimizer::propagateStateVelocitiesFromInitials(
models::State & state) const
{
motion_model_->predict(state);
}
void Optimizer::integrateStateVelocities(
xt::xtensor<float, 2> & trajectory,
const xt::xtensor<float, 2> & sequence) const
{
float initial_yaw = tf2::getYaw(state_.pose.pose.orientation);
const auto vx = xt::view(sequence, xt::all(), 0);
const auto vy = xt::view(sequence, xt::all(), 2);
const auto wz = xt::view(sequence, xt::all(), 1);
auto traj_x = xt::view(trajectory, xt::all(), 0);
auto traj_y = xt::view(trajectory, xt::all(), 1);
auto traj_yaws = xt::view(trajectory, xt::all(), 2);
xt::noalias(traj_yaws) = xt::cumsum(wz * settings_.model_dt, 0) + initial_yaw;
auto && yaw_cos = xt::xtensor<float, 1>::from_shape(traj_yaws.shape());
auto && yaw_sin = xt::xtensor<float, 1>::from_shape(traj_yaws.shape());
const auto yaw_offseted = xt::view(traj_yaws, xt::range(1, _));
xt::noalias(xt::view(yaw_cos, 0)) = cosf(initial_yaw);
xt::noalias(xt::view(yaw_sin, 0)) = sinf(initial_yaw);
xt::noalias(xt::view(yaw_cos, xt::range(1, _))) = xt::cos(yaw_offseted);
xt::noalias(xt::view(yaw_sin, xt::range(1, _))) = xt::sin(yaw_offseted);
auto && dx = xt::eval(vx * yaw_cos);
auto && dy = xt::eval(vx * yaw_sin);
if (isHolonomic()) {
dx = dx - vy * yaw_sin;
dy = dy + vy * yaw_cos;
}
xt::noalias(traj_x) = state_.pose.pose.position.x + xt::cumsum(dx * settings_.model_dt, 0);
xt::noalias(traj_y) = state_.pose.pose.position.y + xt::cumsum(dy * settings_.model_dt, 0);
}
void Optimizer::integrateStateVelocities(
models::Trajectories & trajectories,
const models::State & state) const
{
const float initial_yaw = tf2::getYaw(state.pose.pose.orientation);
xt::noalias(trajectories.yaws) =
xt::cumsum(state.wz * settings_.model_dt, 1) + initial_yaw;
const auto yaws_cutted = xt::view(trajectories.yaws, xt::all(), xt::range(0, -1));
auto && yaw_cos = xt::xtensor<float, 2>::from_shape(trajectories.yaws.shape());
auto && yaw_sin = xt::xtensor<float, 2>::from_shape(trajectories.yaws.shape());
xt::noalias(xt::view(yaw_cos, xt::all(), 0)) = cosf(initial_yaw);
xt::noalias(xt::view(yaw_sin, xt::all(), 0)) = sinf(initial_yaw);
xt::noalias(xt::view(yaw_cos, xt::all(), xt::range(1, _))) = xt::cos(yaws_cutted);
xt::noalias(xt::view(yaw_sin, xt::all(), xt::range(1, _))) = xt::sin(yaws_cutted);
auto && dx = xt::eval(state.vx * yaw_cos);
auto && dy = xt::eval(state.vx * yaw_sin);
if (isHolonomic()) {
dx = dx - state.vy * yaw_sin;
dy = dy + state.vy * yaw_cos;
}
xt::noalias(trajectories.x) = state.pose.pose.position.x +
xt::cumsum(dx * settings_.model_dt, 1);
xt::noalias(trajectories.y) = state.pose.pose.position.y +
xt::cumsum(dy * settings_.model_dt, 1);
}
xt::xtensor<float, 2> Optimizer::getOptimizedTrajectory()
{
auto && sequence =
xt::xtensor<float, 2>::from_shape({settings_.time_steps, isHolonomic() ? 3u : 2u});
auto && trajectories = xt::xtensor<float, 2>::from_shape({settings_.time_steps, 3});
xt::noalias(xt::view(sequence, xt::all(), 0)) = control_sequence_.vx;
xt::noalias(xt::view(sequence, xt::all(), 1)) = control_sequence_.wz;
if (isHolonomic()) {
xt::noalias(xt::view(sequence, xt::all(), 2)) = control_sequence_.vy;
}
integrateStateVelocities(trajectories, sequence);
return std::move(trajectories);
}
void Optimizer::updateControlSequence()
{
auto & s = settings_;
auto bounded_noises_vx = state_.cvx - control_sequence_.vx;
auto bounded_noises_wz = state_.cwz - control_sequence_.wz;
xt::noalias(costs_) +=
s.gamma / powf(s.sampling_std.vx, 2) * xt::sum(
xt::view(control_sequence_.vx, xt::newaxis(), xt::all()) * bounded_noises_vx, 1, immediate);
xt::noalias(costs_) +=
s.gamma / powf(s.sampling_std.wz, 2) * xt::sum(
xt::view(control_sequence_.wz, xt::newaxis(), xt::all()) * bounded_noises_wz, 1, immediate);
if (isHolonomic()) {
auto bounded_noises_vy = state_.cvy - control_sequence_.vy;
xt::noalias(costs_) +=
s.gamma / powf(s.sampling_std.vy, 2) * xt::sum(
xt::view(control_sequence_.vy, xt::newaxis(), xt::all()) * bounded_noises_vy,
1, immediate);
}
auto && costs_normalized = costs_ - xt::amin(costs_, immediate);
auto && exponents = xt::eval(xt::exp(-1 / settings_.temperature * costs_normalized));
auto && softmaxes = xt::eval(exponents / xt::sum(exponents, immediate));
auto && softmaxes_extened = xt::eval(xt::view(softmaxes, xt::all(), xt::newaxis()));
xt::noalias(control_sequence_.vx) = xt::sum(state_.cvx * softmaxes_extened, 0, immediate);
xt::noalias(control_sequence_.wz) = xt::sum(state_.cwz * softmaxes_extened, 0, immediate);
if (isHolonomic()) {
xt::noalias(control_sequence_.vy) = xt::sum(state_.cvy * softmaxes_extened, 0, immediate);
}
applyControlSequenceConstraints();
}
geometry_msgs::msg::TwistStamped Optimizer::getControlFromSequenceAsTwist(
const builtin_interfaces::msg::Time & stamp)
{
unsigned int offset = settings_.shift_control_sequence ? 1 : 0;
auto vx = control_sequence_.vx(offset);
auto wz = control_sequence_.wz(offset);
if (isHolonomic()) {
auto vy = control_sequence_.vy(offset);
return utils::toTwistStamped(vx, vy, wz, stamp, costmap_ros_->getBaseFrameID());
}
return utils::toTwistStamped(vx, wz, stamp, costmap_ros_->getBaseFrameID());
}
void Optimizer::setMotionModel(const std::string & model)
{
if (model == "DiffDrive") {
motion_model_ = std::make_shared<DiffDriveMotionModel>();
} else if (model == "Omni") {
motion_model_ = std::make_shared<OmniMotionModel>();
} else if (model == "Ackermann") {
motion_model_ = std::make_shared<AckermannMotionModel>(parameters_handler_, name_);
} else {
throw std::runtime_error(
std::string(
"Model " + model + " is not valid! Valid options are DiffDrive, Omni, "
"or Ackermann"));
}
}
void Optimizer::setSpeedLimit(double speed_limit, bool percentage)
{
auto & s = settings_;
if (speed_limit == nav2_costmap_2d::NO_SPEED_LIMIT) {
s.constraints.vx_max = s.base_constraints.vx_max;
s.constraints.vx_min = s.base_constraints.vx_min;
s.constraints.vy = s.base_constraints.vy;
s.constraints.wz = s.base_constraints.wz;
} else {
if (percentage) {
// Speed limit is expressed in % from maximum speed of robot
double ratio = speed_limit / 100.0;
s.constraints.vx_max = s.base_constraints.vx_max * ratio;
s.constraints.vx_min = s.base_constraints.vx_min * ratio;
s.constraints.vy = s.base_constraints.vy * ratio;
s.constraints.wz = s.base_constraints.wz * ratio;
} else {
// Speed limit is expressed in absolute value
double ratio = speed_limit / s.base_constraints.vx_max;
s.constraints.vx_max = s.base_constraints.vx_max * ratio;
s.constraints.vx_min = s.base_constraints.vx_min * ratio;
s.constraints.vy = s.base_constraints.vy * ratio;
s.constraints.wz = s.base_constraints.wz * ratio;
}
}
}
models::Trajectories & Optimizer::getGeneratedTrajectories()
{
return generated_trajectories_;
}
} // namespace mppi
@@ -0,0 +1,72 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include "nav2_mppi_controller/tools/parameters_handler.hpp"
namespace mppi
{
ParametersHandler::ParametersHandler(
const rclcpp_lifecycle::LifecycleNode::WeakPtr & parent)
{
node_ = parent;
auto node = node_.lock();
node_name_ = node->get_name();
logger_ = node->get_logger();
}
void ParametersHandler::start()
{
auto node = node_.lock();
on_set_param_handler_ = node->add_on_set_parameters_callback(
std::bind(
&ParametersHandler::dynamicParamsCallback, this,
std::placeholders::_1));
auto get_param = getParamGetter(node_name_);
get_param(verbose_, "verbose", false);
}
rcl_interfaces::msg::SetParametersResult
ParametersHandler::dynamicParamsCallback(
std::vector<rclcpp::Parameter> parameters)
{
rcl_interfaces::msg::SetParametersResult result;
std::lock_guard<std::mutex> lock(parameters_change_mutex_);
for (auto & pre_cb : pre_callbacks_) {
pre_cb();
}
for (auto & param : parameters) {
const std::string & param_name = param.get_name();
if (auto callback = get_param_callbacks_.find(param_name);
callback != get_param_callbacks_.end())
{
callback->second(param);
} else {
RCLCPP_WARN(logger_, "Parameter %s not found", param_name.c_str());
}
}
for (auto & post_cb : post_callbacks_) {
post_cb();
}
result.successful = true;
return result;
}
} // namespace mppi
@@ -0,0 +1,220 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
// Copyright (c) 2023 Dexory
// Copyright (c) 2023 Open Navigation LLC
//
// 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.
#include "nav2_mppi_controller/tools/path_handler.hpp"
#include "nav2_mppi_controller/tools/utils.hpp"
#include "nav2_costmap_2d/costmap_2d_ros.hpp"
namespace mppi
{
void PathHandler::initialize(
rclcpp_lifecycle::LifecycleNode::WeakPtr parent, const std::string & name,
std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap,
std::shared_ptr<tf2_ros::Buffer> buffer, ParametersHandler * param_handler)
{
name_ = name;
costmap_ = costmap;
tf_buffer_ = buffer;
auto node = parent.lock();
logger_ = node->get_logger();
parameters_handler_ = param_handler;
auto getParam = parameters_handler_->getParamGetter(name_);
getParam(max_robot_pose_search_dist_, "max_robot_pose_search_dist", getMaxCostmapDist());
getParam(prune_distance_, "prune_distance", 1.5);
getParam(transform_tolerance_, "transform_tolerance", 0.1);
getParam(enforce_path_inversion_, "enforce_path_inversion", false);
if (enforce_path_inversion_) {
getParam(inversion_xy_tolerance_, "inversion_xy_tolerance", 0.2);
getParam(inversion_yaw_tolerance, "inversion_yaw_tolerance", 0.4);
inversion_locale_ = 0u;
}
}
std::pair<nav_msgs::msg::Path, PathIterator>
PathHandler::getGlobalPlanConsideringBoundsInCostmapFrame(
const geometry_msgs::msg::PoseStamped & global_pose)
{
using nav2_util::geometry_utils::euclidean_distance;
auto begin = global_plan_up_to_inversion_.poses.begin();
// Limit the search for the closest pose up to max_robot_pose_search_dist on the path
auto closest_pose_upper_bound =
nav2_util::geometry_utils::first_after_integrated_distance(
global_plan_up_to_inversion_.poses.begin(), global_plan_up_to_inversion_.poses.end(),
max_robot_pose_search_dist_);
// Find closest point to the robot
auto closest_point = nav2_util::geometry_utils::min_by(
begin, closest_pose_upper_bound,
[&global_pose](const geometry_msgs::msg::PoseStamped & ps) {
return euclidean_distance(global_pose, ps);
});
nav_msgs::msg::Path transformed_plan;
transformed_plan.header.frame_id = costmap_->getGlobalFrameID();
transformed_plan.header.stamp = global_pose.header.stamp;
auto pruned_plan_end =
nav2_util::geometry_utils::first_after_integrated_distance(
closest_point, global_plan_up_to_inversion_.poses.end(), prune_distance_);
unsigned int mx, my;
// Find the furthest relevent pose on the path to consider within costmap
// bounds
// Transforming it to the costmap frame in the same loop
for (auto global_plan_pose = closest_point; global_plan_pose != pruned_plan_end;
++global_plan_pose)
{
// Transform from global plan frame to costmap frame
geometry_msgs::msg::PoseStamped costmap_plan_pose;
global_plan_pose->header.stamp = global_pose.header.stamp;
global_plan_pose->header.frame_id = global_plan_.header.frame_id;
transformPose(costmap_->getGlobalFrameID(), *global_plan_pose, costmap_plan_pose);
// Check if pose is inside the costmap
if (!costmap_->getCostmap()->worldToMap(
costmap_plan_pose.pose.position.x, costmap_plan_pose.pose.position.y, mx, my))
{
return {transformed_plan, closest_point};
}
// Filling the transformed plan to return with the transformed pose
transformed_plan.poses.push_back(costmap_plan_pose);
}
return {transformed_plan, closest_point};
}
geometry_msgs::msg::PoseStamped PathHandler::transformToGlobalPlanFrame(
const geometry_msgs::msg::PoseStamped & pose)
{
if (global_plan_up_to_inversion_.poses.empty()) {
throw std::runtime_error("Received plan with zero length");
}
geometry_msgs::msg::PoseStamped robot_pose;
if (!transformPose(global_plan_up_to_inversion_.header.frame_id, pose, robot_pose)) {
throw std::runtime_error(
"Unable to transform robot pose into global plan's frame");
}
return robot_pose;
}
nav_msgs::msg::Path PathHandler::transformPath(
const geometry_msgs::msg::PoseStamped & robot_pose)
{
// Find relevent bounds of path to use
geometry_msgs::msg::PoseStamped global_pose =
transformToGlobalPlanFrame(robot_pose);
auto [transformed_plan, lower_bound] = getGlobalPlanConsideringBoundsInCostmapFrame(global_pose);
prunePlan(global_plan_up_to_inversion_, lower_bound);
if (enforce_path_inversion_ && inversion_locale_ != 0u) {
if (isWithinInversionTolerances(global_pose)) {
prunePlan(global_plan_, global_plan_.poses.begin() + inversion_locale_);
global_plan_up_to_inversion_ = global_plan_;
inversion_locale_ = utils::removePosesAfterFirstInversion(global_plan_up_to_inversion_);
}
}
if (transformed_plan.poses.empty()) {
throw std::runtime_error("Resulting plan has 0 poses in it.");
}
return transformed_plan;
}
bool PathHandler::transformPose(
const std::string & frame, const geometry_msgs::msg::PoseStamped & in_pose,
geometry_msgs::msg::PoseStamped & out_pose) const
{
if (in_pose.header.frame_id == frame) {
out_pose = in_pose;
return true;
}
try {
tf_buffer_->transform(
in_pose, out_pose, frame,
tf2::durationFromSec(transform_tolerance_));
out_pose.header.frame_id = frame;
return true;
} catch (tf2::TransformException & ex) {
RCLCPP_ERROR(logger_, "Exception in transformPose: %s", ex.what());
}
return false;
}
double PathHandler::getMaxCostmapDist()
{
const auto & costmap = costmap_->getCostmap();
return static_cast<double>(std::max(costmap->getSizeInCellsX(), costmap->getSizeInCellsY())) *
costmap->getResolution() * 0.50;
}
void PathHandler::setPath(const nav_msgs::msg::Path & plan)
{
global_plan_ = plan;
global_plan_up_to_inversion_ = global_plan_;
if (enforce_path_inversion_) {
inversion_locale_ = utils::removePosesAfterFirstInversion(global_plan_up_to_inversion_);
}
}
nav_msgs::msg::Path & PathHandler::getPath() {return global_plan_;}
void PathHandler::prunePlan(nav_msgs::msg::Path & plan, const PathIterator end)
{
plan.poses.erase(plan.poses.begin(), end);
}
geometry_msgs::msg::PoseStamped PathHandler::getTransformedGoal(
const builtin_interfaces::msg::Time & stamp)
{
auto goal = global_plan_.poses.back();
goal.header.frame_id = global_plan_.header.frame_id;
goal.header.stamp = stamp;
if (goal.header.frame_id.empty()) {
throw std::runtime_error("Goal pose has an empty frame_id");
}
geometry_msgs::msg::PoseStamped transformed_goal;
if (!transformPose(costmap_->getGlobalFrameID(), goal, transformed_goal)) {
throw std::runtime_error("Unable to transform goal pose into costmap frame");
}
return transformed_goal;
}
bool PathHandler::isWithinInversionTolerances(const geometry_msgs::msg::PoseStamped & robot_pose)
{
// Keep full path if we are within tolerance of the inversion pose
const auto last_pose = global_plan_up_to_inversion_.poses.back();
float distance = hypotf(
robot_pose.pose.position.x - last_pose.pose.position.x,
robot_pose.pose.position.y - last_pose.pose.position.y);
float angle_distance = angles::shortest_angular_distance(
tf2::getYaw(robot_pose.pose.orientation),
tf2::getYaw(last_pose.pose.orientation));
return distance <= inversion_xy_tolerance_ && fabs(angle_distance) <= inversion_yaw_tolerance;
}
} // namespace mppi
@@ -0,0 +1,130 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include <memory>
#include "nav2_mppi_controller/tools/trajectory_visualizer.hpp"
namespace mppi
{
void TrajectoryVisualizer::on_configure(
rclcpp_lifecycle::LifecycleNode::WeakPtr parent, const std::string & name,
const std::string & frame_id, ParametersHandler * parameters_handler)
{
auto node = parent.lock();
logger_ = node->get_logger();
frame_id_ = frame_id;
trajectories_publisher_ =
node->create_publisher<visualization_msgs::msg::MarkerArray>("/trajectories", 1);
transformed_path_pub_ = node->create_publisher<nav_msgs::msg::Path>("transformed_global_plan", 1);
parameters_handler_ = parameters_handler;
auto getParam = parameters_handler->getParamGetter(name + ".TrajectoryVisualizer");
getParam(trajectory_step_, "trajectory_step", 5);
getParam(time_step_, "time_step", 3);
reset();
}
void TrajectoryVisualizer::on_cleanup()
{
trajectories_publisher_.reset();
transformed_path_pub_.reset();
}
void TrajectoryVisualizer::on_activate()
{
trajectories_publisher_->on_activate();
transformed_path_pub_->on_activate();
}
void TrajectoryVisualizer::on_deactivate()
{
trajectories_publisher_->on_deactivate();
transformed_path_pub_->on_deactivate();
}
void TrajectoryVisualizer::add(
const xt::xtensor<float, 2> & trajectory, const std::string & marker_namespace)
{
auto & size = trajectory.shape()[0];
if (!size) {
return;
}
auto add_marker = [&](auto i) {
float component = static_cast<float>(i) / static_cast<float>(size);
auto pose = utils::createPose(trajectory(i, 0), trajectory(i, 1), 0.06);
auto scale =
i != size - 1 ?
utils::createScale(0.03, 0.03, 0.07) :
utils::createScale(0.07, 0.07, 0.09);
auto color = utils::createColor(0, component, component, 1);
auto marker = utils::createMarker(
marker_id_++, pose, scale, color, frame_id_, marker_namespace);
points_->markers.push_back(marker);
};
for (size_t i = 0; i < size; i++) {
add_marker(i);
}
}
void TrajectoryVisualizer::add(
const models::Trajectories & trajectories, const std::string & marker_namespace)
{
auto & shape = trajectories.x.shape();
const float shape_1 = static_cast<float>(shape[1]);
points_->markers.reserve(floor(shape[0] / trajectory_step_) * floor(shape[1] * time_step_));
for (size_t i = 0; i < shape[0]; i += trajectory_step_) {
for (size_t j = 0; j < shape[1]; j += time_step_) {
const float j_flt = static_cast<float>(j);
float blue_component = 1.0f - j_flt / shape_1;
float green_component = j_flt / shape_1;
auto pose = utils::createPose(trajectories.x(i, j), trajectories.y(i, j), 0.03);
auto scale = utils::createScale(0.03, 0.03, 0.03);
auto color = utils::createColor(0, green_component, blue_component, 1);
auto marker = utils::createMarker(
marker_id_++, pose, scale, color, frame_id_, marker_namespace);
points_->markers.push_back(marker);
}
}
}
void TrajectoryVisualizer::reset()
{
marker_id_ = 0;
points_ = std::make_unique<visualization_msgs::msg::MarkerArray>();
}
void TrajectoryVisualizer::visualize(const nav_msgs::msg::Path & plan)
{
if (trajectories_publisher_->get_subscription_count() > 0) {
trajectories_publisher_->publish(std::move(points_));
}
reset();
if (transformed_path_pub_->get_subscription_count() > 0) {
auto plan_ptr = std::make_unique<nav_msgs::msg::Path>(plan);
transformed_path_pub_->publish(std::move(plan_ptr));
}
}
} // namespace mppi
@@ -0,0 +1,40 @@
set(TEST_NAMES
optimizer_smoke_test
controller_state_transition_test
models_test
noise_generator_test
parameter_handler_test
motion_model_tests
trajectory_visualizer_tests
utils_test
path_handler_test
critic_manager_test
optimizer_unit_tests
)
foreach(name IN LISTS TEST_NAMES)
ament_add_gtest(${name}
${name}.cpp
)
ament_target_dependencies(${name}
${dependencies_pkgs}
)
target_link_libraries(${name}
mppi_controller
)
if(${TEST_DEBUG_INFO})
target_compile_definitions(${name} PUBLIC -DTEST_DEBUG_INFO)
endif()
endforeach()
# This is a special case requiring linking against the critics library
ament_add_gtest(critics_tests critics_tests.cpp)
ament_target_dependencies(critics_tests ${dependencies_pkgs})
target_link_libraries(critics_tests mppi_controller mppi_critics)
if(${TEST_DEBUG_INFO})
target_compile_definitions(critics_tests PUBLIC -DTEST_DEBUG_INFO)
endif()
@@ -0,0 +1,75 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include "gtest/gtest.h"
#include <geometry_msgs/msg/pose_stamped.hpp>
#include <geometry_msgs/msg/twist.hpp>
#include <nav_msgs/msg/path.hpp>
#include <nav2_costmap_2d/costmap_2d.hpp>
#include <nav2_costmap_2d/costmap_2d_ros.hpp>
#include "nav2_mppi_controller/controller.hpp"
#include "utils/utils.hpp"
class RosLockGuard
{
public:
RosLockGuard() {rclcpp::init(0, nullptr);}
~RosLockGuard() {rclcpp::shutdown();}
};
RosLockGuard g_rclcpp;
// Tests basic transition from configure->active->process->deactive->cleanup
TEST(ControllerStateTransitionTest, ControllerNotFail)
{
const bool visualize = true;
TestCostmapSettings costmap_settings{};
// Node Options
rclcpp::NodeOptions options;
std::vector<rclcpp::Parameter> params;
setUpControllerParams(visualize, params);
options.parameter_overrides(params);
auto node = getDummyNode(options);
auto tf_buffer = std::make_shared<tf2_ros::Buffer>(node->get_clock());
auto costmap_ros = getDummyCostmapRos(costmap_settings);
costmap_ros->setRobotFootprint(getDummySquareFootprint(0.01));
auto controller = getDummyController(node, tf_buffer, costmap_ros);
TestPose start_pose = costmap_settings.getCenterPose();
const double path_step = costmap_settings.resolution;
TestPathSettings path_settings{start_pose, 8, path_step, path_step};
// evalControl args
auto pose = getDummyPointStamped(node, start_pose);
auto velocity = getDummyTwist();
auto path = getIncrementalDummyPath(node, path_settings);
path.header.frame_id = costmap_ros->getGlobalFrameID();
pose.header.frame_id = costmap_ros->getGlobalFrameID();
controller->setPlan(path);
EXPECT_NO_THROW(controller->computeVelocityCommands(pose, velocity, {}));
controller->setSpeedLimit(0.5, true);
controller->setSpeedLimit(0.5, false);
controller->setSpeedLimit(1.0, true);
controller->deactivate();
controller->cleanup();
}
@@ -0,0 +1,139 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include <chrono>
#include <thread>
#include "gtest/gtest.h"
#include "rclcpp/rclcpp.hpp"
#include "nav2_mppi_controller/critic_manager.hpp"
// Tests critic manager
class RosLockGuard
{
public:
RosLockGuard() {rclcpp::init(0, nullptr);}
~RosLockGuard() {rclcpp::shutdown();}
};
RosLockGuard g_rclcpp;
using namespace mppi; // NOLINT
using namespace mppi::critics; // NOLINT
class DummyCritic : public CriticFunction
{
public:
virtual void initialize() {initialized_ = true;}
virtual void score(CriticData & /*data*/) {scored_ = true;}
bool initialized_{false}, scored_{false};
};
class CriticManagerWrapper : public CriticManager
{
public:
CriticManagerWrapper()
: CriticManager() {}
virtual void loadCritics()
{
critics_.clear();
auto instance = std::unique_ptr<critics::CriticFunction>(new DummyCritic);
critics_.push_back(std::move(instance));
critics_.back()->on_configure(
parent_, name_, name_ + "." + "DummyCritic", costmap_ros_,
parameters_handler_);
}
std::string getFullNameWrapper(const std::string & name)
{
return getFullName(name);
}
bool getDummyCriticInitialized()
{
return dynamic_cast<DummyCritic *>(critics_[0].get())->initialized_;
}
bool getDummyCriticScored()
{
return dynamic_cast<DummyCritic *>(critics_[0].get())->scored_;
}
};
class CriticManagerWrapperEnum : public CriticManager
{
public:
CriticManagerWrapperEnum()
: CriticManager() {}
unsigned int getCriticNum()
{
return critics_.size();
}
};
TEST(CriticManagerTests, BasicCriticOperations)
{
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
// Configuration should get parameters and initialize critic functions
CriticManagerWrapper critic_manager;
critic_manager.on_configure(node, "critic_manager", costmap_ros, &param_handler);
EXPECT_TRUE(critic_manager.getDummyCriticInitialized());
// Evaluation of critics should score them, but only if failure flag is not set
models::State state;
models::ControlSequence control_sequence;
models::Trajectories generated_trajectories;
models::Path path;
geometry_msgs::msg::Pose goal;
xt::xtensor<float, 1> costs;
float model_dt = 0.1;
CriticData data =
{state, generated_trajectories, path, goal, costs, model_dt, false, nullptr, nullptr,
std::nullopt, std::nullopt};
data.fail_flag = true;
EXPECT_FALSE(critic_manager.getDummyCriticScored());
data.fail_flag = false;
critic_manager.evalTrajectoriesScores(data);
EXPECT_TRUE(critic_manager.getDummyCriticScored());
// This should get the full namespaced name of the critics
EXPECT_EQ(critic_manager.getFullNameWrapper("name"), std::string("mppi::critics::name"));
}
TEST(CriticManagerTests, CriticLoadingTest)
{
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
node->declare_parameter(
"critic_manager.critics",
rclcpp::ParameterValue(std::vector<std::string>{"ConstraintCritic", "PreferForwardCritic"}));
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State state;
costmap_ros->on_configure(state);
// This should grab the critics parameter and load the 2 requested plugins
CriticManagerWrapperEnum critic_manager;
critic_manager.on_configure(node, "critic_manager", costmap_ros, &param_handler);
EXPECT_EQ(critic_manager.getCriticNum(), 2u);
}
@@ -0,0 +1,798 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include <chrono>
#include <thread>
#include "gtest/gtest.h"
#include "rclcpp/rclcpp.hpp"
#include "nav2_mppi_controller/tools/utils.hpp"
#include "nav2_mppi_controller/motion_models.hpp"
#include "nav2_mppi_controller/critics/constraint_critic.hpp"
#include "nav2_mppi_controller/critics/goal_angle_critic.hpp"
#include "nav2_mppi_controller/critics/goal_critic.hpp"
#include "nav2_mppi_controller/critics/obstacles_critic.hpp"
#include "nav2_mppi_controller/critics/cost_critic.hpp"
#include "nav2_mppi_controller/critics/path_align_critic.hpp"
#include "nav2_mppi_controller/critics/path_align_legacy_critic.hpp"
#include "nav2_mppi_controller/critics/path_angle_critic.hpp"
#include "nav2_mppi_controller/critics/path_follow_critic.hpp"
#include "nav2_mppi_controller/critics/prefer_forward_critic.hpp"
#include "nav2_mppi_controller/critics/twirling_critic.hpp"
#include "nav2_mppi_controller/critics/velocity_deadband_critic.hpp"
#include "nav2_core/exceptions.hpp"
#include "utils_test.cpp" // NOLINT
// Tests the various critic plugin functions
// ROS lock used from utils_test.cpp
using namespace mppi; // NOLINT
using namespace mppi::critics; // NOLINT
using namespace mppi::utils; // NOLINT
using xt::evaluation_strategy::immediate;
TEST(CriticTests, ConstraintsCritic)
{
// Standard preamble
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
models::State state;
models::ControlSequence control_sequence;
models::Trajectories generated_trajectories;
models::Path path;
geometry_msgs::msg::Pose goal;
xt::xtensor<float, 1> costs = xt::zeros<float>({1000});
float model_dt = 0.1;
CriticData data =
{state, generated_trajectories, path, goal, costs, model_dt,
false, nullptr, nullptr, std::nullopt, std::nullopt};
data.motion_model = std::make_shared<DiffDriveMotionModel>();
// Initialization testing
// Make sure initializes correctly and that defaults are reasonable
ConstraintCritic critic;
critic.on_configure(node, "mppi", "critic", costmap_ros, &param_handler);
EXPECT_EQ(critic.getName(), "critic");
EXPECT_TRUE(critic.getMaxVelConstraint() > 0.0);
EXPECT_TRUE(critic.getMinVelConstraint() < 0.0);
// Scoring testing
// provide velocities in constraints, should not have any costs
state.vx = 0.40 * xt::ones<float>({1000, 30});
state.vy = xt::zeros<float>({1000, 30});
state.wz = xt::ones<float>({1000, 30});
critic.score(data);
EXPECT_NEAR(xt::sum(costs, immediate)(), 0, 1e-6);
// provide out of maximum velocity constraint
auto last_batch_traj_in_full = xt::view(state.vx, -1, xt::all());
last_batch_traj_in_full = 0.60 * xt::ones<float>({30});
critic.score(data);
EXPECT_GT(xt::sum(costs, immediate)(), 0);
// 4.0 weight * 0.1 model_dt * 0.1 error introduced * 30 timesteps = 1.2
EXPECT_NEAR(costs(999), 1.2, 0.01);
costs = xt::zeros<float>({1000});
// provide out of minimum velocity constraint
auto first_batch_traj_in_full = xt::view(state.vx, 1, xt::all());
first_batch_traj_in_full = -0.45 * xt::ones<float>({30});
critic.score(data);
EXPECT_GT(xt::sum(costs, immediate)(), 0);
// 4.0 weight * 0.1 model_dt * 0.1 error introduced * 30 timesteps = 1.2
EXPECT_NEAR(costs(1), 1.2, 0.01);
costs = xt::zeros<float>({1000});
// Now with ackermann, all in constraint so no costs to score
state.vx = 0.40 * xt::ones<float>({1000, 30});
state.wz = 1.5 * xt::ones<float>({1000, 30});
data.motion_model = std::make_shared<AckermannMotionModel>(&param_handler, node->get_name());
critic.score(data);
EXPECT_NEAR(xt::sum(costs, immediate)(), 0, 1e-6);
// Now violating the ackermann constraints
state.wz = 2.5 * xt::ones<float>({1000, 30});
critic.score(data);
EXPECT_GT(xt::sum(costs, immediate)(), 0);
// 4.0 weight * 0.1 model_dt * (0.2 - 0.4/2.5) * 30 timesteps = 0.48
EXPECT_NEAR(costs(1), 0.48, 0.01);
}
TEST(CriticTests, ObstacleCriticMisalignedParams) {
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
auto getParam = param_handler.getParamGetter("critic");
bool consider_footprint;
getParam(consider_footprint, "consider_footprint", true);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
ObstaclesCritic critic;
// Expect throw when settings mismatched
EXPECT_THROW(
critic.on_configure(node, "mppi", "critic", costmap_ros, &param_handler),
nav2_core::PlannerException
);
}
TEST(CriticTests, ObstacleCriticAlignedParams) {
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
auto getParam = param_handler.getParamGetter("critic");
bool consider_footprint;
getParam(consider_footprint, "consider_footprint", false);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
ObstaclesCritic critic;
critic.on_configure(node, "mppi", "critic", costmap_ros, &param_handler);
EXPECT_EQ(critic.getName(), "critic");
}
TEST(CriticTests, CostCriticMisAlignedParams) {
// Standard preamble
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
auto getParam = param_handler.getParamGetter("critic");
bool consider_footprint;
getParam(consider_footprint, "consider_footprint", true);
costmap_ros->on_configure(lstate);
CostCritic critic;
// Expect throw when settings mismatched
EXPECT_THROW(
critic.on_configure(node, "mppi", "critic", costmap_ros, &param_handler),
nav2_core::PlannerException
);
}
TEST(CriticTests, CostCriticAlignedParams) {
// Standard preamble
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
auto getParam = param_handler.getParamGetter("critic");
bool consider_footprint;
getParam(consider_footprint, "consider_footprint", false);
costmap_ros->on_configure(lstate);
CostCritic critic;
critic.on_configure(node, "mppi", "critic", costmap_ros, &param_handler);
EXPECT_EQ(critic.getName(), "critic");
}
TEST(CriticTests, GoalAngleCritic)
{
// Standard preamble
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
models::State state;
models::ControlSequence control_sequence;
models::Trajectories generated_trajectories;
generated_trajectories.reset(1000, 30);
models::Path path;
geometry_msgs::msg::Pose goal;
xt::xtensor<float, 1> costs = xt::zeros<float>({1000});
float model_dt = 0.1;
CriticData data =
{state, generated_trajectories, path, goal, costs, model_dt,
false, nullptr, nullptr, std::nullopt, std::nullopt};
data.motion_model = std::make_shared<DiffDriveMotionModel>();
// Initialization testing
// Make sure initializes correctly
GoalAngleCritic critic;
critic.on_configure(node, "mppi", "critic", costmap_ros, &param_handler);
EXPECT_EQ(critic.getName(), "critic");
// Scoring testing
// provide state poses and path too far from `threshold_to_consider` to consider
state.pose.pose.position.x = 1.0;
path.reset(10);
path.x(9) = 10.0;
path.y(9) = 0.0;
path.yaws(9) = 3.14;
goal.position.x = 10.0;
critic.score(data);
EXPECT_NEAR(xt::sum(costs, immediate)(), 0, 1e-6);
// Lets move it even closer, just to be sure it still doesn't trigger
state.pose.pose.position.x = 9.2;
critic.score(data);
EXPECT_NEAR(xt::sum(costs, immediate)(), 0, 1e-6);
// provide state pose and path below `threshold_to_consider` to consider
state.pose.pose.position.x = 9.7;
critic.score(data);
EXPECT_GT(xt::sum(costs, immediate)(), 0);
EXPECT_NEAR(costs(0), 9.42, 0.02); // (3.14 - 0.0) * 3.0 weight
}
TEST(CriticTests, GoalCritic)
{
// Standard preamble
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
models::State state;
models::ControlSequence control_sequence;
models::Trajectories generated_trajectories;
generated_trajectories.reset(1000, 30);
models::Path path;
geometry_msgs::msg::Pose goal;
xt::xtensor<float, 1> costs = xt::zeros<float>({1000});
float model_dt = 0.1;
CriticData data =
{state, generated_trajectories, path, goal, costs, model_dt,
false, nullptr, nullptr, std::nullopt, std::nullopt};
data.motion_model = std::make_shared<DiffDriveMotionModel>();
// Initialization testing
// Make sure initializes correctly
GoalCritic critic;
critic.on_configure(node, "mppi", "critic", costmap_ros, &param_handler);
EXPECT_EQ(critic.getName(), "critic");
// Scoring testing with all trajectories set to 0
// provide state poses and path far, should not trigger
state.pose.pose.position.x = 1.0;
path.reset(10);
path.x(9) = 10.0;
path.y(9) = 0.0;
goal.position.x = 10.0;
critic.score(data);
EXPECT_NEAR(costs(2), 0.0, 1e-6); // (0 * 5.0 weight
EXPECT_NEAR(xt::sum(costs, immediate)(), 0.0, 1e-6); // Should all be 0 * 1000
costs = xt::zeros<float>({1000});
// provide state pose and path close
path.x(9) = 0.5;
path.y(9) = 0.0;
goal.position.x = 0.5;
critic.score(data);
EXPECT_NEAR(costs(2), 2.5, 1e-6); // (sqrt(10.0 * 10.0) * 5.0 weight
EXPECT_NEAR(xt::sum(costs, immediate)(), 2500.0, 1e-6); // should be 2.5 * 1000
}
TEST(CriticTests, PathAngleCritic)
{
// Standard preamble
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
models::State state;
state.reset(1000, 30);
models::ControlSequence control_sequence;
models::Trajectories generated_trajectories;
generated_trajectories.reset(1000, 30);
models::Path path;
geometry_msgs::msg::Pose goal;
xt::xtensor<float, 1> costs = xt::zeros<float>({1000});
float model_dt = 0.1;
CriticData data =
{state, generated_trajectories, path, goal, costs, model_dt,
false, nullptr, nullptr, std::nullopt, std::nullopt};
data.motion_model = std::make_shared<DiffDriveMotionModel>();
TestGoalChecker goal_checker; // from utils_tests tolerance of 0.25 positionally
// Initialization testing
// Make sure initializes correctly
PathAngleCritic critic;
critic.on_configure(node, "mppi", "critic", costmap_ros, &param_handler);
EXPECT_EQ(critic.getName(), "critic");
// Scoring testing
// provide state poses and path close, within pose tolerance so won't do anything
state.pose.pose.position.x = 0.0;
state.pose.pose.position.y = 0.0;
path.reset(10);
path.x(9) = 0.15;
goal.position.x = 0.15;
critic.score(data);
EXPECT_NEAR(xt::sum(costs, immediate)(), 0.0, 1e-6);
// provide state pose and path close but outside of tol. with less than PI/2 angular diff.
path.x(9) = 0.95;
goal.position.x = 0.95;
data.furthest_reached_path_point = 2; // So it grabs the 2 + offset_from_furthest_ = 6th point
path.x(6) = 1.0; // angle between path point and pose = 0 < max_angle_to_furthest_
path.y(6) = 0.0;
critic.score(data);
EXPECT_NEAR(xt::sum(costs, immediate)(), 0.0, 1e-6);
// provide state pose and path close but outside of tol. with more than PI/2 angular diff.
path.x(6) = -1.0; // angle between path point and pose > max_angle_to_furthest_
path.y(6) = 4.0;
critic.score(data);
EXPECT_GT(xt::sum(costs, immediate)(), 0.0);
EXPECT_NEAR(costs(0), 3.6315, 1e-2); // atan2(4,-1) [1.81] * 2.0 weight
}
TEST(CriticTests, PreferForwardCritic)
{
// Standard preamble
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
models::State state;
state.reset(1000, 30);
models::ControlSequence control_sequence;
models::Trajectories generated_trajectories;
generated_trajectories.reset(1000, 30);
models::Path path;
geometry_msgs::msg::Pose goal;
xt::xtensor<float, 1> costs = xt::zeros<float>({1000});
float model_dt = 0.1;
CriticData data =
{state, generated_trajectories, path, goal, costs, model_dt,
false, nullptr, nullptr, std::nullopt, std::nullopt};
data.motion_model = std::make_shared<DiffDriveMotionModel>();
TestGoalChecker goal_checker; // from utils_tests tolerance of 0.25 positionally
// Initialization testing
// Make sure initializes correctly
PreferForwardCritic critic;
critic.on_configure(node, "mppi", "critic", costmap_ros, &param_handler);
EXPECT_EQ(critic.getName(), "critic");
// Scoring testing
// provide state poses and path far away, not within positional tolerances
state.pose.pose.position.x = 1.0;
path.reset(10);
path.x(9) = 10.0;
goal.position.x = 10.0;
critic.score(data);
EXPECT_NEAR(xt::sum(costs, immediate)(), 0.0f, 1e-6f);
// provide state pose and path close to trigger behavior but with all forward motion
path.x(9) = 0.15;
goal.position.x = 0.15;
state.vx = xt::ones<float>({1000, 30});
critic.score(data);
EXPECT_NEAR(xt::sum(costs, immediate)(), 0.0f, 1e-6f);
// provide state pose and path close to trigger behavior but with all reverse motion
state.vx = -1.0 * xt::ones<float>({1000, 30});
critic.score(data);
EXPECT_GT(xt::sum(costs, immediate)(), 0.0f);
EXPECT_NEAR(costs(0), 15.0f, 1e-3f); // 1.0 * 0.1 model_dt * 5.0 weight * 30 length
}
TEST(CriticTests, TwirlingCritic)
{
// Standard preamble
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
models::State state;
state.reset(1000, 30);
models::ControlSequence control_sequence;
models::Trajectories generated_trajectories;
generated_trajectories.reset(1000, 30);
models::Path path;
geometry_msgs::msg::Pose goal;
xt::xtensor<float, 1> costs = xt::zeros<float>({1000});
float model_dt = 0.1;
CriticData data =
{state, generated_trajectories, path, goal, costs, model_dt,
false, nullptr, nullptr, std::nullopt, std::nullopt};
data.motion_model = std::make_shared<DiffDriveMotionModel>();
TestGoalChecker goal_checker; // from utils_tests tolerance of 0.25 positionally
data.goal_checker = &goal_checker;
// Initialization testing
// Make sure initializes correctly
TwirlingCritic critic;
critic.on_configure(node, "mppi", "critic", costmap_ros, &param_handler);
EXPECT_EQ(critic.getName(), "critic");
// Scoring testing
// provide state poses and path far away, not within positional tolerances
state.pose.pose.position.x = 1.0;
path.reset(10);
path.x(9) = 10.0;
goal.position.x = 10.0;
critic.score(data);
EXPECT_NEAR(xt::sum(costs, immediate)(), 0.0, 1e-6);
// provide state pose and path close to trigger behavior but with no angular variation
path.x(9) = 0.15;
goal.position.x = 0.15;
state.wz = xt::zeros<float>({1000, 30});
critic.score(data);
EXPECT_NEAR(xt::sum(costs, immediate)(), 0.0, 1e-6);
// Provide nearby with some motion
auto traj_view = xt::view(state.wz, 0, xt::all());
traj_view = 10.0;
critic.score(data);
EXPECT_NEAR(costs(0), 100.0, 1e-6); // (mean(10.0) * 10.0 weight
costs = xt::zeros<float>({1000});
// Now try again with some wiggling noise
traj_view = xt::random::randn<float>({30}, 0.0, 0.5);
critic.score(data);
EXPECT_NEAR(costs(0), 3.3, 4e-1); // (mean of noise with mu=0, sigma=0.5 * 10.0 weight
}
TEST(CriticTests, PathFollowCritic)
{
// Standard preamble
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
models::State state;
state.reset(1000, 30);
models::ControlSequence control_sequence;
models::Trajectories generated_trajectories;
generated_trajectories.reset(1000, 30);
models::Path path;
geometry_msgs::msg::Pose goal;
xt::xtensor<float, 1> costs = xt::zeros<float>({1000});
float model_dt = 0.1;
CriticData data =
{state, generated_trajectories, path, goal, costs, model_dt,
false, nullptr, nullptr, std::nullopt, std::nullopt};
data.motion_model = std::make_shared<DiffDriveMotionModel>();
TestGoalChecker goal_checker; // from utils_tests tolerance of 0.25 positionally
data.goal_checker = &goal_checker;
// Initialization testing
// Make sure initializes correctly
PathFollowCritic critic;
critic.on_configure(node, "mppi", "critic", costmap_ros, &param_handler);
EXPECT_EQ(critic.getName(), "critic");
// Scoring testing
// provide state poses and goal close within positional tolerances
state.pose.pose.position.x = 2.0;
path.reset(6);
path.x(5) = 1.8;
goal.position.x = 1.8;
critic.score(data);
EXPECT_NEAR(xt::sum(costs, immediate)(), 0.0, 1e-6);
// provide state pose and path far enough to enable
// pose differential is (0, 0) and (0.15, 0)
path.x(5) = 0.15;
goal.position.x = 0.15;
critic.score(data);
EXPECT_NEAR(xt::sum(costs, immediate)(), 750.0, 1e-2); // 0.15 * 5 weight * 1000
}
TEST(CriticTests, PathAlignCritic)
{
// Standard preamble
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
models::State state;
state.reset(1000, 30);
models::ControlSequence control_sequence;
models::Trajectories generated_trajectories;
generated_trajectories.reset(1000, 30);
models::Path path;
geometry_msgs::msg::Pose goal;
xt::xtensor<float, 1> costs = xt::zeros<float>({1000});
float model_dt = 0.1;
CriticData data =
{state, generated_trajectories, path, goal, costs, model_dt,
false, nullptr, nullptr, std::nullopt, std::nullopt};
data.motion_model = std::make_shared<DiffDriveMotionModel>();
TestGoalChecker goal_checker; // from utils_tests tolerance of 0.25 positionally
data.goal_checker = &goal_checker;
// Initialization testing
// Make sure initializes correctly
PathAlignCritic critic;
critic.on_configure(node, "mppi", "critic", costmap_ros, &param_handler);
EXPECT_EQ(critic.getName(), "critic");
// Scoring testing
// provide state poses and path close within positional tolerances
state.pose.pose.position.x = 1.0;
path.reset(10);
path.x(9) = 0.85;
goal.position.x = 0.85;
critic.score(data);
EXPECT_NEAR(xt::sum(costs, immediate)(), 0.0, 1e-6);
// provide state pose and path far enough to enable
// but data furthest point reached is 0 and offset default is 20, so returns
path.x(9) = 0.15;
goal.position.x = 0.15;
critic.score(data);
EXPECT_NEAR(xt::sum(costs, immediate)(), 0.0, 1e-6);
// provide state pose and path far enough to enable, with data to pass condition
// but with empty trajectories and paths, should still be zero
*data.furthest_reached_path_point = 21;
path.x(9) = 0.15;
goal.position.x = 0.15;
critic.score(data);
EXPECT_NEAR(xt::sum(costs, immediate)(), 0.0, 1e-6);
// provide state pose and path far enough to enable, with data to pass condition
// and with a valid path to pass invalid path condition
state.pose.pose.position.x = 0.0;
data.path_pts_valid.reset(); // Recompute on new path
path.reset(22);
path.x(0) = 0;
path.x(1) = 0.1;
path.x(2) = 0.2;
path.x(3) = 0.3;
path.x(4) = 0.4;
path.x(5) = 0.5;
path.x(6) = 0.6;
path.x(7) = 0.7;
path.x(8) = 0.8;
path.x(9) = 0.9;
path.x(10) = 0.9;
path.x(11) = 0.9;
path.x(12) = 0.9;
path.x(13) = 0.9;
path.x(14) = 0.9;
path.x(15) = 0.9;
path.x(16) = 0.9;
path.x(17) = 0.9;
path.x(18) = 0.9;
path.x(19) = 0.9;
path.x(20) = 0.9;
path.x(21) = 0.9;
goal.position.x = 0.9;
generated_trajectories.x = 0.66 * xt::ones<float>({1000, 30});
critic.score(data);
// 0.66 * 1000 * 10 weight * 6 num pts eval / 6 normalization term
EXPECT_NEAR(xt::sum(costs, immediate)(), 6600.0, 1e-2);
// provide state pose and path far enough to enable, with data to pass condition
// but path is blocked in collision
auto * costmap = costmap_ros->getCostmap();
// island in the middle of lethal cost to cross. Costmap defaults to size 5x5 @ 10cm resolution
for (unsigned int i = 11; i <= 30; ++i) { // 1.1m-3m
for (unsigned int j = 11; j <= 30; ++j) { // 1.1m-3m
costmap->setCost(i, j, 254);
}
}
data.path_pts_valid.reset(); // Recompute on new path
costs = xt::zeros<float>({1000});
path.x = 1.5 * xt::ones<float>({22});
path.y = 1.5 * xt::ones<float>({22});
goal.position.x = 1.5;
critic.score(data);
EXPECT_NEAR(xt::sum(costs, immediate)(), 0.0, 1e-6);
}
TEST(CriticTests, PathAlignLegacyCritic)
{
// Standard preamble
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
models::State state;
state.reset(1000, 30);
models::ControlSequence control_sequence;
models::Trajectories generated_trajectories;
generated_trajectories.reset(1000, 30);
models::Path path;
geometry_msgs::msg::Pose goal;
xt::xtensor<float, 1> costs = xt::zeros<float>({1000});
float model_dt = 0.1;
CriticData data =
{state, generated_trajectories, path, goal, costs, model_dt,
false, nullptr, nullptr, std::nullopt, std::nullopt};
data.motion_model = std::make_shared<DiffDriveMotionModel>();
TestGoalChecker goal_checker; // from utils_tests tolerance of 0.25 positionally
data.goal_checker = &goal_checker;
// Initialization testing
// Make sure initializes correctly
PathAlignLegacyCritic critic;
critic.on_configure(node, "mppi", "critic", costmap_ros, &param_handler);
EXPECT_EQ(critic.getName(), "critic");
// Scoring testing
// provide state poses and path close within positional tolerances
state.pose.pose.position.x = 1.0;
path.reset(10);
path.x(9) = 0.85;
goal.position.x = 0.85;
critic.score(data);
EXPECT_NEAR(xt::sum(costs, immediate)(), 0.0, 1e-6);
// provide state pose and path far enough to enable
// but data furthest point reached is 0 and offset default is 20, so returns
path.x(9) = 0.15;
goal.position.x = 0.15;
critic.score(data);
EXPECT_NEAR(xt::sum(costs, immediate)(), 0.0, 1e-6);
// provide state pose and path far enough to enable, with data to pass condition
// but with empty trajectories and paths, should still be zero
*data.furthest_reached_path_point = 21;
path.x(9) = 0.15;
goal.position.x = 0.15;
critic.score(data);
EXPECT_NEAR(xt::sum(costs, immediate)(), 0.0, 1e-6);
// provide state pose and path far enough to enable, with data to pass condition
// and with a valid path to pass invalid path condition
state.pose.pose.position.x = 0.0;
data.path_pts_valid.reset(); // Recompute on new path
path.reset(22);
path.x(0) = 0;
path.x(1) = 0.1;
path.x(2) = 0.2;
path.x(3) = 0.3;
path.x(4) = 0.4;
path.x(5) = 0.5;
path.x(6) = 0.6;
path.x(7) = 0.7;
path.x(8) = 0.8;
path.x(9) = 0.9;
path.x(10) = 0.9;
path.x(11) = 0.9;
path.x(12) = 0.9;
path.x(13) = 0.9;
path.x(14) = 0.9;
path.x(15) = 0.9;
path.x(16) = 0.9;
path.x(17) = 0.9;
path.x(18) = 0.9;
path.x(19) = 0.9;
path.x(20) = 0.9;
path.x(21) = 0.9;
goal.position.x = 0.9;
generated_trajectories.x = 0.66 * xt::ones<float>({1000, 30});
critic.score(data);
// 0.04 * 1000 * 10 weight * 6 num pts eval / 6 normalization term
EXPECT_NEAR(xt::sum(costs, immediate)(), 400.0, 1e-2);
// provide state pose and path far enough to enable, with data to pass condition
// but path is blocked in collision
auto * costmap = costmap_ros->getCostmap();
// island in the middle of lethal cost to cross. Costmap defaults to size 5x5 @ 10cm resolution
for (unsigned int i = 11; i <= 30; ++i) { // 1.1m-3m
for (unsigned int j = 11; j <= 30; ++j) { // 1.1m-3m
costmap->setCost(i, j, 254);
}
}
data.path_pts_valid.reset(); // Recompute on new path
costs = xt::zeros<float>({1000});
path.x = 1.5 * xt::ones<float>({22});
path.y = 1.5 * xt::ones<float>({22});
goal.position.x = 1.5;
critic.score(data);
EXPECT_NEAR(xt::sum(costs, immediate)(), 0.0, 1e-6);
}
TEST(CriticTests, VelocityDeadbandCritic)
{
// Standard preamble
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
auto getParam = param_handler.getParamGetter("critic");
std::vector<double> deadband_velocities_;
getParam(deadband_velocities_, "deadband_velocities", std::vector<double>{0.08, 0.08, 0.08});
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
models::State state;
models::ControlSequence control_sequence;
models::Trajectories generated_trajectories;
models::Path path;
geometry_msgs::msg::Pose goal;
xt::xtensor<float, 1> costs = xt::zeros<float>({1000});
float model_dt = 0.1;
CriticData data =
{state, generated_trajectories, path, goal, costs, model_dt,
false, nullptr, nullptr, std::nullopt, std::nullopt};
data.motion_model = std::make_shared<OmniMotionModel>();
// Initialization testing
// Make sure initializes correctly and that defaults are reasonable
VelocityDeadbandCritic critic;
critic.on_configure(node, "mppi", "critic", costmap_ros, &param_handler);
EXPECT_EQ(critic.getName(), "critic");
// Scoring testing
// provide velocities out of deadband bounds, should not have any costs
state.vx = 0.80 * xt::ones<float>({1000, 30});
state.vy = 0.60 * xt::ones<float>({1000, 30});
state.wz = 0.80 * xt::ones<float>({1000, 30});
critic.score(data);
EXPECT_NEAR(xt::sum(costs, immediate)(), 0, 1e-6);
// Test cost value
state.vx = 0.01 * xt::ones<float>({1000, 30});
state.vy = 0.02 * xt::ones<float>({1000, 30});
state.wz = 0.021 * xt::ones<float>({1000, 30});
critic.score(data);
// 35.0 weight * 0.1 model_dt * (0.07 + 0.06 + 0.059) * 30 timesteps = 56.7
EXPECT_NEAR(costs(1), 19.845, 0.01);
}
@@ -0,0 +1,149 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include "gtest/gtest.h"
#include "rclcpp/rclcpp.hpp"
#include "nav2_mppi_controller/models/control_sequence.hpp"
#include "nav2_mppi_controller/models/path.hpp"
#include "nav2_mppi_controller/models/state.hpp"
#include "nav2_mppi_controller/models/trajectories.hpp"
// Tests model classes with methods
class RosLockGuard
{
public:
RosLockGuard() {rclcpp::init(0, nullptr);}
~RosLockGuard() {rclcpp::shutdown();}
};
RosLockGuard g_rclcpp;
using namespace mppi::models; // NOLINT
TEST(ModelsTest, ControlSequenceTest)
{
// populate the object
ControlSequence sequence;
sequence.vx = xt::ones<float>({10});
sequence.vy = xt::ones<float>({10});
sequence.wz = xt::ones<float>({10});
// Show you can get contents
EXPECT_EQ(sequence.vx(4), 1);
EXPECT_EQ(sequence.vy(4), 1);
EXPECT_EQ(sequence.wz(4), 1);
sequence.reset(20);
// Show contents are gone and new size
EXPECT_EQ(sequence.vx(4), 0);
EXPECT_EQ(sequence.vy(4), 0);
EXPECT_EQ(sequence.wz(4), 0);
EXPECT_EQ(sequence.vx.shape(0), 20u);
EXPECT_EQ(sequence.vy.shape(0), 20u);
EXPECT_EQ(sequence.wz.shape(0), 20u);
}
TEST(ModelsTest, PathTest)
{
// populate the object
Path path;
path.x = xt::ones<float>({10});
path.y = xt::ones<float>({10});
path.yaws = xt::ones<float>({10});
// Show you can get contents
EXPECT_EQ(path.x(4), 1);
EXPECT_EQ(path.y(4), 1);
EXPECT_EQ(path.yaws(4), 1);
path.reset(20);
// Show contents are gone and new size
EXPECT_EQ(path.x(4), 0);
EXPECT_EQ(path.y(4), 0);
EXPECT_EQ(path.yaws(4), 0);
EXPECT_EQ(path.x.shape(0), 20u);
EXPECT_EQ(path.y.shape(0), 20u);
EXPECT_EQ(path.yaws.shape(0), 20u);
}
TEST(ModelsTest, StateTest)
{
// populate the object
State state;
state.vx = xt::ones<float>({10, 10});
state.vy = xt::ones<float>({10, 10});
state.wz = xt::ones<float>({10, 10});
state.cvx = xt::ones<float>({10, 10});
state.cvy = xt::ones<float>({10, 10});
state.cwz = xt::ones<float>({10, 10});
// Show you can get contents
EXPECT_EQ(state.cvx(4), 1);
EXPECT_EQ(state.cvy(4), 1);
EXPECT_EQ(state.cwz(4), 1);
EXPECT_EQ(state.vx(4), 1);
EXPECT_EQ(state.vy(4), 1);
EXPECT_EQ(state.wz(4), 1);
state.reset(20, 40);
// Show contents are gone and new size
EXPECT_EQ(state.cvx(4), 0);
EXPECT_EQ(state.cvy(4), 0);
EXPECT_EQ(state.cwz(4), 0);
EXPECT_EQ(state.vx(4), 0);
EXPECT_EQ(state.vy(4), 0);
EXPECT_EQ(state.wz(4), 0);
EXPECT_EQ(state.cvx.shape(0), 20u);
EXPECT_EQ(state.cvy.shape(0), 20u);
EXPECT_EQ(state.cwz.shape(0), 20u);
EXPECT_EQ(state.cvx.shape(1), 40u);
EXPECT_EQ(state.cvy.shape(1), 40u);
EXPECT_EQ(state.cwz.shape(1), 40u);
EXPECT_EQ(state.vx.shape(0), 20u);
EXPECT_EQ(state.vy.shape(0), 20u);
EXPECT_EQ(state.wz.shape(0), 20u);
EXPECT_EQ(state.vx.shape(1), 40u);
EXPECT_EQ(state.vy.shape(1), 40u);
EXPECT_EQ(state.wz.shape(1), 40u);
}
TEST(ModelsTest, TrajectoriesTest)
{
// populate the object
Trajectories trajectories;
trajectories.x = xt::ones<float>({10, 10});
trajectories.y = xt::ones<float>({10, 10});
trajectories.yaws = xt::ones<float>({10, 10});
// Show you can get contents
EXPECT_EQ(trajectories.x(4), 1);
EXPECT_EQ(trajectories.y(4), 1);
EXPECT_EQ(trajectories.yaws(4), 1);
trajectories.reset(20, 40);
// Show contents are gone and new size
EXPECT_EQ(trajectories.x(4), 0);
EXPECT_EQ(trajectories.y(4), 0);
EXPECT_EQ(trajectories.yaws(4), 0);
EXPECT_EQ(trajectories.x.shape(0), 20u);
EXPECT_EQ(trajectories.y.shape(0), 20u);
EXPECT_EQ(trajectories.yaws.shape(0), 20u);
EXPECT_EQ(trajectories.x.shape(1), 40u);
EXPECT_EQ(trajectories.y.shape(1), 40u);
EXPECT_EQ(trajectories.yaws.shape(1), 40u);
}
@@ -0,0 +1,257 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include <chrono>
#include <thread>
#include "gtest/gtest.h"
#include "rclcpp/rclcpp.hpp"
#include "nav2_mppi_controller/motion_models.hpp"
#include "nav2_mppi_controller/models/state.hpp"
#include "nav2_mppi_controller/models/control_sequence.hpp"
// Tests motion models
class RosLockGuard
{
public:
RosLockGuard() {rclcpp::init(0, nullptr);}
~RosLockGuard() {rclcpp::shutdown();}
};
RosLockGuard g_rclcpp;
using namespace mppi; // NOLINT
TEST(MotionModelTests, DiffDriveTest)
{
models::ControlSequence control_sequence;
models::State state;
int batches = 1000;
int timesteps = 50;
control_sequence.reset(timesteps); // populates with zeros
state.reset(batches, timesteps); // populates with zeros
std::unique_ptr<DiffDriveMotionModel> model =
std::make_unique<DiffDriveMotionModel>();
// Check that predict properly populates the trajectory velocities with the control velocities
state.cvx = 10 * xt::ones<float>({batches, timesteps});
state.cvy = 5 * xt::ones<float>({batches, timesteps});
state.cwz = 1 * xt::ones<float>({batches, timesteps});
// Manually set state index 0 from initial conditions which would be the speed of the robot
xt::view(state.vx, xt::all(), 0) = 10;
xt::view(state.wz, xt::all(), 0) = 1;
model->predict(state);
EXPECT_EQ(state.vx, state.cvx);
EXPECT_EQ(state.vy, xt::zeros<float>({batches, timesteps})); // non-holonomic
EXPECT_EQ(state.wz, state.cwz);
// Check that application of constraints are empty for Diff Drive
for (unsigned int i = 0; i != control_sequence.vx.shape(0); i++) {
control_sequence.vx(i) = i * i * i;
control_sequence.wz(i) = i * i * i;
}
models::ControlSequence initial_control_sequence = control_sequence;
model->applyConstraints(control_sequence);
EXPECT_EQ(initial_control_sequence.vx, control_sequence.vx);
EXPECT_EQ(initial_control_sequence.vy, control_sequence.vy);
EXPECT_EQ(initial_control_sequence.wz, control_sequence.wz);
// Check that Diff Drive is properly non-holonomic
EXPECT_EQ(model->isHolonomic(), false);
// Check it cleanly destructs
model.reset();
}
TEST(MotionModelTests, OmniTest)
{
models::ControlSequence control_sequence;
models::State state;
int batches = 1000;
int timesteps = 50;
control_sequence.reset(timesteps); // populates with zeros
state.reset(batches, timesteps); // populates with zeros
std::unique_ptr<OmniMotionModel> model =
std::make_unique<OmniMotionModel>();
// Check that predict properly populates the trajectory velocities with the control velocities
state.cvx = 10 * xt::ones<float>({batches, timesteps});
state.cvy = 5 * xt::ones<float>({batches, timesteps});
state.cwz = 1 * xt::ones<float>({batches, timesteps});
// Manually set state index 0 from initial conditions which would be the speed of the robot
xt::view(state.vx, xt::all(), 0) = 10;
xt::view(state.vy, xt::all(), 0) = 5;
xt::view(state.wz, xt::all(), 0) = 1;
model->predict(state);
EXPECT_EQ(state.vx, state.cvx);
EXPECT_EQ(state.vy, state.cvy); // holonomic
EXPECT_EQ(state.wz, state.cwz);
// Check that application of constraints are empty for Omni Drive
for (unsigned int i = 0; i != control_sequence.vx.shape(0); i++) {
control_sequence.vx(i) = i * i * i;
control_sequence.vy(i) = i * i * i;
control_sequence.wz(i) = i * i * i;
}
models::ControlSequence initial_control_sequence = control_sequence;
model->applyConstraints(control_sequence);
EXPECT_EQ(initial_control_sequence.vx, control_sequence.vx);
EXPECT_EQ(initial_control_sequence.vy, control_sequence.vy);
EXPECT_EQ(initial_control_sequence.wz, control_sequence.wz);
// Check that Omni Drive is properly holonomic
EXPECT_EQ(model->isHolonomic(), true);
// Check it cleanly destructs
model.reset();
}
TEST(MotionModelTests, AckermannTest)
{
models::ControlSequence control_sequence;
models::State state;
int batches = 1000;
int timesteps = 50;
control_sequence.reset(timesteps); // populates with zeros
state.reset(batches, timesteps); // populates with zeros
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
ParametersHandler param_handler(node);
std::unique_ptr<AckermannMotionModel> model =
std::make_unique<AckermannMotionModel>(&param_handler, node->get_name());
// Check that predict properly populates the trajectory velocities with the control velocities
state.cvx = 10 * xt::ones<float>({batches, timesteps});
state.cvy = 5 * xt::ones<float>({batches, timesteps});
state.cwz = 1 * xt::ones<float>({batches, timesteps});
// Manually set state index 0 from initial conditions which would be the speed of the robot
xt::view(state.vx, xt::all(), 0) = 10;
xt::view(state.wz, xt::all(), 0) = 1;
model->predict(state);
EXPECT_EQ(state.vx, state.cvx);
EXPECT_EQ(state.vy, xt::zeros<float>({batches, timesteps})); // non-holonomic
EXPECT_EQ(state.wz, state.cwz);
// Check that application of constraints are non-empty for Ackermann Drive
for (unsigned int i = 0; i != control_sequence.vx.shape(0); i++) {
control_sequence.vx(i) = i * i * i;
control_sequence.wz(i) = i * i * i * i;
}
models::ControlSequence initial_control_sequence = control_sequence;
model->applyConstraints(control_sequence);
// VX equal since this doesn't change, the WZ is reduced if breaking the constraint
EXPECT_EQ(initial_control_sequence.vx, control_sequence.vx);
EXPECT_NE(initial_control_sequence.wz, control_sequence.wz);
for (unsigned int i = 1; i != control_sequence.wz.shape(0); i++) {
EXPECT_GT(control_sequence.wz(i), 0.0);
}
// Now, check the specifics of the minimum curvature constraint
EXPECT_NEAR(model->getMinTurningRadius(), 0.2, 1e-6);
for (unsigned int i = 1; i != control_sequence.vx.shape(0); i++) {
EXPECT_TRUE(fabs(control_sequence.vx(i)) / fabs(control_sequence.wz(i)) >= 0.2);
}
// Check that Ackermann Drive is properly non-holonomic and parameterized
EXPECT_EQ(model->isHolonomic(), false);
// Check it cleanly destructs
model.reset();
}
TEST(MotionModelTests, AckermannReversingTest)
{
models::ControlSequence control_sequence;
models::ControlSequence control_sequence2;
models::State state;
int batches = 1000;
int timesteps = 50;
control_sequence.reset(timesteps); // populates with zeros
control_sequence2.reset(timesteps); // populates with zeros
state.reset(batches, timesteps); // populates with zeros
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
ParametersHandler param_handler(node);
std::unique_ptr<AckermannMotionModel> model =
std::make_unique<AckermannMotionModel>(&param_handler, node->get_name());
// Check that predict properly populates the trajectory velocities with the control velocities
state.cvx = 10 * xt::ones<float>({batches, timesteps});
state.cvy = 5 * xt::ones<float>({batches, timesteps});
state.cwz = 1 * xt::ones<float>({batches, timesteps});
// Manually set state index 0 from initial conditions which would be the speed of the robot
xt::view(state.vx, xt::all(), 0) = 10;
xt::view(state.wz, xt::all(), 0) = 1;
model->predict(state);
EXPECT_EQ(state.vx, state.cvx);
EXPECT_EQ(state.vy, xt::zeros<float>({batches, timesteps})); // non-holonomic
EXPECT_EQ(state.wz, state.cwz);
// Check that application of constraints are non-empty for Ackermann Drive
for (unsigned int i = 0; i != control_sequence.vx.shape(0); i++) {
float idx = static_cast<float>(i);
control_sequence.vx(i) = -idx * idx * idx; // now reversing
control_sequence.wz(i) = idx * idx * idx * idx;
}
models::ControlSequence initial_control_sequence = control_sequence;
model->applyConstraints(control_sequence);
// VX equal since this doesn't change, the WZ is reduced if breaking the constraint
EXPECT_EQ(initial_control_sequence.vx, control_sequence.vx);
EXPECT_NE(initial_control_sequence.wz, control_sequence.wz);
for (unsigned int i = 1; i != control_sequence.wz.shape(0); i++) {
EXPECT_GT(control_sequence.wz(i), 0.0);
}
// Repeat with negative rotation direction
for (unsigned int i = 0; i != control_sequence2.vx.shape(0); i++) {
float idx = static_cast<float>(i);
control_sequence2.vx(i) = -idx * idx * idx; // now reversing
control_sequence2.wz(i) = -idx * idx * idx * idx;
}
models::ControlSequence initial_control_sequence2 = control_sequence2;
model->applyConstraints(control_sequence2);
// VX equal since this doesn't change, the WZ is reduced if breaking the constraint
EXPECT_EQ(initial_control_sequence2.vx, control_sequence2.vx);
EXPECT_NE(initial_control_sequence2.wz, control_sequence2.wz);
for (unsigned int i = 1; i != control_sequence2.wz.shape(0); i++) {
EXPECT_LT(control_sequence2.wz(i), 0.0);
}
// Now, check the specifics of the minimum curvature constraint
EXPECT_NEAR(model->getMinTurningRadius(), 0.2, 1e-6);
for (unsigned int i = 1; i != control_sequence2.vx.shape(0); i++) {
EXPECT_TRUE(fabs(control_sequence2.vx(i)) / fabs(control_sequence2.wz(i)) >= 0.2);
}
// Check that Ackermann Drive is properly non-holonomic and parameterized
EXPECT_EQ(model->isHolonomic(), false);
// Check it cleanly destructs
model.reset();
}
@@ -0,0 +1,131 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include <chrono>
#include <thread>
#include "gtest/gtest.h"
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_lifecycle/lifecycle_node.hpp"
#include "nav2_mppi_controller/tools/noise_generator.hpp"
#include "nav2_mppi_controller/tools/parameters_handler.hpp"
#include "nav2_mppi_controller/models/optimizer_settings.hpp"
#include "nav2_mppi_controller/models/state.hpp"
#include "nav2_mppi_controller/models/control_sequence.hpp"
// Tests noise generator object
class RosLockGuard
{
public:
RosLockGuard() {rclcpp::init(0, nullptr);}
~RosLockGuard() {rclcpp::shutdown();}
};
RosLockGuard g_rclcpp;
using namespace mppi; // NOLINT
TEST(NoiseGeneratorTest, NoiseGeneratorLifecycle)
{
// Tests shuts down internal thread cleanly
NoiseGenerator generator;
mppi::models::OptimizerSettings settings;
settings.batch_size = 100;
settings.time_steps = 25;
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("node");
node->declare_parameter("test_name.regenerate_noises", rclcpp::ParameterValue(false));
ParametersHandler handler(node);
generator.initialize(settings, false, "test_name", &handler);
generator.reset(settings, false);
generator.shutdown();
}
TEST(NoiseGeneratorTest, NoiseGeneratorMain)
{
// Tests shuts down internal thread cleanly
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("node");
node->declare_parameter("test_name.regenerate_noises", rclcpp::ParameterValue(true));
ParametersHandler handler(node);
NoiseGenerator generator;
mppi::models::OptimizerSettings settings;
settings.batch_size = 100;
settings.time_steps = 25;
settings.sampling_std.vx = 0.1;
settings.sampling_std.vy = 0.1;
settings.sampling_std.wz = 0.1;
// Populate a potential control sequence
mppi::models::ControlSequence control_sequence;
control_sequence.reset(25);
for (unsigned int i = 0; i != control_sequence.vx.shape(0); i++) {
control_sequence.vx(i) = i;
control_sequence.vy(i) = i;
control_sequence.wz(i) = i;
}
mppi::models::State state;
state.reset(settings.batch_size, settings.time_steps);
// Request an update with no noise yet generated, should result in identical outputs
generator.initialize(settings, false, "test_name", &handler);
generator.reset(settings, false); // sets initial sizing and zeros out noises
generator.setNoisedControls(state, control_sequence);
EXPECT_EQ(state.cvx(0), 0);
EXPECT_EQ(state.cvy(0), 0);
EXPECT_EQ(state.cwz(0), 0);
EXPECT_EQ(state.cvx(9), 9);
EXPECT_EQ(state.cvy(9), 9);
EXPECT_EQ(state.cwz(9), 9);
// Request an update with noise requested
generator.generateNextNoises();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
generator.setNoisedControls(state, control_sequence);
EXPECT_NE(state.cvx(0), 0);
EXPECT_EQ(state.cvy(0), 0); // Not populated in non-holonomic
EXPECT_NE(state.cwz(0), 0);
EXPECT_NE(state.cvx(9), 9);
EXPECT_EQ(state.cvy(9), 9); // Not populated in non-holonomic
EXPECT_NE(state.cwz(9), 9);
EXPECT_NEAR(state.cvx(0), 0, 0.3);
EXPECT_NEAR(state.cvy(0), 0, 0.3);
EXPECT_NEAR(state.cwz(0), 0, 0.3);
EXPECT_NEAR(state.cvx(9), 9, 0.3);
EXPECT_NEAR(state.cvy(9), 9, 0.3);
EXPECT_NEAR(state.cwz(9), 9, 0.3);
// Test holonomic setting
generator.reset(settings, true); // Now holonomically
generator.generateNextNoises();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
generator.setNoisedControls(state, control_sequence);
EXPECT_NE(state.cvx(0), 0);
EXPECT_NE(state.cvy(0), 0); // Now populated in non-holonomic
EXPECT_NE(state.cwz(0), 0);
EXPECT_NE(state.cvx(9), 9);
EXPECT_NE(state.cvy(9), 9); // Now populated in non-holonomic
EXPECT_NE(state.cwz(9), 9);
EXPECT_NEAR(state.cvx(0), 0, 0.3);
EXPECT_NEAR(state.cvy(0), 0, 0.3);
EXPECT_NEAR(state.cwz(0), 0, 0.3);
EXPECT_NEAR(state.cvx(9), 9, 0.3);
EXPECT_NEAR(state.cvy(9), 9, 0.3);
EXPECT_NEAR(state.cwz(9), 9, 0.3);
generator.shutdown();
}
@@ -0,0 +1,116 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include "gtest/gtest.h"
#include <geometry_msgs/msg/pose_stamped.hpp>
#include <geometry_msgs/msg/twist.hpp>
#include <nav_msgs/msg/path.hpp>
#include <nav2_costmap_2d/cost_values.hpp>
#include <nav2_costmap_2d/costmap_2d.hpp>
#include <nav2_costmap_2d/costmap_2d_ros.hpp>
#include <nav2_core/goal_checker.hpp>
#include <xtensor/xarray.hpp>
#include <xtensor/xio.hpp>
#include <xtensor/xview.hpp>
#include "nav2_mppi_controller/optimizer.hpp"
#include "nav2_mppi_controller/tools/parameters_handler.hpp"
#include "nav2_mppi_controller/motion_models.hpp"
#include "utils/utils.hpp"
class RosLockGuard
{
public:
RosLockGuard() {rclcpp::init(0, nullptr);}
~RosLockGuard() {rclcpp::shutdown();}
};
RosLockGuard g_rclcpp;
// Smoke tests the optimizer
class OptimizerSuite : public ::testing::TestWithParam<std::tuple<std::string,
std::vector<std::string>, bool>> {};
TEST_P(OptimizerSuite, OptimizerTest) {
auto [motion_model, critics, consider_footprint] = GetParam();
int batch_size = 400;
int time_steps = 15;
unsigned int path_points = 50u;
int iteration_count = 1;
double lookahead_distance = 10.0;
TestCostmapSettings costmap_settings{};
auto costmap_ros = getDummyCostmapRos(costmap_settings);
auto costmap = costmap_ros->getCostmap();
TestPose start_pose = costmap_settings.getCenterPose();
double path_step = costmap_settings.resolution;
TestPathSettings path_settings{start_pose, path_points, path_step, path_step};
TestOptimizerSettings optimizer_settings{batch_size, time_steps, iteration_count,
lookahead_distance, motion_model, consider_footprint};
unsigned int offset = 4;
unsigned int obstacle_size = offset * 2;
unsigned char obstacle_cost = 250;
auto [obst_x, obst_y] = costmap_settings.getCenterIJ();
obst_x = obst_x - offset;
obst_y = obst_y - offset;
addObstacle(costmap, {obst_x, obst_y, obstacle_size, obstacle_cost});
printInfo(optimizer_settings, path_settings, critics);
auto node = getDummyNode(optimizer_settings, critics);
auto parameters_handler = std::make_unique<mppi::ParametersHandler>(node);
auto optimizer = getDummyOptimizer(node, costmap_ros, parameters_handler.get());
// evalControl args
auto pose = getDummyPointStamped(node, start_pose);
auto velocity = getDummyTwist();
auto path = getIncrementalDummyPath(node, path_settings);
auto goal = path.poses.back().pose;
nav2_core::GoalChecker * dummy_goal_checker{nullptr};
EXPECT_NO_THROW(optimizer->evalControl(pose, velocity, path, goal, dummy_goal_checker));
}
INSTANTIATE_TEST_SUITE_P(
OptimizerTests,
OptimizerSuite,
::testing::Values(
std::make_tuple(
"Omni",
std::vector<std::string>(
{{"GoalCritic"}, {"GoalAngleCritic"}, {"ObstaclesCritic"}, {"PathAlignCritic"},
{"TwirlingCritic"}, {"PathFollowCritic"}, {"PreferForwardCritic"}}),
true),
std::make_tuple(
"DiffDrive",
std::vector<std::string>(
{{"GoalCritic"}, {"GoalAngleCritic"}, {"CostCritic"},
{"PathAngleCritic"}, {"PathFollowCritic"}, {"PreferForwardCritic"}}),
true),
std::make_tuple(
"Ackermann",
std::vector<std::string>(
{{"GoalCritic"}, {"GoalAngleCritic"}, {"ObstaclesCritic"},
{"PathAngleCritic"}, {"PathFollowCritic"}, {"PreferForwardCritic"}}),
true))
);
@@ -0,0 +1,639 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include <chrono>
#include <thread>
#include "gtest/gtest.h"
#include "rclcpp/rclcpp.hpp"
#include "nav2_mppi_controller/optimizer.hpp"
// Tests main optimizer functions
class RosLockGuard
{
public:
RosLockGuard() {rclcpp::init(0, nullptr);}
~RosLockGuard() {rclcpp::shutdown();}
};
RosLockGuard g_rclcpp;
using namespace mppi; // NOLINT
using namespace mppi::critics; // NOLINT
using namespace mppi::utils; // NOLINT
using xt::evaluation_strategy::immediate;
class OptimizerTester : public Optimizer
{
public:
OptimizerTester()
: Optimizer() {}
void testSetDiffModel()
{
EXPECT_EQ(motion_model_.get(), nullptr);
EXPECT_NO_THROW(setMotionModel("DiffDrive"));
EXPECT_NE(motion_model_.get(), nullptr);
EXPECT_TRUE(dynamic_cast<DiffDriveMotionModel *>(motion_model_.get()));
EXPECT_FALSE(isHolonomic());
}
void testSetOmniModel()
{
EXPECT_EQ(motion_model_.get(), nullptr);
EXPECT_NO_THROW(setMotionModel("Omni"));
EXPECT_NE(motion_model_.get(), nullptr);
EXPECT_TRUE(dynamic_cast<OmniMotionModel *>(motion_model_.get()));
EXPECT_TRUE(isHolonomic());
}
void testSetAckModel()
{
EXPECT_EQ(motion_model_.get(), nullptr);
EXPECT_NO_THROW(setMotionModel("Ackermann"));
EXPECT_NE(motion_model_.get(), nullptr);
EXPECT_TRUE(dynamic_cast<AckermannMotionModel *>(motion_model_.get()));
EXPECT_FALSE(isHolonomic());
}
void testSetRandModel()
{
EXPECT_EQ(motion_model_.get(), nullptr);
try {
setMotionModel("Random");
FAIL();
} catch (...) {
SUCCEED();
}
EXPECT_EQ(motion_model_.get(), nullptr);
}
void resetMotionModel()
{
motion_model_.reset();
}
void setOffsetWrapper(const double freq)
{
return setOffset(freq);
}
bool getShiftControlSequence()
{
return settings_.shift_control_sequence;
}
void fillOptimizerWithGarbage()
{
state_.vx = 0.43432 * xt::ones<float>({1000, 10});
control_sequence_.vx = 342.0 * xt::ones<float>({30});
control_history_[0] = {43, 5646, 32432};
costs_ = 5.32 * xt::ones<float>({56453});
generated_trajectories_.x = 432.234 * xt::ones<float>({7865, 1});
}
void testReset()
{
reset();
EXPECT_EQ(state_.vx, xt::zeros<float>({1000, 50}));
EXPECT_EQ(control_sequence_.vx, xt::zeros<float>({50}));
EXPECT_EQ(control_history_[0].vx, 0.0);
EXPECT_EQ(control_history_[0].vy, 0.0);
EXPECT_NEAR(xt::sum(costs_, immediate)(), 0, 1e-6);
EXPECT_EQ(generated_trajectories_.x, xt::zeros<float>({1000, 50}));
}
bool fallbackWrapper(bool fail)
{
return fallback(fail);
}
void testPrepare(
const geometry_msgs::msg::PoseStamped & robot_pose,
const geometry_msgs::msg::Twist & robot_speed,
const nav_msgs::msg::Path & plan,
const geometry_msgs::msg::Pose & goal,
nav2_core::GoalChecker * goal_checker)
{
prepare(robot_pose, robot_speed, plan, goal, goal_checker);
EXPECT_EQ(critics_data_.goal_checker, nullptr);
EXPECT_NEAR(xt::sum(costs_, immediate)(), 0, 1e-6); // should be reset
EXPECT_FALSE(critics_data_.fail_flag); // should be reset
EXPECT_FALSE(critics_data_.motion_model->isHolonomic()); // object is valid + diff drive
EXPECT_FALSE(critics_data_.furthest_reached_path_point.has_value()); // val is not set
EXPECT_FALSE(critics_data_.path_pts_valid.has_value()); // val is not set
EXPECT_EQ(state_.pose.pose.position.x, 999);
EXPECT_EQ(state_.speed.linear.y, 4.0);
EXPECT_EQ(path_.x.shape(0), 17u);
}
void shiftControlSequenceWrapper()
{
return shiftControlSequence();
}
std::pair<double, double> getVelLimits()
{
auto & s = settings_;
return {s.constraints.vx_min, s.constraints.vx_max};
}
void applyControlSequenceConstraintsWrapper()
{
return applyControlSequenceConstraints();
}
models::ControlSequence & grabControlSequence()
{
return control_sequence_;
}
void testupdateStateVels()
{
// updateInitialStateVelocities
models::State state;
state.reset(1000, 50);
state.speed.linear.x = 5.0;
state.speed.linear.y = 1.0;
state.speed.angular.z = 6.0;
state.cvx = 0.75 * xt::ones<float>({1000, 50});
state.cvy = 0.5 * xt::ones<float>({1000, 50});
state.cwz = 0.1 * xt::ones<float>({1000, 50});
updateInitialStateVelocities(state);
EXPECT_NEAR(state.vx(0, 0), 5.0, 1e-6);
EXPECT_NEAR(state.vy(0, 0), 1.0, 1e-6);
EXPECT_NEAR(state.wz(0, 0), 6.0, 1e-6);
// propagateStateVelocitiesFromInitials
propagateStateVelocitiesFromInitials(state);
EXPECT_NEAR(state.vx(0, 0), 5.0, 1e-6);
EXPECT_NEAR(state.vy(0, 0), 1.0, 1e-6);
EXPECT_NEAR(state.wz(0, 0), 6.0, 1e-6);
EXPECT_NEAR(state.vx(0, 1), 0.75, 1e-6);
EXPECT_NEAR(state.vy(0, 1), 0.5, 1e-6);
EXPECT_NEAR(state.wz(0, 1), 0.1, 1e-6);
// Putting them together: updateStateVelocities
state.reset(1000, 50);
state.speed.linear.x = -5.0;
state.speed.linear.y = -1.0;
state.speed.angular.z = -6.0;
state.cvx = -0.75 * xt::ones<float>({1000, 50});
state.cvy = -0.5 * xt::ones<float>({1000, 50});
state.cwz = -0.1 * xt::ones<float>({1000, 50});
updateStateVelocities(state);
EXPECT_NEAR(state.vx(0, 0), -5.0, 1e-6);
EXPECT_NEAR(state.vy(0, 0), -1.0, 1e-6);
EXPECT_NEAR(state.wz(0, 0), -6.0, 1e-6);
EXPECT_NEAR(state.vx(0, 1), -0.75, 1e-6);
EXPECT_NEAR(state.vy(0, 1), -0.5, 1e-6);
EXPECT_NEAR(state.wz(0, 1), -0.1, 1e-6);
}
geometry_msgs::msg::TwistStamped getControlFromSequenceAsTwistWrapper()
{
builtin_interfaces::msg::Time stamp;
return getControlFromSequenceAsTwist(stamp);
}
void integrateStateVelocitiesWrapper(
models::Trajectories & traj,
const models::State & state)
{
return integrateStateVelocities(traj, state);
}
};
TEST(OptimizerTests, BasicInitializedFunctions)
{
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
OptimizerTester optimizer_tester;
node->declare_parameter("mppic.batch_size", rclcpp::ParameterValue(1000));
node->declare_parameter("mppic.time_steps", rclcpp::ParameterValue(50));
node->declare_parameter("controller_frequency", rclcpp::ParameterValue(30.0));
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
optimizer_tester.initialize(node, "mppic", costmap_ros, &param_handler);
// Should be empty of size batches x time steps
// and tests getting set params: time_steps, batch_size, controller_frequency
auto trajs = optimizer_tester.getGeneratedTrajectories();
EXPECT_EQ(trajs.x.shape(0), 1000u);
EXPECT_EQ(trajs.x.shape(1), 50u);
EXPECT_EQ(trajs.x, xt::zeros<float>({1000, 50}));
optimizer_tester.resetMotionModel();
optimizer_tester.testSetOmniModel();
auto traj = optimizer_tester.getOptimizedTrajectory();
EXPECT_EQ(traj(5, 0), 0.0); // x
EXPECT_EQ(traj(5, 1), 0.0); // y
EXPECT_EQ(traj(5, 2), 0.0); // yaw
EXPECT_EQ(traj.shape(0), 50u);
EXPECT_EQ(traj.shape(1), 3u);
optimizer_tester.reset();
optimizer_tester.shutdown();
}
TEST(OptimizerTests, TestOptimizerMotionModels)
{
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
OptimizerTester optimizer_tester;
node->declare_parameter("controller_frequency", rclcpp::ParameterValue(30.0));
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
optimizer_tester.initialize(node, "mppic", costmap_ros, &param_handler);
// Diff Drive should be non-holonomic
optimizer_tester.resetMotionModel();
optimizer_tester.testSetDiffModel();
// Omni Drive should be holonomic
optimizer_tester.resetMotionModel();
optimizer_tester.testSetOmniModel();
// // Ackermann should be non-holonomic
optimizer_tester.resetMotionModel();
optimizer_tester.testSetAckModel();
// // Rand should fail
optimizer_tester.resetMotionModel();
optimizer_tester.testSetRandModel();
}
TEST(OptimizerTests, setOffsetTests)
{
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
OptimizerTester optimizer_tester;
node->declare_parameter("mppic.model_dt", rclcpp::ParameterValue(0.1));
node->declare_parameter("controller_frequency", rclcpp::ParameterValue(30.0));
node->declare_parameter("mppic.batch_size", rclcpp::ParameterValue(1000));
node->declare_parameter("mppic.time_steps", rclcpp::ParameterValue(50));
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
optimizer_tester.initialize(node, "mppic", costmap_ros, &param_handler);
// Test offsets are properly set based on relationship of model_dt and controller frequency
// Also tests getting set model_dt parameter.
EXPECT_THROW(optimizer_tester.setOffsetWrapper(1.0), std::runtime_error);
EXPECT_NO_THROW(optimizer_tester.setOffsetWrapper(30.0));
EXPECT_FALSE(optimizer_tester.getShiftControlSequence());
EXPECT_NO_THROW(optimizer_tester.setOffsetWrapper(10.0));
EXPECT_TRUE(optimizer_tester.getShiftControlSequence());
}
TEST(OptimizerTests, resetTests)
{
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
OptimizerTester optimizer_tester;
node->declare_parameter("controller_frequency", rclcpp::ParameterValue(30.0));
node->declare_parameter("mppic.batch_size", rclcpp::ParameterValue(1000));
node->declare_parameter("mppic.time_steps", rclcpp::ParameterValue(50));
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
optimizer_tester.initialize(node, "mppic", costmap_ros, &param_handler);
// Tests resetting the full state of all the functions after filling with garbage
optimizer_tester.fillOptimizerWithGarbage();
optimizer_tester.testReset();
}
TEST(OptimizerTests, FallbackTests)
{
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
OptimizerTester optimizer_tester;
node->declare_parameter("controller_frequency", rclcpp::ParameterValue(30.0));
node->declare_parameter("mppic.batch_size", rclcpp::ParameterValue(1000));
node->declare_parameter("mppic.time_steps", rclcpp::ParameterValue(50));
node->declare_parameter("mppic.retry_attempt_limit", rclcpp::ParameterValue(2));
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
optimizer_tester.initialize(node, "mppic", costmap_ros, &param_handler);
// Test fallback logic, also tests getting set param retry_attempt_limit
// Because retry set to 2, it should attempt soft resets 2x before throwing exception
// for hard reset
EXPECT_FALSE(optimizer_tester.fallbackWrapper(false));
EXPECT_TRUE(optimizer_tester.fallbackWrapper(true));
EXPECT_TRUE(optimizer_tester.fallbackWrapper(true));
EXPECT_THROW(optimizer_tester.fallbackWrapper(true), std::runtime_error);
}
TEST(OptimizerTests, PrepareTests)
{
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
OptimizerTester optimizer_tester;
node->declare_parameter("controller_frequency", rclcpp::ParameterValue(30.0));
node->declare_parameter("mppic.batch_size", rclcpp::ParameterValue(1000));
node->declare_parameter("mppic.time_steps", rclcpp::ParameterValue(50));
node->declare_parameter("mppic.retry_attempt_limit", rclcpp::ParameterValue(2));
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
optimizer_tester.initialize(node, "mppic", costmap_ros, &param_handler);
// Test Prepare function to set the state of the robot pose/speed on new cycle
// Populate the contents with things easily identifiable if correct
geometry_msgs::msg::PoseStamped pose;
pose.pose.position.x = 999;
geometry_msgs::msg::Twist speed;
speed.linear.y = 4.0;
nav_msgs::msg::Path path;
geometry_msgs::msg::Pose goal;
path.poses.resize(17);
optimizer_tester.testPrepare(pose, speed, path, goal, nullptr);
}
TEST(OptimizerTests, shiftControlSequenceTests)
{
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
OptimizerTester optimizer_tester;
node->declare_parameter("controller_frequency", rclcpp::ParameterValue(30.0));
node->declare_parameter("mppic.batch_size", rclcpp::ParameterValue(1000));
node->declare_parameter("mppic.time_steps", rclcpp::ParameterValue(50));
node->declare_parameter("mppic.retry_attempt_limit", rclcpp::ParameterValue(2));
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
optimizer_tester.initialize(node, "mppic", costmap_ros, &param_handler);
// Test shiftControlSequence by setting the 2nd value to something unique to neighbors
auto & sequence = optimizer_tester.grabControlSequence();
sequence.reset(100);
sequence.vx(0) = 9999;
sequence.vx(1) = 6;
sequence.vx(2) = 888;
sequence.vy(0) = 9999;
sequence.vy(1) = 6;
sequence.vy(2) = 888;
sequence.wz(0) = 9999;
sequence.wz(1) = 6;
sequence.wz(2) = 888;
optimizer_tester.resetMotionModel();
optimizer_tester.testSetOmniModel();
optimizer_tester.shiftControlSequenceWrapper();
EXPECT_EQ(sequence.vx(0), 6);
EXPECT_EQ(sequence.vy(0), 6);
EXPECT_EQ(sequence.wz(0), 6);
EXPECT_EQ(sequence.vx(1), 888);
EXPECT_EQ(sequence.vy(1), 888);
EXPECT_EQ(sequence.wz(1), 888);
EXPECT_EQ(sequence.vx(2), 0);
EXPECT_EQ(sequence.vy(2), 0);
EXPECT_EQ(sequence.wz(2), 0);
}
TEST(OptimizerTests, SpeedLimitTests)
{
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
OptimizerTester optimizer_tester;
node->declare_parameter("controller_frequency", rclcpp::ParameterValue(30.0));
node->declare_parameter("mppic.batch_size", rclcpp::ParameterValue(1000));
node->declare_parameter("mppic.time_steps", rclcpp::ParameterValue(50));
node->declare_parameter("mppic.retry_attempt_limit", rclcpp::ParameterValue(2));
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
optimizer_tester.initialize(node, "mppic", costmap_ros, &param_handler);
// Test Speed limits API
auto [v_min, v_max] = optimizer_tester.getVelLimits();
EXPECT_EQ(v_max, 0.5f);
EXPECT_EQ(v_min, -0.35f);
optimizer_tester.setSpeedLimit(0, false);
auto [v_min2, v_max2] = optimizer_tester.getVelLimits();
EXPECT_EQ(v_max2, 0.5f);
EXPECT_EQ(v_min2, -0.35f);
optimizer_tester.setSpeedLimit(50.0, true);
auto [v_min3, v_max3] = optimizer_tester.getVelLimits();
EXPECT_NEAR(v_max3, 0.5 / 2.0, 1e-3);
EXPECT_NEAR(v_min3, -0.35 / 2.0, 1e-3);
optimizer_tester.setSpeedLimit(0, true);
auto [v_min4, v_max4] = optimizer_tester.getVelLimits();
EXPECT_EQ(v_max4, 0.5f);
EXPECT_EQ(v_min4, -0.35f);
optimizer_tester.setSpeedLimit(0.75, false);
auto [v_min5, v_max5] = optimizer_tester.getVelLimits();
EXPECT_NEAR(v_max5, 0.75, 1e-3);
EXPECT_NEAR(v_min5, -0.5249, 1e-2);
}
TEST(OptimizerTests, applyControlSequenceConstraintsTests)
{
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
OptimizerTester optimizer_tester;
node->declare_parameter("controller_frequency", rclcpp::ParameterValue(30.0));
node->declare_parameter("mppic.batch_size", rclcpp::ParameterValue(1000));
node->declare_parameter("mppic.time_steps", rclcpp::ParameterValue(50));
node->declare_parameter("mppic.vx_max", rclcpp::ParameterValue(1.0));
node->declare_parameter("mppic.vx_min", rclcpp::ParameterValue(-1.0));
node->declare_parameter("mppic.vy_max", rclcpp::ParameterValue(0.75));
node->declare_parameter("mppic.wz_max", rclcpp::ParameterValue(2.0));
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
optimizer_tester.initialize(node, "mppic", costmap_ros, &param_handler);
// Test constraints being applied to ensure feasibility of trajectories
// Also tests param get of set vx/vy/wz min/maxes
// Set model to omni to consider holonomic vy elements
// Ack is not tested here because `applyConstraints` is covered in detail
// in motion_models_test.cpp
optimizer_tester.resetMotionModel();
optimizer_tester.testSetOmniModel();
auto & sequence = optimizer_tester.grabControlSequence();
// Test boundary of limits
sequence.vx = xt::ones<float>({50});
sequence.vy = 0.75 * xt::ones<float>({50});
sequence.wz = 2.0 * xt::ones<float>({50});
optimizer_tester.applyControlSequenceConstraintsWrapper();
EXPECT_EQ(sequence.vx, xt::ones<float>({50}));
EXPECT_EQ(sequence.vy, 0.75 * xt::ones<float>({50}));
EXPECT_EQ(sequence.wz, 2.0 * xt::ones<float>({50}));
// Test breaking limits sets to maximum
sequence.vx = 5.0 * xt::ones<float>({50});
sequence.vy = 5.0 * xt::ones<float>({50});
sequence.wz = 5.0 * xt::ones<float>({50});
optimizer_tester.applyControlSequenceConstraintsWrapper();
EXPECT_EQ(sequence.vx, xt::ones<float>({50}));
EXPECT_EQ(sequence.vy, 0.75 * xt::ones<float>({50}));
EXPECT_EQ(sequence.wz, 2.0 * xt::ones<float>({50}));
// Test breaking limits sets to minimum
sequence.vx = -5.0 * xt::ones<float>({50});
sequence.vy = -5.0 * xt::ones<float>({50});
sequence.wz = -5.0 * xt::ones<float>({50});
optimizer_tester.applyControlSequenceConstraintsWrapper();
EXPECT_EQ(sequence.vx, -1.0 * xt::ones<float>({50}));
EXPECT_EQ(sequence.vy, -0.75 * xt::ones<float>({50}));
EXPECT_EQ(sequence.wz, -2.0 * xt::ones<float>({50}));
}
TEST(OptimizerTests, updateStateVelocitiesTests)
{
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
OptimizerTester optimizer_tester;
node->declare_parameter("controller_frequency", rclcpp::ParameterValue(30.0));
node->declare_parameter("mppic.batch_size", rclcpp::ParameterValue(1000));
node->declare_parameter("mppic.time_steps", rclcpp::ParameterValue(50));
node->declare_parameter("mppic.vx_max", rclcpp::ParameterValue(1.0));
node->declare_parameter("mppic.vx_min", rclcpp::ParameterValue(-1.0));
node->declare_parameter("mppic.vy_max", rclcpp::ParameterValue(0.60));
node->declare_parameter("mppic.wz_max", rclcpp::ParameterValue(2.0));
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
optimizer_tester.initialize(node, "mppic", costmap_ros, &param_handler);
// Test settings of the state to the initial robot speed to start rollout
// Set model to omni to consider holonomic vy elements
optimizer_tester.resetMotionModel();
optimizer_tester.testSetOmniModel();
optimizer_tester.testupdateStateVels();
}
TEST(OptimizerTests, getControlFromSequenceAsTwistTests)
{
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
OptimizerTester optimizer_tester;
node->declare_parameter("controller_frequency", rclcpp::ParameterValue(30.0));
node->declare_parameter("mppic.batch_size", rclcpp::ParameterValue(1000));
node->declare_parameter("mppic.time_steps", rclcpp::ParameterValue(50));
node->declare_parameter("mppic.vx_max", rclcpp::ParameterValue(1.0));
node->declare_parameter("mppic.vx_min", rclcpp::ParameterValue(-1.0));
node->declare_parameter("mppic.vy_max", rclcpp::ParameterValue(0.60));
node->declare_parameter("mppic.wz_max", rclcpp::ParameterValue(2.0));
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
optimizer_tester.initialize(node, "mppic", costmap_ros, &param_handler);
// Test conversion of control sequence into a Twist command to execute
auto & sequence = optimizer_tester.grabControlSequence();
sequence.vx = 0.25 * xt::ones<float>({10});
sequence.vy = 0.5 * xt::ones<float>({10});
sequence.wz = 0.1 * xt::ones<float>({10});
auto diff_t = optimizer_tester.getControlFromSequenceAsTwistWrapper();
EXPECT_NEAR(diff_t.twist.linear.x, 0.25, 1e-6);
EXPECT_NEAR(diff_t.twist.linear.y, 0.0, 1e-6); // Y should not be populated
EXPECT_NEAR(diff_t.twist.angular.z, 0.1, 1e-6);
// Set model to omni to consider holonomic vy elements
optimizer_tester.resetMotionModel();
optimizer_tester.testSetOmniModel();
auto omni_t = optimizer_tester.getControlFromSequenceAsTwistWrapper();
EXPECT_NEAR(omni_t.twist.linear.x, 0.25, 1e-6);
EXPECT_NEAR(omni_t.twist.linear.y, 0.5, 1e-6); // Now it should be
EXPECT_NEAR(omni_t.twist.angular.z, 0.1, 1e-6);
}
TEST(OptimizerTests, integrateStateVelocitiesTests)
{
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
OptimizerTester optimizer_tester;
node->declare_parameter("controller_frequency", rclcpp::ParameterValue(30.0));
node->declare_parameter("mppic.batch_size", rclcpp::ParameterValue(1000));
node->declare_parameter("mppic.model_dt", rclcpp::ParameterValue(0.1));
node->declare_parameter("mppic.time_steps", rclcpp::ParameterValue(50));
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
optimizer_tester.initialize(node, "mppic", costmap_ros, &param_handler);
optimizer_tester.resetMotionModel();
optimizer_tester.testSetOmniModel();
// Test integration of velocities for trajectory rollout poses
// Give it a couple of easy const traj and check rollout, start from 0
models::State state;
state.reset(1000, 50);
models::Trajectories traj;
state.vx = 0.1 * xt::ones<float>({1000, 50});
xt::view(state.vx, xt::all(), 0) = xt::zeros<float>({1000});
state.vy = xt::zeros<float>({1000, 50});
state.wz = xt::zeros<float>({1000, 50});
optimizer_tester.integrateStateVelocitiesWrapper(traj, state);
EXPECT_EQ(traj.y, xt::zeros<float>({1000, 50}));
EXPECT_EQ(traj.yaws, xt::zeros<float>({1000, 50}));
for (unsigned int i = 0; i != traj.x.shape(1); i++) {
EXPECT_NEAR(traj.x(1, i), i * 0.1 /*vel*/ * 0.1 /*dt*/, 1e-3);
}
// Give it a bit of a more complex trajectory to crunch
state.vy = 0.2 * xt::ones<float>({1000, 50});
xt::view(state.vy, xt::all(), 0) = xt::zeros<float>({1000});
optimizer_tester.integrateStateVelocitiesWrapper(traj, state);
EXPECT_EQ(traj.yaws, xt::zeros<float>({1000, 50}));
for (unsigned int i = 0; i != traj.x.shape(1); i++) {
EXPECT_NEAR(traj.x(1, i), i * 0.1 /*vel*/ * 0.1 /*dt*/, 1e-3);
EXPECT_NEAR(traj.y(1, i), i * 0.2 /*vel*/ * 0.1 /*dt*/, 1e-3);
}
// Lets add some angular motion to the mix
state.vy = xt::zeros<float>({1000, 50});
state.wz = 0.2 * xt::ones<float>({1000, 50});
xt::view(state.wz, xt::all(), 0) = xt::zeros<float>({1000});
optimizer_tester.integrateStateVelocitiesWrapper(traj, state);
float x = 0;
float y = 0;
for (unsigned int i = 1; i != traj.x.shape(1); i++) {
std::cout << i << std::endl;
x += (0.1 /*vx*/ * cos(0.2 /*wz*/ * 0.1 /*model_dt*/ * (i - 1))) * 0.1 /*model_dt*/;
y += (0.1 /*vx*/ * sin(0.2 /*wz*/ * 0.1 /*model_dt*/ * (i - 1))) * 0.1 /*model_dt*/;
EXPECT_NEAR(traj.x(1, i), x, 1e-6);
EXPECT_NEAR(traj.y(1, i), y, 1e-6);
}
}
@@ -0,0 +1,181 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include <chrono>
#include <thread>
#include "gtest/gtest.h"
#include "rclcpp/rclcpp.hpp"
#include "nav2_mppi_controller/tools/parameters_handler.hpp"
// Tests parameter handler object
class RosLockGuard
{
public:
RosLockGuard() {rclcpp::init(0, nullptr);}
~RosLockGuard() {rclcpp::shutdown();}
};
RosLockGuard g_rclcpp;
using namespace mppi; // NOLINT
class ParametersHandlerWrapper : public ParametersHandler
{
public:
ParametersHandlerWrapper() = default;
explicit ParametersHandlerWrapper(
const rclcpp_lifecycle::LifecycleNode::WeakPtr & parent)
: ParametersHandler(parent) {}
template<typename T>
auto asWrapped(rclcpp::Parameter parameter)
{
return ParametersHandler::as<T>(parameter);
}
};
using namespace mppi; // NOLINT
TEST(ParameterHandlerTest, asTypeConversionTest)
{
ParametersHandlerWrapper a;
rclcpp::Parameter int_p("int_parameter", rclcpp::ParameterValue(1));
rclcpp::Parameter double_p("double_parameter", rclcpp::ParameterValue(10.0));
rclcpp::Parameter bool_p("bool_parameter", rclcpp::ParameterValue(false));
rclcpp::Parameter string_p("string_parameter", rclcpp::ParameterValue(std::string("hello")));
rclcpp::Parameter intv_p("intv_parameter", rclcpp::ParameterValue(std::vector<int>{1}));
rclcpp::Parameter doublev_p(
"doublev_parameter", rclcpp::ParameterValue(std::vector<double>{10.0}));
rclcpp::Parameter boolv_p("boolv_parameter", rclcpp::ParameterValue(std::vector<bool>{false}));
rclcpp::Parameter stringv_p(
"stringv_parameter", rclcpp::ParameterValue(std::vector<std::string>{std::string("hello")}));
EXPECT_EQ(a.asWrapped<int>(int_p), 1);
EXPECT_EQ(a.asWrapped<double>(double_p), 10.0);
EXPECT_EQ(a.asWrapped<bool>(bool_p), false);
EXPECT_EQ(a.asWrapped<std::string>(string_p), std::string("hello"));
EXPECT_EQ(a.asWrapped<std::vector<int64_t>>(intv_p)[0], 1);
EXPECT_EQ(a.asWrapped<std::vector<double>>(doublev_p)[0], 10.0);
EXPECT_EQ(a.asWrapped<std::vector<bool>>(boolv_p)[0], false);
EXPECT_EQ(a.asWrapped<std::vector<std::string>>(stringv_p)[0], std::string("hello"));
}
TEST(ParameterHandlerTest, PrePostDynamicCallbackTest)
{
bool pre_triggered = false, post_triggered = false, dynamic_triggered = false;
auto preCb = [&]() {
if (post_triggered) {
throw std::runtime_error("Post-callback triggered before pre-callback!");
}
pre_triggered = true;
};
auto postCb = [&]() {
if (!pre_triggered) {
throw std::runtime_error("Pre-callback was not triggered before post-callback!");
}
post_triggered = true;
};
auto dynamicCb = [&](const rclcpp::Parameter & /*param*/) {
dynamic_triggered = true;
};
rclcpp::Parameter random_param("blah_blah", rclcpp::ParameterValue(true));
rclcpp::Parameter random_param2("use_sim_time", rclcpp::ParameterValue(true));
bool val = false;
ParametersHandlerWrapper a;
a.addPreCallback(preCb);
a.addPostCallback(postCb);
a.addDynamicParamCallback("use_sim_time", dynamicCb);
a.setDynamicParamCallback(val, "blah_blah");
// Dynamic callback should not trigger, wrong parameter, but val should be updated
a.dynamicParamsCallback(std::vector<rclcpp::Parameter>{random_param});
EXPECT_FALSE(dynamic_triggered);
EXPECT_TRUE(pre_triggered);
EXPECT_TRUE(post_triggered);
EXPECT_TRUE(val);
// Now dynamic parameter bool should be updated, right param called!
pre_triggered = false, post_triggered = false;
a.dynamicParamsCallback(std::vector<rclcpp::Parameter>{random_param2});
EXPECT_TRUE(dynamic_triggered);
EXPECT_TRUE(pre_triggered);
EXPECT_TRUE(post_triggered);
}
TEST(ParameterHandlerTest, GetSystemParamsTest)
{
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
node->declare_parameter("param1", rclcpp::ParameterValue(true));
node->declare_parameter("ns.param2", rclcpp::ParameterValue(7));
// Get parameters in global namespace and in subnamespaces
ParametersHandler handler(node);
auto getParamer = handler.getParamGetter("");
bool p1 = false;
int p2 = 0;
getParamer(p1, "param1", false);
getParamer(p2, "ns.param2", 0);
EXPECT_EQ(p1, true);
EXPECT_EQ(p2, 7);
// Get parameters in subnamespaces using name semantics of getter
auto getParamer2 = handler.getParamGetter("ns");
p2 = 0;
getParamer2(p2, "param2", 0);
EXPECT_EQ(p2, 7);
}
TEST(ParameterHandlerTest, DynamicAndStaticParametersTest)
{
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
node->declare_parameter("dynamic_int", rclcpp::ParameterValue(7));
node->declare_parameter("static_int", rclcpp::ParameterValue(7));
ParametersHandlerWrapper handler(node);
handler.start();
// Get parameters and check they have initial values
auto getParamer = handler.getParamGetter("");
int p1 = 0, p2 = 0;
getParamer(p1, "dynamic_int", 0, ParameterType::Dynamic);
getParamer(p2, "static_int", 0, ParameterType::Static);
EXPECT_EQ(p1, 7);
EXPECT_EQ(p2, 7);
// Now change them both via dynamic parameters
auto rec_param = std::make_shared<rclcpp::AsyncParametersClient>(
node->get_node_base_interface(), node->get_node_topics_interface(),
node->get_node_graph_interface(),
node->get_node_services_interface());
auto results = rec_param->set_parameters_atomically(
{rclcpp::Parameter("dynamic_int", 10),
rclcpp::Parameter("static_int", 10)});
rclcpp::spin_until_future_complete(
node->get_node_base_interface(),
results);
// Now, only param1 should change, param 2 should be the same
EXPECT_EQ(p1, 10);
EXPECT_EQ(p2, 7);
}
@@ -0,0 +1,249 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include <chrono>
#include <thread>
#include "gtest/gtest.h"
#include "rclcpp/rclcpp.hpp"
#include "nav2_mppi_controller/tools/path_handler.hpp"
#include "tf2_ros/transform_broadcaster.h"
// Tests path handling
class RosLockGuard
{
public:
RosLockGuard() {rclcpp::init(0, nullptr);}
~RosLockGuard() {rclcpp::shutdown();}
};
RosLockGuard g_rclcpp;
using namespace mppi; // NOLINT
class PathHandlerWrapper : public PathHandler
{
public:
PathHandlerWrapper()
: PathHandler() {}
void pruneGlobalPlanWrapper(nav_msgs::msg::Path & path, const PathIterator end)
{
return prunePlan(path, end);
}
double getMaxCostmapDistWrapper()
{
return getMaxCostmapDist();
}
std::pair<nav_msgs::msg::Path, PathIterator>
getGlobalPlanConsideringBoundsInCostmapFrameWrapper(const geometry_msgs::msg::PoseStamped & pose)
{
return getGlobalPlanConsideringBoundsInCostmapFrame(pose);
}
bool transformPoseWrapper(
const std::string & frame, const geometry_msgs::msg::PoseStamped & in_pose,
geometry_msgs::msg::PoseStamped & out_pose) const
{
return transformPose(frame, in_pose, out_pose);
}
geometry_msgs::msg::PoseStamped transformToGlobalPlanFrameWrapper(
const geometry_msgs::msg::PoseStamped & pose)
{
return transformToGlobalPlanFrame(pose);
}
void setGlobalPlanUpToInversion(const nav_msgs::msg::Path & path)
{
global_plan_up_to_inversion_ = path;
}
bool isWithinInversionTolerancesWrapper(const geometry_msgs::msg::PoseStamped & robot_pose)
{
return isWithinInversionTolerances(robot_pose);
}
nav_msgs::msg::Path & getInvertedPath()
{
return global_plan_up_to_inversion_;
}
};
TEST(PathHandlerTests, GetAndPrunePath)
{
nav_msgs::msg::Path path;
PathHandlerWrapper handler;
path.header.frame_id = "fkframe";
path.poses.resize(11);
handler.setPath(path);
auto & rtn_path = handler.getPath();
EXPECT_EQ(path.header.frame_id, rtn_path.header.frame_id);
EXPECT_EQ(path.poses.size(), rtn_path.poses.size());
PathIterator it = rtn_path.poses.begin() + 5;
handler.pruneGlobalPlanWrapper(rtn_path, it);
auto rtn2_path = handler.getPath();
EXPECT_EQ(rtn2_path.poses.size(), 6u);
}
TEST(PathHandlerTests, TestBounds)
{
PathHandlerWrapper handler;
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
node->declare_parameter("dummy.max_robot_pose_search_dist", rclcpp::ParameterValue(99999.9));
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
auto results = costmap_ros->set_parameters_atomically(
{rclcpp::Parameter("global_frame", "odom"),
rclcpp::Parameter("robot_base_frame", "base_link")});
ParametersHandler param_handler(node);
rclcpp_lifecycle::State state;
costmap_ros->on_configure(state);
// Test initialization and getting costmap basic metadata
handler.initialize(node, "dummy", costmap_ros, costmap_ros->getTfBuffer(), &param_handler);
EXPECT_EQ(handler.getMaxCostmapDistWrapper(), 2.5);
// Set tf between map odom and base_link
std::unique_ptr<tf2_ros::TransformBroadcaster> tf_broadcaster_ =
std::make_unique<tf2_ros::TransformBroadcaster>(node);
geometry_msgs::msg::TransformStamped t;
t.header.frame_id = "map";
t.child_frame_id = "base_link";
tf_broadcaster_->sendTransform(t);
t.header.frame_id = "map";
t.child_frame_id = "odom";
tf_broadcaster_->sendTransform(t);
// Test getting the global plans within a bounds window
nav_msgs::msg::Path path;
path.header.frame_id = "map";
path.poses.resize(100);
for (unsigned int i = 0; i != path.poses.size(); i++) {
path.poses[i].pose.position.x = i;
path.poses[i].header.frame_id = "map";
}
geometry_msgs::msg::PoseStamped robot_pose;
robot_pose.header.frame_id = "odom";
robot_pose.pose.position.x = 25.0;
handler.setPath(path);
auto [transformed_plan, closest] =
handler.getGlobalPlanConsideringBoundsInCostmapFrameWrapper(robot_pose);
auto & path_inverted = handler.getInvertedPath();
EXPECT_EQ(closest - path_inverted.poses.begin(), 25);
handler.pruneGlobalPlanWrapper(path_inverted, closest);
auto & path_pruned = handler.getInvertedPath();
EXPECT_EQ(path_pruned.poses.size(), 75u);
}
TEST(PathHandlerTests, TestTransforms)
{
PathHandlerWrapper handler;
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
node->declare_parameter("dummy.max_robot_pose_search_dist", rclcpp::ParameterValue(99999.9));
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
ParametersHandler param_handler(node);
rclcpp_lifecycle::State state;
costmap_ros->on_configure(state);
// Test basic transformations and path handling
handler.initialize(node, "dummy", costmap_ros, costmap_ros->getTfBuffer(), &param_handler);
// Set tf between map odom and base_link
std::unique_ptr<tf2_ros::TransformBroadcaster> tf_broadcaster_ =
std::make_unique<tf2_ros::TransformBroadcaster>(node);
geometry_msgs::msg::TransformStamped t;
t.header.frame_id = "map";
t.child_frame_id = "base_link";
tf_broadcaster_->sendTransform(t);
t.header.frame_id = "map";
t.child_frame_id = "odom";
tf_broadcaster_->sendTransform(t);
nav_msgs::msg::Path path;
path.header.frame_id = "map";
path.poses.resize(100);
for (unsigned int i = 0; i != path.poses.size(); i++) {
path.poses[i].pose.position.x = i;
path.poses[i].header.frame_id = "map";
}
geometry_msgs::msg::PoseStamped robot_pose, output_pose;
robot_pose.header.frame_id = "odom";
robot_pose.pose.position.x = 2.5;
EXPECT_TRUE(handler.transformPoseWrapper("map", robot_pose, output_pose));
EXPECT_EQ(output_pose.pose.position.x, 2.5);
EXPECT_THROW(handler.transformToGlobalPlanFrameWrapper(robot_pose), std::runtime_error);
handler.setPath(path);
EXPECT_NO_THROW(handler.transformToGlobalPlanFrameWrapper(robot_pose));
auto [path_out, closest] =
handler.getGlobalPlanConsideringBoundsInCostmapFrameWrapper(robot_pose);
// Put it all together
auto final_path = handler.transformPath(robot_pose);
EXPECT_EQ(final_path.poses.size(), path_out.poses.size());
}
TEST(PathHandlerTests, TestInversionToleranceChecks)
{
nav_msgs::msg::Path path;
for (unsigned int i = 0; i != 10; i++) {
geometry_msgs::msg::PoseStamped pose;
pose.pose.position.x = static_cast<double>(i);
path.poses.push_back(pose);
}
path.poses.back().pose.orientation.w = 1;
PathHandlerWrapper handler;
handler.setGlobalPlanUpToInversion(path);
// Not near (0,0)
geometry_msgs::msg::PoseStamped robot_pose;
EXPECT_FALSE(handler.isWithinInversionTolerancesWrapper(robot_pose));
// Exactly on top of it
robot_pose.pose.position.x = 9;
robot_pose.pose.orientation.w = 1.0;
EXPECT_TRUE(handler.isWithinInversionTolerancesWrapper(robot_pose));
// Laterally of it
robot_pose.pose.position.y = 9;
EXPECT_FALSE(handler.isWithinInversionTolerancesWrapper(robot_pose));
// On top but off angled
robot_pose.pose.position.y = 0;
robot_pose.pose.orientation.z = 0.8509035;
robot_pose.pose.orientation.w = 0.525322;
EXPECT_FALSE(handler.isWithinInversionTolerancesWrapper(robot_pose));
// On top but off angled within tolerances
robot_pose.pose.position.y = 0;
robot_pose.pose.orientation.w = 0.9961947;
robot_pose.pose.orientation.z = 0.0871558;
EXPECT_TRUE(handler.isWithinInversionTolerancesWrapper(robot_pose));
// Offset spatially + off angled but both within tolerances
robot_pose.pose.position.x = 9.10;
EXPECT_TRUE(handler.isWithinInversionTolerancesWrapper(robot_pose));
}
@@ -0,0 +1,155 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include <chrono>
#include <thread>
#include "gtest/gtest.h"
#include "rclcpp/rclcpp.hpp"
#include "nav2_mppi_controller/tools/trajectory_visualizer.hpp"
// Tests trajectory visualization
class RosLockGuard
{
public:
RosLockGuard() {rclcpp::init(0, nullptr);}
~RosLockGuard() {rclcpp::shutdown();}
};
RosLockGuard g_rclcpp;
using namespace mppi; // NOLINT
TEST(TrajectoryVisualizerTests, StateTransition)
{
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
auto parameters_handler = std::make_unique<ParametersHandler>(node);
TrajectoryVisualizer vis;
vis.on_configure(node, "my_name", "map", parameters_handler.get());
vis.on_activate();
vis.on_deactivate();
vis.on_cleanup();
}
TEST(TrajectoryVisualizerTests, VisPathRepub)
{
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
auto parameters_handler = std::make_unique<ParametersHandler>(node);
nav_msgs::msg::Path recieved_path;
nav_msgs::msg::Path pub_path;
pub_path.header.frame_id = "fake_frame";
pub_path.poses.resize(5);
auto my_sub = node->create_subscription<nav_msgs::msg::Path>(
"transformed_global_plan", 10,
[&](const nav_msgs::msg::Path msg) {recieved_path = msg;});
TrajectoryVisualizer vis;
vis.on_configure(node, "my_name", "map", parameters_handler.get());
vis.on_activate();
vis.visualize(pub_path);
rclcpp::spin_some(node->get_node_base_interface());
EXPECT_EQ(recieved_path.poses.size(), 5u);
EXPECT_EQ(recieved_path.header.frame_id, "fake_frame");
}
TEST(TrajectoryVisualizerTests, VisOptimalTrajectory)
{
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
auto parameters_handler = std::make_unique<ParametersHandler>(node);
visualization_msgs::msg::MarkerArray recieved_msg;
auto my_sub = node->create_subscription<visualization_msgs::msg::MarkerArray>(
"/trajectories", 10,
[&](const visualization_msgs::msg::MarkerArray msg) {recieved_msg = msg;});
// optimal_trajectory empty, should fail to publish
xt::xtensor<float, 2> optimal_trajectory;
TrajectoryVisualizer vis;
vis.on_configure(node, "my_name", "fkmap", parameters_handler.get());
vis.on_activate();
vis.add(optimal_trajectory, "Optimal Trajectory");
nav_msgs::msg::Path bogus_path;
vis.visualize(bogus_path);
rclcpp::spin_some(node->get_node_base_interface());
EXPECT_EQ(recieved_msg.markers.size(), 0u);
// Now populated with content, should publish
optimal_trajectory = xt::ones<float>({20, 2});
vis.add(optimal_trajectory, "Optimal Trajectory");
vis.visualize(bogus_path);
rclcpp::spin_some(node->get_node_base_interface());
// Should have 20 trajectory points in the map frame
EXPECT_EQ(recieved_msg.markers.size(), 20u);
EXPECT_EQ(recieved_msg.markers[0].header.frame_id, "fkmap");
// Check IDs are properly populated
EXPECT_EQ(recieved_msg.markers[0].id, 0);
EXPECT_EQ(recieved_msg.markers[1].id, 1);
EXPECT_EQ(recieved_msg.markers[10].id, 10);
// Check poses are correct
EXPECT_EQ(recieved_msg.markers[0].pose.position.x, 1);
EXPECT_EQ(recieved_msg.markers[0].pose.position.y, 1);
EXPECT_EQ(recieved_msg.markers[0].pose.position.z, 0.06);
// Check that scales are rational
EXPECT_EQ(recieved_msg.markers[0].scale.x, 0.03);
EXPECT_EQ(recieved_msg.markers[0].scale.y, 0.03);
EXPECT_EQ(recieved_msg.markers[0].scale.z, 0.07);
EXPECT_EQ(recieved_msg.markers[19].scale.x, 0.07);
EXPECT_EQ(recieved_msg.markers[19].scale.y, 0.07);
EXPECT_EQ(recieved_msg.markers[19].scale.z, 0.09);
// Check that the colors are rational
for (unsigned int i = 0; i != recieved_msg.markers.size() - 1; i++) {
EXPECT_LT(recieved_msg.markers[i].color.g, recieved_msg.markers[i + 1].color.g);
EXPECT_LT(recieved_msg.markers[i].color.b, recieved_msg.markers[i + 1].color.b);
EXPECT_EQ(recieved_msg.markers[i].color.r, recieved_msg.markers[i + 1].color.r);
EXPECT_EQ(recieved_msg.markers[i].color.a, recieved_msg.markers[i + 1].color.a);
}
}
TEST(TrajectoryVisualizerTests, VisCandidateTrajectories)
{
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("my_node");
auto parameters_handler = std::make_unique<ParametersHandler>(node);
visualization_msgs::msg::MarkerArray recieved_msg;
auto my_sub = node->create_subscription<visualization_msgs::msg::MarkerArray>(
"/trajectories", 10,
[&](const visualization_msgs::msg::MarkerArray msg) {recieved_msg = msg;});
models::Trajectories candidate_trajectories;
candidate_trajectories.x = xt::ones<float>({200, 12});
candidate_trajectories.y = xt::ones<float>({200, 12});
candidate_trajectories.yaws = xt::ones<float>({200, 12});
TrajectoryVisualizer vis;
vis.on_configure(node, "my_name", "fkmap", parameters_handler.get());
vis.on_activate();
vis.add(candidate_trajectories, "Candidate Trajectories");
nav_msgs::msg::Path bogus_path;
vis.visualize(bogus_path);
rclcpp::spin_some(node->get_node_base_interface());
// 40 * 4, for 5 trajectory steps + 3 point steps
EXPECT_EQ(recieved_msg.markers.size(), 160u);
}
@@ -0,0 +1,247 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#pragma once
#include <memory>
#include <string>
#include <vector>
#include "nav2_costmap_2d/costmap_2d.hpp"
#include "nav2_costmap_2d/costmap_2d_ros.hpp"
#include <geometry_msgs/msg/pose_stamped.hpp>
#include <geometry_msgs/msg/twist.hpp>
#include <nav_msgs/msg/path.hpp>
#include <rclcpp/rclcpp.hpp>
#include <rclcpp_lifecycle/lifecycle_node.hpp>
#include "nav2_mppi_controller/motion_models.hpp"
#include "nav2_mppi_controller/optimizer.hpp"
#include "nav2_mppi_controller/tools/parameters_handler.hpp"
#include "nav2_mppi_controller/controller.hpp"
#include "models.hpp"
namespace detail
{
template<typename TMessage, typename TNode>
void setHeader(TMessage && msg, TNode node, std::string frame)
{
auto time = node->get_clock()->now();
msg.header.frame_id = frame;
msg.header.stamp = time;
}
} // namespace detail
/**
* Adds some parameters for the optimizer to a special container.
*
* @param params_ container for optimizer's parameters.
*/
void setUpOptimizerParams(
const TestOptimizerSettings & s,
const std::vector<std::string> & critics,
std::vector<rclcpp::Parameter> & params_, std::string node_name = std::string("dummy"))
{
constexpr double dummy_freq = 50.0;
params_.emplace_back(rclcpp::Parameter(node_name + ".iteration_count", s.iteration_count));
params_.emplace_back(rclcpp::Parameter(node_name + ".batch_size", s.batch_size));
params_.emplace_back(rclcpp::Parameter(node_name + ".time_steps", s.time_steps));
params_.emplace_back(rclcpp::Parameter(node_name + ".lookahead_dist", s.lookahead_distance));
params_.emplace_back(rclcpp::Parameter(node_name + ".motion_model", s.motion_model));
params_.emplace_back(rclcpp::Parameter(node_name + ".critics", critics));
params_.emplace_back(rclcpp::Parameter("controller_frequency", dummy_freq));
}
void setUpControllerParams(
bool visualize, std::vector<rclcpp::Parameter> & params_,
std::string node_name = std::string("dummy"))
{
double dummy_freq = 50.0;
params_.emplace_back(rclcpp::Parameter(node_name + ".visualize", visualize));
params_.emplace_back(rclcpp::Parameter("controller_frequency", dummy_freq));
}
rclcpp::NodeOptions getOptimizerOptions(
TestOptimizerSettings s,
const std::vector<std::string> & critics)
{
std::vector<rclcpp::Parameter> params;
rclcpp::NodeOptions options;
setUpOptimizerParams(s, critics, params);
options.parameter_overrides(params);
return options;
}
geometry_msgs::msg::Point getDummyPoint(double x, double y)
{
geometry_msgs::msg::Point point;
point.x = x;
point.y = y;
point.z = 0;
return point;
}
std::shared_ptr<nav2_costmap_2d::Costmap2DROS> getDummyCostmapRos()
{
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>("cost_map_node");
costmap_ros->on_configure(rclcpp_lifecycle::State{});
return costmap_ros;
}
std::shared_ptr<nav2_costmap_2d::Costmap2D> getDummyCostmap(TestCostmapSettings s)
{
auto costmap = std::make_shared<nav2_costmap_2d::Costmap2D>(
s.cells_x, s.cells_y, s.resolution, s.origin_x, s.origin_y, s.cost_map_default_value);
return costmap;
}
std::vector<geometry_msgs::msg::Point> getDummySquareFootprint(double a)
{
return {getDummyPoint(a, a), getDummyPoint(-a, -a), getDummyPoint(a, -a), getDummyPoint(-a, a)};
}
std::shared_ptr<nav2_costmap_2d::Costmap2DROS> getDummyCostmapRos(TestCostmapSettings s)
{
auto costmap_ros = getDummyCostmapRos();
auto costmap_ptr = costmap_ros->getCostmap();
auto costmap = getDummyCostmap(s);
*(costmap_ptr) = *costmap;
costmap_ros->setRobotFootprint(getDummySquareFootprint(s.footprint_size));
return costmap_ros;
}
std::shared_ptr<rclcpp_lifecycle::LifecycleNode>
getDummyNode(
TestOptimizerSettings s, std::vector<std::string> critics,
std::string node_name = std::string("dummy"))
{
auto node =
std::make_shared<rclcpp_lifecycle::LifecycleNode>(node_name, getOptimizerOptions(s, critics));
return node;
}
std::shared_ptr<rclcpp_lifecycle::LifecycleNode>
getDummyNode(rclcpp::NodeOptions options, std::string node_name = std::string("dummy"))
{
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>(node_name, options);
return node;
}
template<typename TNode, typename TCostMap, typename TParamHandler>
std::shared_ptr<mppi::Optimizer> getDummyOptimizer(
TNode node, TCostMap costmap_ros,
TParamHandler * params_handler)
{
std::shared_ptr<mppi::Optimizer> optimizer = std::make_shared<mppi::Optimizer>();
std::weak_ptr<rclcpp_lifecycle::LifecycleNode> weak_ptr_node{node};
optimizer->initialize(weak_ptr_node, node->get_name(), costmap_ros, params_handler);
return optimizer;
}
template<typename TNode, typename TCostMap, typename TFBuffer, typename TParamHandler>
mppi::PathHandler getDummyPathHandler(
TNode node, TCostMap costmap_ros, TFBuffer tf_buffer,
TParamHandler * params_handler)
{
mppi::PathHandler path_handler;
std::weak_ptr<rclcpp_lifecycle::LifecycleNode> weak_ptr_node{node};
path_handler.initialize(weak_ptr_node, node->get_name(), costmap_ros, tf_buffer, params_handler);
return path_handler;
}
template<typename TNode, typename TCostMap, typename TFBuffer>
std::shared_ptr<nav2_mppi_controller::MPPIController> getDummyController(
TNode node, TFBuffer tf_buffer,
TCostMap costmap_ros)
{
auto controller = std::make_shared<nav2_mppi_controller::MPPIController>();
std::weak_ptr<rclcpp_lifecycle::LifecycleNode> weak_ptr_node{node};
controller->configure(weak_ptr_node, node->get_name(), tf_buffer, costmap_ros);
controller->activate();
return controller;
}
auto getDummyTwist()
{
geometry_msgs::msg::Twist twist;
return twist;
}
template<typename TNode>
geometry_msgs::msg::PoseStamped
getDummyPointStamped(TNode & node, std::string frame = std::string("odom"))
{
geometry_msgs::msg::PoseStamped point;
detail::setHeader(point, node, frame);
return point;
}
template<typename TNode>
geometry_msgs::msg::PoseStamped getDummyPointStamped(TNode & node, TestPose pose)
{
geometry_msgs::msg::PoseStamped point = getDummyPointStamped(node);
point.pose.position.x = pose.x;
point.pose.position.y = pose.y;
return point;
}
template<typename TNode>
nav_msgs::msg::Path getDummyPath(TNode node, std::string frame = std::string("odom"))
{
nav_msgs::msg::Path path;
detail::setHeader(path, node, frame);
return path;
}
template<typename TNode>
auto getDummyPath(size_t points_count, TNode node)
{
auto path = getDummyPath(node);
for (size_t i = 0; i < points_count; i++) {
path.poses.push_back(getDummyPointStamped(node));
}
return path;
}
template<typename TNode>
nav_msgs::msg::Path getIncrementalDummyPath(TNode node, TestPathSettings s)
{
auto path = getDummyPath(node);
for (size_t i = 0; i < s.poses_count; i++) {
double x = s.start_pose.x + static_cast<double>(i) * s.step_x;
double y = s.start_pose.y + static_cast<double>(i) * s.step_y;
path.poses.push_back(getDummyPointStamped(node, TestPose{x, y}));
}
return path;
}
@@ -0,0 +1,75 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#pragma once
#include <vector>
#include <utility>
#include <string>
#include <rclcpp/rclcpp.hpp>
struct TestOptimizerSettings
{
int batch_size;
int time_steps;
int iteration_count;
double lookahead_distance;
std::string motion_model;
bool consider_footprint;
};
struct TestPose
{
double x;
double y;
};
struct TestCostmapSettings
{
const unsigned int cells_x = 40;
const unsigned int cells_y = 40;
const double origin_x = 0.0;
const double origin_y = 0.0;
const double resolution = 0.1;
const unsigned char cost_map_default_value = 0;
const double footprint_size = 0.15;
std::pair<unsigned int, unsigned int> getCenterIJ()
{
return {
cells_x / 2,
cells_y / 2};
}
TestPose getCenterPose()
{
return {
static_cast<double>(cells_x) * resolution / 2.0,
static_cast<double>(cells_y) * resolution / 2.0};
}
};
struct TestObstaclesSettings
{
unsigned int center_cells_x;
unsigned int center_cells_y;
unsigned int obstacle_size;
unsigned char obstacle_cost;
};
struct TestPathSettings
{
TestPose start_pose;
unsigned int poses_count;
double step_x;
double step_y;
};
@@ -0,0 +1,248 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#pragma once
#include <memory>
#include <string>
#include <vector>
#include <iostream>
#include <string_view>
#include <rclcpp/executors.hpp>
#include "tf2_ros/transform_broadcaster.h"
#include "nav2_costmap_2d/costmap_2d.hpp"
#include "nav2_costmap_2d/costmap_2d_ros.hpp"
#include "models.hpp"
#include "factory.hpp"
using namespace std::chrono_literals; // NOLINT
template<typename TNode>
void waitSome(const std::chrono::nanoseconds & duration, TNode & node)
{
rclcpp::Time start_time = node->now();
while (rclcpp::ok() && node->now() - start_time <= rclcpp::Duration(duration)) {
rclcpp::spin_some(node->get_node_base_interface());
std::this_thread::sleep_for(3ms);
}
}
void sendTf(
std::string_view source, std::string_view dest,
std::shared_ptr<tf2_ros::TransformBroadcaster> tf_broadcaster,
std::shared_ptr<rclcpp_lifecycle::LifecycleNode> node, size_t n)
{
while (--n != 0u) {
auto t = geometry_msgs::msg::TransformStamped();
t.header.frame_id = source;
t.child_frame_id = dest;
t.header.stamp = node->now() + rclcpp::Duration(3ms);
t.transform.translation.x = 0.0;
t.transform.translation.y = 0.0;
t.transform.translation.z = 0.0;
t.transform.rotation.x = 0.0;
t.transform.rotation.y = 0.0;
t.transform.rotation.z = 0.0;
t.transform.rotation.w = 1.0;
tf_broadcaster->sendTransform(t);
// Allow tf_buffer_ to be filled by listener
waitSome(10ms, node);
}
}
/**
* Print costmap to stdout.
* @param costmap map to be printed.
*/
void printMap(const nav2_costmap_2d::Costmap2D & costmap)
{
for (unsigned int i = 0; i < costmap.getSizeInCellsY(); i++) {
for (unsigned int j = 0; j < costmap.getSizeInCellsX(); j++) {
printf("%4d", static_cast<int>(costmap.getCost(j, i)));
}
printf("\n\n");
}
}
/**
* Print costmap with trajectory and goal point to stdout.
* @param costmap map to be printed.
* @param trajectory trajectory container (xt::tensor) to be printed.
* @param goal_point goal point to be printed.
*/
template<typename TTrajectory>
void printMapWithTrajectoryAndGoal(
nav2_costmap_2d::Costmap2D & costmap, const TTrajectory & trajectory,
const geometry_msgs::msg::PoseStamped & goal)
{
const unsigned int trajectory_cost = 1;
const unsigned int goal_cost = 2;
std::cout << "Costmap: \n trajectory = " << trajectory_cost << "\n goal = " << goal_cost
<< "\n obsctacle = 255 \n";
// create new costmap
nav2_costmap_2d::Costmap2D costmap2d(
costmap.getSizeInCellsX(), costmap.getSizeInCellsY(), costmap.getResolution(),
costmap.getOriginX(), costmap.getOriginY(), costmap.getDefaultValue());
// copy obstacles from original costmap
costmap2d = costmap;
// add trajectory on map
unsigned int point_mx = 0;
unsigned int point_my = 0;
for (size_t i = 0; i < trajectory.shape()[0]; ++i) {
costmap2d.worldToMap(trajectory(i, 0), trajectory(i, 1), point_mx, point_my);
costmap2d.setCost(point_mx, point_my, trajectory_cost);
}
unsigned int goal_j{0};
unsigned int goal_i{0};
costmap2d.worldToMap(goal.pose.position.x, goal.pose.position.y, goal_j, goal_i);
std::cout << "Goal Position: " << goal_j << " " << goal_i << "\n";
costmap2d.setCost(goal_j, goal_i, goal_cost);
printMap(costmap2d);
}
/**
* Add a square obstacle to the costmap.
* @param costmap map to be modified.
* @param upper_left_corner_x obstacle upper left corner X coord (on the
* costmap).
* @param upper_left_corner_y obstacle upper left corner Y coord (on the
* costmap).
* @param size obstacle side size.
* @param cost obstacle value on costmap.
*/
void addObstacle(
nav2_costmap_2d::Costmap2D * costmap, unsigned int upper_left_corner_x,
unsigned int upper_left_corner_y, unsigned int size, unsigned char cost)
{
for (unsigned int i = upper_left_corner_x; i < upper_left_corner_x + size; i++) {
for (unsigned int j = upper_left_corner_y; j < upper_left_corner_y + size; j++) {
costmap->setCost(i, j, cost);
}
}
}
void printInfo(
TestOptimizerSettings os, TestPathSettings ps,
const std::vector<std::string> & critics)
{
std::stringstream ss;
for (auto str : critics) {
ss << str << " ";
}
std::cout << //
"\n\n--------------------OPTIMIZER OPTIONS-----------------------------\n" <<
"Critics: " << ss.str() << "\n" \
"Motion model: " << os.motion_model << "\n"
"Consider footprint: " << os.consider_footprint << "\n" <<
"Iterations: " << os.iteration_count << "\n" <<
"Batch size: " << os.batch_size << "\n" <<
"Time steps: " << os.time_steps << "\n" <<
"Path points: " << ps.poses_count << "\n" <<
"\n-------------------------------------------------------------------\n\n";
}
void addObstacle(nav2_costmap_2d::Costmap2D * costmap, TestObstaclesSettings s)
{
addObstacle(costmap, s.center_cells_x, s.center_cells_y, s.obstacle_size, s.obstacle_cost);
}
/**
* Check the trajectory for collisions with obstacles on the map.
* @param trajectory trajectory container (xt::tensor) to be checked.
* @param costmap costmap with obstacles
* @return true - if the trajectory crosses an obstacle on the map, false - if
* not
*/
template<typename TTrajectory>
bool inCollision(const TTrajectory & trajectory, const nav2_costmap_2d::Costmap2D & costmap)
{
unsigned int point_mx = 0;
unsigned int point_my = 0;
for (size_t i = 0; i < trajectory.shape(0); ++i) {
costmap.worldToMap(trajectory(i, 0), trajectory(i, 1), point_mx, point_my);
auto cost_ = costmap.getCost(point_mx, point_my);
if (cost_ > nav2_costmap_2d::FREE_SPACE || cost_ == nav2_costmap_2d::NO_INFORMATION) {
return true;
}
}
return false;
}
unsigned char getCost(const nav2_costmap_2d::Costmap2D & costmap, double x, double y)
{
unsigned int point_mx = 0;
unsigned int point_my = 0;
costmap.worldToMap(x, y, point_mx, point_my);
return costmap.getCost(point_mx, point_my);
}
template<typename TTrajectory>
bool isGoalReached(
const TTrajectory & trajectory, const nav2_costmap_2d::Costmap2D & costmap,
const geometry_msgs::msg::PoseStamped & goal)
{
unsigned int trajectory_j = 0;
unsigned int trajectory_i = 0;
unsigned int goal_j = 0;
unsigned int goal_i = 0;
costmap.worldToMap(goal.pose.position.x, goal.pose.position.y, goal_j, goal_i);
auto match = [](unsigned int i, unsigned int j, unsigned int i_dst, unsigned int j_dst) {
if (i == i_dst && j == j_dst) {
return true;
}
return false;
};
auto match_near = [&](unsigned int i, unsigned int j) {
if (match(i, j, goal_i, goal_j) ||
match(i, j, goal_i + 1, goal_j) ||
match(i, j, goal_i - 1, goal_j) ||
match(i, j, goal_i, goal_j + 1) ||
match(i, j, goal_i, goal_j - 1) ||
match(i, j, goal_i + 1, goal_j + 1) ||
match(i, j, goal_i + 1, goal_j - 1) ||
match(i, j, goal_i - 1, goal_j + 1) ||
match(i, j, goal_i - 1, goal_j - 1))
{
return true;
}
return false;
};
// clang-format on
for (size_t i = 0; i < trajectory.shape(0); ++i) {
costmap.worldToMap(trajectory(i, 0), trajectory(i, 1), trajectory_j, trajectory_i);
if (match_near(trajectory_i, trajectory_j)) {
return true;
}
}
return false;
}
@@ -0,0 +1,445 @@
// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
//
// 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.
#include <chrono>
#include <thread>
#include <xtensor/xrandom.hpp>
#include "gtest/gtest.h"
#include "rclcpp/rclcpp.hpp"
#include "nav2_mppi_controller/tools/utils.hpp"
#include "nav2_mppi_controller/models/path.hpp"
// Tests noise generator object
class RosLockGuard
{
public:
RosLockGuard() {rclcpp::init(0, nullptr);}
~RosLockGuard() {rclcpp::shutdown();}
};
RosLockGuard g_rclcpp;
using namespace mppi::utils; // NOLINT
using namespace mppi; // NOLINT
class TestGoalChecker : public nav2_core::GoalChecker
{
public:
TestGoalChecker() {}
virtual void initialize(
const rclcpp_lifecycle::LifecycleNode::WeakPtr & /*parent*/,
const std::string & /*plugin_name*/,
const std::shared_ptr<nav2_costmap_2d::Costmap2DROS>/*costmap_ros*/) {}
virtual void reset() {}
virtual bool isGoalReached(
const geometry_msgs::msg::Pose & /*query_pose*/,
const geometry_msgs::msg::Pose & /*goal_pose*/,
const geometry_msgs::msg::Twist & /*velocity*/) {return false;}
virtual bool getTolerances(
geometry_msgs::msg::Pose & pose_tolerance,
geometry_msgs::msg::Twist & /*vel_tolerance*/)
{
pose_tolerance.position.x = 0.25;
pose_tolerance.position.y = 0.25;
return true;
}
};
TEST(UtilsTests, MarkerPopulationUtils)
{
auto pose = createPose(1.0, 2.0, 3.0);
EXPECT_EQ(pose.position.x, 1.0);
EXPECT_EQ(pose.position.y, 2.0);
EXPECT_EQ(pose.position.z, 3.0);
EXPECT_EQ(pose.orientation.w, 1.0);
auto scale = createScale(1.0, 2.0, 3.0);
EXPECT_EQ(scale.x, 1.0);
EXPECT_EQ(scale.y, 2.0);
EXPECT_EQ(scale.z, 3.0);
auto color = createColor(1.0, 2.0, 3.0, 0.0);
EXPECT_EQ(color.r, 1.0);
EXPECT_EQ(color.g, 2.0);
EXPECT_EQ(color.b, 3.0);
EXPECT_EQ(color.a, 0.0);
auto marker = createMarker(999, pose, scale, color, "map", "ns");
EXPECT_EQ(marker.header.frame_id, "map");
EXPECT_EQ(marker.id, 999);
EXPECT_EQ(marker.pose, pose);
EXPECT_EQ(marker.scale, scale);
EXPECT_EQ(marker.color, color);
EXPECT_EQ(marker.ns, "ns");
}
TEST(UtilsTests, ConversionTests)
{
geometry_msgs::msg::TwistStamped output;
builtin_interfaces::msg::Time time;
// Check population is correct
output = toTwistStamped(0.5, 0.3, time, "map");
EXPECT_NEAR(output.twist.linear.x, 0.5, 1e-6);
EXPECT_NEAR(output.twist.linear.y, 0.0, 1e-6);
EXPECT_NEAR(output.twist.angular.z, 0.3, 1e-6);
EXPECT_EQ(output.header.frame_id, "map");
EXPECT_EQ(output.header.stamp, time);
output = toTwistStamped(0.5, 0.4, 0.3, time, "map");
EXPECT_NEAR(output.twist.linear.x, 0.5, 1e-6);
EXPECT_NEAR(output.twist.linear.y, 0.4, 1e-6);
EXPECT_NEAR(output.twist.angular.z, 0.3, 1e-6);
EXPECT_EQ(output.header.frame_id, "map");
EXPECT_EQ(output.header.stamp, time);
nav_msgs::msg::Path path;
path.poses.resize(5);
path.poses[2].pose.position.x = 5;
path.poses[2].pose.position.y = 50;
models::Path path_t = toTensor(path);
// Check population is correct
EXPECT_EQ(path_t.x.shape(0), 5u);
EXPECT_EQ(path_t.y.shape(0), 5u);
EXPECT_EQ(path_t.yaws.shape(0), 5u);
EXPECT_EQ(path_t.x(2), 5);
EXPECT_EQ(path_t.y(2), 50);
EXPECT_NEAR(path_t.yaws(2), 0.0, 1e-6);
}
TEST(UtilsTests, WithTolTests)
{
geometry_msgs::msg::Pose pose;
pose.position.x = 10.0;
pose.position.y = 1.0;
nav2_core::GoalChecker * goal_checker = new TestGoalChecker;
nav_msgs::msg::Path path;
path.poses.resize(2);
geometry_msgs::msg::Pose & goal = path.poses.back().pose;
// Create CriticData with state and goal initialized
models::State state;
state.pose.pose = pose;
models::Trajectories generated_trajectories;
models::Path path_critic;
xt::xtensor<float, 1> costs;
float model_dt;
CriticData data = {
state, generated_trajectories, path_critic, goal,
costs, model_dt, false, nullptr, nullptr, std::nullopt, std::nullopt};
// Test not in tolerance
goal.position.x = 0.0;
goal.position.y = 0.0;
EXPECT_FALSE(withinPositionGoalTolerance(goal_checker, pose, goal));
EXPECT_FALSE(withinPositionGoalTolerance(0.25, pose, goal));
// Test in tolerance
goal.position.x = 9.8;
goal.position.y = 0.95;
EXPECT_TRUE(withinPositionGoalTolerance(goal_checker, pose, goal));
EXPECT_TRUE(withinPositionGoalTolerance(0.25, pose, goal));
goal.position.x = 10.0;
goal.position.y = 0.76;
EXPECT_TRUE(withinPositionGoalTolerance(goal_checker, pose, goal));
EXPECT_TRUE(withinPositionGoalTolerance(0.25, pose, goal));
goal.position.x = 9.76;
goal.position.y = 1.0;
EXPECT_TRUE(withinPositionGoalTolerance(goal_checker, pose, goal));
EXPECT_TRUE(withinPositionGoalTolerance(0.25, pose, goal));
delete goal_checker;
goal_checker = nullptr;
EXPECT_FALSE(withinPositionGoalTolerance(goal_checker, pose, goal));
}
TEST(UtilsTests, AnglesTests)
{
// Test angle normalization by creating insane angles
xt::xtensor<float, 1> angles, zero_angles;
angles = xt::ones<float>({100});
for (unsigned int i = 0; i != angles.shape(0); i++) {
angles(i) = i * i;
if (i % 2 == 0) {
angles(i) *= -1;
}
}
auto norm_ang = normalize_angles(angles);
for (unsigned int i = 0; i != norm_ang.shape(0); i++) {
EXPECT_TRUE((norm_ang(i) >= -M_PI) && (norm_ang(i) <= M_PI));
}
// Test shortest angular distance
zero_angles = xt::zeros<float>({100});
auto ang_dist = shortest_angular_distance(angles, zero_angles);
for (unsigned int i = 0; i != ang_dist.shape(0); i++) {
EXPECT_TRUE((ang_dist(i) >= -M_PI) && (ang_dist(i) <= M_PI));
}
// Test point-pose angle
geometry_msgs::msg::Pose pose;
pose.position.x = 0.0;
pose.position.y = 0.0;
pose.orientation.w = 1.0;
double point_x = 1.0, point_y = 0.0;
bool forward_preference = true;
EXPECT_NEAR(posePointAngle(pose, point_x, point_y, forward_preference), 0.0, 1e-6);
forward_preference = false;
EXPECT_NEAR(posePointAngle(pose, point_x, point_y, forward_preference), 0.0, 1e-6);
point_x = -1.0;
EXPECT_NEAR(posePointAngle(pose, point_x, point_y, forward_preference), 0.0, 1e-6);
forward_preference = true;
EXPECT_NEAR(posePointAngle(pose, point_x, point_y, forward_preference), M_PI, 1e-6);
}
TEST(UtilsTests, FurthestAndClosestReachedPoint)
{
models::State state;
models::Trajectories generated_trajectories;
models::Path path;
geometry_msgs::msg::Pose goal;
xt::xtensor<float, 1> costs;
float model_dt = 0.1;
CriticData data =
{state, generated_trajectories, path, goal, costs, model_dt, false, nullptr, nullptr,
std::nullopt, std::nullopt}; /// Caution, keep references
// Attempt to set furthest point if notionally set, should not change
data.furthest_reached_path_point = 99999;
setPathFurthestPointIfNotSet(data);
EXPECT_EQ(data.furthest_reached_path_point, 99999);
// Attempt to set if not set already with no other information, should fail
CriticData data2 =
{state, generated_trajectories, path, goal, costs, model_dt, false, nullptr, nullptr,
std::nullopt, std::nullopt}; /// Caution, keep references
setPathFurthestPointIfNotSet(data2);
EXPECT_EQ(data2.furthest_reached_path_point, 0);
// Test the actual computation of the path point reached
generated_trajectories.x = xt::ones<float>({100, 2});
generated_trajectories.y = xt::zeros<float>({100, 2});
generated_trajectories.yaws = xt::zeros<float>({100, 2});
nav_msgs::msg::Path plan;
plan.poses.resize(10);
for (unsigned int i = 0; i != plan.poses.size(); i++) {
plan.poses[i].pose.position.x = 0.2 * i;
plan.poses[i].pose.position.y = 0.0;
}
path = toTensor(plan);
CriticData data3 =
{state, generated_trajectories, path, goal, costs, model_dt, false, nullptr, nullptr,
std::nullopt, std::nullopt}; /// Caution, keep references
EXPECT_EQ(findPathFurthestReachedPoint(data3), 5u);
EXPECT_EQ(findPathTrajectoryInitialPoint(data3), 5u);
}
TEST(UtilsTests, findPathCosts)
{
models::State state;
models::Trajectories generated_trajectories;
models::Path path;
geometry_msgs::msg::Pose goal;
xt::xtensor<float, 1> costs;
float model_dt = 0.1;
CriticData data =
{state, generated_trajectories, path, goal, costs, model_dt, false, nullptr, nullptr,
std::nullopt, std::nullopt}; /// Caution, keep references
// Test not set if already set, should not change
data.path_pts_valid = std::vector<bool>(10, false);
for (unsigned int i = 0; i != 10; i++) {
(*data.path_pts_valid)[i] = false;
}
EXPECT_TRUE(data.path_pts_valid);
setPathCostsIfNotSet(data, nullptr);
EXPECT_EQ(data.path_pts_valid->size(), 10u);
CriticData data3 =
{state, generated_trajectories, path, goal, costs, model_dt, false, nullptr, nullptr,
std::nullopt, std::nullopt}; /// Caution, keep references
auto costmap_ros = std::make_shared<nav2_costmap_2d::Costmap2DROS>(
"dummy_costmap", "", "dummy_costmap");
rclcpp_lifecycle::State lstate;
costmap_ros->on_configure(lstate);
auto * costmap = costmap_ros->getCostmap();
// island in the middle of lethal cost to cross. Costmap defaults to size 5x5 @ 10cm resolution
for (unsigned int i = 10; i <= 30; ++i) { // 1m-3m
for (unsigned int j = 10; j <= 30; ++j) { // 1m-3m
costmap->setCost(i, j, 254);
}
}
for (unsigned int i = 40; i <= 45; ++i) { // 4m-4.5m
for (unsigned int j = 45; j <= 45; ++j) { // 4m-4.5m
costmap->setCost(i, j, 253);
}
}
path.reset(50);
path.x(1) = 999999999; // OFF COSTMAP
path.y(1) = 999999999;
path.x(10) = 1.5; // IN LETHAL
path.y(10) = 1.5;
path.x(20) = 4.2; // IN INFLATED
path.y(20) = 4.2;
// This should be evaluated and have real outputs now
setPathCostsIfNotSet(data3, costmap_ros);
EXPECT_TRUE(data3.path_pts_valid.has_value());
for (unsigned int i = 0; i != path.x.shape(0) - 1; i++) {
if (i == 1 || i == 10) {
EXPECT_FALSE((*data3.path_pts_valid)[i]);
} else {
EXPECT_TRUE((*data3.path_pts_valid)[i]);
}
}
}
TEST(UtilsTests, SmootherTest)
{
models::ControlSequence noisey_sequence, sequence_init;
noisey_sequence.vx = 0.2 * xt::ones<float>({30});
noisey_sequence.vy = 0.0 * xt::ones<float>({30});
noisey_sequence.wz = 0.3 * xt::ones<float>({30});
// Make the sequence noisy
auto noises = xt::random::randn<float>({30}, 0.0, 0.2);
noisey_sequence.vx += noises;
noisey_sequence.vy += noises;
noisey_sequence.wz += noises;
sequence_init = noisey_sequence;
std::array<mppi::models::Control, 4> history, history_init;
history[3].vx = 0.1;
history[3].vy = 0.0;
history[3].wz = 0.3;
history[2].vx = 0.1;
history[2].vy = 0.0;
history[2].wz = 0.3;
history[1].vx = 0.1;
history[1].vy = 0.0;
history[1].wz = 0.3;
history[0].vx = 0.0;
history[0].vy = 0.0;
history[0].wz = 0.0;
history_init = history;
models::OptimizerSettings settings;
settings.shift_control_sequence = false; // so result stores 0th value in history
savitskyGolayFilter(noisey_sequence, history, settings);
// Check history is propogated backward
EXPECT_NEAR(history_init[3].vx, history[2].vx, 0.02);
EXPECT_NEAR(history_init[3].vy, history[2].vy, 0.02);
EXPECT_NEAR(history_init[3].wz, history[2].wz, 0.02);
// Check history element is updated for first command
EXPECT_NEAR(history[3].vx, 0.2, 0.05);
EXPECT_NEAR(history[3].vy, 0.0, 0.035);
EXPECT_NEAR(history[3].wz, 0.23, 0.02);
// Check that path is smoother
float smoothed_val{0}, original_val{0};
for (unsigned int i = 0; i != noisey_sequence.vx.shape(0); i++) {
smoothed_val += fabs(noisey_sequence.vx(i) - 0.2);
smoothed_val += fabs(noisey_sequence.vy(i) - 0.0);
smoothed_val += fabs(noisey_sequence.wz(i) - 0.3);
original_val += fabs(sequence_init.vx(i) - 0.2);
original_val += fabs(sequence_init.vy(i) - 0.0);
original_val += fabs(sequence_init.wz(i) - 0.3);
}
EXPECT_LT(smoothed_val, original_val);
}
TEST(UtilsTests, FindPathInversionTest)
{
// Straight path, no inversions to be found
nav_msgs::msg::Path path;
for (unsigned int i = 0; i != 10; i++) {
geometry_msgs::msg::PoseStamped pose;
pose.pose.position.x = i;
path.poses.push_back(pose);
}
EXPECT_EQ(utils::findFirstPathInversion(path), 10u);
// To short to process
path.poses.erase(path.poses.begin(), path.poses.begin() + 7);
EXPECT_EQ(utils::findFirstPathInversion(path), 3u);
// Has inversion at index 10, so should return 11 for the first point afterwards
// 0 1 2 3 4 5 6 7 8 9 10 **9** 8 7 6 5 4 3 2 1
path.poses.clear();
for (unsigned int i = 0; i != 10; i++) {
geometry_msgs::msg::PoseStamped pose;
pose.pose.position.x = i;
path.poses.push_back(pose);
}
for (unsigned int i = 0; i != 10; i++) {
geometry_msgs::msg::PoseStamped pose;
pose.pose.position.x = 10 - i;
path.poses.push_back(pose);
}
EXPECT_EQ(utils::findFirstPathInversion(path), 11u);
}
TEST(UtilsTests, RemovePosesAfterPathInversionTest)
{
nav_msgs::msg::Path path;
// straight path
for (unsigned int i = 0; i != 10; i++) {
geometry_msgs::msg::PoseStamped pose;
pose.pose.position.x = i;
path.poses.push_back(pose);
}
EXPECT_EQ(utils::removePosesAfterFirstInversion(path), 0u);
// try empty path
path.poses.clear();
EXPECT_EQ(utils::removePosesAfterFirstInversion(path), 0u);
// cusping path
for (unsigned int i = 0; i != 10; i++) {
geometry_msgs::msg::PoseStamped pose;
pose.pose.position.x = i;
path.poses.push_back(pose);
}
for (unsigned int i = 0; i != 10; i++) {
geometry_msgs::msg::PoseStamped pose;
pose.pose.position.x = 10 - i;
path.poses.push_back(pose);
}
EXPECT_EQ(utils::removePosesAfterFirstInversion(path), 11u);
// Check to see if removed
EXPECT_EQ(path.poses.size(), 11u);
EXPECT_EQ(path.poses.back().pose.position.x, 10);
}