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,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);
}