From 4048c9a45ba49f8191e25858cd8177eee634c06f Mon Sep 17 00:00:00 2001 From: Jakubach Date: Wed, 14 Aug 2024 16:23:08 +0200 Subject: [PATCH 01/51] Added promixity BT node and BT tree --- nav2_behavior_tree/CMakeLists.txt | 3 + .../condition/is_goal_nearby_condition.hpp | 81 +++++++++++++++++++ .../condition/is_goal_nearby_condition.cpp | 42 ++++++++++ ...planning_path_if_not_in_goal_proximity.xml | 46 +++++++++++ 4 files changed, 172 insertions(+) create mode 100644 nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/is_goal_nearby_condition.hpp create mode 100644 nav2_behavior_tree/plugins/condition/is_goal_nearby_condition.cpp create mode 100644 nav2_bt_navigator/behavior_trees/navigate_w_recovery_and_replanning_path_if_not_in_goal_proximity.xml diff --git a/nav2_behavior_tree/CMakeLists.txt b/nav2_behavior_tree/CMakeLists.txt index 8f45cd318f3..a726cefc6fc 100644 --- a/nav2_behavior_tree/CMakeLists.txt +++ b/nav2_behavior_tree/CMakeLists.txt @@ -209,6 +209,9 @@ list(APPEND plugin_libs nav2_progress_checker_selector_bt_node) add_library(nav2_goal_updated_controller_bt_node SHARED plugins/decorator/goal_updated_controller.cpp) list(APPEND plugin_libs nav2_goal_updated_controller_bt_node) +add_library(nav2_is_goal_nearby_condition_bt_node SHARED plugins/condition/is_goal_nearby_condition.cpp) +list(APPEND plugin_libs nav2_is_goal_nearby_condition_bt_node) + foreach(bt_plugin ${plugin_libs}) target_include_directories(${bt_plugin} PRIVATE diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/is_goal_nearby_condition.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/is_goal_nearby_condition.hpp new file mode 100644 index 00000000000..b434e75f1d3 --- /dev/null +++ b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/is_goal_nearby_condition.hpp @@ -0,0 +1,81 @@ +// Copyright (c) 2024 Jakub ChudziƄski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NAV2_BEHAVIOR_TREE__PLUGINS__CONDITION__IS_GOAL_NEARBY_CONDITION_HPP_ +#define NAV2_BEHAVIOR_TREE__PLUGINS__CONDITION__IS_GOAL_NEARBY_CONDITION_HPP_ + +#include +#include "nav_msgs/msg/path.hpp" + +#include "rclcpp/rclcpp.hpp" +#include "behaviortree_cpp/condition_node.h" + + +namespace nav2_behavior_tree +{ + +/** + * @brief A BT::ConditionNode that returns SUCCESS when the IsGoalNearby + * service returns true and FAILURE otherwise + */ +class IsGoalNearbyCondition : public BT::ConditionNode +{ +public: + /** + * @brief A constructor for nav2_behavior_tree::IsGoalNearbyCondition + * @param condition_name Name for the XML tag for this node + * @param conf BT node configuration + */ + IsGoalNearbyCondition( + const std::string & condition_name, + const BT::NodeConfiguration & conf); + + IsGoalNearbyCondition() = delete; + + /** + * @brief The main override required by a BT action + * @return BT::NodeStatus Status of tick execution + */ + BT::NodeStatus tick() override; + + /** + * @brief Creates list of BT ports + * @return BT::PortsList Containing node-specific ports + */ + static BT::PortsList providedPorts() + { + return { + BT::InputPort("path", "Planned Path"), + BT::InputPort( + "proximity_threshold", 3.0, + "Proximity length (m) of the remaining path considered as a nearby"), + }; + } + +private: + + /** + * @brief Checks if the robot is in the goal proximity + * @param goal_path current planned path to the goal + * @param prox_thr proximity length (m) of the remaining path considered as a nearby + * @return whether the robot is in the goal proximity + */ + bool isRobotInGoalProximity( + const nav_msgs::msg::Path& goal_path, + const double& prox_thr); +}; + +} // namespace nav2_behavior_tree + +#endif // NAV2_BEHAVIOR_TREE__PLUGINS__CONDITION__IS_GOAL_NEARBY_CONDITION_HPP_ \ No newline at end of file diff --git a/nav2_behavior_tree/plugins/condition/is_goal_nearby_condition.cpp b/nav2_behavior_tree/plugins/condition/is_goal_nearby_condition.cpp new file mode 100644 index 00000000000..5b2855ebefd --- /dev/null +++ b/nav2_behavior_tree/plugins/condition/is_goal_nearby_condition.cpp @@ -0,0 +1,42 @@ +#include "nav2_behavior_tree/plugins/condition/is_goal_nearby_condition.hpp" +#include "nav2_util/geometry_utils.hpp" + +namespace nav2_behavior_tree +{ + +IsGoalNearbyCondition::IsGoalNearbyCondition( + const std::string & condition_name, + const BT::NodeConfiguration & conf) +: BT::ConditionNode(condition_name, conf) +{ +} + +bool IsGoalNearbyCondition::isRobotInGoalProximity( + const nav_msgs::msg::Path& goal_path, + const double& prox_thr) +{ + return nav2_util::geometry_utils::calculate_path_length(goal_path, 0) < prox_thr; +} + + +BT::NodeStatus IsGoalNearbyCondition::tick() +{ + nav_msgs::msg::Path path; + double prox_thr; + getInput("path", path); + getInput("proximity_threshold", prox_thr); + if (!path.poses.empty()){ + if(isRobotInGoalProximity(path,prox_thr)){ + return BT::NodeStatus::SUCCESS; + } + } + return BT::NodeStatus::FAILURE; +} + +} // namespace nav2_behavior_tree + +#include "behaviortree_cpp/bt_factory.h" +BT_REGISTER_NODES(factory) +{ + factory.registerNodeType("IsGoalNearby"); +} diff --git a/nav2_bt_navigator/behavior_trees/navigate_w_recovery_and_replanning_path_if_not_in_goal_proximity.xml b/nav2_bt_navigator/behavior_trees/navigate_w_recovery_and_replanning_path_if_not_in_goal_proximity.xml new file mode 100644 index 00000000000..42030c00b0d --- /dev/null +++ b/nav2_bt_navigator/behavior_trees/navigate_w_recovery_and_replanning_path_if_not_in_goal_proximity.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From a411a508981783d5dac5fad3567d482480b7b0ab Mon Sep 17 00:00:00 2001 From: Steve Macenski Date: Tue, 29 Jul 2025 17:18:50 -0700 Subject: [PATCH 02/51] CI Limits: Moving MPPI, Smac to secondary job (#5401) * Moving MPPI, Smac to secondary job Signed-off-by: SteveMacenski * Use xlarge resource class Signed-off-by: SteveMacenski * Bump the cache Signed-off-by: SteveMacenski * back to large Signed-off-by: SteveMacenski * fix Signed-off-by: SteveMacenski * Add route server Signed-off-by: SteveMacenski * Shift more packages over Signed-off-by: SteveMacenski --------- Signed-off-by: SteveMacenski --- .circleci/config.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d6766347816..295c319a739 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -33,12 +33,12 @@ _commands: - restore_cache: name: Restore Cache << parameters.key >> keys: - - "<< parameters.key >>-v36\ + - "<< parameters.key >>-v38\ -{{ arch }}\ -{{ .Branch }}\ -{{ .Environment.CIRCLE_PR_NUMBER }}\ -{{ checksum \"<< parameters.workspace >>/lockfile.txt\" }}" - - "<< parameters.key >>-v36\ + - "<< parameters.key >>-v38\ -{{ arch }}\ -main\ -\ @@ -58,7 +58,7 @@ _commands: steps: - save_cache: name: Save Cache << parameters.key >> - key: "<< parameters.key >>-v36\ + key: "<< parameters.key >>-v38\ -{{ arch }}\ -{{ .Branch }}\ -{{ .Environment.CIRCLE_PR_NUMBER }}\ @@ -532,7 +532,7 @@ jobs: _parameters: release_parameters: &release_parameters - packages_skip_regex: "nav2_system_tests" + packages_skip_regex: "'(nav2_system_tests|nav2_smac_planner|nav2_mppi_controller|nav2_route|nav2_rviz_plugins|nav2_bringup|navigation2)'" workflows: version: 2 From f07bfc617f54bb329366eceb339635f1546aacef Mon Sep 17 00:00:00 2001 From: Guillaume Doisy Date: Thu, 31 Jul 2025 20:40:07 +0200 Subject: [PATCH 03/51] [DEX] Enforce 3 digits precision (#5398) Signed-off-by: Guillaume Doisy Co-authored-by: Guillaume Doisy --- .../include/nav2_map_server/map_io.hpp | 8 +++++++ nav2_map_server/src/map_io.cpp | 24 ++++++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/nav2_map_server/include/nav2_map_server/map_io.hpp b/nav2_map_server/include/nav2_map_server/map_io.hpp index 97dc81821c9..5fc34937ee1 100644 --- a/nav2_map_server/include/nav2_map_server/map_io.hpp +++ b/nav2_map_server/include/nav2_map_server/map_io.hpp @@ -98,6 +98,14 @@ bool saveMapToFile( const nav_msgs::msg::OccupancyGrid & map, const SaveParameters & save_parameters); +/** + * @brief to_string_with_precision + * @param value + * @param precision + * @return + */ +std::string to_string_with_precision(double value, int precision); + /** * @brief Expand ~/ to home user dir. * @param yaml_filename Name of input YAML file. diff --git a/nav2_map_server/src/map_io.cpp b/nav2_map_server/src/map_io.cpp index e95d192a9af..989f38f1211 100644 --- a/nav2_map_server/src/map_io.cpp +++ b/nav2_map_server/src/map_io.cpp @@ -594,13 +594,15 @@ void tryWriteMapToFile( std::string image_name = mapdatafile.substr(file_name_index + 1); YAML::Emitter e; - e << YAML::Precision(3); + e << YAML::Precision(7); e << YAML::BeginMap; e << YAML::Key << "image" << YAML::Value << image_name; e << YAML::Key << "mode" << YAML::Value << map_mode_to_string(save_parameters.mode); - e << YAML::Key << "resolution" << YAML::Value << map.info.resolution; - e << YAML::Key << "origin" << YAML::Flow << YAML::BeginSeq << map.info.origin.position.x << - map.info.origin.position.y << yaw << YAML::EndSeq; + e << YAML::Key << "resolution" << YAML::Value << to_string_with_precision(map.info.resolution, + 3); + e << YAML::Key << "origin" << YAML::Flow << YAML::BeginSeq << + to_string_with_precision(map.info.origin.position.x, 3) << + to_string_with_precision(map.info.origin.position.y, 3) << yaw << YAML::EndSeq; e << YAML::Key << "negate" << YAML::Value << 0; if (save_parameters.mode == MapMode::Trinary) { @@ -611,8 +613,10 @@ void tryWriteMapToFile( e << YAML::Key << "occupied_thresh" << YAML::Value << 0.65; e << YAML::Key << "free_thresh" << YAML::Value << 0.196; } else { - e << YAML::Key << "occupied_thresh" << YAML::Value << save_parameters.occupied_thresh; - e << YAML::Key << "free_thresh" << YAML::Value << save_parameters.free_thresh; + e << YAML::Key << "occupied_thresh" << YAML::Value << + to_string_with_precision(save_parameters.occupied_thresh, 3); + e << YAML::Key << "free_thresh" << YAML::Value << + to_string_with_precision(save_parameters.free_thresh, 3); } if (!e.good()) { @@ -648,4 +652,12 @@ bool saveMapToFile( return true; } +std::string to_string_with_precision(double value, int precision) +{ + std::ostringstream out; + out << std::fixed << std::setprecision(precision) << value; + + return out.str(); +} + } // namespace nav2_map_server From fba0358ab241a286286e659bc4afd9a0cbdbab98 Mon Sep 17 00:00:00 2001 From: Guillaume Doisy Date: Thu, 31 Jul 2025 20:40:20 +0200 Subject: [PATCH 04/51] [static_layer] limit comparison precision (#5405) * [DEX] limit comparison precision Signed-off-by: Guillaume Doisy * EPSILON 1e-5 Signed-off-by: Guillaume Doisy --------- Signed-off-by: Guillaume Doisy Co-authored-by: Guillaume Doisy --- .../include/nav2_costmap_2d/static_layer.hpp | 9 +++++++ nav2_costmap_2d/plugins/static_layer.cpp | 26 ++++++++++++++----- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/nav2_costmap_2d/include/nav2_costmap_2d/static_layer.hpp b/nav2_costmap_2d/include/nav2_costmap_2d/static_layer.hpp index 7da443ebd25..0333f03577b 100644 --- a/nav2_costmap_2d/include/nav2_costmap_2d/static_layer.hpp +++ b/nav2_costmap_2d/include/nav2_costmap_2d/static_layer.hpp @@ -153,6 +153,15 @@ class StaticLayer : public CostmapLayer */ unsigned char interpretValue(unsigned char value); + /** + * @brief Check if two double values are equal within a given epsilon + * @param a First double value + * @param b Second double value + * @param epsilon The tolerance for equality check + * @return True if the values are equal within the tolerance, false otherwise + */ + bool isEqual(double a, double b, double epsilon); + /** * @brief Callback executed when a parameter change is detected * @param event ParameterEvent message diff --git a/nav2_costmap_2d/plugins/static_layer.cpp b/nav2_costmap_2d/plugins/static_layer.cpp index e4dbe277723..94aa69f87f8 100644 --- a/nav2_costmap_2d/plugins/static_layer.cpp +++ b/nav2_costmap_2d/plugins/static_layer.cpp @@ -47,6 +47,8 @@ #include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" #include "nav2_ros_common/validate_messages.hpp" +#define EPSILON 1e-5 + PLUGINLIB_EXPORT_CLASS(nav2_costmap_2d::StaticLayer, nav2_costmap_2d::Layer) using nav2_costmap_2d::NO_INFORMATION; @@ -190,9 +192,9 @@ StaticLayer::processMap(const nav_msgs::msg::OccupancyGrid & new_map) Costmap2D * master = layered_costmap_->getCostmap(); if (!layered_costmap_->isRolling() && (master->getSizeInCellsX() != size_x || master->getSizeInCellsY() != size_y || - master->getResolution() != new_map.info.resolution || - master->getOriginX() != new_map.info.origin.position.x || - master->getOriginY() != new_map.info.origin.position.y || + !isEqual(master->getResolution(), new_map.info.resolution, EPSILON) || + !isEqual(master->getOriginX(), new_map.info.origin.position.x, EPSILON) || + !isEqual(master->getOriginY(), new_map.info.origin.position.y, EPSILON) || !layered_costmap_->isSizeLocked())) { // Update the size of the layered costmap (and all layers, including this one) @@ -206,9 +208,9 @@ StaticLayer::processMap(const nav_msgs::msg::OccupancyGrid & new_map) new_map.info.origin.position.y, true); } else if (size_x_ != size_x || size_y_ != size_y || // NOLINT - resolution_ != new_map.info.resolution || - origin_x_ != new_map.info.origin.position.x || - origin_y_ != new_map.info.origin.position.y) + !isEqual(resolution_, new_map.info.resolution, EPSILON) || + !isEqual(origin_x_, new_map.info.origin.position.x, EPSILON) || + !isEqual(origin_y_, new_map.info.origin.position.y, EPSILON)) { // only update the size of the costmap stored locally in this layer RCLCPP_INFO( @@ -469,6 +471,18 @@ StaticLayer::updateCosts( current_ = true; } +/** + * @brief Check if two floating point numbers are equal within a given epsilon + * @param a First number + * @param b Second number + * @param epsilon Tolerance for equality check + * @return True if numbers are equal within the tolerance, false otherwise + */ +bool StaticLayer::isEqual(double a, double b, double epsilon) +{ + return std::abs(a - b) < epsilon; +} + /** * @brief Callback executed when a parameter change is detected * @param event ParameterEvent message From c5531cb74ce5b1e06aba34e44fc21875f5cd0371 Mon Sep 17 00:00:00 2001 From: Steve Macenski Date: Thu, 31 Jul 2025 12:29:24 -0700 Subject: [PATCH 05/51] Moving Nav2 package builds to the system build job to balance CI times for CircleCI limits (#5409) * Moving Nav2 behaviors to the system build job Signed-off-by: SteveMacenski * Adding Docking Signed-off-by: SteveMacenski * moving more over to balance jobs Signed-off-by: SteveMacenski --------- Signed-off-by: SteveMacenski --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 295c319a739..edb996eb1f7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -532,7 +532,7 @@ jobs: _parameters: release_parameters: &release_parameters - packages_skip_regex: "'(nav2_system_tests|nav2_smac_planner|nav2_mppi_controller|nav2_route|nav2_rviz_plugins|nav2_bringup|navigation2)'" + packages_skip_regex: "'(nav2_system_tests|nav2_smac_planner|nav2_mppi_controller|nav2_route|nav2_rviz_plugins|nav2_rotation_shim_controller|nav2_waypoint_follower|nav2_smoother|opennav_docking|nav2_behaviors|nav2_bringup|navigation2)'" workflows: version: 2 From 8e3aee6845f2f1ce29ff49dff78266d2b47e4835 Mon Sep 17 00:00:00 2001 From: Vince Reda <60265874+redvinaa@users.noreply.github.com> Date: Mon, 4 Aug 2025 20:11:32 +0200 Subject: [PATCH 06/51] Add Pause and SequenceWithBlackboardMemory BT nodes (#5247) * Add pause and seq with bb memory BT nodes Signed-off-by: redvinaa * Requested changes Signed-off-by: redvinaa * Lint Signed-off-by: redvinaa * Restructure pause, rename unpaused state to resumed Signed-off-by: redvinaa * Add PauseResumeController test Signed-off-by: redvinaa * Implement tests using xml_txt trees and dummy nodes Signed-off-by: redvinaa * Update include Signed-off-by: redvinaa * One more fix because of sync Signed-off-by: redvinaa * Fix build Signed-off-by: redvinaa * Add tests Signed-off-by: redvinaa * Remove unreachable code Signed-off-by: redvinaa * Fix tests Signed-off-by: redvinaa * Update copyrights Signed-off-by: redvinaa * Fix size calc Signed-off-by: redvinaa * Rename to PersistentSequence Signed-off-by: redvinaa * Fix docstring Signed-off-by: redvinaa --------- Signed-off-by: redvinaa --- nav2_behavior_tree/CMakeLists.txt | 6 + .../control/pause_resume_controller.hpp | 152 ++++++++++ .../plugins/control/persistent_sequence.hpp | 61 ++++ nav2_behavior_tree/nav2_tree_nodes.xml | 10 + .../control/pause_resume_controller.cpp | 190 ++++++++++++ .../plugins/control/persistent_sequence.cpp | 84 +++++ .../test/plugins/control/CMakeLists.txt | 5 + .../control/test_pause_resume_controller.cpp | 286 ++++++++++++++++++ .../control/test_persistent_sequence.cpp | 157 ++++++++++ .../test/utils/get_node_from_tree.hpp | 66 ++++ .../test/utils/test_dummy_tree_node.hpp | 24 +- 11 files changed, 1038 insertions(+), 3 deletions(-) create mode 100644 nav2_behavior_tree/include/nav2_behavior_tree/plugins/control/pause_resume_controller.hpp create mode 100644 nav2_behavior_tree/include/nav2_behavior_tree/plugins/control/persistent_sequence.hpp create mode 100644 nav2_behavior_tree/plugins/control/pause_resume_controller.cpp create mode 100644 nav2_behavior_tree/plugins/control/persistent_sequence.cpp create mode 100644 nav2_behavior_tree/test/plugins/control/test_pause_resume_controller.cpp create mode 100644 nav2_behavior_tree/test/plugins/control/test_persistent_sequence.cpp create mode 100644 nav2_behavior_tree/test/utils/get_node_from_tree.hpp diff --git a/nav2_behavior_tree/CMakeLists.txt b/nav2_behavior_tree/CMakeLists.txt index 03339b19cdc..4059e6da15e 100644 --- a/nav2_behavior_tree/CMakeLists.txt +++ b/nav2_behavior_tree/CMakeLists.txt @@ -218,6 +218,12 @@ list(APPEND plugin_libs nav2_nonblocking_sequence_bt_node) add_library(nav2_round_robin_node_bt_node SHARED plugins/control/round_robin_node.cpp) list(APPEND plugin_libs nav2_round_robin_node_bt_node) +add_library(nav2_pause_resume_controller_bt_node SHARED plugins/control/pause_resume_controller.cpp) +list(APPEND plugin_libs nav2_pause_resume_controller_bt_node) + +add_library(nav2_persistent_sequence_bt_node SHARED plugins/control/persistent_sequence.cpp) +list(APPEND plugin_libs nav2_persistent_sequence_bt_node) + add_library(nav2_single_trigger_bt_node SHARED plugins/decorator/single_trigger_node.cpp) list(APPEND plugin_libs nav2_single_trigger_bt_node) diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/control/pause_resume_controller.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/control/pause_resume_controller.hpp new file mode 100644 index 00000000000..a3f5333b19b --- /dev/null +++ b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/control/pause_resume_controller.hpp @@ -0,0 +1,152 @@ +// Copyright (c) 2025 Enjoy Robotics +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NAV2_BEHAVIOR_TREE__PLUGINS__CONTROL__PAUSE_RESUME_CONTROLLER_HPP_ +#define NAV2_BEHAVIOR_TREE__PLUGINS__CONTROL__PAUSE_RESUME_CONTROLLER_HPP_ + +// Other includes +#include +#include +#include + +// ROS includes +#include "rclcpp/rclcpp.hpp" +#include "rclcpp/callback_group.hpp" +#include "rclcpp/executors/single_threaded_executor.hpp" +#include "behaviortree_cpp/control_node.h" +#include "nav2_ros_common/service_server.hpp" + +// Interface definitions +#include "std_srvs/srv/trigger.hpp" + + +namespace nav2_behavior_tree +{ + +using Trigger = std_srvs::srv::Trigger; + +enum state_t {RESUMED, PAUSED, PAUSE_REQUESTED, ON_PAUSE, RESUME_REQUESTED, ON_RESUME}; +const std::map state_names = { + {RESUMED, "RESUMED"}, + {PAUSED, "PAUSED"}, + {PAUSE_REQUESTED, "PAUSE_REQUESTED"}, + {ON_PAUSE, "ON_PAUSE"}, + {RESUME_REQUESTED, "RESUME_REQUESTED"}, + {ON_RESUME, "ON_RESUME"} +}; +const std::map child_indices = { + {RESUMED, 0}, + {PAUSED, 1}, + {ON_PAUSE, 2}, + {ON_RESUME, 3} +}; + +/* @brief Controlled through service calls to pause and resume the execution of the tree + * It has one mandatory child for the RESUMED, and three optional for the PAUSED state, + * the ON_PAUSE event and the ON_RESUME event. + * It has two input ports: + * - pause_service_name: name of the service to pause + * - resume_service_name: name of the service to resume + * + * Usage: + * + * + * + * + * + * + * + * + * + * + * The controller starts in RESUMED state, and ticks it until it returns success. + * When the pause service is called, ON_PAUSE is ticked until completion, + * then the controller switches to PAUSED state. + * When the resume service is called, ON_RESUME is ticked until completion, + * then the controller switches back to RESUMED state. + * + * The controller only returns success when the RESUMED child returns success. + * The controller returns failure if any child returns failure. + * In any other case, it returns running. + */ + + +class PauseResumeController : public BT::ControlNode +{ +public: + //! @brief Constructor + PauseResumeController( + const std::string & xml_tag_name, + const BT::NodeConfiguration & conf); + + //! @brief Reset state and go to Idle + void halt() override; + + //! @brief Handle transitions if requested and tick child related to the actual state + BT::NodeStatus tick() override; + + //! @brief Declare ports + static BT::PortsList providedPorts() + { + return { + BT::InputPort( + "pause_service_name", + "Name of the service to pause"), + BT::InputPort( + "resume_service_name", + "Name of the service to resume"), + }; + } + + [[nodiscard]] inline state_t getState() const + { + return state_; + } + +private: + //! @brief Set state to PAUSE_REQUESTED + void pauseServiceCallback( + const std::shared_ptr/*request_header*/, + const std::shared_ptr request, + std::shared_ptr response); + + //! @brief Set state to RESUME_REQUESTED + void resumeServiceCallback( + const std::shared_ptr/*request_header*/, + const std::shared_ptr request, + std::shared_ptr response); + + /** @brief Switch to the next state based on the current state + * + * PAUSE_REQUESTED -> ON_PAUSE + * ON_PAUSE -> PAUSED + * + * RESUME_REQUESTED -> ON_RESUME + * ON_RESUME -> RESUMED + * + * Do nothing if in end state + */ + void switchToNextState(); + + rclcpp::Logger logger_{rclcpp::get_logger("PauseResumeController")}; + rclcpp::CallbackGroup::SharedPtr cb_group_; + rclcpp::executors::SingleThreadedExecutor::SharedPtr executor_; + nav2::ServiceServer::SharedPtr pause_srv_; + nav2::ServiceServer::SharedPtr resume_srv_; + state_t state_; +}; + +} // namespace nav2_behavior_tree + +#endif // NAV2_BEHAVIOR_TREE__PLUGINS__CONTROL__PAUSE_RESUME_CONTROLLER_HPP_ diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/control/persistent_sequence.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/control/persistent_sequence.hpp new file mode 100644 index 00000000000..79d494eb8e6 --- /dev/null +++ b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/control/persistent_sequence.hpp @@ -0,0 +1,61 @@ +// Copyright (c) 2025 Enjoy Robotics +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NAV2_BEHAVIOR_TREE__PLUGINS__CONTROL__PERSISTENT_SEQUENCE_HPP_ +#define NAV2_BEHAVIOR_TREE__PLUGINS__CONTROL__PERSISTENT_SEQUENCE_HPP_ + +#include +#include "behaviortree_cpp/control_node.h" +#include "behaviortree_cpp/bt_factory.h" + +namespace nav2_behavior_tree +{ +/** + * @brief The PersistentSequenceNode is similar to the SequenceNode, but it + * stores the index of the last running child in the blackboard (key: "current_child_idx"), + * and it does not reset the index when it got halted. + * It used to tick children in an ordered sequence. If any child returns RUNNING, previous + * children will NOT be ticked again. + * + * - If all the children return SUCCESS, this node returns SUCCESS. + * + * - If a child returns RUNNING, this node returns RUNNING. + * Loop is NOT restarted, the same running child will be ticked again. + * + * - If a child returns FAILURE, stop the loop and return FAILURE. + * Restart the loop only if (reset_on_failure == true) + * + */ +class PersistentSequenceNode : public BT::ControlNode +{ +public: + PersistentSequenceNode(const std::string & name, const BT::NodeConfiguration & conf); + + ~PersistentSequenceNode() override = default; + + //! @brief Declare ports + static BT::PortsList providedPorts() + { + return { + BT::BidirectionalPort("current_child_idx", "The index of the current child"), + }; + } + +private: + BT::NodeStatus tick() override; +}; + +} // namespace nav2_behavior_tree + +#endif // NAV2_BEHAVIOR_TREE__PLUGINS__CONTROL__PERSISTENT_SEQUENCE_HPP_ diff --git a/nav2_behavior_tree/nav2_tree_nodes.xml b/nav2_behavior_tree/nav2_tree_nodes.xml index a9798c0c64c..230ce02f70c 100644 --- a/nav2_behavior_tree/nav2_tree_nodes.xml +++ b/nav2_behavior_tree/nav2_tree_nodes.xml @@ -404,6 +404,16 @@ + + + Service to call to pause + Service to call to resume + + + + Index of the child to execute + + Rate diff --git a/nav2_behavior_tree/plugins/control/pause_resume_controller.cpp b/nav2_behavior_tree/plugins/control/pause_resume_controller.cpp new file mode 100644 index 00000000000..0f039aad8e6 --- /dev/null +++ b/nav2_behavior_tree/plugins/control/pause_resume_controller.cpp @@ -0,0 +1,190 @@ +// Copyright (c) 2025 Enjoy Robotics +// +// 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. + +// Other includes +#include + +// ROS includes +#include "nav2_behavior_tree/plugins/control/pause_resume_controller.hpp" + +// Other includes +#include "behaviortree_cpp/bt_factory.h" + +namespace nav2_behavior_tree +{ + +using namespace std::placeholders; + +PauseResumeController::PauseResumeController( + const std::string & xml_tag_name, + const BT::NodeConfiguration & conf) +: BT::ControlNode(xml_tag_name, conf) +{ + auto node = this->config().blackboard->get("node"); + state_ = RESUMED; + + // Create a separate cb group with a separate executor to spin + cb_group_ = node->create_callback_group( + rclcpp::CallbackGroupType::MutuallyExclusive, false); + + executor_ = + std::make_shared(); + + executor_->add_callback_group(cb_group_, node->get_node_base_interface()); + + std::string pause_service_name; + getInput("pause_service_name", pause_service_name); + pause_srv_ = std::make_shared>( + pause_service_name, + node, + std::bind(&PauseResumeController::pauseServiceCallback, this, _1, _2, _3), + cb_group_); + + std::string resume_service_name; + getInput("resume_service_name", resume_service_name); + resume_srv_ = std::make_shared>( + resume_service_name, + node, + std::bind(&PauseResumeController::resumeServiceCallback, this, _1, _2, _3), + cb_group_); +} + +BT::NodeStatus PauseResumeController::tick() +{ + unsigned int children_count = children_nodes_.size(); + if (children_count < 1 || children_count > 4) { + throw std::runtime_error( + "PauseNode must have at least one and at most four children " + "(currently has " + std::to_string(children_count) + ")"); + } + + if (status() == BT::NodeStatus::IDLE) { + state_ = RESUMED; + } + setStatus(BT::NodeStatus::RUNNING); + + executor_->spin_some(); + + // If pause / resume requested, reset children and switch to transient child + if (state_ == PAUSE_REQUESTED || state_ == RESUME_REQUESTED) { + resetChildren(); + switchToNextState(); + } + + // Return RUNNING and do nothing if specific child is not used + const uint16_t child_idx = child_indices.at(state_); + if (children_nodes_.size() <= child_idx) { + switchToNextState(); + return BT::NodeStatus::RUNNING; + } + + // If child is used, tick it + const BT::NodeStatus child_status = + children_nodes_[child_indices.at(state_)]->executeTick(); + + switch (child_status) { + case BT::NodeStatus::RUNNING: + return BT::NodeStatus::RUNNING; + case BT::NodeStatus::SUCCESS: + case BT::NodeStatus::SKIPPED: + if (state_ == RESUMED) { + // Resumed child returned SUCCESS, we're done + return BT::NodeStatus::SUCCESS; + } + switchToNextState(); + // If any branch other than RESUMED returned SUCCESS, keep ticking + return BT::NodeStatus::RUNNING; + case BT::NodeStatus::FAILURE: + RCLCPP_ERROR( + logger_, "%s child returned FAILURE", state_names.at(state_).c_str()); + return BT::NodeStatus::FAILURE; + default: + throw std::runtime_error("A child node must never return IDLE"); + } +} + +void PauseResumeController::switchToNextState() +{ + static const std::map next_states = { + {PAUSE_REQUESTED, ON_PAUSE}, + {ON_PAUSE, PAUSED}, + {RESUME_REQUESTED, ON_RESUME}, + {ON_RESUME, RESUMED} + }; + + if (state_ == PAUSED || state_ == RESUMED) { + // No next state, do nothing + return; + } + + state_ = next_states.at(state_); + RCLCPP_INFO(logger_, "Switched to state: %s", state_names.at(state_).c_str()); +} + +void PauseResumeController::pauseServiceCallback( + const std::shared_ptr/*request_header*/, + const std::shared_ptr/*request*/, + std::shared_ptr response) +{ + if (state_ != PAUSED) { + RCLCPP_INFO(logger_, "Received pause request"); + response->success = true; + state_ = PAUSE_REQUESTED; + return; + } + + std::string warn_message = "PauseResumeController BT node already in state PAUSED"; + RCLCPP_WARN(logger_, "%s", warn_message.c_str()); + response->success = false; + response->message = warn_message; +} + +void PauseResumeController::resumeServiceCallback( + const std::shared_ptr/*request_header*/, + const std::shared_ptr/*request*/, + std::shared_ptr response) +{ + if (state_ == PAUSED) { + RCLCPP_INFO(logger_, "Received resume request"); + response->success = true; + state_ = RESUME_REQUESTED; + return; + } + + std::string warn_message = "PauseResumeController BT node not in state PAUSED"; + RCLCPP_WARN(logger_, "%s", warn_message.c_str()); + response->success = false; + response->message = warn_message; +} + +void PauseResumeController::halt() +{ + state_ = RESUMED; + ControlNode::halt(); +} + +} // namespace nav2_behavior_tree + +BT_REGISTER_NODES(factory) +{ + BT::NodeBuilder builder = + [](const std::string & name, const BT::NodeConfiguration & config) + { + return std::make_unique( + name, config); + }; + + factory.registerBuilder( + "PauseResumeController", builder); +} diff --git a/nav2_behavior_tree/plugins/control/persistent_sequence.cpp b/nav2_behavior_tree/plugins/control/persistent_sequence.cpp new file mode 100644 index 00000000000..1886deb3d5c --- /dev/null +++ b/nav2_behavior_tree/plugins/control/persistent_sequence.cpp @@ -0,0 +1,84 @@ +// Copyright (c) 2025 Enjoy Robotics +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "nav2_behavior_tree/plugins/control/persistent_sequence.hpp" +#include "behaviortree_cpp/action_node.h" +#include "behaviortree_cpp/bt_factory.h" + +namespace nav2_behavior_tree +{ + +PersistentSequenceNode::PersistentSequenceNode( + const std::string & name, + const BT::NodeConfiguration & conf) +: BT::ControlNode::ControlNode(name, conf) {} + +BT::NodeStatus PersistentSequenceNode::tick() +{ + const int children_count = children_nodes_.size(); + + int current_child_idx; + getInput("current_child_idx", current_child_idx); + + setStatus(BT::NodeStatus::RUNNING); + + while (current_child_idx < children_count) { + TreeNode * current_child_node = children_nodes_[current_child_idx]; + const BT::NodeStatus child_status = current_child_node->executeTick(); + + switch (child_status) { + case BT::NodeStatus::RUNNING: + return child_status; + + case BT::NodeStatus::FAILURE: + // Reset on failure + resetChildren(); + current_child_idx = 0; + setOutput("current_child_idx", 0); + return child_status; + + case BT::NodeStatus::SUCCESS: + case BT::NodeStatus::SKIPPED: + // Skip the child node + current_child_idx++; + setOutput("current_child_idx", current_child_idx); + break; + + case BT::NodeStatus::IDLE: + throw std::runtime_error("A child node must never return IDLE"); + } // end switch + } // end while loop + + // The entire while loop completed. This means that all the children returned SUCCESS. + if (current_child_idx >= children_count) { + resetChildren(); + setOutput("current_child_idx", 0); + } + return BT::NodeStatus::SUCCESS; +} + +} // namespace nav2_behavior_tree + +BT_REGISTER_NODES(factory) +{ + BT::NodeBuilder builder = + [](const std::string & name, const BT::NodeConfiguration & config) + { + return std::make_unique( + name, config); + }; + + factory.registerBuilder( + "PersistentSequence", builder); +} diff --git a/nav2_behavior_tree/test/plugins/control/CMakeLists.txt b/nav2_behavior_tree/test/plugins/control/CMakeLists.txt index 1a8b0cf781a..a625bdad792 100644 --- a/nav2_behavior_tree/test/plugins/control/CMakeLists.txt +++ b/nav2_behavior_tree/test/plugins/control/CMakeLists.txt @@ -5,3 +5,8 @@ plugin_add_test(test_control_pipeline_sequence test_pipeline_sequence.cpp nav2_p plugin_add_test(test_control_round_robin_node test_round_robin_node.cpp nav2_round_robin_node_bt_node) plugin_add_test(test_control_nonblocking_sequence test_nonblocking_sequence.cpp nav2_nonblocking_sequence_bt_node) + +plugin_add_test(test_control_pause_resume_controller + test_pause_resume_controller.cpp nav2_pause_resume_controller_bt_node) + +plugin_add_test(test_persistent_sequence test_persistent_sequence.cpp nav2_persistent_sequence_bt_node) diff --git a/nav2_behavior_tree/test/plugins/control/test_pause_resume_controller.cpp b/nav2_behavior_tree/test/plugins/control/test_pause_resume_controller.cpp new file mode 100644 index 00000000000..8a78c37d89b --- /dev/null +++ b/nav2_behavior_tree/test/plugins/control/test_pause_resume_controller.cpp @@ -0,0 +1,286 @@ +// Copyright (c) 2025 Enjoy Robotics +// +// 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 +#include + +#include "utils/test_behavior_tree_fixture.hpp" +#include "utils/test_dummy_tree_node.hpp" +#include "utils/get_node_from_tree.hpp" +#include "nav2_behavior_tree/plugins/control/pause_resume_controller.hpp" +#include "std_srvs/srv/trigger.hpp" + +class PauseResumeControllerTestFixture : public nav2_behavior_tree::BehaviorTreeTestFixture +{ +public: + static void SetUpTestCase() + { + auto node = std::make_shared("pause_resume_controller_test_fixture"); + executor_ = + std::make_shared(); + cb_group_ = node->create_callback_group( + rclcpp::CallbackGroupType::MutuallyExclusive, false); + executor_->add_callback_group(cb_group_, node->get_node_base_interface()); + pause_client_ = node->create_client( + "pause", rclcpp::ServicesQoS(), cb_group_); + resume_client_ = node->create_client( + "resume", rclcpp::ServicesQoS(), cb_group_); + + factory_ = std::make_shared(); + config_ = new BT::NodeConfiguration(); + config_->blackboard = BT::Blackboard::create(); + config_->blackboard->set("node", node); + + factory_->registerNodeType("PauseResumeController"); + + // Register dummy node for testing + factory_->registerNodeType("DummyNode"); + } + + static void TearDownTestCase() + { + if (config_) { + delete config_; + config_ = nullptr; + } + tree_.reset(); + } + +protected: + static BT::NodeConfiguration * config_; + static std::shared_ptr factory_; + static std::shared_ptr tree_; + static rclcpp::executors::SingleThreadedExecutor::SharedPtr executor_; + static rclcpp::CallbackGroup::SharedPtr cb_group_; + static rclcpp::Client::SharedPtr pause_client_; + static rclcpp::Client::SharedPtr resume_client_; +}; + +rclcpp::executors::SingleThreadedExecutor::SharedPtr +PauseResumeControllerTestFixture::executor_ = nullptr; +rclcpp::CallbackGroup::SharedPtr +PauseResumeControllerTestFixture::cb_group_ = nullptr; +rclcpp::Client::SharedPtr +PauseResumeControllerTestFixture::pause_client_ = nullptr; +rclcpp::Client::SharedPtr +PauseResumeControllerTestFixture::resume_client_ = nullptr; +BT::NodeConfiguration * PauseResumeControllerTestFixture::config_ = nullptr; +std::shared_ptr PauseResumeControllerTestFixture::factory_ = nullptr; +std::shared_ptr PauseResumeControllerTestFixture::tree_ = nullptr; + +TEST_F(PauseResumeControllerTestFixture, test_incorrect_num_children) +{ + // create tree with incorrect number of children + std::string xml_txt = + R"( + + + + + + )"; + EXPECT_THROW( + auto tree = factory_->createTreeFromText(xml_txt, config_->blackboard), + BT::RuntimeError); +} + +TEST_F(PauseResumeControllerTestFixture, test_unused_children) +{ + // create tree with only RESUMED child + std::string xml_txt = + R"( + + + + + + + )"; + tree_ = std::make_shared(factory_->createTreeFromText(xml_txt, config_->blackboard)); + + // get pause_resume_controller so we can check state + auto pause_bt_node = + nav2_behavior_tree::get_node_from_tree(tree_); + ASSERT_NE(pause_bt_node, nullptr); + using state_t = nav2_behavior_tree::state_t; + + // get dummy nodes so we can change their status + auto resumed_child = + nav2_behavior_tree::get_node_from_tree(tree_, 0); + ASSERT_NE(resumed_child, nullptr); + resumed_child->changeStatus(BT::NodeStatus::RUNNING); + + EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); + EXPECT_EQ(pause_bt_node->getState(), state_t::RESUMED); + + const auto & check_request_succeeded = []( + rclcpp::Client::FutureAndRequestId & future) + { + executor_->spin_until_future_complete(future, std::chrono::seconds(1)); + ASSERT_EQ(future.wait_for(std::chrono::seconds(0)), std::future_status::ready); + EXPECT_EQ(future.get()->success, true); + }; + + // Call pause service, expect RUNNING and PAUSED + auto future = pause_client_->async_send_request( + std::make_shared()); + EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); + EXPECT_EQ(pause_bt_node->getState(), state_t::PAUSED); + check_request_succeeded(future); + + // Tick again, expect RUNNING and PAUSED + EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); + EXPECT_EQ(pause_bt_node->getState(), state_t::PAUSED); + + // Call resume service, expect RUNNING and ON_RESUME + future = resume_client_->async_send_request( + std::make_shared()); + EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); + EXPECT_EQ(pause_bt_node->getState(), state_t::RESUMED); + check_request_succeeded(future); + + // Tick again, expect RUNNING and RESUMED + EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); + EXPECT_EQ(pause_bt_node->getState(), state_t::RESUMED); +} + +TEST_F(PauseResumeControllerTestFixture, test_behavior) +{ + // create tree + std::string xml_txt = + R"( + + + + + + + + + + )"; + tree_ = std::make_shared(factory_->createTreeFromText(xml_txt, config_->blackboard)); + + // get pause_resume_controller so we can check state + auto pause_bt_node = + nav2_behavior_tree::get_node_from_tree(tree_); + ASSERT_NE(pause_bt_node, nullptr); + using state_t = nav2_behavior_tree::state_t; + + // get dummy nodes so we can change their status + auto resumed_child = + nav2_behavior_tree::get_node_from_tree(tree_, 0); + auto paused_child = + nav2_behavior_tree::get_node_from_tree(tree_, 1); + auto on_pause_child = + nav2_behavior_tree::get_node_from_tree(tree_, 2); + auto on_resume_child = + nav2_behavior_tree::get_node_from_tree(tree_, 3); + ASSERT_NE(resumed_child, nullptr); + ASSERT_NE(paused_child, nullptr); + ASSERT_NE(on_pause_child, nullptr); + ASSERT_NE(on_resume_child, nullptr); + + resumed_child->changeStatus(BT::NodeStatus::RUNNING); + paused_child->changeStatus(BT::NodeStatus::RUNNING); + on_pause_child->changeStatus(BT::NodeStatus::RUNNING); + on_resume_child->changeStatus(BT::NodeStatus::RUNNING); + + EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); + EXPECT_EQ(pause_bt_node->getState(), state_t::RESUMED); + + const auto & check_future_result = []( + rclcpp::Client::FutureAndRequestId & future, bool success = true) + -> void + { + executor_->spin_until_future_complete(future, std::chrono::seconds(1)); + ASSERT_EQ(future.wait_for(std::chrono::seconds(0)), std::future_status::ready); + EXPECT_EQ(future.get()->success, success); + }; + + // Call pause service, set ON_PAUSE child to RUNNING, expect RUNNING and ON_PAUSE + auto future = pause_client_->async_send_request( + std::make_shared()); + EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); + EXPECT_EQ(pause_bt_node->getState(), state_t::ON_PAUSE); + check_future_result(future); + + // Change ON_PAUSE child to SUCCESS, expect RUNNING and PAUSED + on_pause_child->changeStatus(BT::NodeStatus::SUCCESS); + EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); + EXPECT_EQ(pause_bt_node->getState(), state_t::PAUSED); + + // Set PAUSED child to SUCCESS, expect RUNNING and PAUSED + // (should keep ticking unless RESUMED branch succeeds) + paused_child->changeStatus(BT::NodeStatus::SUCCESS); + EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); + EXPECT_EQ(pause_bt_node->getState(), state_t::PAUSED); + + // Set PAUSED child to SKIPPED, expect RUNNING and PAUSED + paused_child->changeStatus(BT::NodeStatus::SKIPPED); + EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); + EXPECT_EQ(pause_bt_node->getState(), state_t::PAUSED); + + // Call pause service again, expect RUNNING and PAUSED + future = pause_client_->async_send_request( + std::make_shared()); + EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); + EXPECT_EQ(pause_bt_node->getState(), state_t::PAUSED); + check_future_result(future, false); + + // Call resume service, change ON_RESUME child to FAILURE, expect FAILURE + future = resume_client_->async_send_request( + std::make_shared()); + on_resume_child->changeStatus(BT::NodeStatus::FAILURE); + EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::FAILURE); + check_future_result(future); + + // Halt the tree, expect RUNNING and RESUMED + tree_->haltTree(); + EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); + EXPECT_EQ(pause_bt_node->getState(), state_t::RESUMED); + + // Set resumed child to SUCCESS, expect SUCCESS and RESUMED + resumed_child->changeStatus(BT::NodeStatus::SUCCESS); + EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::SUCCESS); + EXPECT_EQ(pause_bt_node->getState(), state_t::RESUMED); + + // Call resume service again, expect RUNNING and RESUMED + future = resume_client_->async_send_request( + std::make_shared()); + EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::SUCCESS); + EXPECT_EQ(pause_bt_node->getState(), state_t::RESUMED); + check_future_result(future, false); +} + +int main(int argc, char ** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + + // initialize ROS + rclcpp::init(argc, argv); + + int all_successful = RUN_ALL_TESTS(); + + // shutdown ROS + rclcpp::shutdown(); + + return all_successful; +} diff --git a/nav2_behavior_tree/test/plugins/control/test_persistent_sequence.cpp b/nav2_behavior_tree/test/plugins/control/test_persistent_sequence.cpp new file mode 100644 index 00000000000..ebf6aa0d6b8 --- /dev/null +++ b/nav2_behavior_tree/test/plugins/control/test_persistent_sequence.cpp @@ -0,0 +1,157 @@ +// Copyright (c) 2025 Enjoy Robotics +// +// 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 +#include + +#include "utils/test_behavior_tree_fixture.hpp" +#include "utils/test_dummy_tree_node.hpp" +#include "utils/get_node_from_tree.hpp" +#include "nav2_behavior_tree/plugins/control/persistent_sequence.hpp" + +class PersistentSequenceTestFixture : public nav2_behavior_tree::BehaviorTreeTestFixture +{ +public: + static void SetUpTestCase() + { + factory_ = std::make_shared(); + config_ = new BT::NodeConfiguration(); + config_->blackboard = BT::Blackboard::create(); + config_->blackboard->set("seq_child_idx", 0); + + factory_->registerNodeType( + "PersistentSequence"); + + // Register dummy node for testing + factory_->registerNodeType("DummyNode"); + } + + static void TearDownTestCase() + { + if (config_) { + delete config_; + config_ = nullptr; + } + tree_.reset(); + } + + void TearDown() override + { + tree_.reset(); + } + +protected: + static BT::NodeConfiguration * config_; + static std::shared_ptr factory_; + static std::shared_ptr tree_; +}; + +BT::NodeConfiguration * PersistentSequenceTestFixture::config_ = nullptr; +std::shared_ptr +PersistentSequenceTestFixture::factory_ = nullptr; +std::shared_ptr PersistentSequenceTestFixture::tree_ = nullptr; + +TEST_F(PersistentSequenceTestFixture, test_tick) +{ + // create tree + std::string xml_txt = + R"( + + + + + + + )"; + tree_ = std::make_shared(factory_->createTreeFromText(xml_txt, config_->blackboard)); + + config_->blackboard->set("seq_child_idx", 0); + EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::SUCCESS); +} + +TEST_F(PersistentSequenceTestFixture, test_behavior) +{ + // create tree + std::string xml_txt = + R"( + + + + + + + + + )"; + tree_ = std::make_shared(factory_->createTreeFromText(xml_txt, config_->blackboard)); + + // get dummy nodes so we can change their status + auto child0 = + nav2_behavior_tree::get_node_from_tree(tree_, 0); + auto child1 = + nav2_behavior_tree::get_node_from_tree(tree_, 1); + auto child2 = + nav2_behavior_tree::get_node_from_tree(tree_, 2); + ASSERT_NE(child0, nullptr); + ASSERT_NE(child1, nullptr); + ASSERT_NE(child2, nullptr); + child0->changeStatus(BT::NodeStatus::RUNNING); + child1->changeStatus(BT::NodeStatus::RUNNING); + child2->changeStatus(BT::NodeStatus::RUNNING); + + // Set child index to 0 and child0 to RUNNING, expect RUNNING and idx = 0 + config_->blackboard->set("seq_child_idx", 0); + child0->changeStatus(BT::NodeStatus::RUNNING); + EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); + EXPECT_EQ(config_->blackboard->get("seq_child_idx"), 0); + + // Set child0 to SUCCESS, expect SUCCESS and idx = 1 + child0->changeStatus(BT::NodeStatus::SUCCESS); + EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); + EXPECT_EQ(config_->blackboard->get("seq_child_idx"), 1); + + // Set child1 to FAILURE, expect FAILURE and idx = 0 + child1->changeStatus(BT::NodeStatus::FAILURE); + EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::FAILURE); + EXPECT_EQ(config_->blackboard->get("seq_child_idx"), 0); + + // Set idx to 1, child0 to FAILURE, child1 to SKIPPED and child2 to RUNNING, + // expect RUNNING and idx = 2 + config_->blackboard->set("seq_child_idx", 1); + child0->changeStatus(BT::NodeStatus::FAILURE); + child1->changeStatus(BT::NodeStatus::SKIPPED); + child2->changeStatus(BT::NodeStatus::RUNNING); + EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); + EXPECT_EQ(config_->blackboard->get("seq_child_idx"), 2); + + // Set child2 to SUCCESS, expect SUCCESS and idx = 0 + child2->changeStatus(BT::NodeStatus::SUCCESS); + EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::SUCCESS); + EXPECT_EQ(config_->blackboard->get("seq_child_idx"), 0); +} + +int main(int argc, char ** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + + // initialize ROS + rclcpp::init(argc, argv); + + int all_successful = RUN_ALL_TESTS(); + + // shutdown ROS + rclcpp::shutdown(); + + return all_successful; +} diff --git a/nav2_behavior_tree/test/utils/get_node_from_tree.hpp b/nav2_behavior_tree/test/utils/get_node_from_tree.hpp new file mode 100644 index 00000000000..068fc924005 --- /dev/null +++ b/nav2_behavior_tree/test/utils/get_node_from_tree.hpp @@ -0,0 +1,66 @@ +// Copyright (c) 2025 Enjoy Robotics +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef UTILS__GET_NODE_FROM_TREE_HPP_ +#define UTILS__GET_NODE_FROM_TREE_HPP_ + +#include +#include + +#include "behaviortree_cpp/bt_factory.h" +#include "rclcpp/rclcpp.hpp" + +namespace nav2_behavior_tree +{ + +/** + * @brief Get node from tree by type and index, casted to NodeT type, const casted away + * Returns true if the node was found, false otherwise + */ +template +NodeT * get_node_from_tree( + const std::shared_ptr tree, + const size_t index = 0) +{ + static rclcpp::Logger logger = rclcpp::get_logger("nav2_behavior_tree::get_node_from_tree"); + + std::vector nodes = tree->getNodesByPath("*"); + if (nodes.empty()) { + RCLCPP_ERROR(logger, "No nodes of given type found"); + return nullptr; + } + if (nodes.size() <= index) { + RCLCPP_ERROR(logger, "Out of bounds (found %zu < %zu nodes)", nodes.size(), index); + return nullptr; + } + + const NodeT * const_bt_node = + dynamic_cast(nodes[index]); + if (const_bt_node == nullptr) { + RCLCPP_ERROR(logger, "Failed to cast node to given type"); + return nullptr; + } + + NodeT * bt_node = const_cast(const_bt_node); + if (bt_node == nullptr) { + RCLCPP_ERROR(logger, "Failed to cast away const from node"); + return nullptr; + } + + return bt_node; +} + +} // namespace nav2_behavior_tree + +#endif // UTILS__GET_NODE_FROM_TREE_HPP_ diff --git a/nav2_behavior_tree/test/utils/test_dummy_tree_node.hpp b/nav2_behavior_tree/test/utils/test_dummy_tree_node.hpp index dcfa52927ac..b4a64d7c78f 100644 --- a/nav2_behavior_tree/test/utils/test_dummy_tree_node.hpp +++ b/nav2_behavior_tree/test/utils/test_dummy_tree_node.hpp @@ -18,6 +18,7 @@ #include #include +#include namespace nav2_behavior_tree { @@ -29,17 +30,20 @@ namespace nav2_behavior_tree class DummyNode : public BT::ActionNodeBase { public: - DummyNode() + DummyNode( + const std::string & /*xml_tag_name*/ = "dummy", + const BT::NodeConfiguration & /*conf*/ = BT::NodeConfiguration()) : BT::ActionNodeBase("dummy", {}) { } void changeStatus(BT::NodeStatus status) { - if (status == BT::NodeStatus::IDLE) { + requested_status = status; + if (requested_status == BT::NodeStatus::IDLE) { resetStatus(); } else { - setStatus(status); + setStatus(requested_status); } } @@ -50,12 +54,26 @@ class DummyNode : public BT::ActionNodeBase BT::NodeStatus tick() override { + if (requested_status == BT::NodeStatus::IDLE) { + resetStatus(); + } else { + setStatus(requested_status); + } return status(); } void halt() override { + resetStatus(); } + + static BT::PortsList providedPorts() + { + return {}; + } + +protected: + BT::NodeStatus requested_status = BT::NodeStatus::IDLE; }; } // namespace nav2_behavior_tree From e8b18110a95ef1c94aac1de36ff62f75c7e72578 Mon Sep 17 00:00:00 2001 From: Guillaume Doisy Date: Mon, 4 Aug 2025 19:15:51 +0100 Subject: [PATCH 07/51] corner case bin check (#5413) Signed-off-by: Guillaume Doisy Co-authored-by: Guillaume Doisy --- nav2_smac_planner/src/smac_planner_hybrid.cpp | 31 +++++++++++-------- .../src/smac_planner_lattice.cpp | 14 +++++---- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/nav2_smac_planner/src/smac_planner_hybrid.cpp b/nav2_smac_planner/src/smac_planner_hybrid.cpp index ad1ae0647c9..1f374af35d0 100644 --- a/nav2_smac_planner/src/smac_planner_hybrid.cpp +++ b/nav2_smac_planner/src/smac_planner_hybrid.cpp @@ -414,15 +414,17 @@ nav_msgs::msg::Path SmacPlannerHybrid::createPlan( std::to_string(start.pose.position.y) + ") was outside bounds"); } - double orientation_bin = std::round(tf2::getYaw(start.pose.orientation) / _angle_bin_size); - while (orientation_bin < 0.0) { - orientation_bin += static_cast(_angle_quantizations); + double start_orientation_bin = std::round(tf2::getYaw(start.pose.orientation) / _angle_bin_size); + while (start_orientation_bin < 0.0) { + start_orientation_bin += static_cast(_angle_quantizations); } // This is needed to handle precision issues - if (orientation_bin >= static_cast(_angle_quantizations)) { - orientation_bin -= static_cast(_angle_quantizations); + if (start_orientation_bin >= static_cast(_angle_quantizations)) { + start_orientation_bin -= static_cast(_angle_quantizations); } - _a_star->setStart(mx_start, my_start, static_cast(orientation_bin)); + unsigned int start_orientation_bin_int = + static_cast(start_orientation_bin); + _a_star->setStart(mx_start, my_start, start_orientation_bin_int); // Set goal point, in A* bin search coordinates if (!costmap->worldToMapContinuous( @@ -435,15 +437,17 @@ nav_msgs::msg::Path SmacPlannerHybrid::createPlan( "Goal Coordinates of(" + std::to_string(goal.pose.position.x) + ", " + std::to_string(goal.pose.position.y) + ") was outside bounds"); } - orientation_bin = std::round(tf2::getYaw(goal.pose.orientation) / _angle_bin_size); - while (orientation_bin < 0.0) { - orientation_bin += static_cast(_angle_quantizations); + double goal_orientation_bin = std::round(tf2::getYaw(goal.pose.orientation) / _angle_bin_size); + while (goal_orientation_bin < 0.0) { + goal_orientation_bin += static_cast(_angle_quantizations); } // This is needed to handle precision issues - if (orientation_bin >= static_cast(_angle_quantizations)) { - orientation_bin -= static_cast(_angle_quantizations); + if (goal_orientation_bin >= static_cast(_angle_quantizations)) { + goal_orientation_bin -= static_cast(_angle_quantizations); } - _a_star->setGoal(mx_goal, my_goal, static_cast(orientation_bin), + unsigned int goal_orientation_bin_int = + static_cast(goal_orientation_bin); + _a_star->setGoal(mx_goal, my_goal, static_cast(goal_orientation_bin_int), _goal_heading_mode, _coarse_search_resolution); // Setup message @@ -460,7 +464,8 @@ nav_msgs::msg::Path SmacPlannerHybrid::createPlan( // Corner case of start and goal being on the same cell if (std::floor(mx_start) == std::floor(mx_goal) && - std::floor(my_start) == std::floor(my_goal)) + std::floor(my_start) == std::floor(my_goal) && + start_orientation_bin_int == goal_orientation_bin_int) { pose.pose = start.pose; pose.pose.orientation = goal.pose.orientation; diff --git a/nav2_smac_planner/src/smac_planner_lattice.cpp b/nav2_smac_planner/src/smac_planner_lattice.cpp index d689507e8f4..fa89e90636c 100644 --- a/nav2_smac_planner/src/smac_planner_lattice.cpp +++ b/nav2_smac_planner/src/smac_planner_lattice.cpp @@ -338,9 +338,9 @@ nav_msgs::msg::Path SmacPlannerLattice::createPlan( "Start Coordinates of(" + std::to_string(start.pose.position.x) + ", " + std::to_string(start.pose.position.y) + ") was outside bounds"); } - _a_star->setStart( - mx_start, my_start, - NodeLattice::motion_table.getClosestAngularBin(tf2::getYaw(start.pose.orientation))); + unsigned int start_bin = + NodeLattice::motion_table.getClosestAngularBin(tf2::getYaw(start.pose.orientation)); + _a_star->setStart(mx_start, my_start, start_bin); // Set goal point, in A* bin search coordinates if (!_costmap->worldToMapContinuous( @@ -353,9 +353,10 @@ nav_msgs::msg::Path SmacPlannerLattice::createPlan( "Goal Coordinates of(" + std::to_string(goal.pose.position.x) + ", " + std::to_string(goal.pose.position.y) + ") was outside bounds"); } + unsigned int goal_bin = + NodeLattice::motion_table.getClosestAngularBin(tf2::getYaw(goal.pose.orientation)); _a_star->setGoal( - mx_goal, my_goal, - NodeLattice::motion_table.getClosestAngularBin(tf2::getYaw(goal.pose.orientation)), + mx_goal, my_goal, goal_bin, _goal_heading_mode, _coarse_search_resolution); // Setup message @@ -372,7 +373,8 @@ nav_msgs::msg::Path SmacPlannerLattice::createPlan( // Corner case of start and goal being on the same cell if (std::floor(mx_start) == std::floor(mx_goal) && - std::floor(my_start) == std::floor(my_goal)) + std::floor(my_start) == std::floor(my_goal) && + start_bin == goal_bin) { pose.pose = start.pose; pose.pose.orientation = goal.pose.orientation; From d93c12974678d4b799c68e6a2a1a311895a4a5bd Mon Sep 17 00:00:00 2001 From: Sushant Chavan Date: Wed, 6 Aug 2025 18:05:34 +0200 Subject: [PATCH 08/51] Add backward_ros dependency (#5404) Signed-off-by: Sushant Chavan --- nav2_amcl/CMakeLists.txt | 1 + nav2_amcl/package.xml | 1 + nav2_behavior_tree/CMakeLists.txt | 1 + nav2_behavior_tree/package.xml | 1 + nav2_behaviors/CMakeLists.txt | 1 + nav2_behaviors/package.xml | 1 + nav2_bringup/CMakeLists.txt | 1 + nav2_bringup/package.xml | 2 ++ nav2_bt_navigator/CMakeLists.txt | 1 + nav2_bt_navigator/package.xml | 1 + nav2_collision_monitor/CMakeLists.txt | 1 + nav2_collision_monitor/package.xml | 1 + nav2_common/CMakeLists.txt | 1 + nav2_common/package.xml | 1 + nav2_constrained_smoother/CMakeLists.txt | 1 + nav2_constrained_smoother/package.xml | 1 + nav2_controller/CMakeLists.txt | 1 + nav2_controller/package.xml | 1 + nav2_core/CMakeLists.txt | 1 + nav2_core/package.xml | 1 + nav2_costmap_2d/CMakeLists.txt | 1 + nav2_costmap_2d/package.xml | 1 + nav2_docking/opennav_docking/CMakeLists.txt | 1 + nav2_docking/opennav_docking/package.xml | 1 + nav2_docking/opennav_docking_bt/CMakeLists.txt | 1 + nav2_docking/opennav_docking_bt/package.xml | 1 + nav2_docking/opennav_docking_core/CMakeLists.txt | 1 + nav2_docking/opennav_docking_core/package.xml | 1 + nav2_dwb_controller/costmap_queue/CMakeLists.txt | 1 + nav2_dwb_controller/costmap_queue/package.xml | 1 + nav2_dwb_controller/dwb_core/CMakeLists.txt | 1 + nav2_dwb_controller/dwb_core/package.xml | 1 + nav2_dwb_controller/dwb_critics/CMakeLists.txt | 1 + nav2_dwb_controller/dwb_critics/package.xml | 1 + nav2_dwb_controller/dwb_msgs/CMakeLists.txt | 1 + nav2_dwb_controller/dwb_msgs/package.xml | 1 + nav2_dwb_controller/dwb_plugins/CMakeLists.txt | 1 + nav2_dwb_controller/dwb_plugins/package.xml | 1 + nav2_dwb_controller/nav2_dwb_controller/CMakeLists.txt | 1 + nav2_dwb_controller/nav2_dwb_controller/package.xml | 2 ++ nav2_dwb_controller/nav_2d_msgs/CMakeLists.txt | 1 + nav2_dwb_controller/nav_2d_msgs/package.xml | 1 + nav2_dwb_controller/nav_2d_utils/CMakeLists.txt | 1 + nav2_dwb_controller/nav_2d_utils/package.xml | 1 + nav2_graceful_controller/CMakeLists.txt | 1 + nav2_graceful_controller/package.xml | 1 + nav2_lifecycle_manager/CMakeLists.txt | 1 + nav2_lifecycle_manager/package.xml | 1 + nav2_map_server/CMakeLists.txt | 1 + nav2_map_server/package.xml | 1 + nav2_mppi_controller/CMakeLists.txt | 1 + nav2_mppi_controller/package.xml | 1 + nav2_msgs/CMakeLists.txt | 1 + nav2_msgs/package.xml | 1 + nav2_navfn_planner/CMakeLists.txt | 1 + nav2_navfn_planner/package.xml | 1 + nav2_planner/CMakeLists.txt | 1 + nav2_planner/package.xml | 1 + nav2_regulated_pure_pursuit_controller/CMakeLists.txt | 1 + nav2_regulated_pure_pursuit_controller/package.xml | 1 + nav2_ros_common/CMakeLists.txt | 1 + nav2_ros_common/package.xml | 1 + nav2_rotation_shim_controller/CMakeLists.txt | 1 + nav2_rotation_shim_controller/package.xml | 1 + nav2_route/CMakeLists.txt | 1 + nav2_route/package.xml | 1 + nav2_rviz_plugins/CMakeLists.txt | 1 + nav2_rviz_plugins/package.xml | 1 + nav2_smac_planner/CMakeLists.txt | 1 + nav2_smac_planner/package.xml | 1 + nav2_smoother/CMakeLists.txt | 1 + nav2_smoother/package.xml | 1 + nav2_theta_star_planner/CMakeLists.txt | 1 + nav2_theta_star_planner/package.xml | 1 + nav2_util/CMakeLists.txt | 1 + nav2_util/package.xml | 1 + nav2_velocity_smoother/CMakeLists.txt | 1 + nav2_velocity_smoother/package.xml | 1 + nav2_voxel_grid/CMakeLists.txt | 1 + nav2_voxel_grid/package.xml | 1 + nav2_waypoint_follower/CMakeLists.txt | 1 + nav2_waypoint_follower/package.xml | 1 + 82 files changed, 84 insertions(+) diff --git a/nav2_amcl/CMakeLists.txt b/nav2_amcl/CMakeLists.txt index aa6cf1a4ed7..4813b3cdd9b 100644 --- a/nav2_amcl/CMakeLists.txt +++ b/nav2_amcl/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(nav2_amcl) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(geometry_msgs REQUIRED) find_package(message_filters REQUIRED) find_package(nav_msgs REQUIRED) diff --git a/nav2_amcl/package.xml b/nav2_amcl/package.xml index c1a2cbc2cea..69ac2d324b5 100644 --- a/nav2_amcl/package.xml +++ b/nav2_amcl/package.xml @@ -24,6 +24,7 @@ ament_cmake nav2_common + backward_ros geometry_msgs message_filters nav_msgs diff --git a/nav2_behavior_tree/CMakeLists.txt b/nav2_behavior_tree/CMakeLists.txt index 4059e6da15e..de7d0f6237f 100644 --- a/nav2_behavior_tree/CMakeLists.txt +++ b/nav2_behavior_tree/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(nav2_behavior_tree CXX) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(action_msgs REQUIRED) find_package(behaviortree_cpp REQUIRED) find_package(geometry_msgs REQUIRED) diff --git a/nav2_behavior_tree/package.xml b/nav2_behavior_tree/package.xml index e931df421e2..69bcb1466f7 100644 --- a/nav2_behavior_tree/package.xml +++ b/nav2_behavior_tree/package.xml @@ -12,6 +12,7 @@ ament_cmake action_msgs + backward_ros behaviortree_cpp geometry_msgs nav2_common diff --git a/nav2_behaviors/CMakeLists.txt b/nav2_behaviors/CMakeLists.txt index c67f9af628b..bd37a097c2f 100644 --- a/nav2_behaviors/CMakeLists.txt +++ b/nav2_behaviors/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(nav2_behaviors) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(geometry_msgs REQUIRED) find_package(nav2_common REQUIRED) find_package(nav2_core REQUIRED) diff --git a/nav2_behaviors/package.xml b/nav2_behaviors/package.xml index c3739a3461e..04f20f80782 100644 --- a/nav2_behaviors/package.xml +++ b/nav2_behaviors/package.xml @@ -11,6 +11,7 @@ ament_cmake nav2_common + backward_ros geometry_msgs nav2_core nav2_costmap_2d diff --git a/nav2_bringup/CMakeLists.txt b/nav2_bringup/CMakeLists.txt index 5537046d246..b0999ee4eb8 100644 --- a/nav2_bringup/CMakeLists.txt +++ b/nav2_bringup/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(nav2_bringup) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(nav2_common REQUIRED) find_package(navigation2 REQUIRED) diff --git a/nav2_bringup/package.xml b/nav2_bringup/package.xml index dc5ebc7bb19..d17f705cd1e 100644 --- a/nav2_bringup/package.xml +++ b/nav2_bringup/package.xml @@ -12,9 +12,11 @@ ament_cmake nav2_common + backward_ros navigation2 launch_ros + backward_ros diff_drive_controller joint_state_broadcaster launch_ros diff --git a/nav2_bt_navigator/CMakeLists.txt b/nav2_bt_navigator/CMakeLists.txt index 2fa895dd8b8..83fa44fab7f 100644 --- a/nav2_bt_navigator/CMakeLists.txt +++ b/nav2_bt_navigator/CMakeLists.txt @@ -3,6 +3,7 @@ project(nav2_bt_navigator) find_package(ament_cmake REQUIRED) find_package(ament_index_cpp REQUIRED) +find_package(backward_ros REQUIRED) find_package(geometry_msgs REQUIRED) find_package(nav2_behavior_tree REQUIRED) find_package(nav2_common REQUIRED) diff --git a/nav2_bt_navigator/package.xml b/nav2_bt_navigator/package.xml index 825e8f9eec3..62e5b7307ea 100644 --- a/nav2_bt_navigator/package.xml +++ b/nav2_bt_navigator/package.xml @@ -11,6 +11,7 @@ nav2_common ament_index_cpp + backward_ros geometry_msgs nav2_behavior_tree nav2_core diff --git a/nav2_collision_monitor/CMakeLists.txt b/nav2_collision_monitor/CMakeLists.txt index a7c22e59a09..e67c8ea7719 100644 --- a/nav2_collision_monitor/CMakeLists.txt +++ b/nav2_collision_monitor/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(nav2_collision_monitor) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(geometry_msgs REQUIRED) find_package(nav2_common REQUIRED) find_package(nav2_costmap_2d REQUIRED) diff --git a/nav2_collision_monitor/package.xml b/nav2_collision_monitor/package.xml index 47ffe9a6d60..6a8e5ace75b 100644 --- a/nav2_collision_monitor/package.xml +++ b/nav2_collision_monitor/package.xml @@ -11,6 +11,7 @@ ament_cmake nav2_common + backward_ros geometry_msgs nav2_util nav2_costmap_2d diff --git a/nav2_common/CMakeLists.txt b/nav2_common/CMakeLists.txt index 8504b8b23de..f0f1e22c32c 100644 --- a/nav2_common/CMakeLists.txt +++ b/nav2_common/CMakeLists.txt @@ -4,6 +4,7 @@ project(nav2_common NONE) find_package(ament_cmake_core REQUIRED) find_package(ament_cmake_python REQUIRED) +find_package(backward_ros REQUIRED) ament_python_install_package(nav2_common) diff --git a/nav2_common/package.xml b/nav2_common/package.xml index 7aa6a3925eb..12bbf129ccd 100644 --- a/nav2_common/package.xml +++ b/nav2_common/package.xml @@ -7,6 +7,7 @@ Carl Delsey Apache-2.0 + backward_ros launch launch_ros osrf_pycommon diff --git a/nav2_constrained_smoother/CMakeLists.txt b/nav2_constrained_smoother/CMakeLists.txt index 31e83e77fe5..c4613f52496 100644 --- a/nav2_constrained_smoother/CMakeLists.txt +++ b/nav2_constrained_smoother/CMakeLists.txt @@ -4,6 +4,7 @@ project(nav2_constrained_smoother) set(CMAKE_BUILD_TYPE Release) # significant Ceres optimization speedup find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(Ceres REQUIRED COMPONENTS SuiteSparse) find_package(Eigen3 REQUIRED) find_package(geometry_msgs REQUIRED) diff --git a/nav2_constrained_smoother/package.xml b/nav2_constrained_smoother/package.xml index 11fb47b1b06..eb5b60ae9b4 100644 --- a/nav2_constrained_smoother/package.xml +++ b/nav2_constrained_smoother/package.xml @@ -12,6 +12,7 @@ eigen nav2_common + backward_ros geometry_msgs libceres-dev nav2_core diff --git a/nav2_controller/CMakeLists.txt b/nav2_controller/CMakeLists.txt index a100f96a214..56ea5d7e794 100644 --- a/nav2_controller/CMakeLists.txt +++ b/nav2_controller/CMakeLists.txt @@ -3,6 +3,7 @@ project(nav2_controller) find_package(ament_cmake REQUIRED) find_package(angles REQUIRED) +find_package(backward_ros REQUIRED) find_package(geometry_msgs REQUIRED) find_package(lifecycle_msgs REQUIRED) find_package(nav2_core REQUIRED) diff --git a/nav2_controller/package.xml b/nav2_controller/package.xml index 16875164c0d..b5be342451c 100644 --- a/nav2_controller/package.xml +++ b/nav2_controller/package.xml @@ -11,6 +11,7 @@ nav2_common angles + backward_ros geometry_msgs lifecycle_msgs nav2_core diff --git a/nav2_core/CMakeLists.txt b/nav2_core/CMakeLists.txt index 1d0e6b724f5..586410c3488 100644 --- a/nav2_core/CMakeLists.txt +++ b/nav2_core/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(nav2_core) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(geometry_msgs REQUIRED) find_package(nav2_behavior_tree REQUIRED) find_package(nav2_common REQUIRED) diff --git a/nav2_core/package.xml b/nav2_core/package.xml index 044eb215676..aa95ad053fd 100644 --- a/nav2_core/package.xml +++ b/nav2_core/package.xml @@ -11,6 +11,7 @@ ament_cmake nav2_common + backward_ros geometry_msgs nav2_behavior_tree nav2_costmap_2d diff --git a/nav2_costmap_2d/CMakeLists.txt b/nav2_costmap_2d/CMakeLists.txt index 221c5595515..851f2428b42 100644 --- a/nav2_costmap_2d/CMakeLists.txt +++ b/nav2_costmap_2d/CMakeLists.txt @@ -3,6 +3,7 @@ project(nav2_costmap_2d) find_package(ament_cmake REQUIRED) find_package(angles REQUIRED) +find_package(backward_ros REQUIRED) find_package(geometry_msgs REQUIRED) find_package(laser_geometry REQUIRED) find_package(map_msgs REQUIRED) diff --git a/nav2_costmap_2d/package.xml b/nav2_costmap_2d/package.xml index c01e7bb2dd6..c69d467b908 100644 --- a/nav2_costmap_2d/package.xml +++ b/nav2_costmap_2d/package.xml @@ -20,6 +20,7 @@ nav2_common angles + backward_ros geometry_msgs laser_geometry map_msgs diff --git a/nav2_docking/opennav_docking/CMakeLists.txt b/nav2_docking/opennav_docking/CMakeLists.txt index 83680e3eeae..a68bd4677ab 100644 --- a/nav2_docking/opennav_docking/CMakeLists.txt +++ b/nav2_docking/opennav_docking/CMakeLists.txt @@ -3,6 +3,7 @@ project(opennav_docking) find_package(ament_cmake REQUIRED) find_package(angles REQUIRED) +find_package(backward_ros REQUIRED) find_package(geometry_msgs REQUIRED) find_package(nav2_common REQUIRED) find_package(nav2_graceful_controller REQUIRED) diff --git a/nav2_docking/opennav_docking/package.xml b/nav2_docking/opennav_docking/package.xml index ecfa8082379..b65cb91ee56 100644 --- a/nav2_docking/opennav_docking/package.xml +++ b/nav2_docking/opennav_docking/package.xml @@ -11,6 +11,7 @@ nav2_common angles + backward_ros geometry_msgs nav2_graceful_controller nav2_msgs diff --git a/nav2_docking/opennav_docking_bt/CMakeLists.txt b/nav2_docking/opennav_docking_bt/CMakeLists.txt index 015265a3753..05e67fb713f 100644 --- a/nav2_docking/opennav_docking_bt/CMakeLists.txt +++ b/nav2_docking/opennav_docking_bt/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(opennav_docking_bt) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(behaviortree_cpp REQUIRED) find_package(geometry_msgs REQUIRED) find_package(nav2_behavior_tree REQUIRED) diff --git a/nav2_docking/opennav_docking_bt/package.xml b/nav2_docking/opennav_docking_bt/package.xml index 3b946c8c296..9fc0d3c0692 100644 --- a/nav2_docking/opennav_docking_bt/package.xml +++ b/nav2_docking/opennav_docking_bt/package.xml @@ -10,6 +10,7 @@ ament_cmake nav2_common + backward_ros behaviortree_cpp geometry_msgs nav2_behavior_tree diff --git a/nav2_docking/opennav_docking_core/CMakeLists.txt b/nav2_docking/opennav_docking_core/CMakeLists.txt index 9575861645a..057bebb0b6c 100644 --- a/nav2_docking/opennav_docking_core/CMakeLists.txt +++ b/nav2_docking/opennav_docking_core/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(opennav_docking_core) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(geometry_msgs REQUIRED) find_package(nav2_common REQUIRED) find_package(rclcpp_lifecycle REQUIRED) diff --git a/nav2_docking/opennav_docking_core/package.xml b/nav2_docking/opennav_docking_core/package.xml index 735f89cf24d..285bf274cce 100644 --- a/nav2_docking/opennav_docking_core/package.xml +++ b/nav2_docking/opennav_docking_core/package.xml @@ -10,6 +10,7 @@ ament_cmake nav2_common + backward_ros geometry_msgs rclcpp_lifecycle tf2_ros diff --git a/nav2_dwb_controller/costmap_queue/CMakeLists.txt b/nav2_dwb_controller/costmap_queue/CMakeLists.txt index 28d47ed3dd5..68dbfa64ef9 100644 --- a/nav2_dwb_controller/costmap_queue/CMakeLists.txt +++ b/nav2_dwb_controller/costmap_queue/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(costmap_queue) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(nav2_common REQUIRED) find_package(nav2_costmap_2d REQUIRED) diff --git a/nav2_dwb_controller/costmap_queue/package.xml b/nav2_dwb_controller/costmap_queue/package.xml index 6d96d276a12..a74b57e69a6 100644 --- a/nav2_dwb_controller/costmap_queue/package.xml +++ b/nav2_dwb_controller/costmap_queue/package.xml @@ -9,6 +9,7 @@ ament_cmake nav2_common + backward_ros nav2_costmap_2d ament_cmake_gtest diff --git a/nav2_dwb_controller/dwb_core/CMakeLists.txt b/nav2_dwb_controller/dwb_core/CMakeLists.txt index ba787fb831c..b57c2291a51 100644 --- a/nav2_dwb_controller/dwb_core/CMakeLists.txt +++ b/nav2_dwb_controller/dwb_core/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(dwb_core) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(builtin_interfaces REQUIRED) find_package(dwb_msgs REQUIRED) find_package(geometry_msgs REQUIRED) diff --git a/nav2_dwb_controller/dwb_core/package.xml b/nav2_dwb_controller/dwb_core/package.xml index ce879804bbc..a76dd8c474c 100644 --- a/nav2_dwb_controller/dwb_core/package.xml +++ b/nav2_dwb_controller/dwb_core/package.xml @@ -10,6 +10,7 @@ ament_cmake nav2_common + backward_ros builtin_interfaces dwb_msgs geometry_msgs diff --git a/nav2_dwb_controller/dwb_critics/CMakeLists.txt b/nav2_dwb_controller/dwb_critics/CMakeLists.txt index b1cb464cd5c..ddda0a9087b 100644 --- a/nav2_dwb_controller/dwb_critics/CMakeLists.txt +++ b/nav2_dwb_controller/dwb_critics/CMakeLists.txt @@ -3,6 +3,7 @@ project(dwb_critics) find_package(ament_cmake REQUIRED) find_package(angles REQUIRED) +find_package(backward_ros REQUIRED) find_package(costmap_queue REQUIRED) find_package(dwb_core REQUIRED) find_package(dwb_msgs REQUIRED) diff --git a/nav2_dwb_controller/dwb_critics/package.xml b/nav2_dwb_controller/dwb_critics/package.xml index 79daf191b20..eb923ca051e 100644 --- a/nav2_dwb_controller/dwb_critics/package.xml +++ b/nav2_dwb_controller/dwb_critics/package.xml @@ -9,6 +9,7 @@ ament_cmake nav2_common + backward_ros angles costmap_queue dwb_core diff --git a/nav2_dwb_controller/dwb_msgs/CMakeLists.txt b/nav2_dwb_controller/dwb_msgs/CMakeLists.txt index 2f34fcdf5ea..574a48c4c6a 100644 --- a/nav2_dwb_controller/dwb_msgs/CMakeLists.txt +++ b/nav2_dwb_controller/dwb_msgs/CMakeLists.txt @@ -1,6 +1,7 @@ cmake_minimum_required(VERSION 3.5) project(dwb_msgs) +find_package(backward_ros REQUIRED) find_package(builtin_interfaces REQUIRED) find_package(geometry_msgs REQUIRED) find_package(nav_2d_msgs REQUIRED) diff --git a/nav2_dwb_controller/dwb_msgs/package.xml b/nav2_dwb_controller/dwb_msgs/package.xml index 7c17869c2f3..9d1b3f47e6b 100644 --- a/nav2_dwb_controller/dwb_msgs/package.xml +++ b/nav2_dwb_controller/dwb_msgs/package.xml @@ -9,6 +9,7 @@ ament_cmake + backward_ros builtin_interfaces geometry_msgs nav_2d_msgs diff --git a/nav2_dwb_controller/dwb_plugins/CMakeLists.txt b/nav2_dwb_controller/dwb_plugins/CMakeLists.txt index f725aa2cf94..7d4228ae427 100644 --- a/nav2_dwb_controller/dwb_plugins/CMakeLists.txt +++ b/nav2_dwb_controller/dwb_plugins/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(dwb_plugins) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(dwb_core REQUIRED) find_package(dwb_msgs REQUIRED) find_package(geometry_msgs REQUIRED) diff --git a/nav2_dwb_controller/dwb_plugins/package.xml b/nav2_dwb_controller/dwb_plugins/package.xml index d07a7b79f08..f25f724d95a 100644 --- a/nav2_dwb_controller/dwb_plugins/package.xml +++ b/nav2_dwb_controller/dwb_plugins/package.xml @@ -12,6 +12,7 @@ ament_cmake nav2_common + backward_ros dwb_core dwb_msgs geometry_msgs diff --git a/nav2_dwb_controller/nav2_dwb_controller/CMakeLists.txt b/nav2_dwb_controller/nav2_dwb_controller/CMakeLists.txt index e900fd292ca..5c1c8fce07a 100644 --- a/nav2_dwb_controller/nav2_dwb_controller/CMakeLists.txt +++ b/nav2_dwb_controller/nav2_dwb_controller/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(nav2_dwb_controller) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(nav2_common REQUIRED) nav2_package() diff --git a/nav2_dwb_controller/nav2_dwb_controller/package.xml b/nav2_dwb_controller/nav2_dwb_controller/package.xml index 7d8363bcfc2..8eea7edacb8 100644 --- a/nav2_dwb_controller/nav2_dwb_controller/package.xml +++ b/nav2_dwb_controller/nav2_dwb_controller/package.xml @@ -11,8 +11,10 @@ Apache-2.0 ament_cmake + backward_ros nav2_common + backward_ros costmap_queue dwb_core dwb_critics diff --git a/nav2_dwb_controller/nav_2d_msgs/CMakeLists.txt b/nav2_dwb_controller/nav_2d_msgs/CMakeLists.txt index c2838963c9b..155c13cdd65 100644 --- a/nav2_dwb_controller/nav_2d_msgs/CMakeLists.txt +++ b/nav2_dwb_controller/nav_2d_msgs/CMakeLists.txt @@ -1,6 +1,7 @@ cmake_minimum_required(VERSION 3.5) project(nav_2d_msgs) +find_package(backward_ros REQUIRED) find_package(geometry_msgs REQUIRED) find_package(std_msgs REQUIRED) find_package(rosidl_default_generators REQUIRED) diff --git a/nav2_dwb_controller/nav_2d_msgs/package.xml b/nav2_dwb_controller/nav_2d_msgs/package.xml index 129c4d3367a..bd343580140 100644 --- a/nav2_dwb_controller/nav_2d_msgs/package.xml +++ b/nav2_dwb_controller/nav_2d_msgs/package.xml @@ -11,6 +11,7 @@ ament_cmake + backward_ros geometry_msgs std_msgs rosidl_default_generators diff --git a/nav2_dwb_controller/nav_2d_utils/CMakeLists.txt b/nav2_dwb_controller/nav_2d_utils/CMakeLists.txt index 8a729af46a9..c3005dffd6c 100644 --- a/nav2_dwb_controller/nav_2d_utils/CMakeLists.txt +++ b/nav2_dwb_controller/nav_2d_utils/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(nav_2d_utils) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(geometry_msgs REQUIRED) find_package(nav_2d_msgs REQUIRED) find_package(nav_msgs REQUIRED) diff --git a/nav2_dwb_controller/nav_2d_utils/package.xml b/nav2_dwb_controller/nav_2d_utils/package.xml index ea8e213c77d..9968469eab4 100644 --- a/nav2_dwb_controller/nav_2d_utils/package.xml +++ b/nav2_dwb_controller/nav_2d_utils/package.xml @@ -10,6 +10,7 @@ ament_cmake nav2_common + backward_ros geometry_msgs nav_2d_msgs nav_msgs diff --git a/nav2_graceful_controller/CMakeLists.txt b/nav2_graceful_controller/CMakeLists.txt index 1a72a698085..fc8859860b3 100644 --- a/nav2_graceful_controller/CMakeLists.txt +++ b/nav2_graceful_controller/CMakeLists.txt @@ -3,6 +3,7 @@ project(nav2_graceful_controller) find_package(ament_cmake REQUIRED) find_package(angles REQUIRED) +find_package(backward_ros REQUIRED) find_package(geometry_msgs REQUIRED) find_package(nav2_common REQUIRED) find_package(nav2_core REQUIRED) diff --git a/nav2_graceful_controller/package.xml b/nav2_graceful_controller/package.xml index a195e466bec..f162f3f0ed3 100644 --- a/nav2_graceful_controller/package.xml +++ b/nav2_graceful_controller/package.xml @@ -11,6 +11,7 @@ nav2_common angles + backward_ros geometry_msgs nav2_core nav2_costmap_2d diff --git a/nav2_lifecycle_manager/CMakeLists.txt b/nav2_lifecycle_manager/CMakeLists.txt index 654b7153d06..64b1f4ad88d 100644 --- a/nav2_lifecycle_manager/CMakeLists.txt +++ b/nav2_lifecycle_manager/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(nav2_lifecycle_manager) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(bondcpp REQUIRED) find_package(diagnostic_updater REQUIRED) find_package(geometry_msgs REQUIRED) diff --git a/nav2_lifecycle_manager/package.xml b/nav2_lifecycle_manager/package.xml index 99b0b2f9250..0b6790935fc 100644 --- a/nav2_lifecycle_manager/package.xml +++ b/nav2_lifecycle_manager/package.xml @@ -10,6 +10,7 @@ ament_cmake nav2_common + backward_ros bondcpp diagnostic_updater geometry_msgs diff --git a/nav2_map_server/CMakeLists.txt b/nav2_map_server/CMakeLists.txt index 438e1aa12fb..55160b2f7de 100644 --- a/nav2_map_server/CMakeLists.txt +++ b/nav2_map_server/CMakeLists.txt @@ -4,6 +4,7 @@ project(nav2_map_server) list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/cmake_modules) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(GRAPHICSMAGICKCPP REQUIRED) find_package(lifecycle_msgs REQUIRED) find_package(nav_msgs REQUIRED) diff --git a/nav2_map_server/package.xml b/nav2_map_server/package.xml index f6d5f75a278..ffb3d3d1fce 100644 --- a/nav2_map_server/package.xml +++ b/nav2_map_server/package.xml @@ -14,6 +14,7 @@ nav2_common + backward_ros rclcpp_lifecycle nav_msgs std_msgs diff --git a/nav2_mppi_controller/CMakeLists.txt b/nav2_mppi_controller/CMakeLists.txt index 5197a8859de..9dabc27507f 100644 --- a/nav2_mppi_controller/CMakeLists.txt +++ b/nav2_mppi_controller/CMakeLists.txt @@ -3,6 +3,7 @@ project(nav2_mppi_controller) find_package(ament_cmake REQUIRED) find_package(angles REQUIRED) +find_package(backward_ros REQUIRED) find_package(geometry_msgs REQUIRED) find_package(nav2_common REQUIRED) find_package(nav2_core REQUIRED) diff --git a/nav2_mppi_controller/package.xml b/nav2_mppi_controller/package.xml index adde8cf93dc..a27716ab997 100644 --- a/nav2_mppi_controller/package.xml +++ b/nav2_mppi_controller/package.xml @@ -12,6 +12,7 @@ ament_cmake_ros angles + backward_ros geometry_msgs libomp-dev nav2_common diff --git a/nav2_msgs/CMakeLists.txt b/nav2_msgs/CMakeLists.txt index 6252ab32e4e..f01fbdba6a5 100644 --- a/nav2_msgs/CMakeLists.txt +++ b/nav2_msgs/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(nav2_msgs) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(nav2_common REQUIRED) find_package(builtin_interfaces REQUIRED) find_package(nav_msgs REQUIRED) diff --git a/nav2_msgs/package.xml b/nav2_msgs/package.xml index 12a085fbf88..61e84addc37 100644 --- a/nav2_msgs/package.xml +++ b/nav2_msgs/package.xml @@ -12,6 +12,7 @@ ament_cmake nav2_common + backward_ros rclcpp std_msgs builtin_interfaces diff --git a/nav2_navfn_planner/CMakeLists.txt b/nav2_navfn_planner/CMakeLists.txt index df0a924a40a..0da443e5e35 100644 --- a/nav2_navfn_planner/CMakeLists.txt +++ b/nav2_navfn_planner/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(nav2_navfn_planner) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(builtin_interfaces REQUIRED) find_package(geometry_msgs REQUIRED) find_package(nav2_common REQUIRED) diff --git a/nav2_navfn_planner/package.xml b/nav2_navfn_planner/package.xml index e00bcefaf29..fe4c11b373a 100644 --- a/nav2_navfn_planner/package.xml +++ b/nav2_navfn_planner/package.xml @@ -12,6 +12,7 @@ ament_cmake nav2_common + backward_ros builtin_interfaces geometry_msgs nav2_costmap_2d diff --git a/nav2_planner/CMakeLists.txt b/nav2_planner/CMakeLists.txt index 9c2e44e69b2..d3037ddf839 100644 --- a/nav2_planner/CMakeLists.txt +++ b/nav2_planner/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(nav2_planner) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(geometry_msgs REQUIRED) find_package(lifecycle_msgs REQUIRED) find_package(nav2_common REQUIRED) diff --git a/nav2_planner/package.xml b/nav2_planner/package.xml index b0cdf1c8846..50ad1add559 100644 --- a/nav2_planner/package.xml +++ b/nav2_planner/package.xml @@ -10,6 +10,7 @@ ament_cmake nav2_common + backward_ros geometry_msgs lifecycle_msgs nav2_core diff --git a/nav2_regulated_pure_pursuit_controller/CMakeLists.txt b/nav2_regulated_pure_pursuit_controller/CMakeLists.txt index 2cd43c87154..493418fda67 100644 --- a/nav2_regulated_pure_pursuit_controller/CMakeLists.txt +++ b/nav2_regulated_pure_pursuit_controller/CMakeLists.txt @@ -3,6 +3,7 @@ project(nav2_regulated_pure_pursuit_controller) find_package(ament_cmake REQUIRED) find_package(angles REQUIRED) +find_package(backward_ros REQUIRED) find_package(geometry_msgs REQUIRED) find_package(nav2_common REQUIRED) find_package(nav2_core REQUIRED) diff --git a/nav2_regulated_pure_pursuit_controller/package.xml b/nav2_regulated_pure_pursuit_controller/package.xml index 454b9542c7d..70df60839d7 100644 --- a/nav2_regulated_pure_pursuit_controller/package.xml +++ b/nav2_regulated_pure_pursuit_controller/package.xml @@ -12,6 +12,7 @@ nav2_common angles + backward_ros geometry_msgs nav2_core nav2_costmap_2d diff --git a/nav2_ros_common/CMakeLists.txt b/nav2_ros_common/CMakeLists.txt index fc529730ea8..13dfd418833 100644 --- a/nav2_ros_common/CMakeLists.txt +++ b/nav2_ros_common/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(nav2_ros_common) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(bond REQUIRED) find_package(bondcpp REQUIRED) find_package(geometry_msgs REQUIRED) diff --git a/nav2_ros_common/package.xml b/nav2_ros_common/package.xml index 33731f20e15..e2e2eb2695d 100644 --- a/nav2_ros_common/package.xml +++ b/nav2_ros_common/package.xml @@ -11,6 +11,7 @@ nav2_common + backward_ros bond bondcpp builtin_interfaces diff --git a/nav2_rotation_shim_controller/CMakeLists.txt b/nav2_rotation_shim_controller/CMakeLists.txt index c8140e661a6..0adcf17d8f1 100644 --- a/nav2_rotation_shim_controller/CMakeLists.txt +++ b/nav2_rotation_shim_controller/CMakeLists.txt @@ -3,6 +3,7 @@ project(nav2_rotation_shim_controller) find_package(ament_cmake REQUIRED) find_package(angles REQUIRED) +find_package(backward_ros REQUIRED) find_package(geometry_msgs REQUIRED) find_package(nav2_common REQUIRED) find_package(nav2_controller REQUIRED) diff --git a/nav2_rotation_shim_controller/package.xml b/nav2_rotation_shim_controller/package.xml index 2505599a071..02fbffd55f0 100644 --- a/nav2_rotation_shim_controller/package.xml +++ b/nav2_rotation_shim_controller/package.xml @@ -11,6 +11,7 @@ nav2_common angles + backward_ros geometry_msgs nav2_core nav2_controller diff --git a/nav2_route/CMakeLists.txt b/nav2_route/CMakeLists.txt index 3ceb96f6753..fcc53119651 100644 --- a/nav2_route/CMakeLists.txt +++ b/nav2_route/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(nav2_route CXX) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(nav2_common REQUIRED) find_package(rclcpp REQUIRED) find_package(rclcpp_lifecycle REQUIRED) diff --git a/nav2_route/package.xml b/nav2_route/package.xml index 855356eefc2..93fffbf8e1a 100644 --- a/nav2_route/package.xml +++ b/nav2_route/package.xml @@ -10,6 +10,7 @@ ament_cmake nav2_common + backward_ros rclcpp rclcpp_lifecycle std_msgs diff --git a/nav2_rviz_plugins/CMakeLists.txt b/nav2_rviz_plugins/CMakeLists.txt index 958047a22c2..62e19b2f7a2 100644 --- a/nav2_rviz_plugins/CMakeLists.txt +++ b/nav2_rviz_plugins/CMakeLists.txt @@ -3,6 +3,7 @@ project(nav2_rviz_plugins) find_package(ament_cmake REQUIRED) find_package(ament_index_cpp REQUIRED) +find_package(backward_ros REQUIRED) find_package(geometry_msgs REQUIRED) find_package(nav2_common REQUIRED) find_package(nav2_lifecycle_manager REQUIRED) diff --git a/nav2_rviz_plugins/package.xml b/nav2_rviz_plugins/package.xml index 91de46fa84c..3a25157d1ce 100644 --- a/nav2_rviz_plugins/package.xml +++ b/nav2_rviz_plugins/package.xml @@ -12,6 +12,7 @@ qtbase5-dev ament_index_cpp + backward_ros geometry_msgs nav2_lifecycle_manager nav2_msgs diff --git a/nav2_smac_planner/CMakeLists.txt b/nav2_smac_planner/CMakeLists.txt index a344a41aa0f..051818f7c9a 100644 --- a/nav2_smac_planner/CMakeLists.txt +++ b/nav2_smac_planner/CMakeLists.txt @@ -4,6 +4,7 @@ project(nav2_smac_planner) find_package(ament_cmake REQUIRED) find_package(ament_index_cpp REQUIRED) find_package(angles REQUIRED) +find_package(backward_ros REQUIRED) find_package(geometry_msgs REQUIRED) find_package(nav2_common REQUIRED) find_package(nav2_core REQUIRED) diff --git a/nav2_smac_planner/package.xml b/nav2_smac_planner/package.xml index f8acf8bd6a6..9400ea3b37b 100644 --- a/nav2_smac_planner/package.xml +++ b/nav2_smac_planner/package.xml @@ -12,6 +12,7 @@ ament_index_cpp angles + backward_ros eigen geometry_msgs nav2_core diff --git a/nav2_smoother/CMakeLists.txt b/nav2_smoother/CMakeLists.txt index b2e3555ab2a..465cadd670d 100644 --- a/nav2_smoother/CMakeLists.txt +++ b/nav2_smoother/CMakeLists.txt @@ -3,6 +3,7 @@ project(nav2_smoother) find_package(ament_cmake REQUIRED) find_package(angles REQUIRED) +find_package(backward_ros REQUIRED) find_package(nav2_common REQUIRED) find_package(nav2_core REQUIRED) find_package(nav2_costmap_2d REQUIRED) diff --git a/nav2_smoother/package.xml b/nav2_smoother/package.xml index e7d4708d790..c2983d916ce 100644 --- a/nav2_smoother/package.xml +++ b/nav2_smoother/package.xml @@ -12,6 +12,7 @@ nav2_common angles + backward_ros nav2_core nav2_costmap_2d nav2_msgs diff --git a/nav2_theta_star_planner/CMakeLists.txt b/nav2_theta_star_planner/CMakeLists.txt index 87da1fa9cbc..0a478f2fb9b 100644 --- a/nav2_theta_star_planner/CMakeLists.txt +++ b/nav2_theta_star_planner/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(nav2_theta_star_planner) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(geometry_msgs REQUIRED) find_package(nav2_common REQUIRED) find_package(nav2_core REQUIRED) diff --git a/nav2_theta_star_planner/package.xml b/nav2_theta_star_planner/package.xml index 0279b52fe7e..414de9f3066 100644 --- a/nav2_theta_star_planner/package.xml +++ b/nav2_theta_star_planner/package.xml @@ -11,6 +11,7 @@ ament_cmake nav2_common + backward_ros geometry_msgs nav2_core nav2_costmap_2d diff --git a/nav2_util/CMakeLists.txt b/nav2_util/CMakeLists.txt index 08a7908f3b3..2a2f4061818 100644 --- a/nav2_util/CMakeLists.txt +++ b/nav2_util/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(nav2_util) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(bond REQUIRED) find_package(bondcpp REQUIRED) find_package(geometry_msgs REQUIRED) diff --git a/nav2_util/package.xml b/nav2_util/package.xml index 0acd0f1a37b..334879c3981 100644 --- a/nav2_util/package.xml +++ b/nav2_util/package.xml @@ -13,6 +13,7 @@ nav2_common + backward_ros bond bondcpp builtin_interfaces diff --git a/nav2_velocity_smoother/CMakeLists.txt b/nav2_velocity_smoother/CMakeLists.txt index 75c59b8a89c..916ee93ed2c 100644 --- a/nav2_velocity_smoother/CMakeLists.txt +++ b/nav2_velocity_smoother/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(nav2_velocity_smoother) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(geometry_msgs REQUIRED) find_package(nav2_common REQUIRED) find_package(nav2_util REQUIRED) diff --git a/nav2_velocity_smoother/package.xml b/nav2_velocity_smoother/package.xml index b0ed66dd3e6..52cfb61d9d7 100644 --- a/nav2_velocity_smoother/package.xml +++ b/nav2_velocity_smoother/package.xml @@ -10,6 +10,7 @@ ament_cmake nav2_common + backward_ros geometry_msgs nav2_util rclcpp diff --git a/nav2_voxel_grid/CMakeLists.txt b/nav2_voxel_grid/CMakeLists.txt index e204367ada8..093fee48a84 100644 --- a/nav2_voxel_grid/CMakeLists.txt +++ b/nav2_voxel_grid/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(nav2_voxel_grid) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(nav2_common REQUIRED) find_package(rclcpp REQUIRED) diff --git a/nav2_voxel_grid/package.xml b/nav2_voxel_grid/package.xml index ef9eedd9b42..36d5b14ba6b 100644 --- a/nav2_voxel_grid/package.xml +++ b/nav2_voxel_grid/package.xml @@ -12,6 +12,7 @@ ament_cmake nav2_common + backward_ros rclcpp ament_lint_common diff --git a/nav2_waypoint_follower/CMakeLists.txt b/nav2_waypoint_follower/CMakeLists.txt index f41c6e5f730..17d406adef9 100644 --- a/nav2_waypoint_follower/CMakeLists.txt +++ b/nav2_waypoint_follower/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.5) project(nav2_waypoint_follower) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) find_package(cv_bridge REQUIRED) find_package(geographic_msgs REQUIRED) find_package(geometry_msgs REQUIRED) diff --git a/nav2_waypoint_follower/package.xml b/nav2_waypoint_follower/package.xml index 4aa90590587..980881aa7a9 100644 --- a/nav2_waypoint_follower/package.xml +++ b/nav2_waypoint_follower/package.xml @@ -10,6 +10,7 @@ ament_cmake nav2_common + backward_ros cv_bridge geographic_msgs geometry_msgs From 6330ee464a82556b2388c683960972cc0937c3dd Mon Sep 17 00:00:00 2001 From: YLFeng Date: Thu, 7 Aug 2025 01:02:41 +0800 Subject: [PATCH 09/51] Add IndexType definition for Nanoflann KDTree in `node_spatial_tree`. (#5420) * fix: Add KDTree type definition to include unsigned int for IndexType Signed-off-by: Ericsii * code format Signed-off-by: Ericsii --------- Signed-off-by: Ericsii --- nav2_route/include/nav2_route/node_spatial_tree.hpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nav2_route/include/nav2_route/node_spatial_tree.hpp b/nav2_route/include/nav2_route/node_spatial_tree.hpp index 15d9e2a5528..744d30791c7 100644 --- a/nav2_route/include/nav2_route/node_spatial_tree.hpp +++ b/nav2_route/include/nav2_route/node_spatial_tree.hpp @@ -55,7 +55,9 @@ struct GraphAdaptor }; typedef nanoflann::KDTreeSingleIndexAdaptor< - nanoflann::L2_Simple_Adaptor, GraphAdaptor, DIMENSION> kd_tree_t; + nanoflann::L2_Simple_Adaptor, GraphAdaptor, DIMENSION, + unsigned int> + kd_tree_t; /** * @class nav2_route::NodeSpatialTree From c7884f583ef72f758b6cf370db19df5a69df9c92 Mon Sep 17 00:00:00 2001 From: mini-1235 Date: Thu, 7 Aug 2025 01:03:01 +0800 Subject: [PATCH 10/51] Update tf2 ros headers (#5381) Signed-off-by: mini-1235 --- nav2_amcl/include/nav2_amcl/amcl_node.hpp | 6 +++--- nav2_amcl/src/amcl_node.cpp | 10 +++++----- .../plugins/action/truncate_path_local_action.hpp | 2 +- .../plugins/condition/are_poses_near_condition.hpp | 2 +- .../plugins/condition/distance_traveled_condition.hpp | 2 +- .../plugins/condition/goal_reached_condition.hpp | 2 +- .../condition/transform_available_condition.hpp | 2 +- .../plugins/decorator/distance_controller.hpp | 2 +- .../include/nav2_behavior_tree/ros_topic_logger.hpp | 2 +- .../plugins/action/truncate_path_local_action.cpp | 4 ++-- .../condition/transform_available_condition.cpp | 2 +- .../plugins/decorator/distance_controller.cpp | 2 +- .../test/utils/test_transform_handler.hpp | 6 +++--- .../include/nav2_behaviors/behavior_server.hpp | 4 ++-- .../include/nav2_behaviors/timed_behavior.hpp | 4 ++-- .../include/nav2_bt_navigator/bt_navigator.hpp | 6 +++--- .../nav2_collision_monitor/collision_detector_node.hpp | 4 ++-- .../nav2_collision_monitor/collision_monitor_node.hpp | 4 ++-- .../include/nav2_collision_monitor/polygon.hpp | 2 +- .../include/nav2_collision_monitor/source.hpp | 2 +- .../nav2_collision_monitor/velocity_polygon.hpp | 2 +- nav2_collision_monitor/src/collision_detector_node.cpp | 2 +- nav2_collision_monitor/src/collision_monitor_node.cpp | 2 +- .../test/collision_detector_node_test.cpp | 2 +- .../test/collision_monitor_node_test.cpp | 2 +- nav2_collision_monitor/test/polygons_test.cpp | 6 +++--- nav2_collision_monitor/test/sources_test.cpp | 6 +++--- nav2_collision_monitor/test/velocity_polygons_test.cpp | 6 +++--- .../test/test_constrained_smoother.cpp | 2 +- .../include/nav2_controller/controller_server.hpp | 2 +- nav2_core/include/nav2_core/behavior.hpp | 2 +- .../include/nav2_core/behavior_tree_navigator.hpp | 2 +- nav2_core/include/nav2_core/controller.hpp | 2 +- nav2_core/include/nav2_core/global_planner.hpp | 2 +- nav2_core/include/nav2_core/smoother.hpp | 4 ++-- .../include/nav2_costmap_2d/costmap_2d_ros.hpp | 4 ++-- nav2_costmap_2d/include/nav2_costmap_2d/layer.hpp | 2 +- .../include/nav2_costmap_2d/observation_buffer.hpp | 2 +- .../include/nav2_costmap_2d/obstacle_layer.hpp | 2 +- .../include/nav2_costmap_2d/plugin_container_layer.hpp | 2 +- nav2_costmap_2d/src/costmap_2d_ros.cpp | 2 +- nav2_costmap_2d/test/integration/costmap_tester.cpp | 2 +- nav2_costmap_2d/test/integration/dyn_params_tests.cpp | 2 +- nav2_costmap_2d/test/integration/footprint_tests.cpp | 2 +- .../test/integration/test_costmap_2d_publisher.cpp | 2 +- .../test_costmap_topic_collision_checker.cpp | 8 ++++---- nav2_costmap_2d/test/unit/binary_filter_test.cpp | 6 +++--- nav2_costmap_2d/test/unit/keepout_filter_test.cpp | 6 +++--- nav2_costmap_2d/test/unit/speed_filter_test.cpp | 6 +++--- .../include/opennav_docking/docking_server.hpp | 2 +- nav2_docking/opennav_docking/test/test_controller.cpp | 2 +- .../include/opennav_docking_core/charging_dock.hpp | 2 +- .../nav_2d_utils/include/nav_2d_utils/tf_help.hpp | 2 +- nav2_mppi_controller/benchmark/optimizer_benchmark.cpp | 2 +- .../optimal_trajectory_validator.hpp | 2 +- .../include/nav2_mppi_controller/optimizer.hpp | 2 +- .../nav2_mppi_controller/tools/path_handler.hpp | 2 +- nav2_mppi_controller/test/optimizer_smoke_test.cpp | 2 +- nav2_mppi_controller/test/optimizer_unit_tests.cpp | 2 +- nav2_mppi_controller/test/path_handler_test.cpp | 2 +- nav2_mppi_controller/test/utils/utils.hpp | 2 +- nav2_planner/include/nav2_planner/planner_server.hpp | 4 ++-- .../test/test_shim_controller.cpp | 2 +- nav2_route/include/nav2_route/edge_scorer.hpp | 2 +- .../include/nav2_route/goal_intent_extractor.hpp | 2 +- nav2_route/include/nav2_route/graph_loader.hpp | 4 ++-- nav2_route/include/nav2_route/graph_saver.hpp | 4 ++-- .../nav2_route/interfaces/edge_cost_function.hpp | 2 +- nav2_route/include/nav2_route/route_planner.hpp | 4 ++-- nav2_route/include/nav2_route/route_server.hpp | 4 ++-- nav2_route/include/nav2_route/route_tracker.hpp | 2 +- nav2_route/test/test_edge_scorers.cpp | 4 ++-- nav2_route/test/test_goal_intent_extractor.cpp | 6 +++--- nav2_route/test/test_goal_intent_search.cpp | 6 +++--- nav2_route/test/test_graph_loader.cpp | 2 +- nav2_route/test/test_graph_saver.cpp | 2 +- nav2_route/test/test_route_server.cpp | 6 +++--- nav2_route/test/test_route_tracker.cpp | 6 +++--- .../include/nav2_rviz_plugins/nav2_panel.hpp | 6 +++--- nav2_smoother/src/nav2_smoother.cpp | 2 +- .../src/behavior_tree/test_behavior_tree_node.cpp | 6 +++--- .../assisted_teleop_behavior_tester.hpp | 4 ++-- .../src/behaviors/wait/wait_behavior_tester.hpp | 4 ++-- nav2_system_tests/src/planning/planner_tester.hpp | 2 +- nav2_util/include/nav2_util/robot_utils.hpp | 2 +- nav2_util/src/base_footprint_publisher.hpp | 8 ++++---- nav2_util/test/test_robot_utils.cpp | 6 +++--- .../nav2_waypoint_follower/waypoint_follower.hpp | 4 ++-- 88 files changed, 146 insertions(+), 146 deletions(-) diff --git a/nav2_amcl/include/nav2_amcl/amcl_node.hpp b/nav2_amcl/include/nav2_amcl/amcl_node.hpp index 0be08c39db6..65787e82b00 100644 --- a/nav2_amcl/include/nav2_amcl/amcl_node.hpp +++ b/nav2_amcl/include/nav2_amcl/amcl_node.hpp @@ -44,9 +44,9 @@ #include "sensor_msgs/msg/laser_scan.hpp" #include "nav2_ros_common/service_server.hpp" #include "std_srvs/srv/empty.hpp" -#include "tf2_ros/message_filter.h" -#include "tf2_ros/transform_broadcaster.h" -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/message_filter.hpp" +#include "tf2_ros/transform_broadcaster.hpp" +#include "tf2_ros/transform_listener.hpp" #define NEW_UNIFORM_SAMPLING 1 diff --git a/nav2_amcl/src/amcl_node.cpp b/nav2_amcl/src/amcl_node.cpp index e7910c583b1..93b8f599a8d 100644 --- a/nav2_amcl/src/amcl_node.cpp +++ b/nav2_amcl/src/amcl_node.cpp @@ -38,11 +38,11 @@ #include "tf2/utils.hpp" #include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" #include "tf2/LinearMath/Transform.hpp" -#include "tf2_ros/buffer.h" -#include "tf2_ros/message_filter.h" -#include "tf2_ros/transform_broadcaster.h" -#include "tf2_ros/transform_listener.h" -#include "tf2_ros/create_timer_ros.h" +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/message_filter.hpp" +#include "tf2_ros/transform_broadcaster.hpp" +#include "tf2_ros/transform_listener.hpp" +#include "tf2_ros/create_timer_ros.hpp" #include "nav2_amcl/portable_utils.hpp" #include "nav2_ros_common/validate_messages.hpp" diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/truncate_path_local_action.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/truncate_path_local_action.hpp index 6ce8a76fd64..9ffc5aa773f 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/truncate_path_local_action.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/truncate_path_local_action.hpp @@ -25,7 +25,7 @@ #include "nav_msgs/msg/path.hpp" #include "nav2_behavior_tree/bt_utils.hpp" #include "nav2_behavior_tree/json_utils.hpp" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "nav2_ros_common/lifecycle_node.hpp" diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/are_poses_near_condition.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/are_poses_near_condition.hpp index db615c06181..eb099f329b0 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/are_poses_near_condition.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/are_poses_near_condition.hpp @@ -20,7 +20,7 @@ #include "nav2_ros_common/lifecycle_node.hpp" #include "behaviortree_cpp/condition_node.h" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "nav2_behavior_tree/bt_utils.hpp" namespace nav2_behavior_tree diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/distance_traveled_condition.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/distance_traveled_condition.hpp index abcf722b872..c75f8b78d98 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/distance_traveled_condition.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/distance_traveled_condition.hpp @@ -23,7 +23,7 @@ #include "nav2_ros_common/lifecycle_node.hpp" #include "geometry_msgs/msg/pose_stamped.hpp" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "nav2_behavior_tree/bt_utils.hpp" namespace nav2_behavior_tree diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/goal_reached_condition.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/goal_reached_condition.hpp index aeebfcb3634..db3c56f24b7 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/goal_reached_condition.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/goal_reached_condition.hpp @@ -23,7 +23,7 @@ #include "behaviortree_cpp/json_export.h" #include "nav2_behavior_tree/bt_utils.hpp" #include "nav2_behavior_tree/json_utils.hpp" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" namespace nav2_behavior_tree diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/transform_available_condition.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/transform_available_condition.hpp index 41a86ee813d..be1bcd6ee18 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/transform_available_condition.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/transform_available_condition.hpp @@ -21,7 +21,7 @@ #include "nav2_ros_common/lifecycle_node.hpp" #include "behaviortree_cpp/condition_node.h" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" namespace nav2_behavior_tree { diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/decorator/distance_controller.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/decorator/distance_controller.hpp index 64e4a7c4a75..782e89a8f89 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/decorator/distance_controller.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/decorator/distance_controller.hpp @@ -20,7 +20,7 @@ #include #include "geometry_msgs/msg/pose_stamped.hpp" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "behaviortree_cpp/decorator_node.h" #include "nav2_behavior_tree/bt_utils.hpp" diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/ros_topic_logger.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/ros_topic_logger.hpp index a8d27ecd96f..57ca3d4ffc3 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/ros_topic_logger.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/ros_topic_logger.hpp @@ -24,7 +24,7 @@ #include "nav2_msgs/msg/behavior_tree_log.hpp" #include "nav2_msgs/msg/behavior_tree_status_change.hpp" #include "tf2/time.hpp" -#include "tf2_ros/buffer_interface.h" +#include "tf2_ros/buffer_interface.hpp" namespace nav2_behavior_tree { diff --git a/nav2_behavior_tree/plugins/action/truncate_path_local_action.cpp b/nav2_behavior_tree/plugins/action/truncate_path_local_action.cpp index bf8e58b9184..7cae783bb9f 100644 --- a/nav2_behavior_tree/plugins/action/truncate_path_local_action.cpp +++ b/nav2_behavior_tree/plugins/action/truncate_path_local_action.cpp @@ -25,8 +25,8 @@ #include "nav_msgs/msg/path.hpp" #include "rclcpp/rclcpp.hpp" #include "tf2/LinearMath/Quaternion.hpp" -#include "tf2_ros/buffer.h" -#include "tf2_ros/create_timer_ros.h" +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/create_timer_ros.hpp" #include "nav2_behavior_tree/plugins/action/truncate_path_local_action.hpp" diff --git a/nav2_behavior_tree/plugins/condition/transform_available_condition.cpp b/nav2_behavior_tree/plugins/condition/transform_available_condition.cpp index b377d47c585..914a2be547a 100644 --- a/nav2_behavior_tree/plugins/condition/transform_available_condition.cpp +++ b/nav2_behavior_tree/plugins/condition/transform_available_condition.cpp @@ -18,7 +18,7 @@ #include "rclcpp/rclcpp.hpp" #include "tf2/time.hpp" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "nav2_behavior_tree/plugins/condition/transform_available_condition.hpp" diff --git a/nav2_behavior_tree/plugins/decorator/distance_controller.cpp b/nav2_behavior_tree/plugins/decorator/distance_controller.cpp index 0fdd47e7ffb..b560cddf2e7 100644 --- a/nav2_behavior_tree/plugins/decorator/distance_controller.cpp +++ b/nav2_behavior_tree/plugins/decorator/distance_controller.cpp @@ -21,7 +21,7 @@ #include "nav2_util/robot_utils.hpp" #include "nav2_util/geometry_utils.hpp" #include "geometry_msgs/msg/pose_stamped.hpp" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "behaviortree_cpp/decorator_node.h" diff --git a/nav2_behavior_tree/test/utils/test_transform_handler.hpp b/nav2_behavior_tree/test/utils/test_transform_handler.hpp index 19cfcdfa222..cf272d132cd 100644 --- a/nav2_behavior_tree/test/utils/test_transform_handler.hpp +++ b/nav2_behavior_tree/test/utils/test_transform_handler.hpp @@ -29,9 +29,9 @@ #include "geometry_msgs/msg/pose.hpp" #include "geometry_msgs/msg/transform_stamped.hpp" #include "tf2_msgs/msg/tf_message.hpp" -#include "tf2_ros/transform_broadcaster.h" -#include "tf2_ros/transform_listener.h" -#include "tf2_ros/buffer.h" +#include "tf2_ros/transform_broadcaster.hpp" +#include "tf2_ros/transform_listener.hpp" +#include "tf2_ros/buffer.hpp" using namespace std::chrono_literals; // NOLINT using namespace std::chrono; // NOLINT diff --git a/nav2_behaviors/include/nav2_behaviors/behavior_server.hpp b/nav2_behaviors/include/nav2_behaviors/behavior_server.hpp index 7301cef3836..82b55c0786f 100644 --- a/nav2_behaviors/include/nav2_behaviors/behavior_server.hpp +++ b/nav2_behaviors/include/nav2_behaviors/behavior_server.hpp @@ -19,8 +19,8 @@ #include #include "nav2_ros_common/lifecycle_node.hpp" -#include "tf2_ros/transform_listener.h" -#include "tf2_ros/create_timer_ros.h" +#include "tf2_ros/transform_listener.hpp" +#include "tf2_ros/create_timer_ros.hpp" #include "pluginlib/class_loader.hpp" #include "pluginlib/class_list_macros.hpp" #include "nav2_core/behavior.hpp" diff --git a/nav2_behaviors/include/nav2_behaviors/timed_behavior.hpp b/nav2_behaviors/include/nav2_behaviors/timed_behavior.hpp index 70d0f30fa30..22db8701700 100644 --- a/nav2_behaviors/include/nav2_behaviors/timed_behavior.hpp +++ b/nav2_behaviors/include/nav2_behaviors/timed_behavior.hpp @@ -26,8 +26,8 @@ #include #include "rclcpp/rclcpp.hpp" -#include "tf2_ros/transform_listener.h" -#include "tf2_ros/create_timer_ros.h" +#include "tf2_ros/transform_listener.hpp" +#include "tf2_ros/create_timer_ros.hpp" #include "geometry_msgs/msg/twist.hpp" #include "nav2_util/robot_utils.hpp" #include "nav2_util/twist_publisher.hpp" diff --git a/nav2_bt_navigator/include/nav2_bt_navigator/bt_navigator.hpp b/nav2_bt_navigator/include/nav2_bt_navigator/bt_navigator.hpp index 8a25e5a2c8d..adea1aba20f 100644 --- a/nav2_bt_navigator/include/nav2_bt_navigator/bt_navigator.hpp +++ b/nav2_bt_navigator/include/nav2_bt_navigator/bt_navigator.hpp @@ -23,9 +23,9 @@ #include "nav2_ros_common/lifecycle_node.hpp" #include "nav2_util/odometry_utils.hpp" #include "rclcpp_action/rclcpp_action.hpp" -#include "tf2_ros/buffer.h" -#include "tf2_ros/transform_listener.h" -#include "tf2_ros/create_timer_ros.h" +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/transform_listener.hpp" +#include "tf2_ros/create_timer_ros.hpp" #include "nav2_core/behavior_tree_navigator.hpp" #include "pluginlib/class_loader.hpp" diff --git a/nav2_collision_monitor/include/nav2_collision_monitor/collision_detector_node.hpp b/nav2_collision_monitor/include/nav2_collision_monitor/collision_detector_node.hpp index 59b807f562e..450e97c8078 100644 --- a/nav2_collision_monitor/include/nav2_collision_monitor/collision_detector_node.hpp +++ b/nav2_collision_monitor/include/nav2_collision_monitor/collision_detector_node.hpp @@ -23,8 +23,8 @@ #include "rclcpp/rclcpp.hpp" #include "tf2/time.hpp" -#include "tf2_ros/buffer.h" -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/transform_listener.hpp" #include "nav2_ros_common/lifecycle_node.hpp" #include "nav2_msgs/msg/collision_detector_state.hpp" diff --git a/nav2_collision_monitor/include/nav2_collision_monitor/collision_monitor_node.hpp b/nav2_collision_monitor/include/nav2_collision_monitor/collision_monitor_node.hpp index d73804cb827..c53540089d7 100644 --- a/nav2_collision_monitor/include/nav2_collision_monitor/collision_monitor_node.hpp +++ b/nav2_collision_monitor/include/nav2_collision_monitor/collision_monitor_node.hpp @@ -26,8 +26,8 @@ #include "geometry_msgs/msg/twist_stamped.hpp" #include "tf2/time.hpp" -#include "tf2_ros/buffer.h" -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/transform_listener.hpp" #include "nav2_ros_common/lifecycle_node.hpp" #include "nav2_util/twist_publisher.hpp" diff --git a/nav2_collision_monitor/include/nav2_collision_monitor/polygon.hpp b/nav2_collision_monitor/include/nav2_collision_monitor/polygon.hpp index 12d64259993..3191de1727e 100644 --- a/nav2_collision_monitor/include/nav2_collision_monitor/polygon.hpp +++ b/nav2_collision_monitor/include/nav2_collision_monitor/polygon.hpp @@ -24,7 +24,7 @@ #include "geometry_msgs/msg/polygon_stamped.hpp" #include "tf2/time.hpp" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "nav2_ros_common/lifecycle_node.hpp" #include "nav2_costmap_2d/footprint_subscriber.hpp" diff --git a/nav2_collision_monitor/include/nav2_collision_monitor/source.hpp b/nav2_collision_monitor/include/nav2_collision_monitor/source.hpp index 312c13ea577..4e91dab13c7 100644 --- a/nav2_collision_monitor/include/nav2_collision_monitor/source.hpp +++ b/nav2_collision_monitor/include/nav2_collision_monitor/source.hpp @@ -22,7 +22,7 @@ #include "rclcpp/rclcpp.hpp" #include "tf2/time.hpp" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "nav2_collision_monitor/types.hpp" #include "nav2_ros_common/lifecycle_node.hpp" diff --git a/nav2_collision_monitor/include/nav2_collision_monitor/velocity_polygon.hpp b/nav2_collision_monitor/include/nav2_collision_monitor/velocity_polygon.hpp index 55f894d87c5..8056973ddf9 100644 --- a/nav2_collision_monitor/include/nav2_collision_monitor/velocity_polygon.hpp +++ b/nav2_collision_monitor/include/nav2_collision_monitor/velocity_polygon.hpp @@ -24,7 +24,7 @@ #include "nav2_collision_monitor/types.hpp" #include "nav2_ros_common/lifecycle_node.hpp" #include "rclcpp/rclcpp.hpp" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" namespace nav2_collision_monitor { diff --git a/nav2_collision_monitor/src/collision_detector_node.cpp b/nav2_collision_monitor/src/collision_detector_node.cpp index 739d0d12207..a45d35dc3cd 100644 --- a/nav2_collision_monitor/src/collision_detector_node.cpp +++ b/nav2_collision_monitor/src/collision_detector_node.cpp @@ -19,7 +19,7 @@ #include #include -#include "tf2_ros/create_timer_ros.h" +#include "tf2_ros/create_timer_ros.hpp" #include "nav2_ros_common/node_utils.hpp" diff --git a/nav2_collision_monitor/src/collision_monitor_node.cpp b/nav2_collision_monitor/src/collision_monitor_node.cpp index f85bdf4a866..7bff3a8a4d1 100644 --- a/nav2_collision_monitor/src/collision_monitor_node.cpp +++ b/nav2_collision_monitor/src/collision_monitor_node.cpp @@ -18,7 +18,7 @@ #include #include -#include "tf2_ros/create_timer_ros.h" +#include "tf2_ros/create_timer_ros.hpp" #include "nav2_ros_common/node_utils.hpp" #include "nav2_util/robot_utils.hpp" diff --git a/nav2_collision_monitor/test/collision_detector_node_test.cpp b/nav2_collision_monitor/test/collision_detector_node_test.cpp index 1bd2eb0ca0c..4bbd37bc4e9 100644 --- a/nav2_collision_monitor/test/collision_detector_node_test.cpp +++ b/nav2_collision_monitor/test/collision_detector_node_test.cpp @@ -34,7 +34,7 @@ #include "geometry_msgs/msg/polygon_stamped.hpp" #include "visualization_msgs/msg/marker_array.hpp" -#include "tf2_ros/transform_broadcaster.h" +#include "tf2_ros/transform_broadcaster.hpp" #include "nav2_collision_monitor/types.hpp" #include "nav2_collision_monitor/collision_detector_node.hpp" diff --git a/nav2_collision_monitor/test/collision_monitor_node_test.cpp b/nav2_collision_monitor/test/collision_monitor_node_test.cpp index d64f0bd71e8..ea0213f468c 100644 --- a/nav2_collision_monitor/test/collision_monitor_node_test.cpp +++ b/nav2_collision_monitor/test/collision_monitor_node_test.cpp @@ -34,7 +34,7 @@ #include "geometry_msgs/msg/polygon_stamped.hpp" #include "visualization_msgs/msg/marker_array.hpp" -#include "tf2_ros/transform_broadcaster.h" +#include "tf2_ros/transform_broadcaster.hpp" #include "nav2_collision_monitor/types.hpp" #include "nav2_collision_monitor/collision_monitor_node.hpp" diff --git a/nav2_collision_monitor/test/polygons_test.cpp b/nav2_collision_monitor/test/polygons_test.cpp index 52eff3c650b..86f393febfa 100644 --- a/nav2_collision_monitor/test/polygons_test.cpp +++ b/nav2_collision_monitor/test/polygons_test.cpp @@ -27,9 +27,9 @@ #include "geometry_msgs/msg/point32.hpp" #include "geometry_msgs/msg/polygon_stamped.hpp" -#include "tf2_ros/buffer.h" -#include "tf2_ros/transform_listener.h" -#include "tf2_ros/transform_broadcaster.h" +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/transform_listener.hpp" +#include "tf2_ros/transform_broadcaster.hpp" #include "nav2_collision_monitor/types.hpp" #include "nav2_collision_monitor/polygon.hpp" diff --git a/nav2_collision_monitor/test/sources_test.cpp b/nav2_collision_monitor/test/sources_test.cpp index 04929bd339c..034cdd40d96 100644 --- a/nav2_collision_monitor/test/sources_test.cpp +++ b/nav2_collision_monitor/test/sources_test.cpp @@ -30,9 +30,9 @@ #include "sensor_msgs/msg/range.hpp" #include "sensor_msgs/point_cloud2_iterator.hpp" -#include "tf2_ros/buffer.h" -#include "tf2_ros/transform_listener.h" -#include "tf2_ros/transform_broadcaster.h" +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/transform_listener.hpp" +#include "tf2_ros/transform_broadcaster.hpp" #include "nav2_collision_monitor/types.hpp" #include "nav2_collision_monitor/scan.hpp" diff --git a/nav2_collision_monitor/test/velocity_polygons_test.cpp b/nav2_collision_monitor/test/velocity_polygons_test.cpp index d13fc873340..88024dd723b 100644 --- a/nav2_collision_monitor/test/velocity_polygons_test.cpp +++ b/nav2_collision_monitor/test/velocity_polygons_test.cpp @@ -27,9 +27,9 @@ #include "geometry_msgs/msg/point32.hpp" #include "geometry_msgs/msg/polygon_stamped.hpp" -#include "tf2_ros/buffer.h" -#include "tf2_ros/transform_listener.h" -#include "tf2_ros/transform_broadcaster.h" +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/transform_listener.hpp" +#include "tf2_ros/transform_broadcaster.hpp" #include "nav2_collision_monitor/types.hpp" #include "nav2_collision_monitor/polygon.hpp" diff --git a/nav2_constrained_smoother/test/test_constrained_smoother.cpp b/nav2_constrained_smoother/test/test_constrained_smoother.cpp index b9285fa9240..6e4e3d4bc90 100644 --- a/nav2_constrained_smoother/test/test_constrained_smoother.cpp +++ b/nav2_constrained_smoother/test/test_constrained_smoother.cpp @@ -23,7 +23,7 @@ #include "gtest/gtest.h" #include "rclcpp/rclcpp.hpp" -#include "tf2_ros/create_timer_ros.h" +#include "tf2_ros/create_timer_ros.hpp" #include "nav2_util/geometry_utils.hpp" #include "nav2_costmap_2d/inflation_layer.hpp" #include "nav2_costmap_2d/footprint_collision_checker.hpp" diff --git a/nav2_controller/include/nav2_controller/controller_server.hpp b/nav2_controller/include/nav2_controller/controller_server.hpp index a112a3c9d66..a838854a52b 100644 --- a/nav2_controller/include/nav2_controller/controller_server.hpp +++ b/nav2_controller/include/nav2_controller/controller_server.hpp @@ -26,7 +26,7 @@ #include "nav2_core/progress_checker.hpp" #include "nav2_core/goal_checker.hpp" #include "nav2_costmap_2d/costmap_2d_ros.hpp" -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/transform_listener.hpp" #include "nav2_msgs/action/follow_path.hpp" #include "nav2_msgs/msg/speed_limit.hpp" #include "nav_2d_utils/odom_subscriber.hpp" diff --git a/nav2_core/include/nav2_core/behavior.hpp b/nav2_core/include/nav2_core/behavior.hpp index b2e00e8cf53..7a58d3aa2a0 100644 --- a/nav2_core/include/nav2_core/behavior.hpp +++ b/nav2_core/include/nav2_core/behavior.hpp @@ -20,7 +20,7 @@ #include "rclcpp/rclcpp.hpp" #include "nav2_ros_common/lifecycle_node.hpp" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "nav2_costmap_2d/costmap_topic_collision_checker.hpp" namespace nav2_core diff --git a/nav2_core/include/nav2_core/behavior_tree_navigator.hpp b/nav2_core/include/nav2_core/behavior_tree_navigator.hpp index 87824c7fd31..fda64cec974 100644 --- a/nav2_core/include/nav2_core/behavior_tree_navigator.hpp +++ b/nav2_core/include/nav2_core/behavior_tree_navigator.hpp @@ -21,7 +21,7 @@ #include #include "nav2_util/odometry_utils.hpp" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "rclcpp/rclcpp.hpp" #include "nav2_ros_common/lifecycle_node.hpp" #include "pluginlib/class_loader.hpp" diff --git a/nav2_core/include/nav2_core/controller.hpp b/nav2_core/include/nav2_core/controller.hpp index 261eec22436..d24e84d6680 100644 --- a/nav2_core/include/nav2_core/controller.hpp +++ b/nav2_core/include/nav2_core/controller.hpp @@ -41,7 +41,7 @@ #include "nav2_costmap_2d/costmap_2d_ros.hpp" #include "nav2_ros_common/lifecycle_node.hpp" -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/transform_listener.hpp" #include "pluginlib/class_loader.hpp" #include "geometry_msgs/msg/pose_stamped.hpp" #include "geometry_msgs/msg/twist_stamped.hpp" diff --git a/nav2_core/include/nav2_core/global_planner.hpp b/nav2_core/include/nav2_core/global_planner.hpp index bcf65da6d4f..fa63d2e519e 100644 --- a/nav2_core/include/nav2_core/global_planner.hpp +++ b/nav2_core/include/nav2_core/global_planner.hpp @@ -18,7 +18,7 @@ #include #include #include "nav2_costmap_2d/costmap_2d_ros.hpp" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "nav_msgs/msg/path.hpp" #include "geometry_msgs/msg/pose_stamped.hpp" #include "nav2_ros_common/lifecycle_node.hpp" diff --git a/nav2_core/include/nav2_core/smoother.hpp b/nav2_core/include/nav2_core/smoother.hpp index 7705106177d..deb4a6107ec 100644 --- a/nav2_core/include/nav2_core/smoother.hpp +++ b/nav2_core/include/nav2_core/smoother.hpp @@ -20,8 +20,8 @@ #include "nav2_costmap_2d/costmap_subscriber.hpp" #include "nav2_costmap_2d/footprint_subscriber.hpp" -#include "tf2_ros/buffer.h" -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/transform_listener.hpp" #include "nav2_ros_common/lifecycle_node.hpp" #include "pluginlib/class_loader.hpp" #include "nav_msgs/msg/path.hpp" diff --git a/nav2_costmap_2d/include/nav2_costmap_2d/costmap_2d_ros.hpp b/nav2_costmap_2d/include/nav2_costmap_2d/costmap_2d_ros.hpp index b18938352a3..5b1fa01591b 100644 --- a/nav2_costmap_2d/include/nav2_costmap_2d/costmap_2d_ros.hpp +++ b/nav2_costmap_2d/include/nav2_costmap_2d/costmap_2d_ros.hpp @@ -56,8 +56,8 @@ #include "pluginlib/class_loader.hpp" #include "tf2/convert.hpp" #include "tf2/LinearMath/Transform.hpp" -#include "tf2_ros/buffer.h" -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/transform_listener.hpp" #include "tf2/time.hpp" #include "tf2/transform_datatypes.hpp" #include "nav2_ros_common/service_server.hpp" diff --git a/nav2_costmap_2d/include/nav2_costmap_2d/layer.hpp b/nav2_costmap_2d/include/nav2_costmap_2d/layer.hpp index 2136db7c856..571df68c9d3 100644 --- a/nav2_costmap_2d/include/nav2_costmap_2d/layer.hpp +++ b/nav2_costmap_2d/include/nav2_costmap_2d/layer.hpp @@ -41,7 +41,7 @@ #include #include -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "rclcpp/rclcpp.hpp" #include "nav2_costmap_2d/costmap_2d.hpp" #include "nav2_costmap_2d/layered_costmap.hpp" diff --git a/nav2_costmap_2d/include/nav2_costmap_2d/observation_buffer.hpp b/nav2_costmap_2d/include/nav2_costmap_2d/observation_buffer.hpp index c54d586b729..09f0e79c114 100644 --- a/nav2_costmap_2d/include/nav2_costmap_2d/observation_buffer.hpp +++ b/nav2_costmap_2d/include/nav2_costmap_2d/observation_buffer.hpp @@ -43,7 +43,7 @@ #include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" #include "rclcpp/time.hpp" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "tf2_sensor_msgs/tf2_sensor_msgs.hpp" #include "sensor_msgs/msg/point_cloud2.hpp" #include "nav2_costmap_2d/observation.hpp" diff --git a/nav2_costmap_2d/include/nav2_costmap_2d/obstacle_layer.hpp b/nav2_costmap_2d/include/nav2_costmap_2d/obstacle_layer.hpp index 16113d61b16..5e66e24795a 100644 --- a/nav2_costmap_2d/include/nav2_costmap_2d/obstacle_layer.hpp +++ b/nav2_costmap_2d/include/nav2_costmap_2d/obstacle_layer.hpp @@ -47,7 +47,7 @@ #include "laser_geometry/laser_geometry.hpp" #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wreorder" -#include "tf2_ros/message_filter.h" +#include "tf2_ros/message_filter.hpp" #pragma GCC diagnostic pop #include "message_filters/subscriber.hpp" diff --git a/nav2_costmap_2d/include/nav2_costmap_2d/plugin_container_layer.hpp b/nav2_costmap_2d/include/nav2_costmap_2d/plugin_container_layer.hpp index 2e00a412637..8a9aeb29623 100644 --- a/nav2_costmap_2d/include/nav2_costmap_2d/plugin_container_layer.hpp +++ b/nav2_costmap_2d/include/nav2_costmap_2d/plugin_container_layer.hpp @@ -27,7 +27,7 @@ #include "nav2_costmap_2d/costmap_layer.hpp" #include "nav2_costmap_2d/observation_buffer.hpp" #include "nav2_costmap_2d/inflation_layer.hpp" -#include "tf2_ros/message_filter.h" +#include "tf2_ros/message_filter.hpp" #include "pluginlib/class_loader.hpp" using nav2_costmap_2d::LETHAL_OBSTACLE; diff --git a/nav2_costmap_2d/src/costmap_2d_ros.cpp b/nav2_costmap_2d/src/costmap_2d_ros.cpp index 49dd1885fea..f43373d286b 100644 --- a/nav2_costmap_2d/src/costmap_2d_ros.cpp +++ b/nav2_costmap_2d/src/costmap_2d_ros.cpp @@ -48,7 +48,7 @@ #include "nav2_util/execution_timer.hpp" #include "nav2_ros_common/node_utils.hpp" #include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" -#include "tf2_ros/create_timer_ros.h" +#include "tf2_ros/create_timer_ros.hpp" #include "nav2_util/robot_utils.hpp" #include "rcl_interfaces/msg/set_parameters_result.hpp" diff --git a/nav2_costmap_2d/test/integration/costmap_tester.cpp b/nav2_costmap_2d/test/integration/costmap_tester.cpp index c7d44e6e70c..8569cae54e3 100644 --- a/nav2_costmap_2d/test/integration/costmap_tester.cpp +++ b/nav2_costmap_2d/test/integration/costmap_tester.cpp @@ -38,7 +38,7 @@ #include "gtest/gtest.h" #include "rclcpp/rclcpp.hpp" -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/transform_listener.hpp" #include "nav2_costmap_2d/costmap_2d_ros.hpp" #include "nav2_costmap_2d/cost_values.hpp" diff --git a/nav2_costmap_2d/test/integration/dyn_params_tests.cpp b/nav2_costmap_2d/test/integration/dyn_params_tests.cpp index a543dda02ae..94e0f72ea77 100644 --- a/nav2_costmap_2d/test/integration/dyn_params_tests.cpp +++ b/nav2_costmap_2d/test/integration/dyn_params_tests.cpp @@ -19,7 +19,7 @@ #include "rclcpp/rclcpp.hpp" #include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" #include "nav2_costmap_2d/costmap_2d_ros.hpp" -#include "tf2_ros/transform_broadcaster.h" +#include "tf2_ros/transform_broadcaster.hpp" class DynParamTestNode { diff --git a/nav2_costmap_2d/test/integration/footprint_tests.cpp b/nav2_costmap_2d/test/integration/footprint_tests.cpp index 2f527b5faac..0bde175ce0a 100644 --- a/nav2_costmap_2d/test/integration/footprint_tests.cpp +++ b/nav2_costmap_2d/test/integration/footprint_tests.cpp @@ -41,7 +41,7 @@ #include "gtest/gtest.h" #include "rclcpp/rclcpp.hpp" #include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/transform_listener.hpp" #include "nav2_costmap_2d/footprint.hpp" class FootprintTestNode diff --git a/nav2_costmap_2d/test/integration/test_costmap_2d_publisher.cpp b/nav2_costmap_2d/test/integration/test_costmap_2d_publisher.cpp index c0616176647..a365cdc9a7d 100644 --- a/nav2_costmap_2d/test/integration/test_costmap_2d_publisher.cpp +++ b/nav2_costmap_2d/test/integration/test_costmap_2d_publisher.cpp @@ -19,7 +19,7 @@ #include "nav2_costmap_2d/costmap_2d.hpp" #include "nav2_costmap_2d/costmap_subscriber.hpp" #include "nav2_costmap_2d/cost_values.hpp" -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/transform_listener.hpp" #include "nav2_costmap_2d/costmap_2d_ros.hpp" class CostmapRosLifecycleNode : public nav2::LifecycleNode diff --git a/nav2_costmap_2d/test/integration/test_costmap_topic_collision_checker.cpp b/nav2_costmap_2d/test/integration/test_costmap_topic_collision_checker.cpp index 1543060ccca..e5b73e4b5cc 100644 --- a/nav2_costmap_2d/test/integration/test_costmap_topic_collision_checker.cpp +++ b/nav2_costmap_2d/test/integration/test_costmap_topic_collision_checker.cpp @@ -29,10 +29,10 @@ #include "nav2_util/robot_utils.hpp" #include "nav2_ros_common/node_utils.hpp" #include "geometry_msgs/msg/pose_stamped.hpp" -#include "tf2_ros/buffer.h" -#include "tf2_ros/transform_listener.h" -#include "tf2_ros/create_timer_ros.h" -#include "tf2_ros/transform_broadcaster.h" +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/transform_listener.hpp" +#include "tf2_ros/create_timer_ros.hpp" +#include "tf2_ros/transform_broadcaster.hpp" #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpedantic" #include "tf2/utils.hpp" diff --git a/nav2_costmap_2d/test/unit/binary_filter_test.cpp b/nav2_costmap_2d/test/unit/binary_filter_test.cpp index befb8d00e1e..e0061e91e88 100644 --- a/nav2_costmap_2d/test/unit/binary_filter_test.cpp +++ b/nav2_costmap_2d/test/unit/binary_filter_test.cpp @@ -24,9 +24,9 @@ #include "rclcpp/rclcpp.hpp" #include "nav2_ros_common/lifecycle_node.hpp" -#include "tf2_ros/buffer.h" -#include "tf2_ros/transform_listener.h" -#include "tf2_ros/transform_broadcaster.h" +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/transform_listener.hpp" +#include "tf2_ros/transform_broadcaster.hpp" #include "nav2_util/geometry_utils.hpp" #include "nav2_util/occ_grid_values.hpp" #include "nav2_costmap_2d/cost_values.hpp" diff --git a/nav2_costmap_2d/test/unit/keepout_filter_test.cpp b/nav2_costmap_2d/test/unit/keepout_filter_test.cpp index c75acf2b7dc..37a5a683299 100644 --- a/nav2_costmap_2d/test/unit/keepout_filter_test.cpp +++ b/nav2_costmap_2d/test/unit/keepout_filter_test.cpp @@ -22,9 +22,9 @@ #include "rclcpp/rclcpp.hpp" #include "nav2_ros_common/lifecycle_node.hpp" -#include "tf2_ros/buffer.h" -#include "tf2_ros/transform_listener.h" -#include "tf2_ros/transform_broadcaster.h" +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/transform_listener.hpp" +#include "tf2_ros/transform_broadcaster.hpp" #include "nav2_util/occ_grid_values.hpp" #include "nav2_costmap_2d/cost_values.hpp" #include "nav_msgs/msg/occupancy_grid.hpp" diff --git a/nav2_costmap_2d/test/unit/speed_filter_test.cpp b/nav2_costmap_2d/test/unit/speed_filter_test.cpp index 76e96187924..5d3eebb6db5 100644 --- a/nav2_costmap_2d/test/unit/speed_filter_test.cpp +++ b/nav2_costmap_2d/test/unit/speed_filter_test.cpp @@ -24,9 +24,9 @@ #include "rclcpp/rclcpp.hpp" #include "nav2_ros_common/lifecycle_node.hpp" -#include "tf2_ros/buffer.h" -#include "tf2_ros/transform_listener.h" -#include "tf2_ros/transform_broadcaster.h" +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/transform_listener.hpp" +#include "tf2_ros/transform_broadcaster.hpp" #include "nav2_util/occ_grid_values.hpp" #include "nav2_costmap_2d/cost_values.hpp" #include "nav_msgs/msg/occupancy_grid.hpp" diff --git a/nav2_docking/opennav_docking/include/opennav_docking/docking_server.hpp b/nav2_docking/opennav_docking/include/opennav_docking/docking_server.hpp index 06c6a7252e0..eb1497f21db 100644 --- a/nav2_docking/opennav_docking/include/opennav_docking/docking_server.hpp +++ b/nav2_docking/opennav_docking/include/opennav_docking/docking_server.hpp @@ -34,7 +34,7 @@ #include "opennav_docking/dock_database.hpp" #include "opennav_docking/navigator.hpp" #include "opennav_docking_core/charging_dock.hpp" -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/transform_listener.hpp" namespace opennav_docking { diff --git a/nav2_docking/opennav_docking/test/test_controller.cpp b/nav2_docking/opennav_docking/test/test_controller.cpp index f0e982a919b..8708b31428d 100644 --- a/nav2_docking/opennav_docking/test/test_controller.cpp +++ b/nav2_docking/opennav_docking/test/test_controller.cpp @@ -21,7 +21,7 @@ #include "nav2_costmap_2d/costmap_2d_ros.hpp" #include "nav2_util/geometry_utils.hpp" #include "nav2_ros_common/node_utils.hpp" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "ament_index_cpp/get_package_share_directory.hpp" // Testing the controller at high level; the nav2_graceful_controller diff --git a/nav2_docking/opennav_docking_core/include/opennav_docking_core/charging_dock.hpp b/nav2_docking/opennav_docking_core/include/opennav_docking_core/charging_dock.hpp index a923cc98d26..67e599405d7 100644 --- a/nav2_docking/opennav_docking_core/include/opennav_docking_core/charging_dock.hpp +++ b/nav2_docking/opennav_docking_core/include/opennav_docking_core/charging_dock.hpp @@ -20,7 +20,7 @@ #include "nav2_ros_common/lifecycle_node.hpp" #include "geometry_msgs/msg/pose_stamped.hpp" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" namespace opennav_docking_core diff --git a/nav2_dwb_controller/nav_2d_utils/include/nav_2d_utils/tf_help.hpp b/nav2_dwb_controller/nav_2d_utils/include/nav_2d_utils/tf_help.hpp index 9bed6cd6810..3f5fadcd4aa 100644 --- a/nav2_dwb_controller/nav_2d_utils/include/nav_2d_utils/tf_help.hpp +++ b/nav2_dwb_controller/nav_2d_utils/include/nav_2d_utils/tf_help.hpp @@ -37,7 +37,7 @@ #include #include -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "nav_2d_utils/conversions.hpp" #include "geometry_msgs/msg/pose_stamped.hpp" #include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" diff --git a/nav2_mppi_controller/benchmark/optimizer_benchmark.cpp b/nav2_mppi_controller/benchmark/optimizer_benchmark.cpp index c59f28e2fad..afe8e83eea9 100644 --- a/nav2_mppi_controller/benchmark/optimizer_benchmark.cpp +++ b/nav2_mppi_controller/benchmark/optimizer_benchmark.cpp @@ -28,7 +28,7 @@ #include #include #include -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "nav2_mppi_controller/optimizer.hpp" #include "nav2_mppi_controller/motion_models.hpp" diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/optimal_trajectory_validator.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/optimal_trajectory_validator.hpp index b5d88008ab1..b3f7b03faab 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/optimal_trajectory_validator.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/optimal_trajectory_validator.hpp @@ -21,7 +21,7 @@ #include "nav2_ros_common/lifecycle_node.hpp" #include "nav2_costmap_2d/costmap_2d_ros.hpp" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "geometry_msgs/msg/twist.hpp" #include "geometry_msgs/msg/pose_stamped.hpp" #include "nav_msgs/msg/path.hpp" diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/optimizer.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/optimizer.hpp index 5eba18b9d5d..e70cfcab786 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/optimizer.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/optimizer.hpp @@ -26,7 +26,7 @@ #include "nav2_costmap_2d/costmap_2d_ros.hpp" #include "nav2_core/goal_checker.hpp" #include "nav2_core/controller_exceptions.hpp" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "pluginlib/class_loader.hpp" #include "geometry_msgs/msg/twist.hpp" diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler.hpp index cfa87f5ace4..544a98467d9 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler.hpp @@ -23,7 +23,7 @@ #include #include "nav2_ros_common/lifecycle_node.hpp" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "geometry_msgs/msg/pose_stamped.hpp" #include "nav_msgs/msg/path.hpp" #include "builtin_interfaces/msg/time.hpp" diff --git a/nav2_mppi_controller/test/optimizer_smoke_test.cpp b/nav2_mppi_controller/test/optimizer_smoke_test.cpp index d201e20eeb8..733229b77df 100644 --- a/nav2_mppi_controller/test/optimizer_smoke_test.cpp +++ b/nav2_mppi_controller/test/optimizer_smoke_test.cpp @@ -21,7 +21,7 @@ #include #include #include -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "nav2_mppi_controller/optimizer.hpp" #include "nav2_mppi_controller/tools/parameters_handler.hpp" diff --git a/nav2_mppi_controller/test/optimizer_unit_tests.cpp b/nav2_mppi_controller/test/optimizer_unit_tests.cpp index 510d9ef300b..eec9ea0ec57 100644 --- a/nav2_mppi_controller/test/optimizer_unit_tests.cpp +++ b/nav2_mppi_controller/test/optimizer_unit_tests.cpp @@ -18,7 +18,7 @@ #include "gtest/gtest.h" #include "rclcpp/rclcpp.hpp" #include "nav2_mppi_controller/optimizer.hpp" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" // Tests main optimizer functions diff --git a/nav2_mppi_controller/test/path_handler_test.cpp b/nav2_mppi_controller/test/path_handler_test.cpp index 375978e98da..eb0bd140c43 100644 --- a/nav2_mppi_controller/test/path_handler_test.cpp +++ b/nav2_mppi_controller/test/path_handler_test.cpp @@ -18,7 +18,7 @@ #include "gtest/gtest.h" #include "rclcpp/rclcpp.hpp" #include "nav2_mppi_controller/tools/path_handler.hpp" -#include "tf2_ros/transform_broadcaster.h" +#include "tf2_ros/transform_broadcaster.hpp" // Tests path handling diff --git a/nav2_mppi_controller/test/utils/utils.hpp b/nav2_mppi_controller/test/utils/utils.hpp index b0185ce7cfe..2772ea9a858 100644 --- a/nav2_mppi_controller/test/utils/utils.hpp +++ b/nav2_mppi_controller/test/utils/utils.hpp @@ -22,7 +22,7 @@ #include -#include "tf2_ros/transform_broadcaster.h" +#include "tf2_ros/transform_broadcaster.hpp" #include "nav2_costmap_2d/costmap_2d.hpp" #include "nav2_costmap_2d/costmap_2d_ros.hpp" diff --git a/nav2_planner/include/nav2_planner/planner_server.hpp b/nav2_planner/include/nav2_planner/planner_server.hpp index c9fc2a46dd6..372f4c7c2f9 100644 --- a/nav2_planner/include/nav2_planner/planner_server.hpp +++ b/nav2_planner/include/nav2_planner/planner_server.hpp @@ -32,8 +32,8 @@ #include "nav2_util/robot_utils.hpp" #include "nav2_ros_common/simple_action_server.hpp" #include "nav2_ros_common/service_server.hpp" -#include "tf2_ros/transform_listener.h" -#include "tf2_ros/create_timer_ros.h" +#include "tf2_ros/transform_listener.hpp" +#include "tf2_ros/create_timer_ros.hpp" #include "nav2_costmap_2d/costmap_2d_ros.hpp" #include "pluginlib/class_loader.hpp" #include "pluginlib/class_list_macros.hpp" diff --git a/nav2_rotation_shim_controller/test/test_shim_controller.cpp b/nav2_rotation_shim_controller/test/test_shim_controller.cpp index 0b97876182f..ee87908d827 100644 --- a/nav2_rotation_shim_controller/test/test_shim_controller.cpp +++ b/nav2_rotation_shim_controller/test/test_shim_controller.cpp @@ -24,7 +24,7 @@ #include "nav2_ros_common/lifecycle_node.hpp" #include "nav2_controller/plugins/simple_goal_checker.hpp" #include "nav2_rotation_shim_controller/nav2_rotation_shim_controller.hpp" -#include "tf2_ros/transform_broadcaster.h" +#include "tf2_ros/transform_broadcaster.hpp" class RotationShimShim : public nav2_rotation_shim_controller::RotationShimController { diff --git a/nav2_route/include/nav2_route/edge_scorer.hpp b/nav2_route/include/nav2_route/edge_scorer.hpp index c294ac47e43..7b6534b50c4 100644 --- a/nav2_route/include/nav2_route/edge_scorer.hpp +++ b/nav2_route/include/nav2_route/edge_scorer.hpp @@ -19,7 +19,7 @@ #include #include -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "pluginlib/class_loader.hpp" #include "pluginlib/class_list_macros.hpp" #include "nav2_ros_common/node_utils.hpp" diff --git a/nav2_route/include/nav2_route/goal_intent_extractor.hpp b/nav2_route/include/nav2_route/goal_intent_extractor.hpp index db26121fcef..8e407d66c91 100644 --- a/nav2_route/include/nav2_route/goal_intent_extractor.hpp +++ b/nav2_route/include/nav2_route/goal_intent_extractor.hpp @@ -19,7 +19,7 @@ #include #include -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/transform_listener.hpp" #include "nav2_core/route_exceptions.hpp" #include "nav2_util/robot_utils.hpp" #include "nav2_ros_common/node_utils.hpp" diff --git a/nav2_route/include/nav2_route/graph_loader.hpp b/nav2_route/include/nav2_route/graph_loader.hpp index 2a39da1bee1..c2235e5e914 100644 --- a/nav2_route/include/nav2_route/graph_loader.hpp +++ b/nav2_route/include/nav2_route/graph_loader.hpp @@ -23,8 +23,8 @@ #include #include "nav2_ros_common/lifecycle_node.hpp" -#include "tf2_ros/buffer.h" -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/transform_listener.hpp" #include "nav2_ros_common/node_utils.hpp" #include "nav2_util/robot_utils.hpp" #include "nav2_route/types.hpp" diff --git a/nav2_route/include/nav2_route/graph_saver.hpp b/nav2_route/include/nav2_route/graph_saver.hpp index 7cc25d83c04..8155689df55 100644 --- a/nav2_route/include/nav2_route/graph_saver.hpp +++ b/nav2_route/include/nav2_route/graph_saver.hpp @@ -23,8 +23,8 @@ #include #include "nav2_ros_common/lifecycle_node.hpp" -#include "tf2_ros/buffer.h" -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/transform_listener.hpp" #include "nav2_ros_common/node_utils.hpp" #include "nav2_util/robot_utils.hpp" #include "nav2_route/types.hpp" diff --git a/nav2_route/include/nav2_route/interfaces/edge_cost_function.hpp b/nav2_route/include/nav2_route/interfaces/edge_cost_function.hpp index e20bade1070..e45980f3a45 100644 --- a/nav2_route/include/nav2_route/interfaces/edge_cost_function.hpp +++ b/nav2_route/include/nav2_route/interfaces/edge_cost_function.hpp @@ -18,7 +18,7 @@ #include #include -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "nav2_ros_common/lifecycle_node.hpp" #include "pluginlib/class_loader.hpp" #include "nav2_route/types.hpp" diff --git a/nav2_route/include/nav2_route/route_planner.hpp b/nav2_route/include/nav2_route/route_planner.hpp index bdadc6c9dd0..cd0c978bc90 100644 --- a/nav2_route/include/nav2_route/route_planner.hpp +++ b/nav2_route/include/nav2_route/route_planner.hpp @@ -22,8 +22,8 @@ #include #include -#include "tf2_ros/buffer.h" -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/transform_listener.hpp" #include "nav2_route/types.hpp" #include "nav2_route/utils.hpp" #include "nav2_route/edge_scorer.hpp" diff --git a/nav2_route/include/nav2_route/route_server.hpp b/nav2_route/include/nav2_route/route_server.hpp index 3d384e93c0b..ce52c07e750 100644 --- a/nav2_route/include/nav2_route/route_server.hpp +++ b/nav2_route/include/nav2_route/route_server.hpp @@ -23,8 +23,8 @@ #include #include "nav2_ros_common/lifecycle_node.hpp" -#include "tf2_ros/transform_listener.h" -#include "tf2_ros/create_timer_ros.h" +#include "tf2_ros/transform_listener.hpp" +#include "tf2_ros/create_timer_ros.hpp" #include "nav2_ros_common/simple_action_server.hpp" #include "nav2_ros_common/node_utils.hpp" #include "nav2_util/robot_utils.hpp" diff --git a/nav2_route/include/nav2_route/route_tracker.hpp b/nav2_route/include/nav2_route/route_tracker.hpp index 9c2e7058aec..a2a3d4e897c 100644 --- a/nav2_route/include/nav2_route/route_tracker.hpp +++ b/nav2_route/include/nav2_route/route_tracker.hpp @@ -16,7 +16,7 @@ #include #include -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/transform_listener.hpp" #include "nav2_util/robot_utils.hpp" #include "nav2_ros_common/simple_action_server.hpp" #include "nav2_msgs/action/compute_and_track_route.hpp" diff --git a/nav2_route/test/test_edge_scorers.cpp b/nav2_route/test/test_edge_scorers.cpp index 07af4293d0b..8c16ced8306 100644 --- a/nav2_route/test/test_edge_scorers.cpp +++ b/nav2_route/test/test_edge_scorers.cpp @@ -27,8 +27,8 @@ #include "nav2_costmap_2d/costmap_2d.hpp" #include "nav2_costmap_2d/costmap_2d_publisher.hpp" #include "nav2_core/route_exceptions.hpp" -#include "tf2_ros/static_transform_broadcaster.h" -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/static_transform_broadcaster.hpp" +#include "tf2_ros/transform_listener.hpp" class RclCppFixture { diff --git a/nav2_route/test/test_goal_intent_extractor.cpp b/nav2_route/test/test_goal_intent_extractor.cpp index c3ac8d176cc..21c49708193 100644 --- a/nav2_route/test/test_goal_intent_extractor.cpp +++ b/nav2_route/test/test_goal_intent_extractor.cpp @@ -19,9 +19,9 @@ #include "gtest/gtest.h" #include "rclcpp/rclcpp.hpp" -#include "tf2_ros/transform_broadcaster.h" -#include "tf2_ros/create_timer_ros.h" -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/transform_broadcaster.hpp" +#include "tf2_ros/create_timer_ros.hpp" +#include "tf2_ros/transform_listener.hpp" #include "nav2_ros_common/lifecycle_node.hpp" #include "nav2_ros_common/service_client.hpp" #include "nav2_ros_common/node_thread.hpp" diff --git a/nav2_route/test/test_goal_intent_search.cpp b/nav2_route/test/test_goal_intent_search.cpp index a829af0370f..1f3e8fd4b1e 100644 --- a/nav2_route/test/test_goal_intent_search.cpp +++ b/nav2_route/test/test_goal_intent_search.cpp @@ -19,9 +19,9 @@ #include "gtest/gtest.h" #include "rclcpp/rclcpp.hpp" -#include "tf2_ros/transform_broadcaster.h" -#include "tf2_ros/create_timer_ros.h" -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/transform_broadcaster.hpp" +#include "tf2_ros/create_timer_ros.hpp" +#include "tf2_ros/transform_listener.hpp" #include "nav2_ros_common/lifecycle_node.hpp" #include "nav2_ros_common/service_client.hpp" #include "nav2_ros_common/node_thread.hpp" diff --git a/nav2_route/test/test_graph_loader.cpp b/nav2_route/test/test_graph_loader.cpp index 99341852f78..e76fe863314 100644 --- a/nav2_route/test/test_graph_loader.cpp +++ b/nav2_route/test/test_graph_loader.cpp @@ -20,7 +20,7 @@ #include "nav2_ros_common/lifecycle_node.hpp" #include "nav2_ros_common/node_utils.hpp" #include "nav2_route/graph_loader.hpp" -#include "tf2_ros/static_transform_broadcaster.h" +#include "tf2_ros/static_transform_broadcaster.hpp" class RclCppFixture { diff --git a/nav2_route/test/test_graph_saver.cpp b/nav2_route/test/test_graph_saver.cpp index a6897f11602..4abdc0b74d3 100644 --- a/nav2_route/test/test_graph_saver.cpp +++ b/nav2_route/test/test_graph_saver.cpp @@ -22,7 +22,7 @@ #include "nav2_ros_common/node_utils.hpp" #include "nav2_route/graph_loader.hpp" #include "nav2_route/graph_saver.hpp" -#include "tf2_ros/static_transform_broadcaster.h" +#include "tf2_ros/static_transform_broadcaster.hpp" class RclCppFixture { diff --git a/nav2_route/test/test_route_server.cpp b/nav2_route/test/test_route_server.cpp index f8906529d31..5fb73b4ce0f 100644 --- a/nav2_route/test/test_route_server.cpp +++ b/nav2_route/test/test_route_server.cpp @@ -19,9 +19,9 @@ #include "gtest/gtest.h" #include "rclcpp/rclcpp.hpp" -#include "tf2_ros/transform_broadcaster.h" -#include "tf2_ros/create_timer_ros.h" -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/transform_broadcaster.hpp" +#include "tf2_ros/create_timer_ros.hpp" +#include "tf2_ros/transform_listener.hpp" #include "nav2_ros_common/lifecycle_node.hpp" #include "std_srvs/srv/trigger.hpp" #include "nav2_ros_common/service_client.hpp" diff --git a/nav2_route/test/test_route_tracker.cpp b/nav2_route/test/test_route_tracker.cpp index 9d874a1dc5a..07ca24a4e91 100644 --- a/nav2_route/test/test_route_tracker.cpp +++ b/nav2_route/test/test_route_tracker.cpp @@ -19,9 +19,9 @@ #include "gtest/gtest.h" #include "rclcpp/rclcpp.hpp" -#include "tf2_ros/transform_broadcaster.h" -#include "tf2_ros/create_timer_ros.h" -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/transform_broadcaster.hpp" +#include "tf2_ros/create_timer_ros.hpp" +#include "tf2_ros/transform_listener.hpp" #include "nav2_ros_common/lifecycle_node.hpp" #include "nav2_core/route_exceptions.hpp" #include "nav2_route/route_tracker.hpp" diff --git a/nav2_rviz_plugins/include/nav2_rviz_plugins/nav2_panel.hpp b/nav2_rviz_plugins/include/nav2_rviz_plugins/nav2_panel.hpp index 3a4cdd9e50f..44ad28d279e 100644 --- a/nav2_rviz_plugins/include/nav2_rviz_plugins/nav2_panel.hpp +++ b/nav2_rviz_plugins/include/nav2_rviz_plugins/nav2_panel.hpp @@ -35,9 +35,9 @@ #include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" #include "visualization_msgs/msg/marker_array.hpp" #include "nav2_util/geometry_utils.hpp" -#include "tf2_ros/transform_listener.h" -#include "tf2_ros/create_timer_ros.h" -#include "tf2_ros/buffer.h" +#include "tf2_ros/transform_listener.hpp" +#include "tf2_ros/create_timer_ros.hpp" +#include "tf2_ros/buffer.hpp" class QPushButton; diff --git a/nav2_smoother/src/nav2_smoother.cpp b/nav2_smoother/src/nav2_smoother.cpp index 4408b5a43e0..eb852c1eb52 100644 --- a/nav2_smoother/src/nav2_smoother.cpp +++ b/nav2_smoother/src/nav2_smoother.cpp @@ -25,7 +25,7 @@ #include "nav2_ros_common/node_utils.hpp" #include "nav_2d_utils/conversions.hpp" #include "nav_2d_utils/tf_help.hpp" -#include "tf2_ros/create_timer_ros.h" +#include "tf2_ros/create_timer_ros.hpp" using namespace std::chrono_literals; diff --git a/nav2_system_tests/src/behavior_tree/test_behavior_tree_node.cpp b/nav2_system_tests/src/behavior_tree/test_behavior_tree_node.cpp index 2f62894abb7..d36535924e0 100644 --- a/nav2_system_tests/src/behavior_tree/test_behavior_tree_node.cpp +++ b/nav2_system_tests/src/behavior_tree/test_behavior_tree_node.cpp @@ -26,9 +26,9 @@ #include "behaviortree_cpp/bt_factory.h" #include "behaviortree_cpp/utils/shared_library.h" -#include "tf2_ros/buffer.h" -#include "tf2_ros/transform_listener.h" -#include "tf2_ros/create_timer_ros.h" +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/transform_listener.hpp" +#include "tf2_ros/create_timer_ros.hpp" #include "nav2_util/odometry_utils.hpp" #include "nav2_util/string_utils.hpp" diff --git a/nav2_system_tests/src/behaviors/assisted_teleop/assisted_teleop_behavior_tester.hpp b/nav2_system_tests/src/behaviors/assisted_teleop/assisted_teleop_behavior_tester.hpp index bb3f3b18159..40e2aa9a754 100644 --- a/nav2_system_tests/src/behaviors/assisted_teleop/assisted_teleop_behavior_tester.hpp +++ b/nav2_system_tests/src/behaviors/assisted_teleop/assisted_teleop_behavior_tester.hpp @@ -35,8 +35,8 @@ #include "std_msgs/msg/empty.hpp" #include "tf2/utils.hpp" -#include "tf2_ros/buffer.h" -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/transform_listener.hpp" namespace nav2_system_tests { diff --git a/nav2_system_tests/src/behaviors/wait/wait_behavior_tester.hpp b/nav2_system_tests/src/behaviors/wait/wait_behavior_tester.hpp index 5a7313c0cd5..bea4506e68e 100644 --- a/nav2_system_tests/src/behaviors/wait/wait_behavior_tester.hpp +++ b/nav2_system_tests/src/behaviors/wait/wait_behavior_tester.hpp @@ -33,8 +33,8 @@ #include "geometry_msgs/msg/pose_with_covariance_stamped.hpp" #include "tf2/utils.hpp" -#include "tf2_ros/buffer.h" -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/transform_listener.hpp" namespace nav2_system_tests { diff --git a/nav2_system_tests/src/planning/planner_tester.hpp b/nav2_system_tests/src/planning/planner_tester.hpp index d878bc49f22..3c14f795d4f 100644 --- a/nav2_system_tests/src/planning/planner_tester.hpp +++ b/nav2_system_tests/src/planning/planner_tester.hpp @@ -32,7 +32,7 @@ #include "geometry_msgs/msg/pose_stamped.hpp" #include "geometry_msgs/msg/transform_stamped.hpp" #include "nav2_planner/planner_server.hpp" -#include "tf2_ros/transform_broadcaster.h" +#include "tf2_ros/transform_broadcaster.hpp" namespace nav2_system_tests { diff --git a/nav2_util/include/nav2_util/robot_utils.hpp b/nav2_util/include/nav2_util/robot_utils.hpp index cffbe80a26b..faca6d301dc 100644 --- a/nav2_util/include/nav2_util/robot_utils.hpp +++ b/nav2_util/include/nav2_util/robot_utils.hpp @@ -26,7 +26,7 @@ #include "geometry_msgs/msg/transform_stamped.hpp" #include "tf2/time.hpp" #include "tf2/transform_datatypes.hpp" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" #include "rclcpp/rclcpp.hpp" diff --git a/nav2_util/src/base_footprint_publisher.hpp b/nav2_util/src/base_footprint_publisher.hpp index 67f50625101..15b16037bed 100644 --- a/nav2_util/src/base_footprint_publisher.hpp +++ b/nav2_util/src/base_footprint_publisher.hpp @@ -21,10 +21,10 @@ #include "rclcpp/rclcpp.hpp" #include "tf2_msgs/msg/tf_message.hpp" #include "geometry_msgs/msg/transform_stamped.hpp" -#include "tf2_ros/create_timer_ros.h" -#include "tf2_ros/transform_listener.h" -#include "tf2_ros/transform_broadcaster.h" -#include "tf2_ros/buffer.h" +#include "tf2_ros/create_timer_ros.hpp" +#include "tf2_ros/transform_listener.hpp" +#include "tf2_ros/transform_broadcaster.hpp" +#include "tf2_ros/buffer.hpp" #include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" #include "tf2/utils.hpp" diff --git a/nav2_util/test/test_robot_utils.cpp b/nav2_util/test/test_robot_utils.cpp index 119addc9355..33c55aa2a3b 100644 --- a/nav2_util/test/test_robot_utils.cpp +++ b/nav2_util/test/test_robot_utils.cpp @@ -16,11 +16,11 @@ #include #include "nav2_ros_common/lifecycle_node.hpp" #include "nav2_util/robot_utils.hpp" -#include "tf2_ros/transform_listener.h" -#include "tf2_ros/transform_broadcaster.h" +#include "tf2_ros/transform_listener.hpp" +#include "tf2_ros/transform_broadcaster.hpp" #include "geometry_msgs/msg/pose_stamped.hpp" #include "gtest/gtest.h" -#include "tf2_ros/create_timer_ros.h" +#include "tf2_ros/create_timer_ros.hpp" TEST(RobotUtils, LookupExceptionError) { diff --git a/nav2_waypoint_follower/include/nav2_waypoint_follower/waypoint_follower.hpp b/nav2_waypoint_follower/include/nav2_waypoint_follower/waypoint_follower.hpp index 83edf012880..816feb9eca3 100644 --- a/nav2_waypoint_follower/include/nav2_waypoint_follower/waypoint_follower.hpp +++ b/nav2_waypoint_follower/include/nav2_waypoint_follower/waypoint_follower.hpp @@ -37,9 +37,9 @@ #include "nav2_core/waypoint_task_executor.hpp" #include "robot_localization/srv/from_ll.hpp" -#include "tf2_ros/buffer.h" +#include "tf2_ros/buffer.hpp" #include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" -#include "tf2_ros/transform_listener.h" +#include "tf2_ros/transform_listener.hpp" namespace nav2_waypoint_follower { From 9cd0f9fc977242a30f4921c8fdb99ada2ec61093 Mon Sep 17 00:00:00 2001 From: mini-1235 Date: Thu, 7 Aug 2025 04:37:29 +0800 Subject: [PATCH 11/51] Move nav_2d_utils to nav2_util (#5414) * Move nav_2d_util to nav2_util Signed-off-by: mini-1235 * Rename frame Signed-off-by: mini-1235 * Replace OdomSubscriber with OdomSmoother Signed-off-by: mini-1235 * Use transformPoseInTargetFrame in all controllers Signed-off-by: mini-1235 --------- Signed-off-by: mini-1235 --- nav2_controller/CMakeLists.txt | 9 -- .../nav2_controller/controller_server.hpp | 14 +-- nav2_controller/package.xml | 2 - .../plugins/pose_progress_checker.cpp | 1 - .../plugins/simple_progress_checker.cpp | 1 - nav2_controller/plugins/test/CMakeLists.txt | 4 +- nav2_controller/plugins/test/goal_checker.cpp | 1 - .../plugins/test/progress_checker.cpp | 1 - nav2_controller/src/controller_server.cpp | 33 +++--- .../opennav_docking/docking_server.hpp | 4 +- .../opennav_docking/src/controller.cpp | 1 - .../opennav_docking/src/docking_server.cpp | 7 +- nav2_dwb_controller/dwb_core/CMakeLists.txt | 1 - .../include/dwb_core/dwb_local_planner.hpp | 2 +- .../dwb_core/src/dwb_local_planner.cpp | 22 ++-- .../dwb_critics/src/goal_align.cpp | 6 +- .../dwb_critics/src/oscillation.cpp | 12 +-- .../dwb_critics/src/path_align.cpp | 4 +- .../dwb_critics/src/rotate_to_goal.cpp | 13 +-- .../dwb_plugins/src/kinematic_parameters.cpp | 1 - .../src/limited_accel_generator.cpp | 4 +- .../src/standard_traj_generator.cpp | 1 - .../dwb_plugins/src/xy_theta_iterator.cpp | 1 - .../nav_2d_utils/CMakeLists.txt | 25 +---- .../include/nav_2d_utils/conversions.hpp | 4 - .../include/nav_2d_utils/odom_subscriber.hpp | 100 ------------------ .../include/nav_2d_utils/parameters.hpp | 76 ------------- .../include/nav_2d_utils/tf_help.hpp | 85 --------------- .../nav_2d_utils/src/conversions.cpp | 28 ----- .../nav_2d_utils/src/tf_help.cpp | 98 ----------------- .../nav_2d_utils/test/2d_utils_test.cpp | 60 +++++------ .../nav_2d_utils/test/CMakeLists.txt | 3 - .../nav_2d_utils/test/tf_help_test.cpp | 98 ----------------- nav2_graceful_controller/CMakeLists.txt | 3 - .../nav2_graceful_controller/path_handler.hpp | 4 +- nav2_graceful_controller/package.xml | 1 - .../src/graceful_controller.cpp | 2 +- nav2_graceful_controller/src/path_handler.cpp | 14 +-- .../tools/path_handler.hpp | 11 -- nav2_mppi_controller/src/path_handler.cpp | 33 ++---- .../test/path_handler_test.cpp | 11 +- .../path_handler.hpp | 16 +-- .../src/path_handler.cpp | 31 ++---- .../src/regulated_pure_pursuit_controller.cpp | 2 +- nav2_smoother/CMakeLists.txt | 3 - nav2_smoother/package.xml | 1 - nav2_smoother/src/nav2_smoother.cpp | 2 - nav2_util/src/robot_utils.cpp | 5 + 48 files changed, 122 insertions(+), 739 deletions(-) delete mode 100644 nav2_dwb_controller/nav_2d_utils/include/nav_2d_utils/odom_subscriber.hpp delete mode 100644 nav2_dwb_controller/nav_2d_utils/include/nav_2d_utils/parameters.hpp delete mode 100644 nav2_dwb_controller/nav_2d_utils/include/nav_2d_utils/tf_help.hpp delete mode 100644 nav2_dwb_controller/nav_2d_utils/src/tf_help.cpp delete mode 100644 nav2_dwb_controller/nav_2d_utils/test/tf_help_test.cpp diff --git a/nav2_controller/CMakeLists.txt b/nav2_controller/CMakeLists.txt index 56ea5d7e794..93885ef7efc 100644 --- a/nav2_controller/CMakeLists.txt +++ b/nav2_controller/CMakeLists.txt @@ -11,8 +11,6 @@ find_package(nav2_common REQUIRED) find_package(nav2_costmap_2d REQUIRED) find_package(nav2_msgs REQUIRED) find_package(nav2_util REQUIRED) -find_package(nav_2d_msgs REQUIRED) -find_package(nav_2d_utils REQUIRED) find_package(pluginlib REQUIRED) find_package(rcl_interfaces REQUIRED) find_package(rclcpp REQUIRED) @@ -42,9 +40,6 @@ target_link_libraries(${library_name} PUBLIC ${nav2_msgs_TARGETS} nav2_util::nav2_util_core nav2_ros_common::nav2_ros_common - ${nav_2d_msgs_TARGETS} - nav_2d_utils::conversions - nav_2d_utils::tf_help pluginlib::pluginlib rclcpp::rclcpp rclcpp_lifecycle::rclcpp_lifecycle @@ -86,7 +81,6 @@ target_link_libraries(simple_progress_checker PUBLIC ) target_link_libraries(simple_progress_checker PRIVATE nav2_util::nav2_util_core - nav_2d_utils::conversions pluginlib::pluginlib ) @@ -107,7 +101,6 @@ target_link_libraries(pose_progress_checker PUBLIC target_link_libraries(pose_progress_checker PRIVATE angles::angles nav2_util::nav2_util_core - nav_2d_utils::conversions pluginlib::pluginlib ) @@ -218,8 +211,6 @@ ament_export_dependencies( nav2_costmap_2d nav2_msgs nav2_util - nav_2d_msgs - nav_2d_utils pluginlib rclcpp rclcpp_lifecycle diff --git a/nav2_controller/include/nav2_controller/controller_server.hpp b/nav2_controller/include/nav2_controller/controller_server.hpp index a838854a52b..b68c8e5bb94 100644 --- a/nav2_controller/include/nav2_controller/controller_server.hpp +++ b/nav2_controller/include/nav2_controller/controller_server.hpp @@ -29,10 +29,10 @@ #include "tf2_ros/transform_listener.hpp" #include "nav2_msgs/action/follow_path.hpp" #include "nav2_msgs/msg/speed_limit.hpp" -#include "nav_2d_utils/odom_subscriber.hpp" #include "nav2_ros_common/lifecycle_node.hpp" #include "nav2_ros_common/simple_action_server.hpp" #include "nav2_util/robot_utils.hpp" +#include "nav2_util/odometry_utils.hpp" #include "nav2_util/twist_publisher.hpp" #include "pluginlib/class_loader.hpp" #include "pluginlib/class_list_macros.hpp" @@ -208,12 +208,12 @@ class ControllerServer : public nav2::LifecycleNode * @param Twist The current Twist from odometry * @return Twist Twist after thresholds applied */ - nav_2d_msgs::msg::Twist2D getThresholdedTwist(const nav_2d_msgs::msg::Twist2D & twist) + geometry_msgs::msg::Twist getThresholdedTwist(const geometry_msgs::msg::Twist & twist) { - nav_2d_msgs::msg::Twist2D twist_thresh; - twist_thresh.x = getThresholdedVelocity(twist.x, min_x_velocity_threshold_); - twist_thresh.y = getThresholdedVelocity(twist.y, min_y_velocity_threshold_); - twist_thresh.theta = getThresholdedVelocity(twist.theta, min_theta_velocity_threshold_); + geometry_msgs::msg::Twist twist_thresh; + twist_thresh.linear.x = getThresholdedVelocity(twist.linear.x, min_x_velocity_threshold_); + twist_thresh.linear.y = getThresholdedVelocity(twist.linear.y, min_y_velocity_threshold_); + twist_thresh.angular.z = getThresholdedVelocity(twist.angular.z, min_theta_velocity_threshold_); return twist_thresh; } @@ -233,7 +233,7 @@ class ControllerServer : public nav2::LifecycleNode std::unique_ptr costmap_thread_; // Publishers and subscribers - std::unique_ptr odom_sub_; + std::unique_ptr odom_sub_; std::unique_ptr vel_publisher_; nav2::Subscription::SharedPtr speed_limit_sub_; diff --git a/nav2_controller/package.xml b/nav2_controller/package.xml index b5be342451c..9360c10d05f 100644 --- a/nav2_controller/package.xml +++ b/nav2_controller/package.xml @@ -18,8 +18,6 @@ nav2_costmap_2d nav2_msgs nav2_util - nav_2d_msgs - nav_2d_utils pluginlib rcl_interfaces rclcpp diff --git a/nav2_controller/plugins/pose_progress_checker.cpp b/nav2_controller/plugins/pose_progress_checker.cpp index 453d20992e8..a891104cac0 100644 --- a/nav2_controller/plugins/pose_progress_checker.cpp +++ b/nav2_controller/plugins/pose_progress_checker.cpp @@ -18,7 +18,6 @@ #include #include #include "angles/angles.h" -#include "nav_2d_utils/conversions.hpp" #include "geometry_msgs/msg/pose_stamped.hpp" #include "geometry_msgs/msg/pose.hpp" #include "nav2_ros_common/node_utils.hpp" diff --git a/nav2_controller/plugins/simple_progress_checker.cpp b/nav2_controller/plugins/simple_progress_checker.cpp index 2e219cdeee8..df3cb77df69 100644 --- a/nav2_controller/plugins/simple_progress_checker.cpp +++ b/nav2_controller/plugins/simple_progress_checker.cpp @@ -17,7 +17,6 @@ #include #include #include -#include "nav_2d_utils/conversions.hpp" #include "geometry_msgs/msg/pose_stamped.hpp" #include "geometry_msgs/msg/pose.hpp" #include "nav2_ros_common/node_utils.hpp" diff --git a/nav2_controller/plugins/test/CMakeLists.txt b/nav2_controller/plugins/test/CMakeLists.txt index cd8048513d4..ba0ce9eb2d5 100644 --- a/nav2_controller/plugins/test/CMakeLists.txt +++ b/nav2_controller/plugins/test/CMakeLists.txt @@ -1,5 +1,5 @@ ament_add_gtest(pctest progress_checker.cpp) -target_link_libraries(pctest simple_progress_checker pose_progress_checker nav_2d_utils::conversions) +target_link_libraries(pctest simple_progress_checker pose_progress_checker) ament_add_gtest(gctest goal_checker.cpp) -target_link_libraries(gctest simple_goal_checker stopped_goal_checker nav_2d_utils::conversions) +target_link_libraries(gctest simple_goal_checker stopped_goal_checker) diff --git a/nav2_controller/plugins/test/goal_checker.cpp b/nav2_controller/plugins/test/goal_checker.cpp index 4730aeb3a04..1aba1ba80bb 100644 --- a/nav2_controller/plugins/test/goal_checker.cpp +++ b/nav2_controller/plugins/test/goal_checker.cpp @@ -38,7 +38,6 @@ #include "gtest/gtest.h" #include "nav2_controller/plugins/simple_goal_checker.hpp" #include "nav2_controller/plugins/stopped_goal_checker.hpp" -#include "nav_2d_utils/conversions.hpp" #include "nav2_util/geometry_utils.hpp" #include "nav2_ros_common/lifecycle_node.hpp" #include "eigen3/Eigen/Geometry" diff --git a/nav2_controller/plugins/test/progress_checker.cpp b/nav2_controller/plugins/test/progress_checker.cpp index 0791b9e9c27..293daf13a25 100644 --- a/nav2_controller/plugins/test/progress_checker.cpp +++ b/nav2_controller/plugins/test/progress_checker.cpp @@ -38,7 +38,6 @@ #include "gtest/gtest.h" #include "nav2_controller/plugins/simple_progress_checker.hpp" #include "nav2_controller/plugins/pose_progress_checker.hpp" -#include "nav_2d_utils/conversions.hpp" #include "nav2_ros_common/lifecycle_node.hpp" #include "nav2_util/geometry_utils.hpp" diff --git a/nav2_controller/src/controller_server.cpp b/nav2_controller/src/controller_server.cpp index 26187087ec1..66b62e667e2 100644 --- a/nav2_controller/src/controller_server.cpp +++ b/nav2_controller/src/controller_server.cpp @@ -21,8 +21,6 @@ #include "lifecycle_msgs/msg/state.hpp" #include "nav2_core/controller_exceptions.hpp" -#include "nav_2d_utils/conversions.hpp" -#include "nav_2d_utils/tf_help.hpp" #include "nav2_ros_common/node_utils.hpp" #include "nav2_util/geometry_utils.hpp" #include "nav2_controller/controller_server.hpp" @@ -65,6 +63,9 @@ ControllerServer::ControllerServer(const rclcpp::NodeOptions & options) declare_parameter("publish_zero_velocity", rclcpp::ParameterValue(true)); declare_parameter("costmap_update_timeout", 0.30); // 300ms + declare_parameter("odom_topic", rclcpp::ParameterValue("odom")); + declare_parameter("odom_duration", rclcpp::ParameterValue(0.3)); + // The costmap node is used in the implementation of the controller costmap_ros_ = std::make_shared( "local_costmap", std::string{get_namespace()}, @@ -125,8 +126,11 @@ ControllerServer::on_configure(const rclcpp_lifecycle::State & state) get_parameter("min_theta_velocity_threshold", min_theta_velocity_threshold_); RCLCPP_INFO(get_logger(), "Controller frequency set to %.4fHz", controller_frequency_); - std::string speed_limit_topic; + std::string speed_limit_topic, odom_topic; get_parameter("speed_limit_topic", speed_limit_topic); + get_parameter("odom_topic", odom_topic); + double odom_duration; + get_parameter("odom_duration", odom_duration); get_parameter("failure_tolerance", failure_tolerance_); get_parameter("use_realtime_priority", use_realtime_priority_); get_parameter("publish_zero_velocity", publish_zero_velocity_); @@ -219,7 +223,7 @@ ControllerServer::on_configure(const rclcpp_lifecycle::State & state) get_logger(), "Controller Server has %s controllers available.", controller_ids_concat_.c_str()); - odom_sub_ = std::make_unique(node); + odom_sub_ = std::make_unique(node, odom_duration, odom_topic); vel_publisher_ = std::make_unique(node, "cmd_vel"); double costmap_update_timeout_dbl; @@ -624,7 +628,7 @@ void ControllerServer::computeAndPublishVelocity() throw nav2_core::FailedToMakeProgress("Failed to make progress"); } - nav_2d_msgs::msg::Twist2D twist = getThresholdedTwist(odom_sub_->getTwist()); + geometry_msgs::msg::Twist twist = getThresholdedTwist(odom_sub_->getRawTwist()); geometry_msgs::msg::TwistStamped cmd_vel_2d; @@ -632,7 +636,7 @@ void ControllerServer::computeAndPublishVelocity() cmd_vel_2d = controllers_[current_controller_]->computeVelocityCommands( pose, - nav_2d_utils::twist2Dto3D(twist), + twist, goal_checkers_[current_goal_checker_].get()); last_valid_cmd_time_ = now(); cmd_vel_2d.header.frame_id = costmap_ros_->getBaseFrameID(); @@ -665,10 +669,9 @@ void ControllerServer::computeAndPublishVelocity() // Find the closest pose to current pose on global path geometry_msgs::msg::PoseStamped robot_pose_in_path_frame; - rclcpp::Duration tolerance(rclcpp::Duration::from_seconds(costmap_ros_->getTransformTolerance())); - if (!nav_2d_utils::transformPose( - costmap_ros_->getTfBuffer(), current_path_.header.frame_id, pose, - robot_pose_in_path_frame, tolerance)) + if (!nav2_util::transformPoseInTargetFrame( + pose, robot_pose_in_path_frame, *costmap_ros_->getTfBuffer(), + current_path_.header.frame_id, costmap_ros_->getTransformTolerance())) { throw nav2_core::ControllerTFError("Failed to transform robot pose to path frame"); } @@ -793,14 +796,12 @@ bool ControllerServer::isGoalReached() return false; } - nav_2d_msgs::msg::Twist2D twist = getThresholdedTwist(odom_sub_->getTwist()); - geometry_msgs::msg::Twist velocity = nav_2d_utils::twist2Dto3D(twist); + geometry_msgs::msg::Twist velocity = getThresholdedTwist(odom_sub_->getRawTwist()); geometry_msgs::msg::PoseStamped transformed_end_pose; - rclcpp::Duration tolerance(rclcpp::Duration::from_seconds(costmap_ros_->getTransformTolerance())); - nav_2d_utils::transformPose( - costmap_ros_->getTfBuffer(), costmap_ros_->getGlobalFrameID(), - end_pose_, transformed_end_pose, tolerance); + nav2_util::transformPoseInTargetFrame( + end_pose_, transformed_end_pose, *costmap_ros_->getTfBuffer(), + costmap_ros_->getGlobalFrameID(), costmap_ros_->getTransformTolerance()); return goal_checkers_[current_goal_checker_]->isGoalReached( pose.pose, transformed_end_pose.pose, diff --git a/nav2_docking/opennav_docking/include/opennav_docking/docking_server.hpp b/nav2_docking/opennav_docking/include/opennav_docking/docking_server.hpp index eb1497f21db..4538b2e447d 100644 --- a/nav2_docking/opennav_docking/include/opennav_docking/docking_server.hpp +++ b/nav2_docking/opennav_docking/include/opennav_docking/docking_server.hpp @@ -27,7 +27,7 @@ #include "nav2_ros_common/node_utils.hpp" #include "nav2_ros_common/simple_action_server.hpp" #include "nav2_util/twist_publisher.hpp" -#include "nav_2d_utils/odom_subscriber.hpp" +#include "nav2_util/odometry_utils.hpp" #include "opennav_docking/controller.hpp" #include "opennav_docking/utils.hpp" #include "opennav_docking/types.hpp" @@ -264,7 +264,7 @@ class DockingServer : public nav2::LifecycleNode rclcpp::Time action_start_time_; std::unique_ptr vel_publisher_; - std::unique_ptr odom_sub_; + std::unique_ptr odom_sub_; typename DockingActionServer::SharedPtr docking_action_server_; typename UndockingActionServer::SharedPtr undocking_action_server_; diff --git a/nav2_docking/opennav_docking/src/controller.cpp b/nav2_docking/opennav_docking/src/controller.cpp index 9048c70a29a..7838158f8af 100644 --- a/nav2_docking/opennav_docking/src/controller.cpp +++ b/nav2_docking/opennav_docking/src/controller.cpp @@ -19,7 +19,6 @@ #include "opennav_docking/controller.hpp" #include "nav2_util/geometry_utils.hpp" #include "nav2_ros_common/node_utils.hpp" -#include "nav_2d_utils/conversions.hpp" #include "tf2/utils.hpp" namespace opennav_docking diff --git a/nav2_docking/opennav_docking/src/docking_server.cpp b/nav2_docking/opennav_docking/src/docking_server.cpp index 3584af4e9d3..17dc1c28da3 100644 --- a/nav2_docking/opennav_docking/src/docking_server.cpp +++ b/nav2_docking/opennav_docking/src/docking_server.cpp @@ -42,6 +42,7 @@ DockingServer::DockingServer(const rclcpp::NodeOptions & options) declare_parameter("dock_backwards", rclcpp::PARAMETER_BOOL); declare_parameter("dock_prestaging_tolerance", 0.5); declare_parameter("odom_topic", "odom"); + declare_parameter("odom_duration", 0.3); declare_parameter("rotation_angular_tolerance", 0.05); } @@ -83,7 +84,9 @@ DockingServer::on_configure(const rclcpp_lifecycle::State & state) // Create odom subscriber for backward blind docking std::string odom_topic; get_parameter("odom_topic", odom_topic); - odom_sub_ = std::make_unique(node, odom_topic); + double odom_duration; + get_parameter("odom_duration", odom_duration); + odom_sub_ = std::make_unique(node, odom_duration, odom_topic); // Create the action servers for dock / undock docking_action_server_ = node->create_action_server( @@ -464,7 +467,7 @@ void DockingServer::rotateToDock(const geometry_msgs::msg::PoseStamped & dock_po } auto current_vel = std::make_unique(); - current_vel->twist.angular.z = odom_sub_->getTwist().theta; + current_vel->twist.angular.z = odom_sub_->getRawTwist().angular.z; auto command = std::make_unique(); command->header = robot_pose.header; diff --git a/nav2_dwb_controller/dwb_core/CMakeLists.txt b/nav2_dwb_controller/dwb_core/CMakeLists.txt index b57c2291a51..00789a46e3d 100644 --- a/nav2_dwb_controller/dwb_core/CMakeLists.txt +++ b/nav2_dwb_controller/dwb_core/CMakeLists.txt @@ -45,7 +45,6 @@ target_link_libraries(dwb_core PUBLIC nav2_util::nav2_util_core ${nav_2d_msgs_TARGETS} nav_2d_utils::conversions - nav_2d_utils::tf_help ${nav_msgs_TARGETS} pluginlib::pluginlib rclcpp::rclcpp diff --git a/nav2_dwb_controller/dwb_core/include/dwb_core/dwb_local_planner.hpp b/nav2_dwb_controller/dwb_core/include/dwb_core/dwb_local_planner.hpp index 41120a7c48a..63013bd091e 100644 --- a/nav2_dwb_controller/dwb_core/include/dwb_core/dwb_local_planner.hpp +++ b/nav2_dwb_controller/dwb_core/include/dwb_core/dwb_local_planner.hpp @@ -199,7 +199,7 @@ class DWBLocalPlanner : public nav2_core::Controller bool prune_plan_; double prune_distance_; bool debug_trajectory_details_; - rclcpp::Duration transform_tolerance_{0, 0}; + double transform_tolerance_{0}; bool shorten_transformed_plan_; double forward_prune_distance_; diff --git a/nav2_dwb_controller/dwb_core/src/dwb_local_planner.cpp b/nav2_dwb_controller/dwb_core/src/dwb_local_planner.cpp index e539c98819a..5c1b47322a6 100644 --- a/nav2_dwb_controller/dwb_core/src/dwb_local_planner.cpp +++ b/nav2_dwb_controller/dwb_core/src/dwb_local_planner.cpp @@ -44,7 +44,6 @@ #include "dwb_msgs/msg/critic_score.hpp" #include "nav_2d_msgs/msg/twist2_d.hpp" #include "nav_2d_utils/conversions.hpp" -#include "nav_2d_utils/tf_help.hpp" #include "nav2_util/geometry_utils.hpp" #include "nav2_ros_common/lifecycle_node.hpp" #include "nav2_ros_common/node_utils.hpp" @@ -112,8 +111,7 @@ void DWBLocalPlanner::configure( std::string traj_generator_name; double transform_tolerance; - node->get_parameter(dwb_plugin_name_ + ".transform_tolerance", transform_tolerance); - transform_tolerance_ = rclcpp::Duration::from_seconds(transform_tolerance); + node->get_parameter(dwb_plugin_name_ + ".transform_tolerance", transform_tolerance_); RCLCPP_INFO(logger_, "Setting transform_tolerance to %f", transform_tolerance); node->get_parameter(dwb_plugin_name_ + ".prune_plan", prune_plan_); @@ -293,9 +291,9 @@ DWBLocalPlanner::prepareGlobalPlan( goal_pose.header.frame_id = global_plan_.header.frame_id; goal_pose.pose = global_plan_.poses.back().pose; - nav_2d_utils::transformPose( - tf_, costmap_ros_->getGlobalFrameID(), goal_pose, - goal_pose, transform_tolerance_); + nav2_util::transformPoseInTargetFrame( + goal_pose, goal_pose, *tf_, + costmap_ros_->getGlobalFrameID(), transform_tolerance_); } nav_2d_msgs::msg::Twist2DStamped @@ -470,9 +468,9 @@ DWBLocalPlanner::transformGlobalPlan( // let's get the pose of the robot in the frame of the plan geometry_msgs::msg::PoseStamped robot_pose; - if (!nav_2d_utils::transformPose( - tf_, global_plan_.header.frame_id, pose, - robot_pose, transform_tolerance_)) + if (!nav2_util::transformPoseInTargetFrame( + pose, robot_pose, *tf_, + global_plan_.header.frame_id, transform_tolerance_)) { throw nav2_core:: ControllerTFError("Unable to transform robot pose into global plan's frame"); @@ -538,9 +536,9 @@ DWBLocalPlanner::transformGlobalPlan( // frame to local auto transformGlobalPoseToLocal = [&](const auto & global_plan_pose) { geometry_msgs::msg::PoseStamped transformed_pose; - nav_2d_utils::transformPose( - tf_, transformed_plan.header.frame_id, - global_plan_pose, transformed_pose, transform_tolerance_); + nav2_util::transformPoseInTargetFrame( + global_plan_pose, transformed_pose, *tf_, + transformed_plan.header.frame_id, transform_tolerance_); return transformed_pose; }; diff --git a/nav2_dwb_controller/dwb_critics/src/goal_align.cpp b/nav2_dwb_controller/dwb_critics/src/goal_align.cpp index 1ae85c2e1dd..f8b713c032f 100644 --- a/nav2_dwb_controller/dwb_critics/src/goal_align.cpp +++ b/nav2_dwb_controller/dwb_critics/src/goal_align.cpp @@ -37,7 +37,6 @@ #include #include "dwb_critics/alignment_util.hpp" #include "pluginlib/class_list_macros.hpp" -#include "nav_2d_utils/parameters.hpp" namespace dwb_critics { @@ -52,9 +51,8 @@ void GoalAlignCritic::onInit() throw std::runtime_error{"Failed to lock node"}; } - forward_point_distance_ = nav_2d_utils::searchAndGetParam( - node, - dwb_plugin_name_ + "." + name_ + ".forward_point_distance", 0.325); + forward_point_distance_ = node->declare_or_get_parameter(dwb_plugin_name_ + "." + name_ + + ".forward_point_distance", 0.325); } bool GoalAlignCritic::prepare( diff --git a/nav2_dwb_controller/dwb_critics/src/oscillation.cpp b/nav2_dwb_controller/dwb_critics/src/oscillation.cpp index bad8e40e3ff..170a9764c1f 100644 --- a/nav2_dwb_controller/dwb_critics/src/oscillation.cpp +++ b/nav2_dwb_controller/dwb_critics/src/oscillation.cpp @@ -37,7 +37,6 @@ #include #include #include -#include "nav_2d_utils/parameters.hpp" #include "nav2_ros_common/node_utils.hpp" #include "dwb_core/exceptions.hpp" #include "pluginlib/class_list_macros.hpp" @@ -99,16 +98,13 @@ void OscillationCritic::onInit() clock_ = node->get_clock(); - oscillation_reset_dist_ = nav_2d_utils::searchAndGetParam( - node, - dwb_plugin_name_ + "." + name_ + ".oscillation_reset_dist", 0.05); + oscillation_reset_dist_ = node->declare_or_get_parameter(dwb_plugin_name_ + "." + name_ + + ".oscillation_reset_dist", 0.05); oscillation_reset_dist_sq_ = oscillation_reset_dist_ * oscillation_reset_dist_; - oscillation_reset_angle_ = nav_2d_utils::searchAndGetParam( - node, + oscillation_reset_angle_ = node->declare_or_get_parameter( dwb_plugin_name_ + "." + name_ + ".oscillation_reset_angle", 0.2); oscillation_reset_time_ = rclcpp::Duration::from_seconds( - nav_2d_utils::searchAndGetParam( - node, + node->declare_or_get_parameter( dwb_plugin_name_ + "." + name_ + ".oscillation_reset_time", -1.0)); nav2::declare_parameter_if_not_declared( diff --git a/nav2_dwb_controller/dwb_critics/src/path_align.cpp b/nav2_dwb_controller/dwb_critics/src/path_align.cpp index 6c4b28ea6f5..d91414586dd 100644 --- a/nav2_dwb_controller/dwb_critics/src/path_align.cpp +++ b/nav2_dwb_controller/dwb_critics/src/path_align.cpp @@ -37,7 +37,6 @@ #include #include "dwb_critics/alignment_util.hpp" #include "pluginlib/class_list_macros.hpp" -#include "nav_2d_utils/parameters.hpp" namespace dwb_critics { @@ -52,8 +51,7 @@ void PathAlignCritic::onInit() throw std::runtime_error{"Failed to lock node"}; } - forward_point_distance_ = nav_2d_utils::searchAndGetParam( - node, + forward_point_distance_ = node->declare_or_get_parameter( dwb_plugin_name_ + "." + name_ + ".forward_point_distance", 0.325); } diff --git a/nav2_dwb_controller/dwb_critics/src/rotate_to_goal.cpp b/nav2_dwb_controller/dwb_critics/src/rotate_to_goal.cpp index f4323188b18..34f2ae95335 100644 --- a/nav2_dwb_controller/dwb_critics/src/rotate_to_goal.cpp +++ b/nav2_dwb_controller/dwb_critics/src/rotate_to_goal.cpp @@ -35,7 +35,6 @@ #include "dwb_critics/rotate_to_goal.hpp" #include #include -#include "nav_2d_utils/parameters.hpp" #include "dwb_core/exceptions.hpp" #include "pluginlib/class_list_macros.hpp" #include "dwb_core/trajectory_utils.hpp" @@ -58,19 +57,15 @@ void RotateToGoalCritic::onInit() throw std::runtime_error{"Failed to lock node"}; } - xy_goal_tolerance_ = nav_2d_utils::searchAndGetParam( - node, + xy_goal_tolerance_ = node->declare_or_get_parameter( dwb_plugin_name_ + ".xy_goal_tolerance", 0.25); xy_goal_tolerance_sq_ = xy_goal_tolerance_ * xy_goal_tolerance_; - double stopped_xy_velocity = nav_2d_utils::searchAndGetParam( - node, + double stopped_xy_velocity = node->declare_or_get_parameter( dwb_plugin_name_ + ".trans_stopped_velocity", 0.25); stopped_xy_velocity_sq_ = stopped_xy_velocity * stopped_xy_velocity; - slowing_factor_ = nav_2d_utils::searchAndGetParam( - node, + slowing_factor_ = node->declare_or_get_parameter( dwb_plugin_name_ + "." + name_ + ".slowing_factor", 5.0); - lookahead_time_ = nav_2d_utils::searchAndGetParam( - node, + lookahead_time_ = node->declare_or_get_parameter( dwb_plugin_name_ + "." + name_ + ".lookahead_time", -1.0); reset(); } diff --git a/nav2_dwb_controller/dwb_plugins/src/kinematic_parameters.cpp b/nav2_dwb_controller/dwb_plugins/src/kinematic_parameters.cpp index dde49133011..c1152bc3f5b 100644 --- a/nav2_dwb_controller/dwb_plugins/src/kinematic_parameters.cpp +++ b/nav2_dwb_controller/dwb_plugins/src/kinematic_parameters.cpp @@ -38,7 +38,6 @@ #include #include -#include "nav_2d_utils/parameters.hpp" #include "nav2_ros_common/node_utils.hpp" #include "nav2_costmap_2d/costmap_filters/filter_values.hpp" diff --git a/nav2_dwb_controller/dwb_plugins/src/limited_accel_generator.cpp b/nav2_dwb_controller/dwb_plugins/src/limited_accel_generator.cpp index 24d1c255973..4c096f7e1f1 100644 --- a/nav2_dwb_controller/dwb_plugins/src/limited_accel_generator.cpp +++ b/nav2_dwb_controller/dwb_plugins/src/limited_accel_generator.cpp @@ -36,7 +36,6 @@ #include #include #include -#include "nav_2d_utils/parameters.hpp" #include "pluginlib/class_list_macros.hpp" #include "dwb_core/exceptions.hpp" #include "nav2_ros_common/node_utils.hpp" @@ -65,8 +64,7 @@ void LimitedAccelGenerator::initialize( RCLCPP_WARN( rclcpp::get_logger("LimitedAccelGenerator"), "'sim_period' parameter is not set for %s", plugin_name.c_str()); - double controller_frequency = nav_2d_utils::searchAndGetParam( - nh, "controller_frequency", 20.0); + double controller_frequency = nh->declare_or_get_parameter("controller_frequency", 20.0); if (controller_frequency > 0) { acceleration_time_ = 1.0 / controller_frequency; } else { diff --git a/nav2_dwb_controller/dwb_plugins/src/standard_traj_generator.cpp b/nav2_dwb_controller/dwb_plugins/src/standard_traj_generator.cpp index 8e68a399f08..a11dd8ece0b 100644 --- a/nav2_dwb_controller/dwb_plugins/src/standard_traj_generator.cpp +++ b/nav2_dwb_controller/dwb_plugins/src/standard_traj_generator.cpp @@ -38,7 +38,6 @@ #include #include #include "dwb_plugins/xy_theta_iterator.hpp" -#include "nav_2d_utils/parameters.hpp" #include "pluginlib/class_list_macros.hpp" #include "dwb_core/exceptions.hpp" #include "nav2_ros_common/node_utils.hpp" diff --git a/nav2_dwb_controller/dwb_plugins/src/xy_theta_iterator.cpp b/nav2_dwb_controller/dwb_plugins/src/xy_theta_iterator.cpp index c319bef4a72..01dab35cb7b 100644 --- a/nav2_dwb_controller/dwb_plugins/src/xy_theta_iterator.cpp +++ b/nav2_dwb_controller/dwb_plugins/src/xy_theta_iterator.cpp @@ -38,7 +38,6 @@ #include #include -#include "nav_2d_utils/parameters.hpp" #include "nav2_ros_common/node_utils.hpp" namespace dwb_plugins diff --git a/nav2_dwb_controller/nav_2d_utils/CMakeLists.txt b/nav2_dwb_controller/nav_2d_utils/CMakeLists.txt index c3005dffd6c..7aad657a142 100644 --- a/nav2_dwb_controller/nav_2d_utils/CMakeLists.txt +++ b/nav2_dwb_controller/nav_2d_utils/CMakeLists.txt @@ -56,28 +56,7 @@ target_link_libraries(path_ops PRIVATE ${geometry_msgs_TARGETS} ) -add_library(tf_help SHARED - src/tf_help.cpp -) -target_include_directories(tf_help - PUBLIC - "$" - "$" -) -target_link_libraries(tf_help PUBLIC - conversions - ${geometry_msgs_TARGETS} - ${nav_2d_msgs_TARGETS} - rclcpp::rclcpp - nav2_ros_common::nav2_ros_common - tf2::tf2 - tf2_geometry_msgs::tf2_geometry_msgs - tf2_ros::tf2_ros - rclcpp_lifecycle::rclcpp_lifecycle - rcl_lifecycle::rcl_lifecycle -) - -install(TARGETS conversions path_ops tf_help +install(TARGETS conversions path_ops EXPORT ${PROJECT_NAME} ARCHIVE DESTINATION lib LIBRARY DESTINATION lib @@ -101,7 +80,7 @@ if(BUILD_TESTING) endif() ament_export_include_directories(include/${PROJECT_NAME}) -ament_export_libraries(conversions path_ops tf_help) +ament_export_libraries(conversions path_ops) ament_export_dependencies( geometry_msgs nav_2d_msgs diff --git a/nav2_dwb_controller/nav_2d_utils/include/nav_2d_utils/conversions.hpp b/nav2_dwb_controller/nav_2d_utils/include/nav_2d_utils/conversions.hpp index 35f3ec2018f..3a30fd03050 100644 --- a/nav2_dwb_controller/nav_2d_utils/include/nav_2d_utils/conversions.hpp +++ b/nav2_dwb_controller/nav_2d_utils/include/nav_2d_utils/conversions.hpp @@ -49,10 +49,6 @@ namespace nav_2d_utils { geometry_msgs::msg::Twist twist2Dto3D(const nav_2d_msgs::msg::Twist2D & cmd_vel_2d); nav_2d_msgs::msg::Twist2D twist3Dto2D(const geometry_msgs::msg::Twist & cmd_vel); -geometry_msgs::msg::PoseStamped poseToPoseStamped( - const geometry_msgs::msg::Pose & pose, - const std::string & frame, const rclcpp::Time & stamp); -nav_msgs::msg::Path posesToPath(const std::vector & poses); nav_msgs::msg::Path posesToPath( const std::vector & poses, const std::string & frame, const rclcpp::Time & stamp); diff --git a/nav2_dwb_controller/nav_2d_utils/include/nav_2d_utils/odom_subscriber.hpp b/nav2_dwb_controller/nav_2d_utils/include/nav_2d_utils/odom_subscriber.hpp deleted file mode 100644 index 2f989e71f2f..00000000000 --- a/nav2_dwb_controller/nav_2d_utils/include/nav_2d_utils/odom_subscriber.hpp +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Software License Agreement (BSD License) - * - * Copyright (c) 2017, Locus Robotics - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * * Neither the name of the copyright holder nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -#ifndef NAV_2D_UTILS__ODOM_SUBSCRIBER_HPP_ -#define NAV_2D_UTILS__ODOM_SUBSCRIBER_HPP_ - -#include -#include -#include -#include -#include "nav_2d_msgs/msg/twist2_d_stamped.hpp" -#include "nav_msgs/msg/odometry.hpp" -#include "nav2_ros_common/lifecycle_node.hpp" -#include "rclcpp/rclcpp.hpp" -#include "nav2_ros_common/node_utils.hpp" - -namespace nav_2d_utils -{ - -/** - * @class OdomSubscriber - * Wrapper for some common odometry operations. Subscribes to the topic with a mutex. - */ -class OdomSubscriber -{ -public: - /** - * @brief Constructor that subscribes to an Odometry topic - * - * @param nh NodeHandle for creating subscriber - * @param default_topic Name of the topic that will be loaded of the odom_topic param is not set. - */ - explicit OdomSubscriber( - nav2::LifecycleNode::SharedPtr nh, - std::string default_topic = "odom") - { - nav2::declare_parameter_if_not_declared( - nh, "odom_topic", rclcpp::ParameterValue(default_topic)); - - std::string odom_topic; - nh->get_parameter_or("odom_topic", odom_topic, default_topic); - odom_sub_ = - nh->create_subscription( - odom_topic, - std::bind(&OdomSubscriber::odomCallback, this, std::placeholders::_1)); - } - - inline nav_2d_msgs::msg::Twist2D getTwist() {return odom_vel_.velocity;} - inline nav_2d_msgs::msg::Twist2DStamped getTwistStamped() {return odom_vel_;} - -protected: - void odomCallback(const nav_msgs::msg::Odometry::SharedPtr msg) - { - // ROS_INFO_ONCE("odom received!"); - std::lock_guard lock(odom_mutex_); - odom_vel_.header = msg->header; - odom_vel_.velocity.x = msg->twist.twist.linear.x; - odom_vel_.velocity.y = msg->twist.twist.linear.y; - odom_vel_.velocity.theta = msg->twist.twist.angular.z; - } - - nav2::Subscription::SharedPtr odom_sub_; - nav_2d_msgs::msg::Twist2DStamped odom_vel_; - std::mutex odom_mutex_; -}; - -} // namespace nav_2d_utils - -#endif // NAV_2D_UTILS__ODOM_SUBSCRIBER_HPP_ diff --git a/nav2_dwb_controller/nav_2d_utils/include/nav_2d_utils/parameters.hpp b/nav2_dwb_controller/nav_2d_utils/include/nav_2d_utils/parameters.hpp deleted file mode 100644 index 09b039487e0..00000000000 --- a/nav2_dwb_controller/nav_2d_utils/include/nav_2d_utils/parameters.hpp +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Software License Agreement (BSD License) - * - * Copyright (c) 2017, Locus Robotics - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * * Neither the name of the copyright holder nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -#ifndef NAV_2D_UTILS__PARAMETERS_HPP_ -#define NAV_2D_UTILS__PARAMETERS_HPP_ - -#include -#include - -#include "rclcpp/rclcpp.hpp" -#include "nav2_ros_common/lifecycle_node.hpp" -#include "nav2_ros_common/node_utils.hpp" - -// TODO(crdelsey): Remove when code is re-enabled -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" -namespace nav_2d_utils -{ - -/** - * @brief Search for a parameter and load it, or use the default value - * - * This templated function shortens a commonly used ROS pattern in which you - * search for a parameter and get its value if it exists, otherwise returning a default value. - * - * @param nh NodeHandle to start the parameter search from - * @param param_name Name of the parameter to search for - * @param default_value Value to return if not found - * @return Value of parameter if found, otherwise the default_value - */ -template -param_t searchAndGetParam( - const nav2::LifecycleNode::SharedPtr & nh, const std::string & param_name, - const param_t & default_value) -{ - nav2::declare_parameter_if_not_declared( - nh, param_name, - rclcpp::ParameterValue(default_value)); - return nh->get_parameter(param_name).get_value(); -} - -} // namespace nav_2d_utils -#pragma GCC diagnostic pop - -#endif // NAV_2D_UTILS__PARAMETERS_HPP_ diff --git a/nav2_dwb_controller/nav_2d_utils/include/nav_2d_utils/tf_help.hpp b/nav2_dwb_controller/nav_2d_utils/include/nav_2d_utils/tf_help.hpp deleted file mode 100644 index 3f5fadcd4aa..00000000000 --- a/nav2_dwb_controller/nav_2d_utils/include/nav_2d_utils/tf_help.hpp +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Software License Agreement (BSD License) - * - * Copyright (c) 2017, Locus Robotics - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * * Neither the name of the copyright holder nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -#ifndef NAV_2D_UTILS__TF_HELP_HPP_ -#define NAV_2D_UTILS__TF_HELP_HPP_ - -#include -#include -#include "tf2_ros/buffer.hpp" -#include "nav_2d_utils/conversions.hpp" -#include "geometry_msgs/msg/pose_stamped.hpp" -#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" - -namespace nav_2d_utils -{ -/** - * @brief Transform a PoseStamped from one frame to another while catching exceptions - * - * Also returns immediately if the frames are equal. - * @param tf Smart pointer to TFListener - * @param frame Frame to transform the pose into - * @param in_pose Pose to transform - * @param out_pose Place to store the resulting transformed pose - * @return True if successful transform - */ -bool transformPose( - const std::shared_ptr tf, - const std::string frame, - const geometry_msgs::msg::PoseStamped & in_pose, - geometry_msgs::msg::PoseStamped & out_pose, - rclcpp::Duration & transform_tolerance -); - -/** - * @brief Transform a PoseStamped from one frame to another while catching exceptions - * - * Also returns immediately if the frames are equal. - * @param tf Smart pointer to TFListener - * @param frame Frame to transform the pose into - * @param in_pose Pose to transform - * @param out_pose Place to store the resulting transformed pose - * @return True if successful transform - */ -bool transformPose( - const std::shared_ptr tf, - const std::string frame, - const geometry_msgs::msg::PoseStamped & in_pose, - geometry_msgs::msg::PoseStamped & out_pose, - rclcpp::Duration & transform_tolerance -); - -} // namespace nav_2d_utils - -#endif // NAV_2D_UTILS__TF_HELP_HPP_ diff --git a/nav2_dwb_controller/nav_2d_utils/src/conversions.cpp b/nav2_dwb_controller/nav_2d_utils/src/conversions.cpp index 4ec9a73fb99..3629e8bb989 100644 --- a/nav2_dwb_controller/nav_2d_utils/src/conversions.cpp +++ b/nav2_dwb_controller/nav_2d_utils/src/conversions.cpp @@ -72,34 +72,6 @@ nav_2d_msgs::msg::Twist2D twist3Dto2D(const geometry_msgs::msg::Twist & cmd_vel) return cmd_vel_2d; } - -geometry_msgs::msg::PoseStamped poseToPoseStamped( - const geometry_msgs::msg::Pose & pose_in, - const std::string & frame, const rclcpp::Time & stamp) -{ - geometry_msgs::msg::PoseStamped pose; - pose.header.frame_id = frame; - pose.header.stamp = stamp; - pose.pose = pose_in; - return pose; -} - -nav_msgs::msg::Path posesToPath(const std::vector & poses) -{ - nav_msgs::msg::Path path; - if (poses.empty()) { - return path; - } - path.poses.resize(poses.size()); - path.header.frame_id = poses[0].header.frame_id; - path.header.stamp = poses[0].header.stamp; - for (unsigned int i = 0; i < poses.size(); i++) { - path.poses[i] = poses[i]; - } - return path; -} - - nav_msgs::msg::Path posesToPath( const std::vector & poses, const std::string & frame, const rclcpp::Time & stamp) diff --git a/nav2_dwb_controller/nav_2d_utils/src/tf_help.cpp b/nav2_dwb_controller/nav_2d_utils/src/tf_help.cpp deleted file mode 100644 index 0916cc6a402..00000000000 --- a/nav2_dwb_controller/nav_2d_utils/src/tf_help.cpp +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Software License Agreement (BSD License) - * - * Copyright (c) 2017, Locus Robotics - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * * Neither the name of the copyright holder nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -#include -#include -#include "nav_2d_utils/tf_help.hpp" - -namespace nav_2d_utils -{ - -bool transformPose( - const std::shared_ptr tf, - const std::string frame, - const geometry_msgs::msg::PoseStamped & in_pose, - geometry_msgs::msg::PoseStamped & out_pose, - rclcpp::Duration & transform_tolerance -) -{ - if (in_pose.header.frame_id == frame) { - out_pose = in_pose; - return true; - } - - try { - tf->transform(in_pose, out_pose, frame); - return true; - } catch (tf2::ExtrapolationException & ex) { - auto transform = tf->lookupTransform( - frame, - in_pose.header.frame_id, - tf2::TimePointZero - ); - if ( - (rclcpp::Time(in_pose.header.stamp) - rclcpp::Time(transform.header.stamp)) > - transform_tolerance) - { - RCLCPP_ERROR( - rclcpp::get_logger("tf_help"), - "Transform data too old when converting from %s to %s", - in_pose.header.frame_id.c_str(), - frame.c_str() - ); - RCLCPP_ERROR( - rclcpp::get_logger("tf_help"), - "Data time: %ds %uns, Transform time: %ds %uns", - in_pose.header.stamp.sec, - in_pose.header.stamp.nanosec, - transform.header.stamp.sec, - transform.header.stamp.nanosec - ); - return false; - } else { - tf2::doTransform(in_pose, out_pose, transform); - return true; - } - } catch (tf2::TransformException & ex) { - RCLCPP_ERROR( - rclcpp::get_logger("tf_help"), - "Exception in transformPose: %s", - ex.what() - ); - return false; - } - return false; -} - -} // namespace nav_2d_utils diff --git a/nav2_dwb_controller/nav_2d_utils/test/2d_utils_test.cpp b/nav2_dwb_controller/nav_2d_utils/test/2d_utils_test.cpp index c7f5c502f76..fd7d209d3d7 100644 --- a/nav2_dwb_controller/nav_2d_utils/test/2d_utils_test.cpp +++ b/nav2_dwb_controller/nav_2d_utils/test/2d_utils_test.cpp @@ -48,59 +48,55 @@ using nav_2d_utils::posesToPath; TEST(nav_2d_utils, PosesToPathEmpty) { - std::vector poses; - nav_msgs::msg::Path path = posesToPath(poses); + std::vector poses; + auto node = std::make_shared("twod_utils_test_node"); + rclcpp::Time time = node->now(); + std::string frame = "test_frame"; + + nav_msgs::msg::Path path = posesToPath(poses, frame, time); EXPECT_EQ(path.poses.size(), 0ul); + EXPECT_EQ(path.header.frame_id, frame); + EXPECT_EQ(path.header.stamp, time); } TEST(nav_2d_utils, PosesToPathNonEmpty) { - std::vector poses; - geometry_msgs::msg::PoseStamped pose1; - rclcpp::Time time1, time2; auto node = std::make_shared("twod_utils_test_node"); - time1 = node->now(); + rclcpp::Time time = node->now(); + std::string frame = "map"; tf2::Quaternion quat1, quat2; quat1.setRPY(0, 0, 0.123); - pose1.pose.position.x = 1.0; - pose1.pose.position.y = 2.0; - pose1.pose.orientation.w = quat1.w(); - pose1.pose.orientation.x = quat1.x(); - pose1.pose.orientation.y = quat1.y(); - pose1.pose.orientation.z = quat1.z(); - pose1.header.stamp = time1; - pose1.header.frame_id = "frame1_id"; - - geometry_msgs::msg::PoseStamped pose2; - pose2.pose.position.x = 4.0; - pose2.pose.position.y = 5.0; quat2.setRPY(0, 0, 0.987); - pose2.pose.orientation.w = quat2.w(); - pose2.pose.orientation.x = quat2.x(); - pose2.pose.orientation.y = quat2.y(); - pose2.pose.orientation.z = quat2.z(); - time2 = node->now(); - pose2.header.stamp = time2; - pose2.header.frame_id = "frame2_id"; + geometry_msgs::msg::Pose pose1; + pose1.position.x = 1.0; + pose1.position.y = 2.0; + pose1.orientation = tf2::toMsg(quat1); - poses.push_back(pose1); - poses.push_back(pose2); + geometry_msgs::msg::Pose pose2; + pose2.position.x = 4.0; + pose2.position.y = 5.0; + pose2.orientation = tf2::toMsg(quat2); - nav_msgs::msg::Path path = posesToPath(poses); + std::vector poses = {pose1, pose2}; + + nav_msgs::msg::Path path = posesToPath(poses, frame, time); EXPECT_EQ(path.poses.size(), 2ul); EXPECT_EQ(path.poses[0].pose.position.x, 1.0); EXPECT_EQ(path.poses[0].pose.position.y, 2.0); - EXPECT_EQ(path.poses[0].header.stamp, time1); - EXPECT_EQ(path.poses[0].header.frame_id, "frame1_id"); EXPECT_EQ(path.poses[1].pose.position.x, 4.0); EXPECT_EQ(path.poses[1].pose.position.y, 5.0); - EXPECT_EQ(path.poses[1].header.frame_id, "frame2_id"); - EXPECT_EQ(path.header.stamp, time1); + for (const auto & stamped_pose : path.poses) { + EXPECT_EQ(stamped_pose.header.frame_id, frame); + EXPECT_EQ(stamped_pose.header.stamp, time); + } + + EXPECT_EQ(path.header.frame_id, frame); + EXPECT_EQ(path.header.stamp, time); } diff --git a/nav2_dwb_controller/nav_2d_utils/test/CMakeLists.txt b/nav2_dwb_controller/nav_2d_utils/test/CMakeLists.txt index f0cf339a322..fd861291fca 100644 --- a/nav2_dwb_controller/nav_2d_utils/test/CMakeLists.txt +++ b/nav2_dwb_controller/nav_2d_utils/test/CMakeLists.txt @@ -11,6 +11,3 @@ target_include_directories(2d_utils_tests ament_add_gtest(path_ops_tests path_ops_test.cpp) target_link_libraries(path_ops_tests path_ops) - -ament_add_gtest(tf_help_tests tf_help_test.cpp) -target_link_libraries(tf_help_tests tf_help conversions) diff --git a/nav2_dwb_controller/nav_2d_utils/test/tf_help_test.cpp b/nav2_dwb_controller/nav_2d_utils/test/tf_help_test.cpp deleted file mode 100644 index 4095567473a..00000000000 --- a/nav2_dwb_controller/nav_2d_utils/test/tf_help_test.cpp +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Software License Agreement (BSD License) - * - * Copyright (c) 2018, Wilco Bonestroo - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * * Neither the name of the copyright holder nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -#include -#include - -#include "gtest/gtest.h" -#include "nav_2d_utils/tf_help.hpp" - -TEST(TF_Help, TransformToSelf) { - bool result; - - std::shared_ptr tf; - std::string frame = "frame_id"; - geometry_msgs::msg::PoseStamped in_pose; - in_pose.header.frame_id = "frame_id"; - in_pose.pose.position.x = 1.0; - in_pose.pose.position.y = 2.0; - in_pose.pose.position.z = 3.0; - tf2::Quaternion qt; - qt.setRPY(0.5, 1.0, 1.5); - in_pose.pose.orientation.w = qt.w(); - in_pose.pose.orientation.x = qt.x(); - in_pose.pose.orientation.y = qt.y(); - in_pose.pose.orientation.z = qt.z(); - - geometry_msgs::msg::PoseStamped out_pose; - rclcpp::Duration transform_tolerance(0, 500); - - result = nav_2d_utils::transformPose(tf, frame, in_pose, out_pose, transform_tolerance); - - EXPECT_TRUE(result); - EXPECT_EQ(out_pose.header.frame_id, "frame_id"); - EXPECT_EQ(out_pose.pose.position.x, 1.0); - EXPECT_EQ(out_pose.pose.position.y, 2.0); - EXPECT_EQ(out_pose.pose.position.z, 3.0); - EXPECT_EQ(out_pose.pose.orientation.w, qt.w()); - EXPECT_EQ(out_pose.pose.orientation.x, qt.x()); - EXPECT_EQ(out_pose.pose.orientation.y, qt.y()); - EXPECT_EQ(out_pose.pose.orientation.z, qt.z()); -} - -TEST(TF_Help, EmptyBuffer) { - auto clock = std::make_shared(RCL_ROS_TIME); - auto buffer = std::make_shared(clock); - - std::string frame = "frame_id"; - geometry_msgs::msg::PoseStamped in_pose; - in_pose.header.frame_id = "other_frame_id"; - in_pose.pose.position.x = 1.0; - in_pose.pose.position.y = 2.0; - in_pose.pose.position.z = 3.0; - tf2::Quaternion qt; - qt.setRPY(0.5, 1.0, 1.5); - in_pose.pose.orientation.w = qt.w(); - in_pose.pose.orientation.x = qt.x(); - in_pose.pose.orientation.y = qt.y(); - in_pose.pose.orientation.z = qt.z(); - - geometry_msgs::msg::PoseStamped out_pose; - rclcpp::Duration transform_tolerance(0, 500); - - bool result; - result = nav_2d_utils::transformPose(buffer, frame, in_pose, out_pose, transform_tolerance); - - EXPECT_FALSE(result); -} diff --git a/nav2_graceful_controller/CMakeLists.txt b/nav2_graceful_controller/CMakeLists.txt index fc8859860b3..d9720ca6401 100644 --- a/nav2_graceful_controller/CMakeLists.txt +++ b/nav2_graceful_controller/CMakeLists.txt @@ -9,7 +9,6 @@ find_package(nav2_common REQUIRED) find_package(nav2_core REQUIRED) find_package(nav2_costmap_2d REQUIRED) find_package(nav2_util REQUIRED) -find_package(nav_2d_utils REQUIRED) find_package(nav_msgs REQUIRED) find_package(pluginlib REQUIRED) find_package(rclcpp REQUIRED) @@ -58,7 +57,6 @@ target_link_libraries(${library_name} PUBLIC nav2_ros_common::nav2_ros_common nav2_util::nav2_util_core nav2_costmap_2d::nav2_costmap_2d_core - nav_2d_utils::tf_help ${nav_msgs_TARGETS} pluginlib::pluginlib rclcpp::rclcpp @@ -104,7 +102,6 @@ ament_export_dependencies( nav2_core nav2_util nav2_costmap_2d - nav_2d_utils nav_msgs pluginlib rclcpp diff --git a/nav2_graceful_controller/include/nav2_graceful_controller/path_handler.hpp b/nav2_graceful_controller/include/nav2_graceful_controller/path_handler.hpp index aaa64f1bb62..a24a84c2228 100644 --- a/nav2_graceful_controller/include/nav2_graceful_controller/path_handler.hpp +++ b/nav2_graceful_controller/include/nav2_graceful_controller/path_handler.hpp @@ -35,7 +35,7 @@ class PathHandler * @brief Constructor for nav2_graceful_controller::PathHandler */ PathHandler( - tf2::Duration transform_tolerance, + double transform_tolerance, std::shared_ptr tf, std::shared_ptr costmap_ros); @@ -71,7 +71,7 @@ class PathHandler nav_msgs::msg::Path getPlan() {return global_plan_;} protected: - rclcpp::Duration transform_tolerance_{0, 0}; + double transform_tolerance_{0}; std::shared_ptr tf_buffer_; std::shared_ptr costmap_ros_; nav_msgs::msg::Path global_plan_; diff --git a/nav2_graceful_controller/package.xml b/nav2_graceful_controller/package.xml index f162f3f0ed3..98b6e1e257a 100644 --- a/nav2_graceful_controller/package.xml +++ b/nav2_graceful_controller/package.xml @@ -16,7 +16,6 @@ nav2_core nav2_costmap_2d nav2_util - nav_2d_utils nav_msgs pluginlib rclcpp diff --git a/nav2_graceful_controller/src/graceful_controller.cpp b/nav2_graceful_controller/src/graceful_controller.cpp index fb0010698f4..0706c86378d 100644 --- a/nav2_graceful_controller/src/graceful_controller.cpp +++ b/nav2_graceful_controller/src/graceful_controller.cpp @@ -48,7 +48,7 @@ void GracefulController::configure( // Handles global path transformations path_handler_ = std::make_unique( - tf2::durationFromSec(params_->transform_tolerance), tf_buffer_, costmap_ros_); + params_->transform_tolerance, tf_buffer_, costmap_ros_); // Handles the control law to generate the velocity commands control_law_ = std::make_unique( diff --git a/nav2_graceful_controller/src/path_handler.cpp b/nav2_graceful_controller/src/path_handler.cpp index 9dbfc291f8d..b60b4a874f0 100644 --- a/nav2_graceful_controller/src/path_handler.cpp +++ b/nav2_graceful_controller/src/path_handler.cpp @@ -22,7 +22,7 @@ #include "nav2_core/controller_exceptions.hpp" #include "nav2_ros_common/node_utils.hpp" #include "nav2_util/geometry_utils.hpp" -#include "nav_2d_utils/tf_help.hpp" +#include "nav2_util/robot_utils.hpp" #include "nav2_graceful_controller/path_handler.hpp" namespace nav2_graceful_controller @@ -31,7 +31,7 @@ namespace nav2_graceful_controller using nav2_util::geometry_utils::euclidean_distance; PathHandler::PathHandler( - tf2::Duration transform_tolerance, + double transform_tolerance, std::shared_ptr tf, std::shared_ptr costmap_ros) : transform_tolerance_(transform_tolerance), tf_buffer_(tf), costmap_ros_(costmap_ros) @@ -49,8 +49,8 @@ nav_msgs::msg::Path PathHandler::transformGlobalPlan( // Let's get the pose of the robot in the frame of the plan geometry_msgs::msg::PoseStamped robot_pose; - if (!nav_2d_utils::transformPose( - tf_buffer_, global_plan_.header.frame_id, pose, robot_pose, + if (!nav2_util::transformPoseInTargetFrame( + pose, robot_pose, *tf_buffer_, global_plan_.header.frame_id, transform_tolerance_)) { throw nav2_core::ControllerTFError("Unable to transform robot pose into global plan's frame"); @@ -88,9 +88,9 @@ nav_msgs::msg::Path PathHandler::transformGlobalPlan( stamped_pose.header.frame_id = global_plan_.header.frame_id; stamped_pose.header.stamp = robot_pose.header.stamp; stamped_pose.pose = global_plan_pose.pose; - if (!nav_2d_utils::transformPose( - tf_buffer_, costmap_ros_->getBaseFrameID(), stamped_pose, - transformed_pose, transform_tolerance_)) + if (!nav2_util::transformPoseInTargetFrame( + stamped_pose, transformed_pose, *tf_buffer_, + costmap_ros_->getBaseFrameID(), transform_tolerance_)) { throw nav2_core::ControllerTFError("Unable to transform plan pose into local frame"); } diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler.hpp index 544a98467d9..4233c91ef28 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler.hpp @@ -98,17 +98,6 @@ class PathHandler geometry_msgs::msg::PoseStamped getTransformedGoal(const builtin_interfaces::msg::Time & stamp); protected: - /** - * @brief Transform a pose to another frame - * @param frame Frame to transform to - * @param in_pose Input pose - * @param out_pose Output pose - * @return Bool if successful - */ - bool transformPose( - const std::string & frame, const geometry_msgs::msg::PoseStamped & in_pose, - geometry_msgs::msg::PoseStamped & out_pose) const; - /** * @brief Get largest dimension of costmap (radially) * @return Max distance from center of costmap to edge diff --git a/nav2_mppi_controller/src/path_handler.cpp b/nav2_mppi_controller/src/path_handler.cpp index 93593982c5c..55931f6bc62 100644 --- a/nav2_mppi_controller/src/path_handler.cpp +++ b/nav2_mppi_controller/src/path_handler.cpp @@ -17,6 +17,7 @@ #include "nav2_mppi_controller/tools/path_handler.hpp" #include "nav2_mppi_controller/tools/utils.hpp" #include "nav2_costmap_2d/costmap_2d_ros.hpp" +#include "nav2_util/robot_utils.hpp" namespace mppi { @@ -85,7 +86,8 @@ PathHandler::getGlobalPlanConsideringBoundsInCostmapFrame( geometry_msgs::msg::PoseStamped costmap_plan_pose; global_plan_pose->header.stamp = global_pose.header.stamp; global_plan_pose->header.frame_id = global_plan_.header.frame_id; - transformPose(costmap_->getGlobalFrameID(), *global_plan_pose, costmap_plan_pose); + nav2_util::transformPoseInTargetFrame(*global_plan_pose, costmap_plan_pose, *tf_buffer_, + costmap_->getGlobalFrameID(), transform_tolerance_); // Check if pose is inside the costmap if (!costmap_->getCostmap()->worldToMap( @@ -109,7 +111,9 @@ geometry_msgs::msg::PoseStamped PathHandler::transformToGlobalPlanFrame( } geometry_msgs::msg::PoseStamped robot_pose; - if (!transformPose(global_plan_up_to_inversion_.header.frame_id, pose, robot_pose)) { + if (!nav2_util::transformPoseInTargetFrame(pose, robot_pose, *tf_buffer_, + global_plan_up_to_inversion_.header.frame_id, transform_tolerance_)) + { throw nav2_core::ControllerTFError( "Unable to transform robot pose into global plan's frame"); } @@ -142,27 +146,6 @@ nav_msgs::msg::Path PathHandler::transformPath( return transformed_plan; } -bool PathHandler::transformPose( - const std::string & frame, const geometry_msgs::msg::PoseStamped & in_pose, - geometry_msgs::msg::PoseStamped & out_pose) const -{ - if (in_pose.header.frame_id == frame) { - out_pose = in_pose; - return true; - } - - try { - tf_buffer_->transform( - in_pose, out_pose, frame, - tf2::durationFromSec(transform_tolerance_)); - out_pose.header.frame_id = frame; - return true; - } catch (tf2::TransformException & ex) { - RCLCPP_ERROR(logger_, "Exception in transformPose: %s", ex.what()); - } - return false; -} - double PathHandler::getMaxCostmapDist() { const auto & costmap = costmap_->getCostmap(); @@ -196,7 +179,9 @@ geometry_msgs::msg::PoseStamped PathHandler::getTransformedGoal( throw nav2_core::ControllerTFError("Goal pose has an empty frame_id"); } geometry_msgs::msg::PoseStamped transformed_goal; - if (!transformPose(costmap_->getGlobalFrameID(), goal, transformed_goal)) { + if (!nav2_util::transformPoseInTargetFrame(goal, transformed_goal, *tf_buffer_, + costmap_->getGlobalFrameID(), transform_tolerance_)) + { throw nav2_core::ControllerTFError("Unable to transform goal pose into costmap frame"); } return transformed_goal; diff --git a/nav2_mppi_controller/test/path_handler_test.cpp b/nav2_mppi_controller/test/path_handler_test.cpp index eb0bd140c43..e0f17e48411 100644 --- a/nav2_mppi_controller/test/path_handler_test.cpp +++ b/nav2_mppi_controller/test/path_handler_test.cpp @@ -46,13 +46,6 @@ class PathHandlerWrapper : public PathHandler 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) { @@ -180,12 +173,10 @@ TEST(PathHandlerTests, TestTransforms) path.poses[i].header.frame_id = "map"; } - geometry_msgs::msg::PoseStamped robot_pose, output_pose; + geometry_msgs::msg::PoseStamped robot_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); diff --git a/nav2_regulated_pure_pursuit_controller/include/nav2_regulated_pure_pursuit_controller/path_handler.hpp b/nav2_regulated_pure_pursuit_controller/include/nav2_regulated_pure_pursuit_controller/path_handler.hpp index cc8b7915330..135a304dc26 100644 --- a/nav2_regulated_pure_pursuit_controller/include/nav2_regulated_pure_pursuit_controller/path_handler.hpp +++ b/nav2_regulated_pure_pursuit_controller/include/nav2_regulated_pure_pursuit_controller/path_handler.hpp @@ -43,7 +43,7 @@ class PathHandler * @brief Constructor for nav2_regulated_pure_pursuit_controller::PathHandler */ PathHandler( - tf2::Duration transform_tolerance, + double transform_tolerance, std::shared_ptr tf, std::shared_ptr costmap_ros); @@ -65,18 +65,6 @@ class PathHandler const geometry_msgs::msg::PoseStamped & pose, double max_robot_pose_search_dist, bool reject_unit_path = false); - /** - * @brief Transform a pose to another frame. - * @param frame Frame ID to transform to - * @param in_pose Pose input to transform - * @param out_pose transformed output - * @return bool if successful - */ - bool transformPose( - const std::string frame, - const geometry_msgs::msg::PoseStamped & in_pose, - geometry_msgs::msg::PoseStamped & out_pose) const; - void setPlan(const nav_msgs::msg::Path & path) {global_plan_ = path;} nav_msgs::msg::Path getPlan() {return global_plan_;} @@ -89,7 +77,7 @@ class PathHandler double getCostmapMaxExtent() const; rclcpp::Logger logger_ {rclcpp::get_logger("RPPPathHandler")}; - tf2::Duration transform_tolerance_; + double transform_tolerance_; std::shared_ptr tf_; std::shared_ptr costmap_ros_; nav_msgs::msg::Path global_plan_; diff --git a/nav2_regulated_pure_pursuit_controller/src/path_handler.cpp b/nav2_regulated_pure_pursuit_controller/src/path_handler.cpp index 1fcc39b7e37..c91bb8dc8f7 100644 --- a/nav2_regulated_pure_pursuit_controller/src/path_handler.cpp +++ b/nav2_regulated_pure_pursuit_controller/src/path_handler.cpp @@ -23,6 +23,7 @@ #include "nav2_core/controller_exceptions.hpp" #include "nav2_ros_common/node_utils.hpp" #include "nav2_util/geometry_utils.hpp" +#include "nav2_util/robot_utils.hpp" namespace nav2_regulated_pure_pursuit_controller { @@ -30,7 +31,7 @@ namespace nav2_regulated_pure_pursuit_controller using nav2_util::geometry_utils::euclidean_distance; PathHandler::PathHandler( - tf2::Duration transform_tolerance, + double transform_tolerance, std::shared_ptr tf, std::shared_ptr costmap_ros) : transform_tolerance_(transform_tolerance), tf_(tf), costmap_ros_(costmap_ros) @@ -60,7 +61,9 @@ nav_msgs::msg::Path PathHandler::transformGlobalPlan( // let's get the pose of the robot in the frame of the plan geometry_msgs::msg::PoseStamped robot_pose; - if (!transformPose(global_plan_.header.frame_id, pose, robot_pose)) { + if (!nav2_util::transformPoseInTargetFrame(pose, robot_pose, *tf_, global_plan_.header.frame_id, + transform_tolerance_)) + { throw nav2_core::ControllerTFError("Unable to transform robot pose into global plan's frame"); } @@ -101,7 +104,9 @@ nav_msgs::msg::Path PathHandler::transformGlobalPlan( stamped_pose.header.frame_id = global_plan_.header.frame_id; stamped_pose.header.stamp = robot_pose.header.stamp; stamped_pose.pose = global_plan_pose.pose; - if (!transformPose(costmap_ros_->getBaseFrameID(), stamped_pose, transformed_pose)) { + if (!nav2_util::transformPoseInTargetFrame(stamped_pose, transformed_pose, *tf_, + costmap_ros_->getBaseFrameID(), transform_tolerance_)) + { throw nav2_core::ControllerTFError("Unable to transform plan pose into local frame"); } transformed_pose.pose.position.z = 0.0; @@ -128,24 +133,4 @@ nav_msgs::msg::Path PathHandler::transformGlobalPlan( return transformed_plan; } -bool PathHandler::transformPose( - const std::string frame, - const geometry_msgs::msg::PoseStamped & in_pose, - geometry_msgs::msg::PoseStamped & out_pose) const -{ - if (in_pose.header.frame_id == frame) { - out_pose = in_pose; - return true; - } - - try { - tf_->transform(in_pose, out_pose, frame, transform_tolerance_); - out_pose.header.frame_id = frame; - return true; - } catch (tf2::TransformException & ex) { - RCLCPP_ERROR(logger_, "Exception in transformPose: %s", ex.what()); - } - return false; -} - } // namespace nav2_regulated_pure_pursuit_controller diff --git a/nav2_regulated_pure_pursuit_controller/src/regulated_pure_pursuit_controller.cpp b/nav2_regulated_pure_pursuit_controller/src/regulated_pure_pursuit_controller.cpp index d8153ea16bd..1bd12ae3a06 100644 --- a/nav2_regulated_pure_pursuit_controller/src/regulated_pure_pursuit_controller.cpp +++ b/nav2_regulated_pure_pursuit_controller/src/regulated_pure_pursuit_controller.cpp @@ -61,7 +61,7 @@ void RegulatedPurePursuitController::configure( // Handles global path transformations path_handler_ = std::make_unique( - tf2::durationFromSec(params_->transform_tolerance), tf_, costmap_ros_); + params_->transform_tolerance, tf_, costmap_ros_); // Checks for imminent collisions collision_checker_ = std::make_unique(node, costmap_ros_, params_); diff --git a/nav2_smoother/CMakeLists.txt b/nav2_smoother/CMakeLists.txt index 465cadd670d..5831d10189c 100644 --- a/nav2_smoother/CMakeLists.txt +++ b/nav2_smoother/CMakeLists.txt @@ -9,7 +9,6 @@ find_package(nav2_core REQUIRED) find_package(nav2_costmap_2d REQUIRED) find_package(nav2_msgs REQUIRED) find_package(nav2_util REQUIRED) -find_package(nav_2d_utils REQUIRED) find_package(pluginlib REQUIRED) find_package(rclcpp REQUIRED) find_package(rclcpp_components REQUIRED) @@ -45,8 +44,6 @@ target_link_libraries(${library_name} PUBLIC nav2_ros_common::nav2_ros_common ) target_link_libraries(${library_name} PRIVATE - nav_2d_utils::conversions - nav_2d_utils::tf_help rclcpp_components::component tf2::tf2 ) diff --git a/nav2_smoother/package.xml b/nav2_smoother/package.xml index c2983d916ce..339bdeb5586 100644 --- a/nav2_smoother/package.xml +++ b/nav2_smoother/package.xml @@ -17,7 +17,6 @@ nav2_costmap_2d nav2_msgs nav2_util - nav_2d_utils pluginlib rclcpp rclcpp_components diff --git a/nav2_smoother/src/nav2_smoother.cpp b/nav2_smoother/src/nav2_smoother.cpp index eb852c1eb52..66e3fd926ff 100644 --- a/nav2_smoother/src/nav2_smoother.cpp +++ b/nav2_smoother/src/nav2_smoother.cpp @@ -23,8 +23,6 @@ #include "nav2_core/smoother_exceptions.hpp" #include "nav2_smoother/nav2_smoother.hpp" #include "nav2_ros_common/node_utils.hpp" -#include "nav_2d_utils/conversions.hpp" -#include "nav_2d_utils/tf_help.hpp" #include "tf2_ros/create_timer_ros.hpp" using namespace std::chrono_literals; diff --git a/nav2_util/src/robot_utils.cpp b/nav2_util/src/robot_utils.cpp index 912d73d0859..d333df9cf14 100644 --- a/nav2_util/src/robot_utils.cpp +++ b/nav2_util/src/robot_utils.cpp @@ -50,6 +50,11 @@ bool transformPoseInTargetFrame( { static rclcpp::Logger logger = rclcpp::get_logger("transformPoseInTargetFrame"); + if(input_pose.header.frame_id == target_frame) { + transformed_pose = input_pose; + return true; + } + try { transformed_pose = tf_buffer.transform( input_pose, target_frame, From 1468484f1f96fe0f06241819315c8371311d0f52 Mon Sep 17 00:00:00 2001 From: Patrick Roncagliolo Date: Thu, 7 Aug 2025 18:10:59 +0200 Subject: [PATCH 12/51] Construct TF listeners passing nodes, spinning on separate thread (#5406) * Construct TF listeners passing nodes, spinning on separate thread Signed-off-by: Patrick Roncagliolo * (tentative) pin down of the impacting change Signed-off-by: Patrick Roncagliolo --------- Signed-off-by: Patrick Roncagliolo --- nav2_amcl/src/amcl_node.cpp | 2 +- nav2_behaviors/src/behavior_server.cpp | 2 +- nav2_bt_navigator/src/bt_navigator.cpp | 2 +- nav2_collision_monitor/src/collision_detector_node.cpp | 2 +- nav2_collision_monitor/src/collision_monitor_node.cpp | 2 +- nav2_docking/opennav_docking/src/docking_server.cpp | 2 +- nav2_route/src/route_server.cpp | 2 +- nav2_smoother/src/nav2_smoother.cpp | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/nav2_amcl/src/amcl_node.cpp b/nav2_amcl/src/amcl_node.cpp index 93b8f599a8d..b3b21631a59 100644 --- a/nav2_amcl/src/amcl_node.cpp +++ b/nav2_amcl/src/amcl_node.cpp @@ -1339,7 +1339,7 @@ AmclNode::initTransforms() get_node_timers_interface(), callback_group_); tf_buffer_->setCreateTimerInterface(timer_interface); - tf_listener_ = std::make_shared(*tf_buffer_); + tf_listener_ = std::make_shared(*tf_buffer_, this, true); tf_broadcaster_ = std::make_shared(shared_from_this()); sent_first_transform_ = false; diff --git a/nav2_behaviors/src/behavior_server.cpp b/nav2_behaviors/src/behavior_server.cpp index 54ace7bac0a..0f4db4b3bf6 100644 --- a/nav2_behaviors/src/behavior_server.cpp +++ b/nav2_behaviors/src/behavior_server.cpp @@ -87,7 +87,7 @@ BehaviorServer::on_configure(const rclcpp_lifecycle::State & state) get_node_base_interface(), get_node_timers_interface()); tf_->setCreateTimerInterface(timer_interface); - transform_listener_ = std::make_shared(*tf_); + transform_listener_ = std::make_shared(*tf_, this, true); behavior_types_.resize(behavior_ids_.size()); if (!loadBehaviorPlugins()) { diff --git a/nav2_bt_navigator/src/bt_navigator.cpp b/nav2_bt_navigator/src/bt_navigator.cpp index 5f99c1452db..95d3c7dcaa7 100644 --- a/nav2_bt_navigator/src/bt_navigator.cpp +++ b/nav2_bt_navigator/src/bt_navigator.cpp @@ -54,7 +54,7 @@ BtNavigator::on_configure(const rclcpp_lifecycle::State & state) get_node_base_interface(), get_node_timers_interface()); tf_->setCreateTimerInterface(timer_interface); tf_->setUsingDedicatedThread(true); - tf_listener_ = std::make_shared(*tf_, this, false); + tf_listener_ = std::make_shared(*tf_, this, true); global_frame_ = this->declare_or_get_parameter("global_frame", std::string("map")); robot_frame_ = this->declare_or_get_parameter("robot_base_frame", std::string("base_link")); diff --git a/nav2_collision_monitor/src/collision_detector_node.cpp b/nav2_collision_monitor/src/collision_detector_node.cpp index a45d35dc3cd..91023afae4f 100644 --- a/nav2_collision_monitor/src/collision_detector_node.cpp +++ b/nav2_collision_monitor/src/collision_detector_node.cpp @@ -50,7 +50,7 @@ CollisionDetector::on_configure(const rclcpp_lifecycle::State & state) this->get_node_base_interface(), this->get_node_timers_interface()); tf_buffer_->setCreateTimerInterface(timer_interface); - tf_listener_ = std::make_shared(*tf_buffer_); + tf_listener_ = std::make_shared(*tf_buffer_, this, true); state_pub_ = this->create_publisher( "collision_detector_state"); diff --git a/nav2_collision_monitor/src/collision_monitor_node.cpp b/nav2_collision_monitor/src/collision_monitor_node.cpp index 7bff3a8a4d1..d7ec6131220 100644 --- a/nav2_collision_monitor/src/collision_monitor_node.cpp +++ b/nav2_collision_monitor/src/collision_monitor_node.cpp @@ -52,7 +52,7 @@ CollisionMonitor::on_configure(const rclcpp_lifecycle::State & state) this->get_node_base_interface(), this->get_node_timers_interface()); tf_buffer_->setCreateTimerInterface(timer_interface); - tf_listener_ = std::make_shared(*tf_buffer_); + tf_listener_ = std::make_shared(*tf_buffer_, this, true); std::string cmd_vel_in_topic; std::string cmd_vel_out_topic; diff --git a/nav2_docking/opennav_docking/src/docking_server.cpp b/nav2_docking/opennav_docking/src/docking_server.cpp index 17dc1c28da3..1208ade5521 100644 --- a/nav2_docking/opennav_docking/src/docking_server.cpp +++ b/nav2_docking/opennav_docking/src/docking_server.cpp @@ -121,7 +121,7 @@ DockingServer::on_activate(const rclcpp_lifecycle::State & /*state*/) auto node = shared_from_this(); - tf2_listener_ = std::make_unique(*tf2_buffer_); + tf2_listener_ = std::make_unique(*tf2_buffer_, this, true); dock_db_->activate(); navigator_->activate(); vel_publisher_->on_activate(); diff --git a/nav2_route/src/route_server.cpp b/nav2_route/src/route_server.cpp index 2a44005111b..fee6bf7b671 100644 --- a/nav2_route/src/route_server.cpp +++ b/nav2_route/src/route_server.cpp @@ -35,7 +35,7 @@ RouteServer::on_configure(const rclcpp_lifecycle::State & /*state*/) get_node_base_interface(), get_node_timers_interface()); tf_->setCreateTimerInterface(timer_interface); - transform_listener_ = std::make_shared(*tf_); + transform_listener_ = std::make_shared(*tf_, this, true); auto node = shared_from_this(); graph_vis_publisher_ = diff --git a/nav2_smoother/src/nav2_smoother.cpp b/nav2_smoother/src/nav2_smoother.cpp index 66e3fd926ff..74714bafb81 100644 --- a/nav2_smoother/src/nav2_smoother.cpp +++ b/nav2_smoother/src/nav2_smoother.cpp @@ -78,7 +78,7 @@ SmootherServer::on_configure(const rclcpp_lifecycle::State & state) auto timer_interface = std::make_shared( get_node_base_interface(), get_node_timers_interface()); tf_->setCreateTimerInterface(timer_interface); - transform_listener_ = std::make_shared(*tf_); + transform_listener_ = std::make_shared(*tf_, this, true); std::string costmap_topic, footprint_topic, robot_base_frame; double transform_tolerance = 0.1; From ec2885d0849429ba8a08aa149d8acbf7d61ff0a5 Mon Sep 17 00:00:00 2001 From: Steve Macenski Date: Thu, 7 Aug 2025 11:29:53 -0700 Subject: [PATCH 13/51] Adding new API docs website Signed-off-by: Steve Macenski --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4ef84c7c30f..06448b37c73 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ For detailed instructions on how to: - [Concepts](https://docs.nav2.org/concepts/index.html) - [Build](https://docs.nav2.org/development_guides/build_docs/index.html#build) - [Install](https://docs.nav2.org/development_guides/build_docs/index.html#install) +- [API Docs](https://api.nav2.org/) - [General Tutorials](https://docs.nav2.org/tutorials/index.html) and [Algorithm Developer Tutorials](https://docs.nav2.org/plugin_tutorials/index.html) - [Configure](https://docs.nav2.org/configuration/index.html) - [Navigation Plugins](https://docs.nav2.org/plugins/index.html) From d590533fb9bdd181dc28d3daed58bd4a3007c600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Kurtulu=C5=9F=20Alt=C4=B1parmak?= Date: Fri, 8 Aug 2025 00:41:30 +0300 Subject: [PATCH 14/51] Smooth path even if goal pose is so much near to the robot (#5423) * Smooth path even if goal pose is so much near to the robot Signed-off-by: CihatAltiparmak * Apply suggestions Signed-off-by: CihatAltiparmak * Remove unnecessary diff Signed-off-by: CihatAltiparmak --------- Signed-off-by: CihatAltiparmak --- nav2_smoother/src/simple_smoother.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nav2_smoother/src/simple_smoother.cpp b/nav2_smoother/src/simple_smoother.cpp index db9f323e8a1..5179784f83a 100644 --- a/nav2_smoother/src/simple_smoother.cpp +++ b/nav2_smoother/src/simple_smoother.cpp @@ -81,7 +81,7 @@ bool SimpleSmoother::smooth( std::lock_guard lock(*(costmap->getMutex())); for (unsigned int i = 0; i != path_segments.size(); i++) { - if (path_segments[i].end - path_segments[i].start > 9) { + if (path_segments[i].end - path_segments[i].start > 3) { // Populate path segment curr_path_segment.poses.clear(); std::copy( From df3effd125da78da1bd655bcceb5f36488abf9b9 Mon Sep 17 00:00:00 2001 From: Vince Reda <60265874+redvinaa@users.noreply.github.com> Date: Fri, 8 Aug 2025 17:35:08 +0200 Subject: [PATCH 15/51] Make pause resume controller use nav2::LifecycleNode (#5427) * Make pause resume controller use nav2::LifecycleNode Signed-off-by: redvinaa * Fix tests Signed-off-by: redvinaa * Use new nav2 create_service Signed-off-by: redvinaa * Use attribues from parent, fix future wait, update docstring Signed-off-by: redvinaa --------- Signed-off-by: redvinaa --- .../control/pause_resume_controller.hpp | 1 + .../control/pause_resume_controller.cpp | 8 +- .../control/test_pause_resume_controller.cpp | 109 ++++++------------ .../test/utils/get_node_from_tree.hpp | 2 +- 4 files changed, 43 insertions(+), 77 deletions(-) diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/control/pause_resume_controller.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/control/pause_resume_controller.hpp index a3f5333b19b..fa5bfa35f2f 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/control/pause_resume_controller.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/control/pause_resume_controller.hpp @@ -26,6 +26,7 @@ #include "rclcpp/executors/single_threaded_executor.hpp" #include "behaviortree_cpp/control_node.h" #include "nav2_ros_common/service_server.hpp" +#include "nav2_ros_common/lifecycle_node.hpp" // Interface definitions #include "std_srvs/srv/trigger.hpp" diff --git a/nav2_behavior_tree/plugins/control/pause_resume_controller.cpp b/nav2_behavior_tree/plugins/control/pause_resume_controller.cpp index 0f039aad8e6..60b269fa2e0 100644 --- a/nav2_behavior_tree/plugins/control/pause_resume_controller.cpp +++ b/nav2_behavior_tree/plugins/control/pause_resume_controller.cpp @@ -31,7 +31,7 @@ PauseResumeController::PauseResumeController( const BT::NodeConfiguration & conf) : BT::ControlNode(xml_tag_name, conf) { - auto node = this->config().blackboard->get("node"); + auto node = config().blackboard->get("node"); state_ = RESUMED; // Create a separate cb group with a separate executor to spin @@ -45,17 +45,15 @@ PauseResumeController::PauseResumeController( std::string pause_service_name; getInput("pause_service_name", pause_service_name); - pause_srv_ = std::make_shared>( + pause_srv_ = node->create_service( pause_service_name, - node, std::bind(&PauseResumeController::pauseServiceCallback, this, _1, _2, _3), cb_group_); std::string resume_service_name; getInput("resume_service_name", resume_service_name); - resume_srv_ = std::make_shared>( + resume_srv_ = node->create_service( resume_service_name, - node, std::bind(&PauseResumeController::resumeServiceCallback, this, _1, _2, _3), cb_group_); } diff --git a/nav2_behavior_tree/test/plugins/control/test_pause_resume_controller.cpp b/nav2_behavior_tree/test/plugins/control/test_pause_resume_controller.cpp index 8a78c37d89b..a546bcfb565 100644 --- a/nav2_behavior_tree/test/plugins/control/test_pause_resume_controller.cpp +++ b/nav2_behavior_tree/test/plugins/control/test_pause_resume_controller.cpp @@ -26,58 +26,46 @@ class PauseResumeControllerTestFixture : public nav2_behavior_tree::BehaviorTree public: static void SetUpTestCase() { - auto node = std::make_shared("pause_resume_controller_test_fixture"); - executor_ = - std::make_shared(); - cb_group_ = node->create_callback_group( - rclcpp::CallbackGroupType::MutuallyExclusive, false); - executor_->add_callback_group(cb_group_, node->get_node_base_interface()); - pause_client_ = node->create_client( - "pause", rclcpp::ServicesQoS(), cb_group_); - resume_client_ = node->create_client( - "resume", rclcpp::ServicesQoS(), cb_group_); - - factory_ = std::make_shared(); - config_ = new BT::NodeConfiguration(); - config_->blackboard = BT::Blackboard::create(); - config_->blackboard->set("node", node); + nav2_behavior_tree::BehaviorTreeTestFixture::SetUpTestCase(); - factory_->registerNodeType("PauseResumeController"); + ASSERT_TRUE(node_); + pause_client_ = node_->create_client("pause", true); + resume_client_ = node_->create_client("resume", true); - // Register dummy node for testing + ASSERT_TRUE(factory_); + factory_->registerNodeType("PauseResumeController"); factory_->registerNodeType("DummyNode"); } static void TearDownTestCase() { - if (config_) { - delete config_; - config_ = nullptr; - } tree_.reset(); + pause_client_.reset(); + resume_client_.reset(); + } + + static void CheckServiceResult( + const nav2::ServiceClient::SharedPtr & client, + std::shared_future> future, + bool success = true) + { + const auto res = client->spin_until_complete(future, std::chrono::seconds(1)); + ASSERT_EQ(res, rclcpp::FutureReturnCode::SUCCESS); + ASSERT_EQ(future.wait_for(std::chrono::seconds(0)), std::future_status::ready); + EXPECT_EQ(future.get()->success, success); } protected: - static BT::NodeConfiguration * config_; - static std::shared_ptr factory_; static std::shared_ptr tree_; - static rclcpp::executors::SingleThreadedExecutor::SharedPtr executor_; - static rclcpp::CallbackGroup::SharedPtr cb_group_; - static rclcpp::Client::SharedPtr pause_client_; - static rclcpp::Client::SharedPtr resume_client_; + static nav2::ServiceClient::SharedPtr pause_client_; + static nav2::ServiceClient::SharedPtr resume_client_; }; -rclcpp::executors::SingleThreadedExecutor::SharedPtr -PauseResumeControllerTestFixture::executor_ = nullptr; -rclcpp::CallbackGroup::SharedPtr -PauseResumeControllerTestFixture::cb_group_ = nullptr; -rclcpp::Client::SharedPtr +std::shared_ptr PauseResumeControllerTestFixture::tree_ = nullptr; +nav2::ServiceClient::SharedPtr PauseResumeControllerTestFixture::pause_client_ = nullptr; -rclcpp::Client::SharedPtr +nav2::ServiceClient::SharedPtr PauseResumeControllerTestFixture::resume_client_ = nullptr; -BT::NodeConfiguration * PauseResumeControllerTestFixture::config_ = nullptr; -std::shared_ptr PauseResumeControllerTestFixture::factory_ = nullptr; -std::shared_ptr PauseResumeControllerTestFixture::tree_ = nullptr; TEST_F(PauseResumeControllerTestFixture, test_incorrect_num_children) { @@ -128,31 +116,22 @@ TEST_F(PauseResumeControllerTestFixture, test_unused_children) EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); EXPECT_EQ(pause_bt_node->getState(), state_t::RESUMED); - const auto & check_request_succeeded = []( - rclcpp::Client::FutureAndRequestId & future) - { - executor_->spin_until_future_complete(future, std::chrono::seconds(1)); - ASSERT_EQ(future.wait_for(std::chrono::seconds(0)), std::future_status::ready); - EXPECT_EQ(future.get()->success, true); - }; - // Call pause service, expect RUNNING and PAUSED - auto future = pause_client_->async_send_request( - std::make_shared()); + auto req = std::make_shared(); + auto future = pause_client_->async_call(req); EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); EXPECT_EQ(pause_bt_node->getState(), state_t::PAUSED); - check_request_succeeded(future); + CheckServiceResult(pause_client_, future); // Tick again, expect RUNNING and PAUSED EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); EXPECT_EQ(pause_bt_node->getState(), state_t::PAUSED); // Call resume service, expect RUNNING and ON_RESUME - future = resume_client_->async_send_request( - std::make_shared()); + future = resume_client_->async_call(req); EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); EXPECT_EQ(pause_bt_node->getState(), state_t::RESUMED); - check_request_succeeded(future); + CheckServiceResult(resume_client_, future); // Tick again, expect RUNNING and RESUMED EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); @@ -206,21 +185,12 @@ TEST_F(PauseResumeControllerTestFixture, test_behavior) EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); EXPECT_EQ(pause_bt_node->getState(), state_t::RESUMED); - const auto & check_future_result = []( - rclcpp::Client::FutureAndRequestId & future, bool success = true) - -> void - { - executor_->spin_until_future_complete(future, std::chrono::seconds(1)); - ASSERT_EQ(future.wait_for(std::chrono::seconds(0)), std::future_status::ready); - EXPECT_EQ(future.get()->success, success); - }; - // Call pause service, set ON_PAUSE child to RUNNING, expect RUNNING and ON_PAUSE - auto future = pause_client_->async_send_request( - std::make_shared()); + auto req = std::make_shared(); + auto future = pause_client_->async_call(req); EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); EXPECT_EQ(pause_bt_node->getState(), state_t::ON_PAUSE); - check_future_result(future); + CheckServiceResult(pause_client_, future); // Change ON_PAUSE child to SUCCESS, expect RUNNING and PAUSED on_pause_child->changeStatus(BT::NodeStatus::SUCCESS); @@ -239,18 +209,16 @@ TEST_F(PauseResumeControllerTestFixture, test_behavior) EXPECT_EQ(pause_bt_node->getState(), state_t::PAUSED); // Call pause service again, expect RUNNING and PAUSED - future = pause_client_->async_send_request( - std::make_shared()); + future = pause_client_->async_call(req); EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::RUNNING); EXPECT_EQ(pause_bt_node->getState(), state_t::PAUSED); - check_future_result(future, false); + CheckServiceResult(pause_client_, future, false); // Call resume service, change ON_RESUME child to FAILURE, expect FAILURE - future = resume_client_->async_send_request( - std::make_shared()); + future = resume_client_->async_call(req); on_resume_child->changeStatus(BT::NodeStatus::FAILURE); EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::FAILURE); - check_future_result(future); + CheckServiceResult(resume_client_, future); // Halt the tree, expect RUNNING and RESUMED tree_->haltTree(); @@ -263,11 +231,10 @@ TEST_F(PauseResumeControllerTestFixture, test_behavior) EXPECT_EQ(pause_bt_node->getState(), state_t::RESUMED); // Call resume service again, expect RUNNING and RESUMED - future = resume_client_->async_send_request( - std::make_shared()); + future = resume_client_->async_call(req); EXPECT_EQ(tree_->rootNode()->executeTick(), BT::NodeStatus::SUCCESS); EXPECT_EQ(pause_bt_node->getState(), state_t::RESUMED); - check_future_result(future, false); + CheckServiceResult(resume_client_, future, false); } int main(int argc, char ** argv) diff --git a/nav2_behavior_tree/test/utils/get_node_from_tree.hpp b/nav2_behavior_tree/test/utils/get_node_from_tree.hpp index 068fc924005..5ea720872d4 100644 --- a/nav2_behavior_tree/test/utils/get_node_from_tree.hpp +++ b/nav2_behavior_tree/test/utils/get_node_from_tree.hpp @@ -26,7 +26,7 @@ namespace nav2_behavior_tree /** * @brief Get node from tree by type and index, casted to NodeT type, const casted away - * Returns true if the node was found, false otherwise + * Returns node if it was found, nullptr otherwise. */ template NodeT * get_node_from_tree( From 773a8aa39bb86a4f4f3df777c3f71a1def9ebf47 Mon Sep 17 00:00:00 2001 From: Steve Macenski Date: Fri, 8 Aug 2025 09:14:44 -0700 Subject: [PATCH 16/51] Updating readme table for docs links (#5430) * Updating readme info Signed-off-by: SteveMacenski * Adding configuration guide Signed-off-by: SteveMacenski * Conciseness Signed-off-by: SteveMacenski --------- Signed-off-by: SteveMacenski Signed-off-by: Steve Macenski --- README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 06448b37c73..48a40d00e21 100644 --- a/README.md +++ b/README.md @@ -8,17 +8,16 @@

For detailed instructions on how to: -- [Getting Started](https://docs.nav2.org/getting_started/index.html) -- [Concepts](https://docs.nav2.org/concepts/index.html) -- [Build](https://docs.nav2.org/development_guides/build_docs/index.html#build) -- [Install](https://docs.nav2.org/development_guides/build_docs/index.html#install) -- [API Docs](https://api.nav2.org/) +- [Concepts](https://docs.nav2.org/concepts/index.html) and [Getting Started](https://docs.nav2.org/getting_started/index.html) +- [First Time Setup Guide](https://docs.nav2.org/setup_guides/index.html) +- [ROS Distribution Statuses](https://docs.nav2.org/#distributions) +- [Build & Install](https://docs.nav2.org/development_guides/build_docs/index.html#build) and [Docker Containers](https://github.com/orgs/ros-navigation/packages/container/package/navigation2) - [General Tutorials](https://docs.nav2.org/tutorials/index.html) and [Algorithm Developer Tutorials](https://docs.nav2.org/plugin_tutorials/index.html) -- [Configure](https://docs.nav2.org/configuration/index.html) +- [Configuration Guide](https://docs.nav2.org/configuration/index.html) - [Navigation Plugins](https://docs.nav2.org/plugins/index.html) -- [ROSCon Talks](https://docs.nav2.org/about/roscon.html) +- [API Docs](https://api.nav2.org/) +- [ROSCon Talks](https://docs.nav2.org/about/roscon.html) and [Citations](https://docs.nav2.org/citations.html) - [Migration Guides](https://docs.nav2.org/migration/index.html) -- [Container Images for Building Nav2](https://github.com/orgs/ros-navigation/packages/container/package/navigation2) - [Contribute](https://docs.nav2.org/development_guides/involvement_docs/index.html) Please visit our [documentation site](https://docs.nav2.org/). [Please visit our community Slack here](https://join.slack.com/t/navigation2/shared_invite/zt-uj428p0x-jKx8U7OzK1IOWp5TnDS2rA) (if this link does not work, please contact maintainers to reactivate). From e690ef0ed2391bbf93e856fe4a6cd4b371335318 Mon Sep 17 00:00:00 2001 From: mini-1235 Date: Sat, 9 Aug 2025 02:07:48 +0800 Subject: [PATCH 17/51] Add support for dynamically changing keepout zone (#5429) * Add support for dynamically changing keepout zone Signed-off-by: mini-1235 * Linting Signed-off-by: mini-1235 * Revert binary and speed changes Signed-off-by: mini-1235 --------- Signed-off-by: mini-1235 --- .../costmap_filters/costmap_filter.hpp | 2 +- .../costmap_filters/keepout_filter.hpp | 20 ++++++++++++ .../costmap_filters/keepout_filter.cpp | 31 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/nav2_costmap_2d/include/nav2_costmap_2d/costmap_filters/costmap_filter.hpp b/nav2_costmap_2d/include/nav2_costmap_2d/costmap_filters/costmap_filter.hpp index e51809df696..25238cf8a35 100644 --- a/nav2_costmap_2d/include/nav2_costmap_2d/costmap_filters/costmap_filter.hpp +++ b/nav2_costmap_2d/include/nav2_costmap_2d/costmap_filters/costmap_filter.hpp @@ -99,7 +99,7 @@ class CostmapFilter : public Layer */ void updateBounds( double robot_x, double robot_y, double robot_yaw, - double * min_x, double * min_y, double * max_x, double * max_y) final; + double * min_x, double * min_y, double * max_x, double * max_y) override; /** * @brief Update the costs in the master costmap in the window diff --git a/nav2_costmap_2d/include/nav2_costmap_2d/costmap_filters/keepout_filter.hpp b/nav2_costmap_2d/include/nav2_costmap_2d/costmap_filters/keepout_filter.hpp index e615ba465a0..b23e03a5e49 100644 --- a/nav2_costmap_2d/include/nav2_costmap_2d/costmap_filters/keepout_filter.hpp +++ b/nav2_costmap_2d/include/nav2_costmap_2d/costmap_filters/keepout_filter.hpp @@ -68,6 +68,20 @@ class KeepoutFilter : public CostmapFilter void initializeFilter( const std::string & filter_info_topic); + /** + * @brief Update the bounds of the master costmap by this layer's update dimensions + * @param robot_x X pose of robot + * @param robot_y Y pose of robot + * @param robot_yaw Robot orientation + * @param min_x X min map coord of the window to update + * @param min_y Y min map coord of the window to update + * @param max_x X max map coord of the window to update + * @param max_y Y max map coord of the window to update + */ + void updateBounds( + double robot_x, double robot_y, double robot_yaw, + double * min_x, double * min_y, double * max_x, double * max_y) override; + /** * @brief Process the keepout layer at the current pose / bounds / grid */ @@ -108,6 +122,12 @@ class KeepoutFilter : public CostmapFilter bool last_pose_lethal_{false}; // If true, last pose was lethal unsigned int lethal_state_update_min_x_{999999u}, lethal_state_update_min_y_{999999u}; unsigned int lethal_state_update_max_x_{0u}, lethal_state_update_max_y_{0u}; + + unsigned int x_{0}; + unsigned int y_{0}; + unsigned int width_{0}; + unsigned int height_{0}; + bool has_updated_data_{false}; }; } // namespace nav2_costmap_2d diff --git a/nav2_costmap_2d/plugins/costmap_filters/keepout_filter.cpp b/nav2_costmap_2d/plugins/costmap_filters/keepout_filter.cpp index e727d00634e..5fa544dec1c 100644 --- a/nav2_costmap_2d/plugins/costmap_filters/keepout_filter.cpp +++ b/nav2_costmap_2d/plugins/costmap_filters/keepout_filter.cpp @@ -155,6 +155,37 @@ void KeepoutFilter::maskCallback( // Store filter_mask_ filter_mask_ = msg; + has_updated_data_ = true; + x_ = y_ = 0; + width_ = msg->info.width; + height_ = msg->info.height; +} + +void KeepoutFilter::updateBounds( + double robot_x, double robot_y, double robot_yaw, + double * min_x, double * min_y, double * max_x, double * max_y) +{ + if (!enabled_) { + return; + } + + CostmapFilter::updateBounds(robot_x, robot_y, robot_yaw, min_x, min_y, max_x, max_y); + + if(!has_updated_data_) { + return; + } + + double wx, wy; + + layered_costmap_->getCostmap()->mapToWorld(x_, y_, wx, wy); + *min_x = std::min(wx, *min_x); + *min_y = std::min(wy, *min_y); + + layered_costmap_->getCostmap()->mapToWorld(x_ + width_, y_ + height_, wx, wy); + *max_x = std::max(wx, *max_x); + *max_y = std::max(wy, *max_y); + + has_updated_data_ = false; } void KeepoutFilter::process( From 2a947bf8f16615207a0cbf844536774277bd2bb6 Mon Sep 17 00:00:00 2001 From: Sushant Chavan Date: Mon, 11 Aug 2025 19:17:54 +0200 Subject: [PATCH 18/51] Fix KeepoutFilter on the ARM architecture (#5436) Signed-off-by: Sushant Chavan --- nav2_costmap_2d/plugins/costmap_filters/costmap_filter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nav2_costmap_2d/plugins/costmap_filters/costmap_filter.cpp b/nav2_costmap_2d/plugins/costmap_filters/costmap_filter.cpp index bc4e89f878e..06d7ed2d025 100644 --- a/nav2_costmap_2d/plugins/costmap_filters/costmap_filter.cpp +++ b/nav2_costmap_2d/plugins/costmap_filters/costmap_filter.cpp @@ -222,7 +222,7 @@ unsigned char CostmapFilter::getMaskCost( { const unsigned int index = my * filter_mask->info.width + mx; - const char data = filter_mask->data[index]; + const signed char data = filter_mask->data[index]; if (data == nav2_util::OCC_GRID_UNKNOWN) { return NO_INFORMATION; } else { From bab2a867514512c2745ba21c62af0cb5d67a8c74 Mon Sep 17 00:00:00 2001 From: Sakshay Mahna Date: Mon, 11 Aug 2025 23:13:07 +0530 Subject: [PATCH 19/51] Calculate next path point using lookahead for Graceful Controller (#5003) * Add controller utils Signed-off-by: Sakshay Mahna * Fix linting issues Signed-off-by: Sakshay Mahna * Update regulated pure pursuit Signed-off-by: Sakshay Mahna * Update graceful controller Signed-off-by: Sakshay Mahna * Remove interpolate after goal & Use orientationAroundZAxis Signed-off-by: Sakshay Mahna * Update nav2_util/src/controller_utils.cpp Signed-off-by: Steve Macenski * Update nav2_util/src/controller_utils.cpp Signed-off-by: Steve Macenski * Update nav2_util/src/controller_utils.cpp Signed-off-by: Steve Macenski * Remove interpolate_after_goal parameter from test Signed-off-by: Sakshay Mahna --------- Signed-off-by: Sakshay Mahna Signed-off-by: Steve Macenski Co-authored-by: Steve Macenski --- .../graceful_controller.hpp | 18 ++ .../src/graceful_controller.cpp | 104 ++++--- .../regulated_pure_pursuit_controller.hpp | 26 -- .../src/regulated_pure_pursuit_controller.cpp | 99 +------ .../test/test_regulated_pp.cpp | 273 ------------------ .../include/nav2_util/controller_utils.hpp | 54 ++++ nav2_util/src/CMakeLists.txt | 1 + nav2_util/src/controller_utils.cpp | 142 +++++++++ nav2_util/test/CMakeLists.txt | 3 + nav2_util/test/test_controller_utils.cpp | 265 +++++++++++++++++ 10 files changed, 556 insertions(+), 429 deletions(-) create mode 100644 nav2_util/include/nav2_util/controller_utils.hpp create mode 100644 nav2_util/src/controller_utils.cpp create mode 100644 nav2_util/test/test_controller_utils.cpp diff --git a/nav2_graceful_controller/include/nav2_graceful_controller/graceful_controller.hpp b/nav2_graceful_controller/include/nav2_graceful_controller/graceful_controller.hpp index 96358ae9607..28bec7eddd8 100644 --- a/nav2_graceful_controller/include/nav2_graceful_controller/graceful_controller.hpp +++ b/nav2_graceful_controller/include/nav2_graceful_controller/graceful_controller.hpp @@ -107,6 +107,24 @@ class GracefulController : public nav2_core::Controller void setSpeedLimit(const double & speed_limit, const bool & percentage) override; protected: + /** + * @brief Validate a given target pose for calculating command velocity + * @param target_pose Target pose to validate + * @param dist_to_target Distance to target pose + * @param dist_to_goal Distance to navigation goal + * @param trajectory Trajectory to validate in simulation + * @param costmap_transform Transform between global and local costmap + * @param cmd_vel Initial command velocity to validate in simulation + * @return true if target pose is valid, false otherwise + */ + bool validateTargetPose( + geometry_msgs::msg::PoseStamped & target_pose, + double dist_to_target, + double dist_to_goal, + nav_msgs::msg::Path & trajectory, + geometry_msgs::msg::TransformStamped & costmap_transform, + geometry_msgs::msg::TwistStamped & cmd_vel); + /** * @brief Simulate trajectory calculating in every step the new velocity command based on * a new curvature value and checking for collisions. diff --git a/nav2_graceful_controller/src/graceful_controller.cpp b/nav2_graceful_controller/src/graceful_controller.cpp index 0706c86378d..b8cce0b0aed 100644 --- a/nav2_graceful_controller/src/graceful_controller.cpp +++ b/nav2_graceful_controller/src/graceful_controller.cpp @@ -18,6 +18,7 @@ #include "angles/angles.h" #include "nav2_core/controller_exceptions.hpp" #include "nav2_util/geometry_utils.hpp" +#include "nav2_util/controller_utils.hpp" #include "nav2_graceful_controller/graceful_controller.hpp" #include "nav2_costmap_2d/costmap_filters/filter_values.hpp" @@ -195,46 +196,38 @@ geometry_msgs::msg::TwistStamped GracefulController::computeVelocityCommands( // Else, fall through and see if we should follow control law longer } - // Precompute distance to candidate poses + // Find a valid target pose and its trajectory + nav_msgs::msg::Path local_plan; + geometry_msgs::msg::PoseStamped target_pose; + + double dist_to_target; std::vector target_distances; computeDistanceAlongPath(transformed_plan.poses, target_distances); - // Work back from the end of plan to find valid target pose + bool is_first_iteration = true; for (int i = transformed_plan.poses.size() - 1; i >= 0; --i) { - // Underlying control law needs a single target pose, which should: - // * Be as far away as possible from the robot (for smoothness) - // * But no further than the max_lookahed_ distance - // * Be feasible to reach in a collision free manner - geometry_msgs::msg::PoseStamped target_pose = transformed_plan.poses[i]; - double dist_to_target = target_distances[i]; - - // Continue if target_pose is too far away from robot - if (dist_to_target > params_->max_lookahead) {continue;} - - if (dist_to_goal < params_->max_lookahead) { - if (params_->prefer_final_rotation) { - // Avoid instability and big sweeping turns at the end of paths by - // ignoring final heading - double yaw = std::atan2(target_pose.pose.position.y, target_pose.pose.position.x); - target_pose.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(yaw); - } - } else if (dist_to_target < params_->min_lookahead) { - // Make sure target is far enough away to avoid instability - break; - } - - // Flip the orientation of the motion target if the robot is moving backwards - bool reversing = false; - if (params_->allow_backward && target_pose.pose.position.x < 0.0) { - reversing = true; - target_pose.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis( - tf2::getYaw(target_pose.pose.orientation) + M_PI); + if (is_first_iteration) { + // Calculate target pose through lookahead interpolation to get most accurate + // lookahead point, if possible + dist_to_target = params_->max_lookahead; + // Interpolate after goal false for graceful controller + // Requires interpolating the orientation which is not yet implemented + // Updates dist_to_target for target_pose returned if using the point on the path + target_pose = nav2_util::getLookAheadPoint(dist_to_target, transformed_plan, false); + is_first_iteration = false; + } else { + // Underlying control law needs a single target pose, which should: + // * Be as far away as possible from the robot (for smoothness) + // * But no further than the max_lookahed_ distance + // * Be feasible to reach in a collision free manner + dist_to_target = target_distances[i]; + target_pose = transformed_plan.poses[i]; } - // Actually simulate our path - nav_msgs::msg::Path local_plan; - if (simulateTrajectory(target_pose, costmap_transform, local_plan, cmd_vel, reversing)) { - // Successfully simulated to target_pose - compute velocity at this moment + // Compute velocity at this moment if valid target pose is found + if (validateTargetPose( + target_pose, dist_to_target, dist_to_goal, local_plan, costmap_transform, cmd_vel)) + { // Publish the selected target_pose motion_target_pub_->publish(target_pose); // Publish marker for slowdown radius around motion target for debugging / visualization @@ -283,6 +276,49 @@ void GracefulController::setSpeedLimit( } } +bool GracefulController::validateTargetPose( + geometry_msgs::msg::PoseStamped & target_pose, + double dist_to_target, + double dist_to_goal, + nav_msgs::msg::Path & trajectory, + geometry_msgs::msg::TransformStamped & costmap_transform, + geometry_msgs::msg::TwistStamped & cmd_vel) +{ + // Continue if target_pose is too far away from robot + if (dist_to_target > params_->max_lookahead) { + return false; + } + + if (dist_to_goal < params_->max_lookahead) { + if (params_->prefer_final_rotation) { + // Avoid instability and big sweeping turns at the end of paths by + // ignoring final heading + double yaw = std::atan2(target_pose.pose.position.y, target_pose.pose.position.x); + target_pose.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(yaw); + } + } else if (dist_to_target < params_->min_lookahead) { + // Make sure target is far enough away to avoid instability + return false; + } + + // Flip the orientation of the motion target if the robot is moving backwards + bool reversing = false; + if (params_->allow_backward && target_pose.pose.position.x < 0.0) { + reversing = true; + target_pose.pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis( + tf2::getYaw(target_pose.pose.orientation) + M_PI); + } + + // Actually simulate the path + if (simulateTrajectory(target_pose, costmap_transform, trajectory, cmd_vel, reversing)) { + // Successfully simulated to target_pose + return true; + } + + // Validation not successful + return false; +} + bool GracefulController::simulateTrajectory( const geometry_msgs::msg::PoseStamped & motion_target, const geometry_msgs::msg::TransformStamped & costmap_transform, diff --git a/nav2_regulated_pure_pursuit_controller/include/nav2_regulated_pure_pursuit_controller/regulated_pure_pursuit_controller.hpp b/nav2_regulated_pure_pursuit_controller/include/nav2_regulated_pure_pursuit_controller/regulated_pure_pursuit_controller.hpp index 2eb901dc11f..fd79208d291 100644 --- a/nav2_regulated_pure_pursuit_controller/include/nav2_regulated_pure_pursuit_controller/regulated_pure_pursuit_controller.hpp +++ b/nav2_regulated_pure_pursuit_controller/include/nav2_regulated_pure_pursuit_controller/regulated_pure_pursuit_controller.hpp @@ -174,32 +174,6 @@ class RegulatedPurePursuitController : public nav2_core::Controller const double & pose_cost, const nav_msgs::msg::Path & path, double & linear_vel, double & sign); - /** - * @brief Find the intersection a circle and a line segment. - * This assumes the circle is centered at the origin. - * If no intersection is found, a floating point error will occur. - * @param p1 first endpoint of line segment - * @param p2 second endpoint of line segment - * @param r radius of circle - * @return point of intersection - */ - static geometry_msgs::msg::Point circleSegmentIntersection( - const geometry_msgs::msg::Point & p1, - const geometry_msgs::msg::Point & p2, - double r); - - /** - * @brief Get lookahead point - * @param lookahead_dist Optimal lookahead distance - * @param path Current global path - * @param interpolate_after_goal If true, interpolate the lookahead point after the goal based - * on the orientation given by the position of the last two pose of the path - * @return Lookahead point - */ - geometry_msgs::msg::PoseStamped getLookAheadPoint( - const double &, const nav_msgs::msg::Path &, - bool interpolate_after_goal = false); - /** * @brief checks for the cusp position * @param pose Pose input to determine the cusp position diff --git a/nav2_regulated_pure_pursuit_controller/src/regulated_pure_pursuit_controller.cpp b/nav2_regulated_pure_pursuit_controller/src/regulated_pure_pursuit_controller.cpp index 1bd12ae3a06..fced1dff734 100644 --- a/nav2_regulated_pure_pursuit_controller/src/regulated_pure_pursuit_controller.cpp +++ b/nav2_regulated_pure_pursuit_controller/src/regulated_pure_pursuit_controller.cpp @@ -25,6 +25,7 @@ #include "nav2_core/controller_exceptions.hpp" #include "nav2_ros_common/node_utils.hpp" #include "nav2_util/geometry_utils.hpp" +#include "nav2_util/controller_utils.hpp" #include "nav2_costmap_2d/costmap_filters/filter_values.hpp" using std::hypot; @@ -204,7 +205,7 @@ geometry_msgs::msg::TwistStamped RegulatedPurePursuitController::computeVelocity } // Get the particular point on the path at the lookahead distance - auto carrot_pose = getLookAheadPoint(lookahead_dist, transformed_plan); + auto carrot_pose = nav2_util::getLookAheadPoint(lookahead_dist, transformed_plan); auto rotate_to_path_carrot_pose = carrot_pose; carrot_pub_->publish(createCarrotMsg(carrot_pose)); @@ -214,7 +215,7 @@ geometry_msgs::msg::TwistStamped RegulatedPurePursuitController::computeVelocity double regulation_curvature = lookahead_curvature; if (params_->use_fixed_curvature_lookahead) { - auto curvature_lookahead_pose = getLookAheadPoint( + auto curvature_lookahead_pose = nav2_util::getLookAheadPoint( curv_lookahead_dist, transformed_plan, params_->interpolate_curvature_after_goal); rotate_to_path_carrot_pose = curvature_lookahead_pose; @@ -358,100 +359,6 @@ void RegulatedPurePursuitController::rotateToHeading( } } -geometry_msgs::msg::Point RegulatedPurePursuitController::circleSegmentIntersection( - const geometry_msgs::msg::Point & p1, - const geometry_msgs::msg::Point & p2, - double r) -{ - // Formula for intersection of a line with a circle centered at the origin, - // modified to always return the point that is on the segment between the two points. - // https://mathworld.wolfram.com/Circle-LineIntersection.html - // This works because the poses are transformed into the robot frame. - // This can be derived from solving the system of equations of a line and a circle - // which results in something that is just a reformulation of the quadratic formula. - // Interactive illustration in doc/circle-segment-intersection.ipynb as well as at - // https://www.desmos.com/calculator/td5cwbuocd - double x1 = p1.x; - double x2 = p2.x; - double y1 = p1.y; - double y2 = p2.y; - - double dx = x2 - x1; - double dy = y2 - y1; - double dr2 = dx * dx + dy * dy; - double D = x1 * y2 - x2 * y1; - - // Augmentation to only return point within segment - double d1 = x1 * x1 + y1 * y1; - double d2 = x2 * x2 + y2 * y2; - double dd = d2 - d1; - - geometry_msgs::msg::Point p; - double sqrt_term = std::sqrt(r * r * dr2 - D * D); - p.x = (D * dy + std::copysign(1.0, dd) * dx * sqrt_term) / dr2; - p.y = (-D * dx + std::copysign(1.0, dd) * dy * sqrt_term) / dr2; - return p; -} - -geometry_msgs::msg::PoseStamped RegulatedPurePursuitController::getLookAheadPoint( - const double & lookahead_dist, - const nav_msgs::msg::Path & transformed_plan, - bool interpolate_after_goal) -{ - // Find the first pose which is at a distance greater than the lookahead distance - auto goal_pose_it = std::find_if( - transformed_plan.poses.begin(), transformed_plan.poses.end(), [&](const auto & ps) { - return hypot(ps.pose.position.x, ps.pose.position.y) >= lookahead_dist; - }); - - // If the no pose is not far enough, take the last pose - if (goal_pose_it == transformed_plan.poses.end()) { - if (interpolate_after_goal) { - auto last_pose_it = std::prev(transformed_plan.poses.end()); - auto prev_last_pose_it = std::prev(last_pose_it); - - double end_path_orientation = atan2( - last_pose_it->pose.position.y - prev_last_pose_it->pose.position.y, - last_pose_it->pose.position.x - prev_last_pose_it->pose.position.x); - - // Project the last segment out to guarantee it is beyond the look ahead - // distance - auto projected_position = last_pose_it->pose.position; - projected_position.x += cos(end_path_orientation) * lookahead_dist; - projected_position.y += sin(end_path_orientation) * lookahead_dist; - - // Use the circle intersection to find the position at the correct look - // ahead distance - const auto interpolated_position = circleSegmentIntersection( - last_pose_it->pose.position, projected_position, lookahead_dist); - - geometry_msgs::msg::PoseStamped interpolated_pose; - interpolated_pose.header = last_pose_it->header; - interpolated_pose.pose.position = interpolated_position; - return interpolated_pose; - } else { - goal_pose_it = std::prev(transformed_plan.poses.end()); - } - } else if (goal_pose_it != transformed_plan.poses.begin()) { - // Find the point on the line segment between the two poses - // that is exactly the lookahead distance away from the robot pose (the origin) - // This can be found with a closed form for the intersection of a segment and a circle - // Because of the way we did the std::find_if, prev_pose is guaranteed to be inside the circle, - // and goal_pose is guaranteed to be outside the circle. - auto prev_pose_it = std::prev(goal_pose_it); - auto point = circleSegmentIntersection( - prev_pose_it->pose.position, - goal_pose_it->pose.position, lookahead_dist); - geometry_msgs::msg::PoseStamped pose; - pose.header.frame_id = prev_pose_it->header.frame_id; - pose.header.stamp = goal_pose_it->header.stamp; - pose.pose.position = point; - return pose; - } - - return *goal_pose_it; -} - void RegulatedPurePursuitController::applyConstraints( const double & curvature, const geometry_msgs::msg::Twist & /*curr_speed*/, const double & pose_cost, const nav_msgs::msg::Path & path, double & linear_vel, double & sign) diff --git a/nav2_regulated_pure_pursuit_controller/test/test_regulated_pp.cpp b/nav2_regulated_pure_pursuit_controller/test/test_regulated_pp.cpp index 197cae4c5a0..4b3dbb905f8 100644 --- a/nav2_regulated_pure_pursuit_controller/test/test_regulated_pp.cpp +++ b/nav2_regulated_pure_pursuit_controller/test/test_regulated_pp.cpp @@ -52,28 +52,6 @@ class BasicAPIRPP : public nav2_regulated_pure_pursuit_controller::RegulatedPure return getLookAheadDistance(twist); } - static geometry_msgs::msg::Point circleSegmentIntersectionWrapper( - const geometry_msgs::msg::Point & p1, - const geometry_msgs::msg::Point & p2, - double r) - { - return circleSegmentIntersection(p1, p2, r); - } - - geometry_msgs::msg::PoseStamped - projectCarrotPastGoalWrapper( - const double & dist, - const nav_msgs::msg::Path & path) - { - return getLookAheadPoint(dist, path, true); - } - - geometry_msgs::msg::PoseStamped getLookAheadPointWrapper( - const double & dist, const nav_msgs::msg::Path & path) - { - return getLookAheadPoint(dist, path); - } - bool shouldRotateToPathWrapper( const geometry_msgs::msg::PoseStamped & carrot_pose, double & angle_to_path) { @@ -207,239 +185,6 @@ TEST(RegulatedPurePursuitTest, findVelocitySignChange) EXPECT_EQ(rtn, std::numeric_limits::max()); } -using CircleSegmentIntersectionParam = std::tuple< - std::pair, - std::pair, - double, - std::pair ->; - -class CircleSegmentIntersectionTest - : public ::testing::TestWithParam -{}; - -TEST_P(CircleSegmentIntersectionTest, circleSegmentIntersection) -{ - auto pair1 = std::get<0>(GetParam()); - auto pair2 = std::get<1>(GetParam()); - auto r = std::get<2>(GetParam()); - auto expected_pair = std::get<3>(GetParam()); - auto pair_to_point = [](std::pair p) -> geometry_msgs::msg::Point { - geometry_msgs::msg::Point point; - point.x = p.first; - point.y = p.second; - point.z = 0.0; - return point; - }; - auto p1 = pair_to_point(pair1); - auto p2 = pair_to_point(pair2); - auto actual = BasicAPIRPP::circleSegmentIntersectionWrapper(p1, p2, r); - auto expected_point = pair_to_point(expected_pair); - EXPECT_DOUBLE_EQ(actual.x, expected_point.x); - EXPECT_DOUBLE_EQ(actual.y, expected_point.y); - // Expect that the intersection point is actually r away from the origin - EXPECT_DOUBLE_EQ(r, std::hypot(actual.x, actual.y)); -} - -INSTANTIATE_TEST_SUITE_P( - InterpolationTest, - CircleSegmentIntersectionTest, - testing::Values( - // Origin to the positive X axis - CircleSegmentIntersectionParam{ - {0.0, 0.0}, - {2.0, 0.0}, - 1.0, - {1.0, 0.0} -}, - // Origin to the negative X axis - CircleSegmentIntersectionParam{ - {0.0, 0.0}, - {-2.0, 0.0}, - 1.0, - {-1.0, 0.0} -}, - // Origin to the positive Y axis - CircleSegmentIntersectionParam{ - {0.0, 0.0}, - {0.0, 2.0}, - 1.0, - {0.0, 1.0} -}, - // Origin to the negative Y axis - CircleSegmentIntersectionParam{ - {0.0, 0.0}, - {0.0, -2.0}, - 1.0, - {0.0, -1.0} -}, - // non-origin to the X axis with non-unit circle, with the second point inside - CircleSegmentIntersectionParam{ - {4.0, 0.0}, - {-1.0, 0.0}, - 2.0, - {2.0, 0.0} -}, - // non-origin to the Y axis with non-unit circle, with the second point inside - CircleSegmentIntersectionParam{ - {0.0, 4.0}, - {0.0, -0.5}, - 2.0, - {0.0, 2.0} -}, - // origin to the positive X axis, on the circle - CircleSegmentIntersectionParam{ - {2.0, 0.0}, - {0.0, 0.0}, - 2.0, - {2.0, 0.0} -}, - // origin to the positive Y axis, on the circle - CircleSegmentIntersectionParam{ - {0.0, 0.0}, - {0.0, 2.0}, - 2.0, - {0.0, 2.0} -}, - // origin to the upper-right quadrant (3-4-5 triangle) - CircleSegmentIntersectionParam{ - {0.0, 0.0}, - {6.0, 8.0}, - 5.0, - {3.0, 4.0} -}, - // origin to the lower-left quadrant (3-4-5 triangle) - CircleSegmentIntersectionParam{ - {0.0, 0.0}, - {-6.0, -8.0}, - 5.0, - {-3.0, -4.0} -}, - // origin to the upper-left quadrant (3-4-5 triangle) - CircleSegmentIntersectionParam{ - {0.0, 0.0}, - {-6.0, 8.0}, - 5.0, - {-3.0, 4.0} -}, - // origin to the lower-right quadrant (3-4-5 triangle) - CircleSegmentIntersectionParam{ - {0.0, 0.0}, - {6.0, -8.0}, - 5.0, - {3.0, -4.0} -} -)); - -TEST(RegulatedPurePursuitTest, projectCarrotPastGoal) { - auto ctrl = std::make_shared(); - auto node = std::make_shared("testRPP"); - std::string name = "PathFollower"; - auto tf = std::make_shared(node->get_clock()); - auto costmap = - std::make_shared("fake_costmap"); - rclcpp_lifecycle::State state; - costmap->on_configure(state); - ctrl->configure(node, name, tf, costmap); - - double EPSILON = std::numeric_limits::epsilon(); - - nav_msgs::msg::Path path; - // More than 2 poses - path.poses.resize(4); - path.poses[0].pose.position.x = 0.0; - path.poses[1].pose.position.x = 1.0; - path.poses[2].pose.position.x = 2.0; - path.poses[3].pose.position.x = 3.0; - auto pt = ctrl->projectCarrotPastGoalWrapper(10.0, path); - EXPECT_NEAR(pt.pose.position.x, 10.0, EPSILON); - EXPECT_NEAR(pt.pose.position.y, 0.0, EPSILON); - - // 2 poses fwd - path.poses.clear(); - path.poses.resize(2); - path.poses[0].pose.position.x = 2.0; - path.poses[1].pose.position.x = 3.0; - pt = ctrl->projectCarrotPastGoalWrapper(10.0, path); - EXPECT_NEAR(pt.pose.position.x, 10.0, EPSILON); - EXPECT_NEAR(pt.pose.position.y, 0.0, EPSILON); - - // 2 poses at 45° - path.poses.clear(); - path.poses.resize(2); - path.poses[0].pose.position.x = 2.0; - path.poses[0].pose.position.y = 2.0; - path.poses[1].pose.position.x = 3.0; - path.poses[1].pose.position.y = 3.0; - pt = ctrl->projectCarrotPastGoalWrapper(10.0, path); - EXPECT_NEAR(pt.pose.position.x, cos(45.0 * M_PI / 180) * 10.0, EPSILON); - EXPECT_NEAR(pt.pose.position.y, sin(45.0 * M_PI / 180) * 10.0, EPSILON); - - // 2 poses at 90° - path.poses.clear(); - path.poses.resize(2); - path.poses[0].pose.position.x = 0.0; - path.poses[0].pose.position.y = 2.0; - path.poses[1].pose.position.x = 0.0; - path.poses[1].pose.position.y = 3.0; - pt = ctrl->projectCarrotPastGoalWrapper(10.0, path); - EXPECT_NEAR(pt.pose.position.x, cos(90.0 * M_PI / 180) * 10.0, EPSILON); - EXPECT_NEAR(pt.pose.position.y, sin(90.0 * M_PI / 180) * 10.0, EPSILON); - - // 2 poses at 135° - path.poses.clear(); - path.poses.resize(2); - path.poses[0].pose.position.x = -2.0; - path.poses[0].pose.position.y = 2.0; - path.poses[1].pose.position.x = -3.0; - path.poses[1].pose.position.y = 3.0; - pt = ctrl->projectCarrotPastGoalWrapper(10.0, path); - EXPECT_NEAR(pt.pose.position.x, cos(135.0 * M_PI / 180) * 10.0, EPSILON); - EXPECT_NEAR(pt.pose.position.y, sin(135.0 * M_PI / 180) * 10.0, EPSILON); - - // 2 poses back - path.poses.clear(); - path.poses.resize(2); - path.poses[0].pose.position.x = -2.0; - path.poses[1].pose.position.x = -3.0; - pt = ctrl->projectCarrotPastGoalWrapper(10.0, path); - EXPECT_NEAR(pt.pose.position.x, -10.0, EPSILON); - EXPECT_NEAR(pt.pose.position.y, 0.0, EPSILON); - - // 2 poses at -135° - path.poses.clear(); - path.poses.resize(2); - path.poses[0].pose.position.x = -2.0; - path.poses[0].pose.position.y = -2.0; - path.poses[1].pose.position.x = -3.0; - path.poses[1].pose.position.y = -3.0; - pt = ctrl->projectCarrotPastGoalWrapper(10.0, path); - EXPECT_NEAR(pt.pose.position.x, cos(-135.0 * M_PI / 180) * 10.0, EPSILON); - EXPECT_NEAR(pt.pose.position.y, sin(-135.0 * M_PI / 180) * 10.0, EPSILON); - - // 2 poses at -90° - path.poses.clear(); - path.poses.resize(2); - path.poses[0].pose.position.x = 0.0; - path.poses[0].pose.position.y = -2.0; - path.poses[1].pose.position.x = 0.0; - path.poses[1].pose.position.y = -3.0; - pt = ctrl->projectCarrotPastGoalWrapper(10.0, path); - EXPECT_NEAR(pt.pose.position.x, cos(-90.0 * M_PI / 180) * 10.0, EPSILON); - EXPECT_NEAR(pt.pose.position.y, sin(-90.0 * M_PI / 180) * 10.0, EPSILON); - - // 2 poses at -45° - path.poses.clear(); - path.poses.resize(2); - path.poses[0].pose.position.x = 2.0; - path.poses[0].pose.position.y = -2.0; - path.poses[1].pose.position.x = 3.0; - path.poses[1].pose.position.y = -3.0; - pt = ctrl->projectCarrotPastGoalWrapper(10.0, path); - EXPECT_NEAR(pt.pose.position.x, cos(-45.0 * M_PI / 180) * 10.0, EPSILON); - EXPECT_NEAR(pt.pose.position.y, sin(-45.0 * M_PI / 180) * 10.0, EPSILON); -} - TEST(RegulatedPurePursuitTest, lookaheadAPI) { auto ctrl = std::make_shared(); @@ -476,24 +221,6 @@ TEST(RegulatedPurePursuitTest, lookaheadAPI) twist.linear.x = 0.0; rtn = ctrl->getLookAheadDistanceWrapper(twist); EXPECT_EQ(rtn, 0.3); - - // test getLookAheadPoint - double dist = 1.0; - nav_msgs::msg::Path path; - path.poses.resize(10); - for (uint i = 0; i != path.poses.size(); i++) { - path.poses[i].pose.position.x = static_cast(i); - } - - // test exact hits - auto pt = ctrl->getLookAheadPointWrapper(dist, path); - EXPECT_EQ(pt.pose.position.x, 1.0); - - // test interpolation - ctrl->configure(node, name, tf, costmap); - dist = 3.8; - pt = ctrl->getLookAheadPointWrapper(dist, path); - EXPECT_EQ(pt.pose.position.x, 3.8); } TEST(RegulatedPurePursuitTest, rotateTests) diff --git a/nav2_util/include/nav2_util/controller_utils.hpp b/nav2_util/include/nav2_util/controller_utils.hpp new file mode 100644 index 00000000000..dbe04b1f2f3 --- /dev/null +++ b/nav2_util/include/nav2_util/controller_utils.hpp @@ -0,0 +1,54 @@ +// Copyright (c) 2020 Shrijit Singh +// Copyright (c) 2020 Samsung Research America +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +#ifndef NAV2_UTIL__CONTROLLER_UTILS_HPP_ +#define NAV2_UTIL__CONTROLLER_UTILS_HPP_ + +#include "geometry_msgs/msg/pose_stamped.hpp" +#include "geometry_msgs/msg/point.hpp" +#include "nav_msgs/msg/path.hpp" + +namespace nav2_util +{ +/** +* @brief Find the intersection a circle and a line segment. +* This assumes the circle is centered at the origin. +* If no intersection is found, a floating point error will occur. +* @param p1 first endpoint of line segment +* @param p2 second endpoint of line segment +* @param r radius of circle +* @return point of intersection +*/ +geometry_msgs::msg::Point circleSegmentIntersection( + const geometry_msgs::msg::Point & p1, + const geometry_msgs::msg::Point & p2, + double r); + +/** +* @brief Get lookahead point +* @param lookahead_dist Optimal lookahead distance +* @param path Current global path +* @param interpolate_after_goal If true, interpolate the lookahead point after the goal based +* on the orientation given by the position of the last two pose of the path +* @return Lookahead point +*/ +geometry_msgs::msg::PoseStamped getLookAheadPoint( + double &, const nav_msgs::msg::Path &, + const bool interpolate_after_goal = false); + +} // namespace nav2_util + +#endif // NAV2_UTIL__CONTROLLER_UTILS_HPP_ diff --git a/nav2_util/src/CMakeLists.txt b/nav2_util/src/CMakeLists.txt index 563742e8b72..99d24ee6348 100644 --- a/nav2_util/src/CMakeLists.txt +++ b/nav2_util/src/CMakeLists.txt @@ -3,6 +3,7 @@ add_library(${library_name} SHARED lifecycle_service_client.cpp string_utils.cpp robot_utils.cpp + controller_utils.cpp odometry_utils.cpp array_parser.cpp ) diff --git a/nav2_util/src/controller_utils.cpp b/nav2_util/src/controller_utils.cpp new file mode 100644 index 00000000000..18f2ae42dd3 --- /dev/null +++ b/nav2_util/src/controller_utils.cpp @@ -0,0 +1,142 @@ +// Copyright (c) 2020 Shrijit Singh +// Copyright (c) 2020 Samsung Research America +// +// 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 +#include "nav2_util/controller_utils.hpp" +#include "nav2_util/geometry_utils.hpp" + +namespace nav2_util +{ +geometry_msgs::msg::Point circleSegmentIntersection( + const geometry_msgs::msg::Point & p1, + const geometry_msgs::msg::Point & p2, + double r) +{ + // Formula for intersection of a line with a circle centered at the origin, + // modified to always return the point that is on the segment between the two points. + // https://mathworld.wolfram.com/Circle-LineIntersection.html + // This works because the poses are transformed into the robot frame. + // This can be derived from solving the system of equations of a line and a circle + // which results in something that is just a reformulation of the quadratic formula. + // Interactive illustration in doc/circle-segment-intersection.ipynb as well as at + // https://www.desmos.com/calculator/td5cwbuocd + double x1 = p1.x; + double x2 = p2.x; + double y1 = p1.y; + double y2 = p2.y; + + double dx = x2 - x1; + double dy = y2 - y1; + double dr2 = dx * dx + dy * dy; + double D = x1 * y2 - x2 * y1; + + // Augmentation to only return point within segment + double d1 = x1 * x1 + y1 * y1; + double d2 = x2 * x2 + y2 * y2; + double dd = d2 - d1; + + geometry_msgs::msg::Point p; + double sqrt_term = std::sqrt(r * r * dr2 - D * D); + p.x = (D * dy + std::copysign(1.0, dd) * dx * sqrt_term) / dr2; + p.y = (-D * dx + std::copysign(1.0, dd) * dy * sqrt_term) / dr2; + return p; +} + +geometry_msgs::msg::PoseStamped getLookAheadPoint( + double & lookahead_dist, + const nav_msgs::msg::Path & transformed_plan, + const bool interpolate_after_goal) +{ + // Find the first pose which is at a distance greater than the lookahead distance + // Using distance along the path + const auto & poses = transformed_plan.poses; + auto goal_pose_it = poses.begin(); + double d = 0.0; + + bool pose_found = false; + for (size_t i = 1; i < poses.size(); i++) { + const auto & prev_pose = poses[i - 1].pose.position; + const auto & curr_pose = poses[i].pose.position; + + d += std::hypot(curr_pose.x - prev_pose.x, curr_pose.y - prev_pose.y); + if (d >= lookahead_dist) { + goal_pose_it = poses.begin() + i; + pose_found = true; + break; + } + } + + if (!pose_found) { + goal_pose_it = poses.end(); + } + + // If the no pose is not far enough, take the last pose + if (goal_pose_it == transformed_plan.poses.end()) { + if (interpolate_after_goal) { + auto last_pose_it = std::prev(transformed_plan.poses.end()); + auto prev_last_pose_it = std::prev(last_pose_it); + + double end_path_orientation = atan2( + last_pose_it->pose.position.y - prev_last_pose_it->pose.position.y, + last_pose_it->pose.position.x - prev_last_pose_it->pose.position.x); + + // Project the last segment out to guarantee it is beyond the look ahead + // distance + auto projected_position = last_pose_it->pose.position; + projected_position.x += cos(end_path_orientation) * lookahead_dist; + projected_position.y += sin(end_path_orientation) * lookahead_dist; + + // Use the circle intersection to find the position at the correct look + // ahead distance + const auto interpolated_position = circleSegmentIntersection( + last_pose_it->pose.position, projected_position, lookahead_dist); + + geometry_msgs::msg::PoseStamped interpolated_pose; + interpolated_pose.header = last_pose_it->header; + interpolated_pose.pose.position = interpolated_position; + + return interpolated_pose; + } else { + lookahead_dist = d; // Updating lookahead distance since using the final point + goal_pose_it = std::prev(transformed_plan.poses.end()); + } + } else if (goal_pose_it != transformed_plan.poses.begin()) { + // Find the point on the line segment between the two poses + // that is exactly the lookahead distance away from the robot pose (the origin) + // This can be found with a closed form for the intersection of a segment and a circle + // Because of the way we did the std::find_if, prev_pose is guaranteed to be inside the circle, + // and goal_pose is guaranteed to be outside the circle. + auto prev_pose_it = std::prev(goal_pose_it); + auto point = circleSegmentIntersection( + prev_pose_it->pose.position, + goal_pose_it->pose.position, lookahead_dist); + + // Calculate orientation towards interpolated position + // Convert yaw to quaternion + double yaw = atan2( + point.y - prev_pose_it->pose.position.y, + point.x - prev_pose_it->pose.position.x); + + geometry_msgs::msg::PoseStamped pose; + pose.header.frame_id = prev_pose_it->header.frame_id; + pose.header.stamp = goal_pose_it->header.stamp; + pose.pose.position = point; + pose.pose.orientation = geometry_utils::orientationAroundZAxis(yaw); + return pose; + } + + return *goal_pose_it; +} +} // namespace nav2_util diff --git a/nav2_util/test/CMakeLists.txt b/nav2_util/test/CMakeLists.txt index 4527fd8373d..09d7d4a38c0 100644 --- a/nav2_util/test/CMakeLists.txt +++ b/nav2_util/test/CMakeLists.txt @@ -16,6 +16,9 @@ target_link_libraries(test_odometry_utils ${library_name} ${nav_msgs_TARGETS} ${ ament_add_gtest(test_robot_utils test_robot_utils.cpp) target_link_libraries(test_robot_utils ${library_name} ${geometry_msgs_TARGETS}) +ament_add_gtest(test_controller_utils test_controller_utils.cpp) +target_link_libraries(test_controller_utils ${library_name} ${nav_msgs_TARGETS} ${geometry_msgs_TARGETS}) + ament_add_gtest(test_base_footprint_publisher test_base_footprint_publisher.cpp) target_include_directories(test_base_footprint_publisher PRIVATE "$") diff --git a/nav2_util/test/test_controller_utils.cpp b/nav2_util/test/test_controller_utils.cpp new file mode 100644 index 00000000000..3c13d8e9bbc --- /dev/null +++ b/nav2_util/test/test_controller_utils.cpp @@ -0,0 +1,265 @@ +// Copyright (c) 2020 Samsung Research +// +// 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 +#include "nav2_util/controller_utils.hpp" +#include "geometry_msgs/msg/point.hpp" +#include "gtest/gtest.h" + + +using CircleSegmentIntersectionParam = std::tuple< + std::pair, + std::pair, + double, + std::pair +>; + +class CircleSegmentIntersectionTest + : public ::testing::TestWithParam +{}; + +TEST_P(CircleSegmentIntersectionTest, circleSegmentIntersection) +{ + auto pair1 = std::get<0>(GetParam()); + auto pair2 = std::get<1>(GetParam()); + auto r = std::get<2>(GetParam()); + auto expected_pair = std::get<3>(GetParam()); + auto pair_to_point = [](std::pair p) -> geometry_msgs::msg::Point { + geometry_msgs::msg::Point point; + point.x = p.first; + point.y = p.second; + point.z = 0.0; + return point; + }; + auto p1 = pair_to_point(pair1); + auto p2 = pair_to_point(pair2); + auto actual = nav2_util::circleSegmentIntersection(p1, p2, r); + auto expected_point = pair_to_point(expected_pair); + EXPECT_DOUBLE_EQ(actual.x, expected_point.x); + EXPECT_DOUBLE_EQ(actual.y, expected_point.y); + // Expect that the intersection point is actually r away from the origin + EXPECT_DOUBLE_EQ(r, std::hypot(actual.x, actual.y)); +} + +INSTANTIATE_TEST_SUITE_P( + InterpolationTest, + CircleSegmentIntersectionTest, + testing::Values( + // Origin to the positive X axis + CircleSegmentIntersectionParam{ + {0.0, 0.0}, + {2.0, 0.0}, + 1.0, + {1.0, 0.0} +}, + // Origin to the negative X axis + CircleSegmentIntersectionParam{ + {0.0, 0.0}, + {-2.0, 0.0}, + 1.0, + {-1.0, 0.0} +}, + // Origin to the positive Y axis + CircleSegmentIntersectionParam{ + {0.0, 0.0}, + {0.0, 2.0}, + 1.0, + {0.0, 1.0} +}, + // Origin to the negative Y axis + CircleSegmentIntersectionParam{ + {0.0, 0.0}, + {0.0, -2.0}, + 1.0, + {0.0, -1.0} +}, + // non-origin to the X axis with non-unit circle, with the second point inside + CircleSegmentIntersectionParam{ + {4.0, 0.0}, + {-1.0, 0.0}, + 2.0, + {2.0, 0.0} +}, + // non-origin to the Y axis with non-unit circle, with the second point inside + CircleSegmentIntersectionParam{ + {0.0, 4.0}, + {0.0, -0.5}, + 2.0, + {0.0, 2.0} +}, + // origin to the positive X axis, on the circle + CircleSegmentIntersectionParam{ + {2.0, 0.0}, + {0.0, 0.0}, + 2.0, + {2.0, 0.0} +}, + // origin to the positive Y axis, on the circle + CircleSegmentIntersectionParam{ + {0.0, 0.0}, + {0.0, 2.0}, + 2.0, + {0.0, 2.0} +}, + // origin to the upper-right quadrant (3-4-5 triangle) + CircleSegmentIntersectionParam{ + {0.0, 0.0}, + {6.0, 8.0}, + 5.0, + {3.0, 4.0} +}, + // origin to the lower-left quadrant (3-4-5 triangle) + CircleSegmentIntersectionParam{ + {0.0, 0.0}, + {-6.0, -8.0}, + 5.0, + {-3.0, -4.0} +}, + // origin to the upper-left quadrant (3-4-5 triangle) + CircleSegmentIntersectionParam{ + {0.0, 0.0}, + {-6.0, 8.0}, + 5.0, + {-3.0, 4.0} +}, + // origin to the lower-right quadrant (3-4-5 triangle) + CircleSegmentIntersectionParam{ + {0.0, 0.0}, + {6.0, -8.0}, + 5.0, + {3.0, -4.0} +} +)); + +TEST(InterpolationUtils, lookaheadInterpolation) +{ + // test Lookahead Point Interpolation + double dist = 1.0; + nav_msgs::msg::Path path; + path.poses.resize(10); + for (uint i = 0; i != path.poses.size(); i++) { + path.poses[i].pose.position.x = static_cast(i); + } + + // test exact hits + auto pt = nav2_util::getLookAheadPoint(dist, path); + EXPECT_EQ(pt.pose.position.x, 1.0); + + // test interpolation + dist = 3.8; + pt = nav2_util::getLookAheadPoint(dist, path); + EXPECT_EQ(pt.pose.position.x, 3.8); +} + +TEST(InterpolationUtils, lookaheadExtrapolation) +{ + // Test extrapolation beyond goal + double EPSILON = std::numeric_limits::epsilon(); + + nav_msgs::msg::Path path; + double lookahead_dist = 10.0; + // More than 2 poses + path.poses.resize(4); + path.poses[0].pose.position.x = 0.0; + path.poses[1].pose.position.x = 1.0; + path.poses[2].pose.position.x = 2.0; + path.poses[3].pose.position.x = 3.0; + auto pt = nav2_util::getLookAheadPoint(lookahead_dist, path, true); + EXPECT_NEAR(pt.pose.position.x, 10.0, EPSILON); + EXPECT_NEAR(pt.pose.position.y, 0.0, EPSILON); + + // 2 poses fwd + path.poses.clear(); + path.poses.resize(2); + path.poses[0].pose.position.x = 2.0; + path.poses[1].pose.position.x = 3.0; + pt = nav2_util::getLookAheadPoint(lookahead_dist, path, true); + EXPECT_NEAR(pt.pose.position.x, 10.0, EPSILON); + EXPECT_NEAR(pt.pose.position.y, 0.0, EPSILON); + + // 2 poses at 45° + path.poses.clear(); + path.poses.resize(2); + path.poses[0].pose.position.x = 2.0; + path.poses[0].pose.position.y = 2.0; + path.poses[1].pose.position.x = 3.0; + path.poses[1].pose.position.y = 3.0; + pt = nav2_util::getLookAheadPoint(lookahead_dist, path, true); + EXPECT_NEAR(pt.pose.position.x, cos(45.0 * M_PI / 180) * 10.0, EPSILON); + EXPECT_NEAR(pt.pose.position.y, sin(45.0 * M_PI / 180) * 10.0, EPSILON); + + // 2 poses at 90° + path.poses.clear(); + path.poses.resize(2); + path.poses[0].pose.position.x = 0.0; + path.poses[0].pose.position.y = 2.0; + path.poses[1].pose.position.x = 0.0; + path.poses[1].pose.position.y = 3.0; + pt = nav2_util::getLookAheadPoint(lookahead_dist, path, true); + EXPECT_NEAR(pt.pose.position.x, cos(90.0 * M_PI / 180) * 10.0, EPSILON); + EXPECT_NEAR(pt.pose.position.y, sin(90.0 * M_PI / 180) * 10.0, EPSILON); + + // 2 poses at 135° + path.poses.clear(); + path.poses.resize(2); + path.poses[0].pose.position.x = -2.0; + path.poses[0].pose.position.y = 2.0; + path.poses[1].pose.position.x = -3.0; + path.poses[1].pose.position.y = 3.0; + pt = nav2_util::getLookAheadPoint(lookahead_dist, path, true); + EXPECT_NEAR(pt.pose.position.x, cos(135.0 * M_PI / 180) * 10.0, EPSILON); + EXPECT_NEAR(pt.pose.position.y, sin(135.0 * M_PI / 180) * 10.0, EPSILON); + + // 2 poses back + path.poses.clear(); + path.poses.resize(2); + path.poses[0].pose.position.x = -2.0; + path.poses[1].pose.position.x = -3.0; + pt = nav2_util::getLookAheadPoint(lookahead_dist, path, true); + EXPECT_NEAR(pt.pose.position.x, -10.0, EPSILON); + EXPECT_NEAR(pt.pose.position.y, 0.0, EPSILON); + + // 2 poses at -135° + path.poses.clear(); + path.poses.resize(2); + path.poses[0].pose.position.x = -2.0; + path.poses[0].pose.position.y = -2.0; + path.poses[1].pose.position.x = -3.0; + path.poses[1].pose.position.y = -3.0; + pt = nav2_util::getLookAheadPoint(lookahead_dist, path, true); + EXPECT_NEAR(pt.pose.position.x, cos(-135.0 * M_PI / 180) * 10.0, EPSILON); + EXPECT_NEAR(pt.pose.position.y, sin(-135.0 * M_PI / 180) * 10.0, EPSILON); + + // 2 poses at -90° + path.poses.clear(); + path.poses.resize(2); + path.poses[0].pose.position.x = 0.0; + path.poses[0].pose.position.y = -2.0; + path.poses[1].pose.position.x = 0.0; + path.poses[1].pose.position.y = -3.0; + pt = nav2_util::getLookAheadPoint(lookahead_dist, path, true); + EXPECT_NEAR(pt.pose.position.x, cos(-90.0 * M_PI / 180) * 10.0, EPSILON); + EXPECT_NEAR(pt.pose.position.y, sin(-90.0 * M_PI / 180) * 10.0, EPSILON); + + // 2 poses at -45° + path.poses.clear(); + path.poses.resize(2); + path.poses[0].pose.position.x = 2.0; + path.poses[0].pose.position.y = -2.0; + path.poses[1].pose.position.x = 3.0; + path.poses[1].pose.position.y = -3.0; + pt = nav2_util::getLookAheadPoint(lookahead_dist, path, true); + EXPECT_NEAR(pt.pose.position.x, cos(-45.0 * M_PI / 180) * 10.0, EPSILON); + EXPECT_NEAR(pt.pose.position.y, sin(-45.0 * M_PI / 180) * 10.0, EPSILON); +} From 935ffb9925275eec72d6daae964bf605db0a6081 Mon Sep 17 00:00:00 2001 From: Steve Macenski Date: Mon, 11 Aug 2025 11:37:39 -0700 Subject: [PATCH 20/51] Bump the cache for file movements to upload codecov results Signed-off-by: Steve Macenski --- .circleci/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index edb996eb1f7..841d60042d1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -33,12 +33,12 @@ _commands: - restore_cache: name: Restore Cache << parameters.key >> keys: - - "<< parameters.key >>-v38\ + - "<< parameters.key >>-v39\ -{{ arch }}\ -{{ .Branch }}\ -{{ .Environment.CIRCLE_PR_NUMBER }}\ -{{ checksum \"<< parameters.workspace >>/lockfile.txt\" }}" - - "<< parameters.key >>-v38\ + - "<< parameters.key >>-v39\ -{{ arch }}\ -main\ -\ @@ -58,7 +58,7 @@ _commands: steps: - save_cache: name: Save Cache << parameters.key >> - key: "<< parameters.key >>-v38\ + key: "<< parameters.key >>-v39\ -{{ arch }}\ -{{ .Branch }}\ -{{ .Environment.CIRCLE_PR_NUMBER }}\ From f7b665406c46494b0f6fbfe80bd16d4f0d85e1df Mon Sep 17 00:00:00 2001 From: Steve Macenski Date: Mon, 11 Aug 2025 14:09:58 -0700 Subject: [PATCH 21/51] Fix lifecycle manager deadlock during shutdown (#5438) * Fix lifecycle manager deadlock during shutdown Add stop() method to ServiceClient that cancels internal executor operations and call it in LifecycleServiceClient destructor to prevent deadlock when CTRL+C is pressed during lifecycle node bringup. This addresses issue #5437 where spin_until_future_complete can hang indefinitely during shutdown when bringup and shutdown sequences run concurrently. Co-authored-by: Steve Macenski * Update service_client.hpp Signed-off-by: Steve Macenski --------- Signed-off-by: Steve Macenski Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- .../include/nav2_ros_common/service_client.hpp | 10 ++++++++++ .../include/nav2_util/lifecycle_service_client.hpp | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/nav2_ros_common/include/nav2_ros_common/service_client.hpp b/nav2_ros_common/include/nav2_ros_common/service_client.hpp index c1bc2bd8435..1cf32f25fe0 100644 --- a/nav2_ros_common/include/nav2_ros_common/service_client.hpp +++ b/nav2_ros_common/include/nav2_ros_common/service_client.hpp @@ -218,6 +218,16 @@ class ServiceClient return service_name_; } + /** + * @brief Stop any running spin operations on the internal executor + */ + void stop() + { + if (client_) { + callback_group_executor_->cancel(); + } + } + protected: std::string service_name_; rclcpp::Clock::SharedPtr clock_; diff --git a/nav2_util/include/nav2_util/lifecycle_service_client.hpp b/nav2_util/include/nav2_util/lifecycle_service_client.hpp index 64860f47e87..56eea6b5ccd 100644 --- a/nav2_util/include/nav2_util/lifecycle_service_client.hpp +++ b/nav2_util/include/nav2_util/lifecycle_service_client.hpp @@ -57,6 +57,12 @@ class LifecycleServiceClient } } + ~LifecycleServiceClient() + { + change_state_.stop(); + get_state_.stop(); + } + /// Trigger a state change /** * Throws std::runtime_error on failure From 2a377c36fa4b9eb5ac39e38714447a40e18ebe0e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 07:27:28 -0700 Subject: [PATCH 22/51] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20Bump=20actions/ch?= =?UTF-8?q?eckout=20from=204=20to=205=20(#5445)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build_main_against_distros.yml | 2 +- .github/workflows/claude.yml | 2 +- .github/workflows/lint.yml | 4 ++-- .github/workflows/update_ci_image.yaml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build_main_against_distros.yml b/.github/workflows/build_main_against_distros.yml index 5b4aa08f1e1..95941856d3e 100644 --- a/.github/workflows/build_main_against_distros.yml +++ b/.github/workflows/build_main_against_distros.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Docker build context run: | diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 3ffbfa392fd..73467b184cc 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -34,7 +34,7 @@ jobs: id-token: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 1 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 62645e662aa..337b0966f0d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,7 +13,7 @@ jobs: matrix: linter: [xmllint, cpplint, uncrustify, pep257, flake8, mypy] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install typeshed for mypy if: matrix.linter == 'mypy' @@ -30,7 +30,7 @@ jobs: name: pre-commit runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 - uses: pre-commit/action@v3.0.1 env: diff --git a/.github/workflows/update_ci_image.yaml b/.github/workflows/update_ci_image.yaml index ef39facf09c..3c7743f1ad3 100644 --- a/.github/workflows/update_ci_image.yaml +++ b/.github/workflows/update_ci_image.yaml @@ -66,7 +66,7 @@ jobs: - check_ci_image runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub From b066a1872bcc3b5baa6b1db981918b202bd378a4 Mon Sep 17 00:00:00 2001 From: ElSayed ElSheikh Date: Tue, 12 Aug 2025 19:41:55 +0300 Subject: [PATCH 23/51] Include option to use PointCloud Transport (#5264) * Rebase Signed-off-by: ElSayed ElSheikh * Make linters happy Signed-off-by: ElSayed ElSheikh * Rename point_cloud_transport parameter Signed-off-by: ElSayed ElSheikh * Rebase Signed-off-by: ElSayed ElSheikh * Feedback Signed-off-by: ElSayed ElSheikh * Feedback Signed-off-by: ElSayed ElSheikh * Fix Signed-off-by: ElSayed ElSheikh --------- Signed-off-by: ElSayed ElSheikh --- nav2_collision_monitor/CMakeLists.txt | 4 ++++ .../nav2_collision_monitor/pointcloud.hpp | 9 +++++++++ nav2_collision_monitor/package.xml | 2 ++ .../params/collision_detector_params.yaml | 1 + .../params/collision_monitor_params.yaml | 1 + nav2_collision_monitor/src/pointcloud.cpp | 17 +++++++++++++++++ nav2_costmap_2d/CMakeLists.txt | 3 +++ .../include/nav2_costmap_2d/obstacle_layer.hpp | 1 + nav2_costmap_2d/package.xml | 2 ++ nav2_costmap_2d/plugins/obstacle_layer.cpp | 15 ++++++++++++--- 10 files changed, 52 insertions(+), 3 deletions(-) diff --git a/nav2_collision_monitor/CMakeLists.txt b/nav2_collision_monitor/CMakeLists.txt index e67c8ea7719..040e3f51a37 100644 --- a/nav2_collision_monitor/CMakeLists.txt +++ b/nav2_collision_monitor/CMakeLists.txt @@ -17,6 +17,7 @@ find_package(tf2 REQUIRED) find_package(tf2_ros REQUIRED) find_package(visualization_msgs REQUIRED) find_package(nav2_ros_common REQUIRED) +find_package(point_cloud_transport REQUIRED) nav2_package() @@ -54,6 +55,7 @@ target_link_libraries(${monitor_library_name} PUBLIC tf2::tf2 tf2_ros::tf2_ros ${visualization_msgs_TARGETS} + point_cloud_transport::point_cloud_transport ) target_link_libraries(${monitor_library_name} PRIVATE rclcpp_components::component @@ -88,6 +90,7 @@ target_link_libraries(${detector_library_name} PUBLIC tf2_ros::tf2_ros tf2::tf2 ${visualization_msgs_TARGETS} + point_cloud_transport::point_cloud_transport ) target_link_libraries(${detector_library_name} PRIVATE rclcpp_components::component @@ -169,6 +172,7 @@ ament_export_dependencies( tf2_ros nav2_ros_common visualization_msgs + point_cloud_transport ) ament_export_targets(export_${PROJECT_NAME}) diff --git a/nav2_collision_monitor/include/nav2_collision_monitor/pointcloud.hpp b/nav2_collision_monitor/include/nav2_collision_monitor/pointcloud.hpp index 8a46d2b65c5..c94180f94cf 100644 --- a/nav2_collision_monitor/include/nav2_collision_monitor/pointcloud.hpp +++ b/nav2_collision_monitor/include/nav2_collision_monitor/pointcloud.hpp @@ -20,6 +20,7 @@ #include #include "sensor_msgs/msg/point_cloud2.hpp" +#include "point_cloud_transport/point_cloud_transport.hpp" #include "nav2_collision_monitor/source.hpp" @@ -98,7 +99,15 @@ class PointCloud : public Source // ----- Variables ----- /// @brief PointCloud data subscriber + #if RCLCPP_VERSION_GTE(30, 0, 0) + std::shared_ptr pct_; + point_cloud_transport::Subscriber data_sub_; + #else nav2::Subscription::SharedPtr data_sub_; + #endif + + // Transport type used for PointCloud messages (e.g., raw or compressed) + std::string transport_type_; // Minimum and maximum height of PointCloud projected to 2D space double min_height_, max_height_; diff --git a/nav2_collision_monitor/package.xml b/nav2_collision_monitor/package.xml index 6a8e5ace75b..6b009f1634b 100644 --- a/nav2_collision_monitor/package.xml +++ b/nav2_collision_monitor/package.xml @@ -25,6 +25,8 @@ tf2_ros visualization_msgs nav2_ros_common + point_cloud_transport + point_cloud_transport_plugins ament_cmake_gtest ament_lint_common diff --git a/nav2_collision_monitor/params/collision_detector_params.yaml b/nav2_collision_monitor/params/collision_detector_params.yaml index eb3ee0a8739..bbeaadc5154 100644 --- a/nav2_collision_monitor/params/collision_detector_params.yaml +++ b/nav2_collision_monitor/params/collision_detector_params.yaml @@ -23,6 +23,7 @@ collision_detector: pointcloud: type: "pointcloud" topic: "/intel_realsense_r200_depth/points" + transport_type: "raw" # Options: raw, zlib, draco, zstd min_height: 0.1 max_height: 0.5 enabled: True diff --git a/nav2_collision_monitor/params/collision_monitor_params.yaml b/nav2_collision_monitor/params/collision_monitor_params.yaml index eac7486913c..28224362ea9 100644 --- a/nav2_collision_monitor/params/collision_monitor_params.yaml +++ b/nav2_collision_monitor/params/collision_monitor_params.yaml @@ -96,6 +96,7 @@ collision_monitor: pointcloud: type: "pointcloud" topic: "/intel_realsense_r200_depth/points" + transport_type: "raw" # Options: raw, zlib, draco, zstd min_height: 0.1 max_height: 0.5 enabled: True diff --git a/nav2_collision_monitor/src/pointcloud.cpp b/nav2_collision_monitor/src/pointcloud.cpp index 590eacb3cb8..72ef467b9a5 100644 --- a/nav2_collision_monitor/src/pointcloud.cpp +++ b/nav2_collision_monitor/src/pointcloud.cpp @@ -45,7 +45,11 @@ PointCloud::PointCloud( PointCloud::~PointCloud() { RCLCPP_INFO(logger_, "[%s]: Destroying PointCloud", source_name_.c_str()); + #if RCLCPP_VERSION_GTE(30, 0, 0) + data_sub_.shutdown(); + #else data_sub_.reset(); + #endif } void PointCloud::configure() @@ -60,10 +64,20 @@ void PointCloud::configure() getParameters(source_topic); + #if RCLCPP_VERSION_GTE(30, 0, 0) + const point_cloud_transport::TransportHints hint(transport_type_); + pct_ = std::make_shared(*node); + data_sub_ = pct_->subscribe( + source_topic, nav2::qos::SensorDataQoS(), + std::bind(&PointCloud::dataCallback, this, std::placeholders::_1), + {}, &hint + ); + #else data_sub_ = node->create_subscription( source_topic, std::bind(&PointCloud::dataCallback, this, std::placeholders::_1), nav2::qos::SensorDataQoS()); + #endif // Add callback for dynamic parameters dyn_params_handler_ = node->add_on_set_parameters_callback( @@ -131,6 +145,9 @@ void PointCloud::getParameters(std::string & source_topic) nav2::declare_parameter_if_not_declared( node, source_name_ + ".min_range", rclcpp::ParameterValue(0.0)); min_range_ = node->get_parameter(source_name_ + ".min_range").as_double(); + nav2::declare_parameter_if_not_declared( + node, source_name_ + ".transport_type", rclcpp::ParameterValue(std::string("raw"))); + transport_type_ = node->get_parameter(source_name_ + ".transport_type").as_string(); } void PointCloud::dataCallback(sensor_msgs::msg::PointCloud2::ConstSharedPtr msg) diff --git a/nav2_costmap_2d/CMakeLists.txt b/nav2_costmap_2d/CMakeLists.txt index 851f2428b42..d7d74038949 100644 --- a/nav2_costmap_2d/CMakeLists.txt +++ b/nav2_costmap_2d/CMakeLists.txt @@ -26,6 +26,7 @@ find_package(tf2_ros REQUIRED) find_package(nav2_ros_common REQUIRED) find_package(tf2_sensor_msgs REQUIRED) find_package(visualization_msgs REQUIRED) +find_package(point_cloud_transport REQUIRED) nav2_package() @@ -88,6 +89,7 @@ target_link_libraries(layers PUBLIC nav2_ros_common::nav2_ros_common tf2::tf2 nav2_ros_common::nav2_ros_common + point_cloud_transport::point_cloud_transport ) target_link_libraries(layers PRIVATE pluginlib::pluginlib @@ -222,6 +224,7 @@ ament_export_dependencies( tf2_ros tf2_sensor_msgs nav2_ros_common + point_cloud_transport ) pluginlib_export_plugin_description_file(nav2_costmap_2d costmap_plugins.xml) ament_package() diff --git a/nav2_costmap_2d/include/nav2_costmap_2d/obstacle_layer.hpp b/nav2_costmap_2d/include/nav2_costmap_2d/obstacle_layer.hpp index 5e66e24795a..32b17ca5d5b 100644 --- a/nav2_costmap_2d/include/nav2_costmap_2d/obstacle_layer.hpp +++ b/nav2_costmap_2d/include/nav2_costmap_2d/obstacle_layer.hpp @@ -51,6 +51,7 @@ #pragma GCC diagnostic pop #include "message_filters/subscriber.hpp" +#include "point_cloud_transport/subscriber_filter.hpp" #include "nav_msgs/msg/occupancy_grid.hpp" #include "sensor_msgs/msg/laser_scan.hpp" diff --git a/nav2_costmap_2d/package.xml b/nav2_costmap_2d/package.xml index c69d467b908..4e4940a59af 100644 --- a/nav2_costmap_2d/package.xml +++ b/nav2_costmap_2d/package.xml @@ -42,6 +42,8 @@ tf2_sensor_msgs visualization_msgs nav2_ros_common + point_cloud_transport + point_cloud_transport_plugins ament_lint_common ament_lint_auto diff --git a/nav2_costmap_2d/plugins/obstacle_layer.cpp b/nav2_costmap_2d/plugins/obstacle_layer.cpp index bac51b92f92..b897ac515be 100644 --- a/nav2_costmap_2d/plugins/obstacle_layer.cpp +++ b/nav2_costmap_2d/plugins/obstacle_layer.cpp @@ -141,7 +141,7 @@ void ObstacleLayer::onInitialize() while (ss >> source) { // get the parameters for the specific topic double observation_keep_time, expected_update_rate, min_obstacle_height, max_obstacle_height; - std::string topic, sensor_frame, data_type; + std::string topic, sensor_frame, data_type, transport_type; bool inf_is_valid, clearing, marking; declareParameter(source + "." + "topic", rclcpp::ParameterValue(source)); @@ -158,6 +158,7 @@ void ObstacleLayer::onInitialize() declareParameter(source + "." + "obstacle_min_range", rclcpp::ParameterValue(0.0)); declareParameter(source + "." + "raytrace_max_range", rclcpp::ParameterValue(3.0)); declareParameter(source + "." + "raytrace_min_range", rclcpp::ParameterValue(0.0)); + declareParameter(source + "." + "transport_type", rclcpp::ParameterValue(std::string("raw"))); node->get_parameter(name_ + "." + source + "." + "topic", topic); node->get_parameter(name_ + "." + source + "." + "sensor_frame", sensor_frame); @@ -173,6 +174,7 @@ void ObstacleLayer::onInitialize() node->get_parameter(name_ + "." + source + "." + "inf_is_valid", inf_is_valid); node->get_parameter(name_ + "." + source + "." + "marking", marking); node->get_parameter(name_ + "." + source + "." + "clearing", clearing); + node->get_parameter(name_ + "." + source + "." + "transport_type", transport_type); if (!(data_type == "PointCloud2" || data_type == "LaserScan")) { RCLCPP_FATAL( @@ -287,16 +289,23 @@ void ObstacleLayer::onInitialize() tf_filter_tolerance)); } else { + // For Rolling and Newer Support from PointCloudTransport API change + #if RCLCPP_VERSION_GTE(30, 0, 0) + std::shared_ptr sub; // For Kilted and Older Support from Message Filters API change - #if RCLCPP_VERSION_GTE(29, 6, 0) + #elif RCLCPP_VERSION_GTE(29, 6, 0) std::shared_ptr> sub; #else std::shared_ptr> sub; #endif + // For Rolling compatibility in PointCloudTransport API change + #if RCLCPP_VERSION_GTE(30, 0, 0) + sub = std::make_shared( + *node, topic, transport_type, custom_qos_profile, sub_opt); // For Kilted compatibility in Message Filters API change - #if RCLCPP_VERSION_GTE(29, 6, 0) + #elif RCLCPP_VERSION_GTE(29, 6, 0) sub = std::make_shared>( node, topic, custom_qos_profile, sub_opt); // For Jazzy compatibility in Message Filters API change From 920dff0fa07bfa27aff7b9ec94972e82fa0e8eba Mon Sep 17 00:00:00 2001 From: mini-1235 Date: Wed, 13 Aug 2025 03:07:56 +0800 Subject: [PATCH 24/51] Add option to override lethal cost in keepout zone (#5433) * Add option to override lethal cost in keepout zone Signed-off-by: mini-1235 * Linting Signed-off-by: mini-1235 * Update nav2_costmap_2d/plugins/costmap_filters/keepout_filter.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: mini-1235 * Base class call Signed-off-by: mini-1235 --------- Signed-off-by: mini-1235 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../costmap_filters/keepout_filter.hpp | 5 +- .../costmap_filters/keepout_filter.cpp | 121 +++++++++--------- 2 files changed, 67 insertions(+), 59 deletions(-) diff --git a/nav2_costmap_2d/include/nav2_costmap_2d/costmap_filters/keepout_filter.hpp b/nav2_costmap_2d/include/nav2_costmap_2d/costmap_filters/keepout_filter.hpp index b23e03a5e49..118fc3ef295 100644 --- a/nav2_costmap_2d/include/nav2_costmap_2d/costmap_filters/keepout_filter.hpp +++ b/nav2_costmap_2d/include/nav2_costmap_2d/costmap_filters/keepout_filter.hpp @@ -120,8 +120,9 @@ class KeepoutFilter : public CostmapFilter bool override_lethal_cost_{false}; // If true, lethal cost will be overridden unsigned char lethal_override_cost_{252}; // Value to override lethal cost with bool last_pose_lethal_{false}; // If true, last pose was lethal - unsigned int lethal_state_update_min_x_{999999u}, lethal_state_update_min_y_{999999u}; - unsigned int lethal_state_update_max_x_{0u}, lethal_state_update_max_y_{0u}; + bool is_pose_lethal_{false}; + double lethal_state_update_min_x_, lethal_state_update_min_y_; + double lethal_state_update_max_x_, lethal_state_update_max_y_; unsigned int x_{0}; unsigned int y_{0}; diff --git a/nav2_costmap_2d/plugins/costmap_filters/keepout_filter.cpp b/nav2_costmap_2d/plugins/costmap_filters/keepout_filter.cpp index 5fa544dec1c..48cd6c74e42 100644 --- a/nav2_costmap_2d/plugins/costmap_filters/keepout_filter.cpp +++ b/nav2_costmap_2d/plugins/costmap_filters/keepout_filter.cpp @@ -43,6 +43,7 @@ #include "nav2_costmap_2d/costmap_filters/keepout_filter.hpp" #include "nav2_costmap_2d/costmap_filters/filter_values.hpp" +#include "nav2_util/geometry_utils.hpp" namespace nav2_costmap_2d { @@ -84,6 +85,8 @@ void KeepoutFilter::initializeFilter( // clamp lethal_override_cost_ in case if higher than MAX_NON_OBSTACLE is given lethal_override_cost_ = \ std::clamp(lethal_override_cost_, FREE_SPACE, MAX_NON_OBSTACLE); + lethal_state_update_max_x_ = lethal_state_update_max_y_ = std::numeric_limits::lowest(); + lethal_state_update_min_x_ = lethal_state_update_min_y_ = std::numeric_limits::max(); } void KeepoutFilter::filterInfoCallback( @@ -171,27 +174,70 @@ void KeepoutFilter::updateBounds( CostmapFilter::updateBounds(robot_x, robot_y, robot_yaw, min_x, min_y, max_x, max_y); - if(!has_updated_data_) { - return; - } + // If new keepout zone received + if(has_updated_data_) { + double wx, wy; + layered_costmap_->getCostmap()->mapToWorld(x_, y_, wx, wy); + *min_x = std::min(wx, *min_x); + *min_y = std::min(wy, *min_y); - double wx, wy; + layered_costmap_->getCostmap()->mapToWorld(x_ + width_, y_ + height_, wx, wy); + *max_x = std::max(wx, *max_x); + *max_y = std::max(wy, *max_y); - layered_costmap_->getCostmap()->mapToWorld(x_, y_, wx, wy); - *min_x = std::min(wx, *min_x); - *min_y = std::min(wy, *min_y); + has_updated_data_ = false; + return; + } - layered_costmap_->getCostmap()->mapToWorld(x_ + width_, y_ + height_, wx, wy); - *max_x = std::max(wx, *max_x); - *max_y = std::max(wy, *max_y); + // Let's find the pose's cost if we are allowed to override the lethal cost + is_pose_lethal_ = false; + if (override_lethal_cost_) { + geometry_msgs::msg::Pose pose; + pose.position.x = robot_x; + pose.position.y = robot_y; + pose.position.z = 0.0; + pose.orientation = nav2_util::geometry_utils::orientationAroundZAxis(robot_yaw); + geometry_msgs::msg::Pose mask_pose; + if (transformPose(global_frame_, pose, filter_mask_->header.frame_id, mask_pose)) { + unsigned int mask_robot_i, mask_robot_j; + if (worldToMask(filter_mask_, mask_pose.position.x, mask_pose.position.y, mask_robot_i, + mask_robot_j)) + { + auto data = getMaskCost(filter_mask_, mask_robot_i, mask_robot_j); + is_pose_lethal_ = (data == INSCRIBED_INFLATED_OBSTACLE || data == LETHAL_OBSTACLE); + if (is_pose_lethal_) { + RCLCPP_WARN_THROTTLE( + logger_, *(clock_), 2000, + "KeepoutFilter: Pose is in keepout zone, reducing cost override to navigate out."); + } + } + } - has_updated_data_ = false; + // If in lethal space or just exited lethal space, + // we need to update all possible spaces touched during this state + if (is_pose_lethal_ || (last_pose_lethal_ && !is_pose_lethal_)) { + lethal_state_update_min_x_ = std::min(*min_x, lethal_state_update_min_x_); + *min_x = lethal_state_update_min_x_; + lethal_state_update_min_y_ = std::min(*min_y, lethal_state_update_min_y_); + *min_y = lethal_state_update_min_y_; + lethal_state_update_max_x_ = std::max(*max_x, lethal_state_update_max_x_); + *max_x = lethal_state_update_max_x_; + lethal_state_update_max_y_ = std::max(*max_y, lethal_state_update_max_y_); + *max_y = lethal_state_update_max_y_; + } else { + // If out of lethal space, reset managed lethal state sizes + lethal_state_update_min_x_ = std::numeric_limits::max(); + lethal_state_update_min_y_ = std::numeric_limits::max(); + lethal_state_update_max_x_ = std::numeric_limits::lowest(); + lethal_state_update_max_y_ = std::numeric_limits::lowest(); + } + } } void KeepoutFilter::process( nav2_costmap_2d::Costmap2D & master_grid, int min_i, int min_j, int max_i, int max_j, - const geometry_msgs::msg::Pose & pose) + const geometry_msgs::msg::Pose & /*pose*/) { std::lock_guard guard(*getMutex()); @@ -286,49 +332,10 @@ void KeepoutFilter::process( } // unsigned<-signed conversions. - unsigned int mg_min_x_u = static_cast(mg_min_x); - unsigned int mg_min_y_u = static_cast(mg_min_y); - unsigned int mg_max_x_u = static_cast(mg_max_x); - unsigned int mg_max_y_u = static_cast(mg_max_y); - - // Let's find the pose's cost if we are allowed to override the lethal cost - bool is_pose_lethal = false; - if (override_lethal_cost_) { - geometry_msgs::msg::Pose mask_pose; - if (transformPose(global_frame_, pose, filter_mask_->header.frame_id, mask_pose)) { - unsigned int mask_robot_i, mask_robot_j; - if (worldToMask(filter_mask_, mask_pose.position.x, mask_pose.position.y, mask_robot_i, - mask_robot_j)) - { - auto data = getMaskCost(filter_mask_, mask_robot_i, mask_robot_j); - is_pose_lethal = (data == INSCRIBED_INFLATED_OBSTACLE || data == LETHAL_OBSTACLE); - if (is_pose_lethal) { - RCLCPP_WARN_THROTTLE( - logger_, *(clock_), 2000, - "KeepoutFilter: Pose is in keepout zone, reducing cost override to navigate out."); - } - } - } - - // If in lethal space or just exited lethal space, - // we need to update all possible spaces touched during this state - if (is_pose_lethal || (last_pose_lethal_ && !is_pose_lethal)) { - lethal_state_update_min_x_ = std::min(mg_min_x_u, lethal_state_update_min_x_); - mg_min_x_u = lethal_state_update_min_x_; - lethal_state_update_min_y_ = std::min(mg_min_y_u, lethal_state_update_min_y_); - mg_min_y_u = lethal_state_update_min_y_; - lethal_state_update_max_x_ = std::max(mg_max_x_u, lethal_state_update_max_x_); - mg_max_x_u = lethal_state_update_max_x_; - lethal_state_update_max_y_ = std::max(mg_max_y_u, lethal_state_update_max_y_); - mg_max_y_u = lethal_state_update_max_y_; - } else { - // If out of lethal space, reset managed lethal state sizes - lethal_state_update_min_x_ = master_grid.getSizeInCellsX(); - lethal_state_update_min_y_ = master_grid.getSizeInCellsY(); - lethal_state_update_max_x_ = 0u; - lethal_state_update_max_y_ = 0u; - } - } + const unsigned int mg_min_x_u = static_cast(mg_min_x); + const unsigned int mg_min_y_u = static_cast(mg_min_y); + const unsigned int mg_max_x_u = static_cast(mg_max_x); + const unsigned int mg_max_y_u = static_cast(mg_max_y); unsigned int i, j; // master_grid iterators unsigned int index; // corresponding index of master_grid @@ -367,7 +374,7 @@ void KeepoutFilter::process( } if (data > old_data || old_data == NO_INFORMATION) { - if (override_lethal_cost_ && is_pose_lethal) { + if (override_lethal_cost_ && is_pose_lethal_) { master_array[index] = lethal_override_cost_; } else { master_array[index] = data; @@ -377,7 +384,7 @@ void KeepoutFilter::process( } } - last_pose_lethal_ = is_pose_lethal; + last_pose_lethal_ = is_pose_lethal_; } void KeepoutFilter::resetFilter() From 1ecf27cdde7d2afae5eb3e14c5049a6a6427a933 Mon Sep 17 00:00:00 2001 From: Dhruv Patel Date: Thu, 14 Aug 2025 20:39:36 +0200 Subject: [PATCH 25/51] Make MPPI Controller and Critics Apple Clang Friendly (#5455) * fix: adjust C++ standard and compiler flags for Apple Clang Signed-off-by: Dhruv Patel * Update: Only mppi_critics target will require C++20 Signed-off-by: Dhruv Patel * cmake_minimum_required(VERSION 3.8) update Signed-off-by: Dhruv Patel --------- Signed-off-by: Dhruv Patel --- nav2_amcl/CMakeLists.txt | 2 +- nav2_behavior_tree/CMakeLists.txt | 2 +- nav2_behaviors/CMakeLists.txt | 2 +- nav2_bringup/CMakeLists.txt | 2 +- nav2_bt_navigator/CMakeLists.txt | 2 +- nav2_collision_monitor/CMakeLists.txt | 2 +- nav2_common/CMakeLists.txt | 2 +- nav2_constrained_smoother/CMakeLists.txt | 2 +- nav2_controller/CMakeLists.txt | 2 +- nav2_core/CMakeLists.txt | 2 +- nav2_costmap_2d/CMakeLists.txt | 2 +- nav2_docking/opennav_docking/CMakeLists.txt | 2 +- nav2_docking/opennav_docking_bt/CMakeLists.txt | 2 +- nav2_docking/opennav_docking_core/CMakeLists.txt | 2 +- nav2_dwb_controller/costmap_queue/CMakeLists.txt | 2 +- nav2_dwb_controller/dwb_core/CMakeLists.txt | 2 +- nav2_dwb_controller/dwb_critics/CMakeLists.txt | 2 +- nav2_dwb_controller/dwb_msgs/CMakeLists.txt | 2 +- nav2_dwb_controller/dwb_plugins/CMakeLists.txt | 2 +- nav2_dwb_controller/nav2_dwb_controller/CMakeLists.txt | 2 +- nav2_dwb_controller/nav_2d_msgs/CMakeLists.txt | 2 +- nav2_dwb_controller/nav_2d_utils/CMakeLists.txt | 2 +- nav2_graceful_controller/CMakeLists.txt | 2 +- nav2_lifecycle_manager/CMakeLists.txt | 2 +- nav2_map_server/CMakeLists.txt | 2 +- nav2_mppi_controller/CMakeLists.txt | 10 ++++++++-- nav2_msgs/CMakeLists.txt | 2 +- nav2_navfn_planner/CMakeLists.txt | 2 +- nav2_planner/CMakeLists.txt | 2 +- nav2_regulated_pure_pursuit_controller/CMakeLists.txt | 2 +- nav2_ros_common/CMakeLists.txt | 2 +- nav2_rotation_shim_controller/CMakeLists.txt | 2 +- nav2_route/CMakeLists.txt | 2 +- nav2_rviz_plugins/CMakeLists.txt | 2 +- nav2_smac_planner/CMakeLists.txt | 2 +- nav2_smoother/CMakeLists.txt | 2 +- nav2_system_tests/CMakeLists.txt | 2 +- nav2_velocity_smoother/CMakeLists.txt | 2 +- nav2_voxel_grid/CMakeLists.txt | 2 +- nav2_waypoint_follower/CMakeLists.txt | 2 +- navigation2/CMakeLists.txt | 2 +- 41 files changed, 48 insertions(+), 42 deletions(-) diff --git a/nav2_amcl/CMakeLists.txt b/nav2_amcl/CMakeLists.txt index 4813b3cdd9b..e29f641cb84 100644 --- a/nav2_amcl/CMakeLists.txt +++ b/nav2_amcl/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_amcl) find_package(ament_cmake REQUIRED) diff --git a/nav2_behavior_tree/CMakeLists.txt b/nav2_behavior_tree/CMakeLists.txt index de7d0f6237f..de9cde5f2ab 100644 --- a/nav2_behavior_tree/CMakeLists.txt +++ b/nav2_behavior_tree/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_behavior_tree CXX) find_package(ament_cmake REQUIRED) diff --git a/nav2_behaviors/CMakeLists.txt b/nav2_behaviors/CMakeLists.txt index bd37a097c2f..292191ea013 100644 --- a/nav2_behaviors/CMakeLists.txt +++ b/nav2_behaviors/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_behaviors) find_package(ament_cmake REQUIRED) diff --git a/nav2_bringup/CMakeLists.txt b/nav2_bringup/CMakeLists.txt index b0999ee4eb8..af08ad0265b 100644 --- a/nav2_bringup/CMakeLists.txt +++ b/nav2_bringup/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_bringup) find_package(ament_cmake REQUIRED) diff --git a/nav2_bt_navigator/CMakeLists.txt b/nav2_bt_navigator/CMakeLists.txt index 83fa44fab7f..156ec4afff6 100644 --- a/nav2_bt_navigator/CMakeLists.txt +++ b/nav2_bt_navigator/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_bt_navigator) find_package(ament_cmake REQUIRED) diff --git a/nav2_collision_monitor/CMakeLists.txt b/nav2_collision_monitor/CMakeLists.txt index 040e3f51a37..eb91f295d47 100644 --- a/nav2_collision_monitor/CMakeLists.txt +++ b/nav2_collision_monitor/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_collision_monitor) find_package(ament_cmake REQUIRED) diff --git a/nav2_common/CMakeLists.txt b/nav2_common/CMakeLists.txt index f0f1e22c32c..71a771b29c4 100644 --- a/nav2_common/CMakeLists.txt +++ b/nav2_common/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_common NONE) diff --git a/nav2_constrained_smoother/CMakeLists.txt b/nav2_constrained_smoother/CMakeLists.txt index c4613f52496..6e179687f94 100644 --- a/nav2_constrained_smoother/CMakeLists.txt +++ b/nav2_constrained_smoother/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_constrained_smoother) set(CMAKE_BUILD_TYPE Release) # significant Ceres optimization speedup diff --git a/nav2_controller/CMakeLists.txt b/nav2_controller/CMakeLists.txt index 93885ef7efc..c8a655f4340 100644 --- a/nav2_controller/CMakeLists.txt +++ b/nav2_controller/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_controller) find_package(ament_cmake REQUIRED) diff --git a/nav2_core/CMakeLists.txt b/nav2_core/CMakeLists.txt index 586410c3488..1f7ae52aa05 100644 --- a/nav2_core/CMakeLists.txt +++ b/nav2_core/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_core) find_package(ament_cmake REQUIRED) diff --git a/nav2_costmap_2d/CMakeLists.txt b/nav2_costmap_2d/CMakeLists.txt index d7d74038949..00cb720aa15 100644 --- a/nav2_costmap_2d/CMakeLists.txt +++ b/nav2_costmap_2d/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_costmap_2d) find_package(ament_cmake REQUIRED) diff --git a/nav2_docking/opennav_docking/CMakeLists.txt b/nav2_docking/opennav_docking/CMakeLists.txt index a68bd4677ab..8801b18d5a8 100644 --- a/nav2_docking/opennav_docking/CMakeLists.txt +++ b/nav2_docking/opennav_docking/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(opennav_docking) find_package(ament_cmake REQUIRED) diff --git a/nav2_docking/opennav_docking_bt/CMakeLists.txt b/nav2_docking/opennav_docking_bt/CMakeLists.txt index 05e67fb713f..2d0306397aa 100644 --- a/nav2_docking/opennav_docking_bt/CMakeLists.txt +++ b/nav2_docking/opennav_docking_bt/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(opennav_docking_bt) find_package(ament_cmake REQUIRED) diff --git a/nav2_docking/opennav_docking_core/CMakeLists.txt b/nav2_docking/opennav_docking_core/CMakeLists.txt index 057bebb0b6c..e8a4f821884 100644 --- a/nav2_docking/opennav_docking_core/CMakeLists.txt +++ b/nav2_docking/opennav_docking_core/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(opennav_docking_core) find_package(ament_cmake REQUIRED) diff --git a/nav2_dwb_controller/costmap_queue/CMakeLists.txt b/nav2_dwb_controller/costmap_queue/CMakeLists.txt index 68dbfa64ef9..c330e96a9f0 100644 --- a/nav2_dwb_controller/costmap_queue/CMakeLists.txt +++ b/nav2_dwb_controller/costmap_queue/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(costmap_queue) find_package(ament_cmake REQUIRED) diff --git a/nav2_dwb_controller/dwb_core/CMakeLists.txt b/nav2_dwb_controller/dwb_core/CMakeLists.txt index 00789a46e3d..f69d546f551 100644 --- a/nav2_dwb_controller/dwb_core/CMakeLists.txt +++ b/nav2_dwb_controller/dwb_core/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(dwb_core) find_package(ament_cmake REQUIRED) diff --git a/nav2_dwb_controller/dwb_critics/CMakeLists.txt b/nav2_dwb_controller/dwb_critics/CMakeLists.txt index ddda0a9087b..be02bcc98f8 100644 --- a/nav2_dwb_controller/dwb_critics/CMakeLists.txt +++ b/nav2_dwb_controller/dwb_critics/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(dwb_critics) find_package(ament_cmake REQUIRED) diff --git a/nav2_dwb_controller/dwb_msgs/CMakeLists.txt b/nav2_dwb_controller/dwb_msgs/CMakeLists.txt index 574a48c4c6a..9ff83591d2e 100644 --- a/nav2_dwb_controller/dwb_msgs/CMakeLists.txt +++ b/nav2_dwb_controller/dwb_msgs/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(dwb_msgs) find_package(backward_ros REQUIRED) diff --git a/nav2_dwb_controller/dwb_plugins/CMakeLists.txt b/nav2_dwb_controller/dwb_plugins/CMakeLists.txt index 7d4228ae427..b462e7b3aab 100644 --- a/nav2_dwb_controller/dwb_plugins/CMakeLists.txt +++ b/nav2_dwb_controller/dwb_plugins/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(dwb_plugins) find_package(ament_cmake REQUIRED) diff --git a/nav2_dwb_controller/nav2_dwb_controller/CMakeLists.txt b/nav2_dwb_controller/nav2_dwb_controller/CMakeLists.txt index 5c1c8fce07a..3fd5af5cc2a 100644 --- a/nav2_dwb_controller/nav2_dwb_controller/CMakeLists.txt +++ b/nav2_dwb_controller/nav2_dwb_controller/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_dwb_controller) find_package(ament_cmake REQUIRED) diff --git a/nav2_dwb_controller/nav_2d_msgs/CMakeLists.txt b/nav2_dwb_controller/nav_2d_msgs/CMakeLists.txt index 155c13cdd65..f597712deda 100644 --- a/nav2_dwb_controller/nav_2d_msgs/CMakeLists.txt +++ b/nav2_dwb_controller/nav_2d_msgs/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav_2d_msgs) find_package(backward_ros REQUIRED) diff --git a/nav2_dwb_controller/nav_2d_utils/CMakeLists.txt b/nav2_dwb_controller/nav_2d_utils/CMakeLists.txt index 7aad657a142..5378f2e4b83 100644 --- a/nav2_dwb_controller/nav_2d_utils/CMakeLists.txt +++ b/nav2_dwb_controller/nav_2d_utils/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav_2d_utils) find_package(ament_cmake REQUIRED) diff --git a/nav2_graceful_controller/CMakeLists.txt b/nav2_graceful_controller/CMakeLists.txt index d9720ca6401..95d682a8cad 100644 --- a/nav2_graceful_controller/CMakeLists.txt +++ b/nav2_graceful_controller/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_graceful_controller) find_package(ament_cmake REQUIRED) diff --git a/nav2_lifecycle_manager/CMakeLists.txt b/nav2_lifecycle_manager/CMakeLists.txt index 64b1f4ad88d..50874482167 100644 --- a/nav2_lifecycle_manager/CMakeLists.txt +++ b/nav2_lifecycle_manager/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_lifecycle_manager) find_package(ament_cmake REQUIRED) diff --git a/nav2_map_server/CMakeLists.txt b/nav2_map_server/CMakeLists.txt index 55160b2f7de..a003a8fb087 100644 --- a/nav2_map_server/CMakeLists.txt +++ b/nav2_map_server/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_map_server) list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/cmake_modules) diff --git a/nav2_mppi_controller/CMakeLists.txt b/nav2_mppi_controller/CMakeLists.txt index 9dabc27507f..931af234887 100644 --- a/nav2_mppi_controller/CMakeLists.txt +++ b/nav2_mppi_controller/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_mppi_controller) find_package(ament_cmake REQUIRED) @@ -83,7 +83,13 @@ add_library(mppi_critics SHARED src/critics/twirling_critic.cpp src/critics/velocity_deadband_critic.cpp ) -target_compile_options(mppi_critics PUBLIC -fconcepts -O3) +if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND APPLE) + # Apple Clang: use C++20 and optimization, omit -fconcepts + target_compile_features(mppi_critics PUBLIC cxx_std_20) + target_compile_options(mppi_critics PUBLIC -O3) +else() + target_compile_options(mppi_critics PUBLIC -fconcepts -O3) +endif() target_include_directories(mppi_critics PUBLIC "$" diff --git a/nav2_msgs/CMakeLists.txt b/nav2_msgs/CMakeLists.txt index f01fbdba6a5..93f7998c56a 100644 --- a/nav2_msgs/CMakeLists.txt +++ b/nav2_msgs/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_msgs) find_package(ament_cmake REQUIRED) diff --git a/nav2_navfn_planner/CMakeLists.txt b/nav2_navfn_planner/CMakeLists.txt index 0da443e5e35..1b50472013f 100644 --- a/nav2_navfn_planner/CMakeLists.txt +++ b/nav2_navfn_planner/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_navfn_planner) find_package(ament_cmake REQUIRED) diff --git a/nav2_planner/CMakeLists.txt b/nav2_planner/CMakeLists.txt index d3037ddf839..0285fbe9c1a 100644 --- a/nav2_planner/CMakeLists.txt +++ b/nav2_planner/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_planner) find_package(ament_cmake REQUIRED) diff --git a/nav2_regulated_pure_pursuit_controller/CMakeLists.txt b/nav2_regulated_pure_pursuit_controller/CMakeLists.txt index 493418fda67..bec081d3987 100644 --- a/nav2_regulated_pure_pursuit_controller/CMakeLists.txt +++ b/nav2_regulated_pure_pursuit_controller/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_regulated_pure_pursuit_controller) find_package(ament_cmake REQUIRED) diff --git a/nav2_ros_common/CMakeLists.txt b/nav2_ros_common/CMakeLists.txt index 13dfd418833..4765454321d 100644 --- a/nav2_ros_common/CMakeLists.txt +++ b/nav2_ros_common/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_ros_common) find_package(ament_cmake REQUIRED) diff --git a/nav2_rotation_shim_controller/CMakeLists.txt b/nav2_rotation_shim_controller/CMakeLists.txt index 0adcf17d8f1..68b64fdc0a8 100644 --- a/nav2_rotation_shim_controller/CMakeLists.txt +++ b/nav2_rotation_shim_controller/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_rotation_shim_controller) find_package(ament_cmake REQUIRED) diff --git a/nav2_route/CMakeLists.txt b/nav2_route/CMakeLists.txt index fcc53119651..db9c139c6f1 100644 --- a/nav2_route/CMakeLists.txt +++ b/nav2_route/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_route CXX) find_package(ament_cmake REQUIRED) diff --git a/nav2_rviz_plugins/CMakeLists.txt b/nav2_rviz_plugins/CMakeLists.txt index 62e19b2f7a2..b917cbffc33 100644 --- a/nav2_rviz_plugins/CMakeLists.txt +++ b/nav2_rviz_plugins/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_rviz_plugins) find_package(ament_cmake REQUIRED) diff --git a/nav2_smac_planner/CMakeLists.txt b/nav2_smac_planner/CMakeLists.txt index 051818f7c9a..08563b3df41 100644 --- a/nav2_smac_planner/CMakeLists.txt +++ b/nav2_smac_planner/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_smac_planner) find_package(ament_cmake REQUIRED) diff --git a/nav2_smoother/CMakeLists.txt b/nav2_smoother/CMakeLists.txt index 5831d10189c..f65d18fa2f9 100644 --- a/nav2_smoother/CMakeLists.txt +++ b/nav2_smoother/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_smoother) find_package(ament_cmake REQUIRED) diff --git a/nav2_system_tests/CMakeLists.txt b/nav2_system_tests/CMakeLists.txt index 88922fc900f..9817f2f3563 100644 --- a/nav2_system_tests/CMakeLists.txt +++ b/nav2_system_tests/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_system_tests) find_package(ament_cmake REQUIRED) diff --git a/nav2_velocity_smoother/CMakeLists.txt b/nav2_velocity_smoother/CMakeLists.txt index 916ee93ed2c..f7437297d42 100644 --- a/nav2_velocity_smoother/CMakeLists.txt +++ b/nav2_velocity_smoother/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_velocity_smoother) find_package(ament_cmake REQUIRED) diff --git a/nav2_voxel_grid/CMakeLists.txt b/nav2_voxel_grid/CMakeLists.txt index 093fee48a84..e4d6d1866a8 100644 --- a/nav2_voxel_grid/CMakeLists.txt +++ b/nav2_voxel_grid/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_voxel_grid) find_package(ament_cmake REQUIRED) diff --git a/nav2_waypoint_follower/CMakeLists.txt b/nav2_waypoint_follower/CMakeLists.txt index 17d406adef9..aafd4548a5e 100644 --- a/nav2_waypoint_follower/CMakeLists.txt +++ b/nav2_waypoint_follower/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(nav2_waypoint_follower) find_package(ament_cmake REQUIRED) diff --git a/navigation2/CMakeLists.txt b/navigation2/CMakeLists.txt index 3b82e6c0027..ac8cac17ffa 100644 --- a/navigation2/CMakeLists.txt +++ b/navigation2/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) project(navigation2) find_package(ament_cmake REQUIRED) From 1856b67b0a3f4efaa1caeeaf46bf546ef8154556 Mon Sep 17 00:00:00 2001 From: Tim Clephas Date: Fri, 15 Aug 2025 18:47:03 +0200 Subject: [PATCH 26/51] Fix missing dependency (#5460) --- nav2_loopback_sim/package.xml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nav2_loopback_sim/package.xml b/nav2_loopback_sim/package.xml index 9d55a6d4837..1df96f1a27e 100644 --- a/nav2_loopback_sim/package.xml +++ b/nav2_loopback_sim/package.xml @@ -7,12 +7,13 @@ steve macenski Apache-2.0 - rclpy geometry_msgs + nav2_simple_commander nav_msgs - tf_transformations - tf2_ros python3-transforms3d + rclpy + tf2_ros + tf_transformations ament_copyright ament_flake8 From 57a639ec9b3a9def3f793918e91f7f40df3d1c7e Mon Sep 17 00:00:00 2001 From: Jad EL HAJJ Date: Tue, 19 Aug 2025 20:09:52 +0300 Subject: [PATCH 27/51] Support loading multiple behavior tree files as subtrees (#5426) * Support loading multiple behavior tree files as subtrees Signed-off-by: Jad El Hajj * Fix code style Signed-off-by: Jad El Hajj * Added default param value Signed-off-by: Jad El Hajj * Added recursive check to loadBehaviorTree and adapted unit test accordingly Signed-off-by: Jad El Hajj * Removed nested loadBehaviorTree check in navigators Signed-off-by: Jad El Hajj * Removed whitespace cpplint Signed-off-by: Jad El Hajj * Fixed goalReceived Signed-off-by: Jad El Hajj * Let loadbehaviorTree use its own search_directories var Signed-off-by: Jad El Hajj * PR fixes-format-lint and test Signed-off-by: Jad El Hajj * fix pointer Signed-off-by: Jad El Hajj * Added unit test for BT xml validity Signed-off-by: Jad El Hajj * CPPLint Signed-off-by: Jad El Hajj * Check non existent search directory for bt Signed-off-by: Jad El Hajj * CPPLint Signed-off-by: Jad El Hajj * Fixed BT tests Signed-off-by: Jad El Hajj * Fixed BT tests Signed-off-by: Jad El Hajj --------- Signed-off-by: Jad El Hajj --- .../behavior_tree_engine.hpp | 6 + .../nav2_behavior_tree/bt_action_server.hpp | 5 +- .../bt_action_server_impl.hpp | 37 ++- .../src/behavior_tree_engine.cpp | 7 + nav2_bringup/params/nav2_params.yaml | 2 + .../src/navigators/navigate_through_poses.cpp | 1 - .../src/navigators/navigate_to_pose.cpp | 1 - .../nav2_core/behavior_tree_navigator.hpp | 7 + .../behavior_tree/test_behavior_tree_node.cpp | 247 +++++++++++++----- 9 files changed, 232 insertions(+), 81 deletions(-) diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/behavior_tree_engine.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/behavior_tree_engine.hpp index 96fb5df0dce..b17a914a541 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/behavior_tree_engine.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/behavior_tree_engine.hpp @@ -98,6 +98,12 @@ class BehaviorTreeEngine */ void resetGrootMonitor(); + /** + * @brief Function to register a BT from an XML file + * @param file_path Path to BT XML file + */ + void registerTreeFromFile(const std::string & file_path); + /** * @brief Function to explicitly reset all BT nodes to initial state * @param tree Tree to halt diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/bt_action_server.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/bt_action_server.hpp index d5d1fa4c3a4..8e345abe0d7 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/bt_action_server.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/bt_action_server.hpp @@ -53,6 +53,7 @@ class BtActionServer const std::string & action_name, const std::vector & plugin_lib_names, const std::string & default_bt_xml_filename, + const std::vector & search_directories, OnGoalReceivedCallback on_goal_received_callback, OnLoopCallback on_loop_callback, OnPreemptCallback on_preempt_callback, @@ -102,7 +103,8 @@ class BtActionServer * @return bool true if the resulting BT correspond to the one in bt_xml_filename. false * if something went wrong, and previous BT is maintained */ - bool loadBehaviorTree(const std::string & bt_xml_filename = ""); + bool loadBehaviorTree( + const std::string & bt_xml_filename = ""); /** * @brief Getter function for BT Blackboard @@ -245,6 +247,7 @@ class BtActionServer // The XML file that contains the Behavior Tree to create std::string current_bt_xml_filename_; std::string default_bt_xml_filename_; + std::vector search_directories_; // The wrapper class for the BT functionality std::unique_ptr bt_; diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/bt_action_server_impl.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/bt_action_server_impl.hpp index 184f3d94aca..b8fb983f91c 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/bt_action_server_impl.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/bt_action_server_impl.hpp @@ -39,12 +39,14 @@ BtActionServer::BtActionServer( const std::string & action_name, const std::vector & plugin_lib_names, const std::string & default_bt_xml_filename, + const std::vector & search_directories, OnGoalReceivedCallback on_goal_received_callback, OnLoopCallback on_loop_callback, OnPreemptCallback on_preempt_callback, OnCompletionCallback on_completion_callback) : action_name_(action_name), default_bt_xml_filename_(default_bt_xml_filename), + search_directories_(search_directories), plugin_lib_names_(plugin_lib_names), node_(parent), on_goal_received_callback_(on_goal_received_callback), @@ -246,6 +248,8 @@ void BtActionServer::setGrootMonitoring( template bool BtActionServer::loadBehaviorTree(const std::string & bt_xml_filename) { + namespace fs = std::filesystem; + // Empty filename is default for backward compatibility auto filename = bt_xml_filename.empty() ? default_bt_xml_filename_ : bt_xml_filename; @@ -255,19 +259,38 @@ bool BtActionServer::loadBehaviorTree(const std::string & bt_xml return true; } - // if a new tree is created, than the Groot2 Publisher must be destroyed + // Reset any existing Groot2 monitoring bt_->resetGrootMonitor(); - // Read the input BT XML from the specified file into a string std::ifstream xml_file(filename); - if (!xml_file.good()) { setInternalError(ActionT::Result::FAILED_TO_LOAD_BEHAVIOR_TREE, - "Couldn't open input XML file: " + filename); + "Couldn't open BT XML file: " + filename); return false; } - // Create the Behavior Tree from the XML input + const auto canonical_main_bt = fs::canonical(filename); + + // Register all XML behavior Subtrees found in the given directories + for (const auto & directory : search_directories_) { + try { + for (const auto & entry : fs::directory_iterator(directory)) { + if (entry.path().extension() == ".xml") { + // Skip registering the main tree file + if (fs::equivalent(fs::canonical(entry.path()), canonical_main_bt)) { + continue; + } + bt_->registerTreeFromFile(entry.path().string()); + } + } + } catch (const std::exception & e) { + setInternalError(ActionT::Result::FAILED_TO_LOAD_BEHAVIOR_TREE, + "Exception reading behavior tree directory: " + std::string(e.what())); + return false; + } + } + + // Try to load the main BT tree try { tree_ = bt_->createTreeFromFile(filename, blackboard_); for (auto & subtree : tree_.subtrees) { @@ -281,15 +304,15 @@ bool BtActionServer::loadBehaviorTree(const std::string & bt_xml } } catch (const std::exception & e) { setInternalError(ActionT::Result::FAILED_TO_LOAD_BEHAVIOR_TREE, - std::string("Exception when loading BT: ") + e.what()); + std::string("Exception when creating BT tree from file: ") + e.what()); return false; } + // Optional logging and monitoring topic_logger_ = std::make_unique(client_node_, tree_); current_bt_xml_filename_ = filename; - // Enable monitoring with Groot2 if (enable_groot_monitoring_) { bt_->addGrootMonitoring(&tree_, groot_server_port_); RCLCPP_DEBUG( diff --git a/nav2_behavior_tree/src/behavior_tree_engine.cpp b/nav2_behavior_tree/src/behavior_tree_engine.cpp index 21131be010a..a2b93f7d086 100644 --- a/nav2_behavior_tree/src/behavior_tree_engine.cpp +++ b/nav2_behavior_tree/src/behavior_tree_engine.cpp @@ -102,6 +102,13 @@ BehaviorTreeEngine::createTreeFromFile( return factory_.createTreeFromFile(file_path, blackboard); } +/// @brief Register a tree from an XML file and return the tree +void BehaviorTreeEngine::registerTreeFromFile( + const std::string & file_path) +{ + factory_.registerBehaviorTreeFromFile(file_path); +} + void BehaviorTreeEngine::addGrootMonitoring( BT::Tree * tree, diff --git a/nav2_bringup/params/nav2_params.yaml b/nav2_bringup/params/nav2_params.yaml index 23e509760cc..e66d409478f 100644 --- a/nav2_bringup/params/nav2_params.yaml +++ b/nav2_bringup/params/nav2_params.yaml @@ -58,6 +58,8 @@ bt_navigator: plugin: "nav2_bt_navigator::NavigateThroughPosesNavigator" enable_groot_monitoring: false groot_server_port: 1669 + bt_search_directories: + - $(find-pkg-share nav2_bt_navigator)/behavior_trees # 'default_nav_through_poses_bt_xml' and 'default_nav_to_pose_bt_xml' are use defaults: # nav2_bt_navigator/navigate_to_pose_w_replanning_and_recovery.xml # nav2_bt_navigator/navigate_through_poses_w_replanning_and_recovery.xml diff --git a/nav2_bt_navigator/src/navigators/navigate_through_poses.cpp b/nav2_bt_navigator/src/navigators/navigate_through_poses.cpp index 1127c3ab2cd..5b8bbbc5b30 100644 --- a/nav2_bt_navigator/src/navigators/navigate_through_poses.cpp +++ b/nav2_bt_navigator/src/navigators/navigate_through_poses.cpp @@ -73,7 +73,6 @@ bool NavigateThroughPosesNavigator::goalReceived(ActionT::Goal::ConstSharedPtr goal) { auto bt_xml_filename = goal->behavior_tree; - if (!bt_action_server_->loadBehaviorTree(bt_xml_filename)) { bt_action_server_->setInternalError(ActionT::Result::FAILED_TO_LOAD_BEHAVIOR_TREE, "Error loading XML file: " + bt_xml_filename + ". Navigation canceled."); diff --git a/nav2_bt_navigator/src/navigators/navigate_to_pose.cpp b/nav2_bt_navigator/src/navigators/navigate_to_pose.cpp index 03d77451e16..a3b64ec2cc2 100644 --- a/nav2_bt_navigator/src/navigators/navigate_to_pose.cpp +++ b/nav2_bt_navigator/src/navigators/navigate_to_pose.cpp @@ -83,7 +83,6 @@ bool NavigateToPoseNavigator::goalReceived(ActionT::Goal::ConstSharedPtr goal) { auto bt_xml_filename = goal->behavior_tree; - if (!bt_action_server_->loadBehaviorTree(bt_xml_filename)) { bt_action_server_->setInternalError(ActionT::Result::FAILED_TO_LOAD_BEHAVIOR_TREE, std::string("Error loading XML file: ") + bt_xml_filename + ". Navigation canceled."); diff --git a/nav2_core/include/nav2_core/behavior_tree_navigator.hpp b/nav2_core/include/nav2_core/behavior_tree_navigator.hpp index fda64cec974..9e984bc6550 100644 --- a/nav2_core/include/nav2_core/behavior_tree_navigator.hpp +++ b/nav2_core/include/nav2_core/behavior_tree_navigator.hpp @@ -201,6 +201,12 @@ class BehaviorTreeNavigator : public NavigatorBase // get the default behavior tree for this navigator std::string default_bt_xml_filename = getDefaultBTFilepath(parent_node); + auto search_directories = node->declare_or_get_parameter( + "bt_search_directories", + std::vector{ament_index_cpp::get_package_share_directory( + "nav2_bt_navigator") + "/behavior_trees"} + ); + // Create the Behavior Tree Action Server for this navigator bt_action_server_ = std::make_unique>( @@ -208,6 +214,7 @@ class BehaviorTreeNavigator : public NavigatorBase getName(), plugin_lib_names, default_bt_xml_filename, + search_directories, std::bind(&BehaviorTreeNavigator::onGoalReceived, this, std::placeholders::_1), std::bind(&BehaviorTreeNavigator::onLoop, this), std::bind(&BehaviorTreeNavigator::onPreempt, this, std::placeholders::_1), diff --git a/nav2_system_tests/src/behavior_tree/test_behavior_tree_node.cpp b/nav2_system_tests/src/behavior_tree/test_behavior_tree_node.cpp index d36535924e0..0c23468dc45 100644 --- a/nav2_system_tests/src/behavior_tree/test_behavior_tree_node.cpp +++ b/nav2_system_tests/src/behavior_tree/test_behavior_tree_node.cpp @@ -71,55 +71,80 @@ class BehaviorTreeHandler } } - bool loadBehaviorTree(const std::string & filename) + BT::Blackboard::Ptr setBlackboardVariables() { - // Read the input BT XML from the specified file into a string - std::ifstream xml_file(filename); + // Create and populate the blackboard + blackboard = BT::Blackboard::create(); + blackboard->set("node", node_); + blackboard->set("server_timeout", std::chrono::milliseconds(20)); + blackboard->set("bt_loop_duration", std::chrono::milliseconds(10)); + blackboard->set("wait_for_service_timeout", + std::chrono::milliseconds(1000)); + blackboard->set("tf_buffer", tf_); + blackboard->set("initial_pose_received", false); + blackboard->set("number_recoveries", 0); + blackboard->set("odom_smoother", odom_smoother_); + + // Create dummy goal + geometry_msgs::msg::PoseStamped goal; + goal.header.stamp = node_->now(); + goal.header.frame_id = "map"; + blackboard->set("goal", goal); + return blackboard; + } + bool behaviorTreeFileValidation( + const std::string & filename) + { + std::ifstream xml_file(filename); if (!xml_file.good()) { - RCLCPP_ERROR(node_->get_logger(), "Couldn't open input XML file: %s", filename.c_str()); + RCLCPP_ERROR(node_->get_logger(), + "Couldn't open BT XML file: %s", filename.c_str()); return false; } + return true; + } - std::stringstream buffer; - buffer << xml_file.rdbuf(); - xml_file.close(); - std::string xml_string = buffer.str(); - // Create the blackboard that will be shared by all of the nodes in the tree - blackboard = BT::Blackboard::create(); - // Put items on the blackboard - blackboard->set("node", node_); // NOLINT - blackboard->set( - "server_timeout", std::chrono::milliseconds(20)); // NOLINT - blackboard->set( - "bt_loop_duration", std::chrono::milliseconds(10)); // NOLINT - blackboard->set( - "wait_for_service_timeout", std::chrono::milliseconds(1000)); // NOLINT - blackboard->set("tf_buffer", tf_); // NOLINT - blackboard->set("initial_pose_received", false); // NOLINT - blackboard->set("number_recoveries", 0); // NOLINT - blackboard->set("odom_smoother", odom_smoother_); // NOLINT - - // set dummy goal on blackboard - geometry_msgs::msg::PoseStamped goal; - goal.header.stamp = node_->now(); - goal.header.frame_id = "map"; - goal.pose.position.x = 0.0; - goal.pose.position.y = 0.0; - goal.pose.position.z = 0.0; - goal.pose.orientation.x = 0.0; - goal.pose.orientation.y = 0.0; - goal.pose.orientation.z = 0.0; - goal.pose.orientation.w = 1.0; + bool loadBehaviorTree( + const std::string & filename, + const std::vector & search_directories) + { + if (!behaviorTreeFileValidation(filename)) { + return false; + } + + namespace fs = std::filesystem; + const auto canonical_main_bt = fs::canonical(filename); + + // Register all XML behavior Subtrees found in the given directories + for (const auto & directory : search_directories) { + try { + for (const auto & entry : fs::directory_iterator(directory)) { + if (entry.path().extension() == ".xml") { + // Skip registering the main tree file + if (fs::equivalent(fs::canonical(entry.path()), canonical_main_bt)) { + continue; + } + factory_.registerBehaviorTreeFromFile(entry.path().string()); + } + } + } catch (const std::exception & e) { + RCLCPP_ERROR(node_->get_logger(), + "Exception reading behavior tree directory: %s", e.what()); + return false; + } + } - blackboard->set("goal", goal); // NOLINT + // Create and populate the blackboard + blackboard = setBlackboardVariables(); - // Create the Behavior Tree from the XML input + // Build the tree from the XML string try { - tree = factory_.createTreeFromText(xml_string, blackboard); + tree = factory_.createTreeFromFile(filename, blackboard); } catch (BT::RuntimeError & exp) { - RCLCPP_ERROR(node_->get_logger(), "%s: %s", filename.c_str(), exp.what()); + RCLCPP_ERROR(node_->get_logger(), "Failed to create BT from %s: %s", filename.c_str(), + exp.what()); return false; } @@ -196,19 +221,75 @@ std::shared_ptr BehaviorTreeTestFixture::bt_handler = nullp TEST_F(BehaviorTreeTestFixture, TestBTXMLFiles) { - std::filesystem::path root = ament_index_cpp::get_package_share_directory("nav2_bt_navigator"); - root /= "behavior_trees/"; - - if (std::filesystem::exists(root) && std::filesystem::is_directory(root)) { - for (auto const & entry : std::filesystem::recursive_directory_iterator(root)) { - if (std::filesystem::is_regular_file(entry) && entry.path().extension() == ".xml") { - std::cout << entry.path().string() << std::endl; - EXPECT_EQ(bt_handler->loadBehaviorTree(entry.path().string()), true); - } + // Get the BT root directory + const auto root_dir = std::filesystem::path( + ament_index_cpp::get_package_share_directory("nav2_bt_navigator") + ) / "behavior_trees"; + + ASSERT_TRUE(std::filesystem::exists(root_dir)); + ASSERT_TRUE(std::filesystem::is_directory(root_dir)); + + std::vector search_directories = {root_dir.string()}; + + for (auto const & entry : std::filesystem::recursive_directory_iterator(root_dir)) { + if (entry.is_regular_file() && entry.path().extension() == ".xml") { + std::string main_bt = entry.path().string(); + std::cout << "Testing BT file: " << main_bt << std::endl; + + EXPECT_TRUE(bt_handler->loadBehaviorTree(main_bt, search_directories)) + << "Failed to load: " << main_bt; } } } +TEST_F(BehaviorTreeTestFixture, TestWrongBTFormatXML) +{ + auto write_file = [](const std::string & path, const std::string & content) { + std::ofstream ofs(path); + ofs << content; + }; + + // File paths + std::string valid_subtree = "/tmp/valid_subtree.xml"; + std::string invalid_subtree = "/tmp/invalid_subtree.xml"; + std::string main_file = "/tmp/test_main_tree.xml"; + std::string malformed_main = "/tmp/malformed_main.xml"; + + // Valid subtree + write_file(valid_subtree, + "\n" + "\n" + " \n" + " \n" + " \n" + "\n"); + + // Invalid subtree (malformed XML) + write_file(invalid_subtree, ""); + + // Main tree referencing the valid subtree + write_file(main_file, + "\n" + "\n" + " \n" + " \n" + " \n" + "\n"); + + // Malformed main tree + write_file(malformed_main, ""); + + std::vector search_directories = {"/tmp"}; + + EXPECT_FALSE(bt_handler->loadBehaviorTree(main_file, search_directories)); + EXPECT_FALSE(bt_handler->loadBehaviorTree(malformed_main, search_directories)); + + std::remove(valid_subtree.c_str()); + std::remove(main_file.c_str()); + std::remove(invalid_subtree.c_str()); + std::remove(malformed_main.c_str()); +} + /** * Test scenario: * @@ -218,10 +299,14 @@ TEST_F(BehaviorTreeTestFixture, TestBTXMLFiles) TEST_F(BehaviorTreeTestFixture, TestAllSuccess) { // Load behavior tree from file - std::filesystem::path bt_file = ament_index_cpp::get_package_share_directory("nav2_bt_navigator"); - bt_file /= "behavior_trees/"; - bt_file /= "navigate_to_pose_w_replanning_and_recovery.xml"; - EXPECT_EQ(bt_handler->loadBehaviorTree(bt_file.string()), true); + const auto root_dir = std::filesystem::path( + ament_index_cpp::get_package_share_directory("nav2_bt_navigator") + ) / "behavior_trees"; + auto bt_file = root_dir / "navigate_to_pose_w_replanning_and_recovery.xml"; + + std::vector search_directories = {root_dir.string()}; + + EXPECT_EQ(bt_handler->loadBehaviorTree(bt_file.string(), search_directories), true); BT::NodeStatus result = BT::NodeStatus::RUNNING; @@ -265,10 +350,14 @@ TEST_F(BehaviorTreeTestFixture, TestAllSuccess) TEST_F(BehaviorTreeTestFixture, TestAllFailure) { // Load behavior tree from file - std::filesystem::path bt_file = ament_index_cpp::get_package_share_directory("nav2_bt_navigator"); - bt_file /= "behavior_trees/"; - bt_file /= "navigate_to_pose_w_replanning_and_recovery.xml"; - EXPECT_EQ(bt_handler->loadBehaviorTree(bt_file.string()), true); + const auto root_dir = std::filesystem::path( + ament_index_cpp::get_package_share_directory("nav2_bt_navigator") + ) / "behavior_trees"; + auto bt_file = root_dir / "navigate_to_pose_w_replanning_and_recovery.xml"; + + std::vector search_directories = {root_dir.string()}; + + EXPECT_EQ(bt_handler->loadBehaviorTree(bt_file.string(), search_directories), true); // Set all action server to fail the first 100 times Ranges failureRange; @@ -321,10 +410,14 @@ TEST_F(BehaviorTreeTestFixture, TestAllFailure) TEST_F(BehaviorTreeTestFixture, TestNavigateSubtreeRecoveries) { // Load behavior tree from file - std::filesystem::path bt_file = ament_index_cpp::get_package_share_directory("nav2_bt_navigator"); - bt_file /= "behavior_trees/"; - bt_file /= "navigate_to_pose_w_replanning_and_recovery.xml"; - EXPECT_EQ(bt_handler->loadBehaviorTree(bt_file.string()), true); + const auto root_dir = std::filesystem::path( + ament_index_cpp::get_package_share_directory("nav2_bt_navigator") + ) / "behavior_trees"; + auto bt_file = root_dir / "navigate_to_pose_w_replanning_and_recovery.xml"; + + std::vector search_directories = {root_dir.string()}; + + EXPECT_EQ(bt_handler->loadBehaviorTree(bt_file.string(), search_directories), true); // Set ComputePathToPose and FollowPath action servers to fail for the first action Ranges failureRange; @@ -380,10 +473,14 @@ TEST_F(BehaviorTreeTestFixture, TestNavigateSubtreeRecoveries) TEST_F(BehaviorTreeTestFixture, TestNavigateRecoverySimple) { // Load behavior tree from file - std::filesystem::path bt_file = ament_index_cpp::get_package_share_directory("nav2_bt_navigator"); - bt_file /= "behavior_trees/"; - bt_file /= "navigate_to_pose_w_replanning_and_recovery.xml"; - EXPECT_EQ(bt_handler->loadBehaviorTree(bt_file.string()), true); + const auto root_dir = std::filesystem::path( + ament_index_cpp::get_package_share_directory("nav2_bt_navigator") + ) / "behavior_trees"; + auto bt_file = root_dir / "navigate_to_pose_w_replanning_and_recovery.xml"; + + std::vector search_directories = {root_dir.string()}; + + EXPECT_EQ(bt_handler->loadBehaviorTree(bt_file.string(), search_directories), true); // Set ComputePathToPose action server to fail for the first action Ranges plannerFailureRange; @@ -478,10 +575,14 @@ TEST_F(BehaviorTreeTestFixture, TestNavigateRecoverySimple) TEST_F(BehaviorTreeTestFixture, TestNavigateRecoveryComplex) { // Load behavior tree from file - std::filesystem::path bt_file = ament_index_cpp::get_package_share_directory("nav2_bt_navigator"); - bt_file /= "behavior_trees/"; - bt_file /= "navigate_to_pose_w_replanning_and_recovery.xml"; - EXPECT_EQ(bt_handler->loadBehaviorTree(bt_file.string()), true); + const auto root_dir = std::filesystem::path( + ament_index_cpp::get_package_share_directory("nav2_bt_navigator") + ) / "behavior_trees"; + auto bt_file = root_dir / "navigate_to_pose_w_replanning_and_recovery.xml"; + + std::vector search_directories = {root_dir.string()}; + + EXPECT_EQ(bt_handler->loadBehaviorTree(bt_file.string(), search_directories), true); // Set FollowPath action server to fail for the first 2 actions Ranges controllerFailureRange; @@ -546,10 +647,14 @@ TEST_F(BehaviorTreeTestFixture, TestNavigateRecoveryComplex) TEST_F(BehaviorTreeTestFixture, TestRecoverySubtreeGoalUpdated) { // Load behavior tree from file - std::filesystem::path bt_file = ament_index_cpp::get_package_share_directory("nav2_bt_navigator"); - bt_file /= "behavior_trees/"; - bt_file /= "navigate_to_pose_w_replanning_and_recovery.xml"; - EXPECT_EQ(bt_handler->loadBehaviorTree(bt_file.string()), true); + const auto root_dir = std::filesystem::path( + ament_index_cpp::get_package_share_directory("nav2_bt_navigator") + ) / "behavior_trees"; + auto bt_file = root_dir / "navigate_to_pose_w_replanning_and_recovery.xml"; + + std::vector search_directories = {root_dir.string()}; + + EXPECT_EQ(bt_handler->loadBehaviorTree(bt_file.string(), search_directories), true); // Set FollowPath action server to fail for the first 2 actions Ranges controllerFailureRange; From 16acda895280e8b1e11db575d964bd717fb35b93 Mon Sep 17 00:00:00 2001 From: Steve Macenski Date: Tue, 19 Aug 2025 13:45:51 -0700 Subject: [PATCH 28/51] Revert "Unclamp noise velocity. (#5266)" (#5467) This reverts commit fb25b2fcc4fa41d00754a048753acb3ef6ee52d3. --- .../include/nav2_mppi_controller/motion_models.hpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/motion_models.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/motion_models.hpp index c10684b0c79..3115cc4e0a1 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/motion_models.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/motion_models.hpp @@ -89,13 +89,15 @@ class MotionModel 0).select(state.vx.col(i - 1) + max_delta_vx, state.vx.col(i - 1) - min_delta_vx); - state.vx.col(i) = state.cvx.col(i - 1) + state.cvx.col(i - 1) = state.cvx.col(i - 1) .cwiseMax(lower_bound_vx) .cwiseMin(upper_bound_vx); + state.vx.col(i) = state.cvx.col(i - 1); - state.wz.col(i) = state.cwz.col(i - 1) + state.cwz.col(i - 1) = state.cwz.col(i - 1) .cwiseMax(state.wz.col(i - 1) - max_delta_wz) .cwiseMin(state.wz.col(i - 1) + max_delta_wz); + state.wz.col(i) = state.cwz.col(i - 1); if (is_holo) { auto lower_bound_vy = (state.vy.col(i - 1) > @@ -104,10 +106,10 @@ class MotionModel auto upper_bound_vy = (state.vy.col(i - 1) > 0).select(state.vy.col(i - 1) + max_delta_vy, state.vy.col(i - 1) - min_delta_vy); - - state.vy.col(i) = state.cvy.col(i - 1) + state.cvy.col(i - 1) = state.cvy.col(i - 1) .cwiseMax(lower_bound_vy) .cwiseMin(upper_bound_vy); + state.vy.col(i) = state.cvy.col(i - 1); } } } From 8aedf20896997953239ebcc3214f04fcd0ebe9dd Mon Sep 17 00:00:00 2001 From: DavidG-Develop <147402604+DavidG-Develop@users.noreply.github.com> Date: Thu, 21 Aug 2025 17:58:25 +0200 Subject: [PATCH 29/51] fix naming for nav2_path_expiring_timer_condition_bt_node (#5471) Signed-off-by: David G --- nav2_behavior_tree/CMakeLists.txt | 4 ++-- nav2_behavior_tree/test/plugins/condition/CMakeLists.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nav2_behavior_tree/CMakeLists.txt b/nav2_behavior_tree/CMakeLists.txt index de9cde5f2ab..2004b5c62b3 100644 --- a/nav2_behavior_tree/CMakeLists.txt +++ b/nav2_behavior_tree/CMakeLists.txt @@ -123,8 +123,8 @@ list(APPEND plugin_libs nav2_is_path_valid_condition_bt_node) add_library(nav2_time_expired_condition_bt_node SHARED plugins/condition/time_expired_condition.cpp) list(APPEND plugin_libs nav2_time_expired_condition_bt_node) -add_library(nav2_path_expiring_timer_condition SHARED plugins/condition/path_expiring_timer_condition.cpp) -list(APPEND plugin_libs nav2_path_expiring_timer_condition) +add_library(nav2_path_expiring_timer_condition_bt_node SHARED plugins/condition/path_expiring_timer_condition.cpp) +list(APPEND plugin_libs nav2_path_expiring_timer_condition_bt_node) add_library(nav2_distance_traveled_condition_bt_node SHARED plugins/condition/distance_traveled_condition.cpp) list(APPEND plugin_libs nav2_distance_traveled_condition_bt_node) diff --git a/nav2_behavior_tree/test/plugins/condition/CMakeLists.txt b/nav2_behavior_tree/test/plugins/condition/CMakeLists.txt index d7d32012733..51f7a908801 100644 --- a/nav2_behavior_tree/test/plugins/condition/CMakeLists.txt +++ b/nav2_behavior_tree/test/plugins/condition/CMakeLists.txt @@ -2,7 +2,7 @@ plugin_add_test(test_condition_distance_traveled test_distance_traveled.cpp nav2 plugin_add_test(test_condition_time_expired test_time_expired.cpp nav2_time_expired_condition_bt_node) -plugin_add_test(test_condition_path_expiring_timer test_path_expiring_timer.cpp nav2_path_expiring_timer_condition) +plugin_add_test(test_condition_path_expiring_timer test_path_expiring_timer.cpp nav2_path_expiring_timer_condition_bt_node) plugin_add_test(test_condition_goal_reached test_goal_reached.cpp nav2_goal_reached_condition_bt_node) From 69a60dfacb80eb3c9c2ad47f7590eea8defbdaec Mon Sep 17 00:00:00 2001 From: Steve Macenski Date: Thu, 21 Aug 2025 12:20:37 -0700 Subject: [PATCH 30/51] fix: Move SmootherParams declaration outside smooth_path conditional (#5473) Fixes crash when dynamically changing smooth_path parameter from false to true. The issue occurred because SmootherParams were only declared when smooth_path was initially true, causing ParameterModifiedInCallbackException when trying to declare parameters within the dynamic parameter callback. Now SmootherParams are always declared, making them available for dynamic reconfiguration regardless of the initial smooth_path value. Fixes #5472 Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- nav2_smac_planner/src/smac_planner_hybrid.cpp | 4 ++-- nav2_smac_planner/src/smac_planner_lattice.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nav2_smac_planner/src/smac_planner_hybrid.cpp b/nav2_smac_planner/src/smac_planner_hybrid.cpp index 1f374af35d0..07a30839374 100644 --- a/nav2_smac_planner/src/smac_planner_hybrid.cpp +++ b/nav2_smac_planner/src/smac_planner_hybrid.cpp @@ -273,9 +273,9 @@ void SmacPlannerHybrid::configure( _angle_quantizations); // Initialize path smoother + SmootherParams params; + params.get(node, name); if (smooth_path) { - SmootherParams params; - params.get(node, name); _smoother = std::make_unique(params); _smoother->initialize(_minimum_turning_radius_global_coords); } diff --git a/nav2_smac_planner/src/smac_planner_lattice.cpp b/nav2_smac_planner/src/smac_planner_lattice.cpp index fa89e90636c..c6e110cbfd4 100644 --- a/nav2_smac_planner/src/smac_planner_lattice.cpp +++ b/nav2_smac_planner/src/smac_planner_lattice.cpp @@ -230,9 +230,9 @@ void SmacPlannerLattice::configure( _metadata.number_of_headings); // Initialize path smoother + SmootherParams params; + params.get(node, name); if (smooth_path) { - SmootherParams params; - params.get(node, name); _smoother = std::make_unique(params); _smoother->initialize(_metadata.min_turning_radius); } From 7941300c916bff276c17abc1201fdcc09c553856 Mon Sep 17 00:00:00 2001 From: Emerson Knapp <537409+emersonknapp@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:18:41 -0700 Subject: [PATCH 31/51] Return early from edge interpolation for zero-length edges (#5453) * Return early from edge interpolation for zero-length edges Signed-off-by: Emerson Knapp * Move the check and add a test Signed-off-by: Emerson Knapp --------- Signed-off-by: Emerson Knapp --- nav2_route/src/path_converter.cpp | 5 +++++ nav2_route/test/test_path_converter.cpp | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/nav2_route/src/path_converter.cpp b/nav2_route/src/path_converter.cpp index ff48773dcea..46776c5f0e9 100644 --- a/nav2_route/src/path_converter.cpp +++ b/nav2_route/src/path_converter.cpp @@ -141,6 +141,11 @@ void PathConverter::interpolateEdge( // Find number of points to populate by given density const float mag = hypotf(x1 - x0, y1 - y0); const unsigned int num_pts = ceil(mag / density_); + // For zero-length edges, we can just push the start point and return + if (num_pts < 1) { + return; + } + const float iterpolated_dist = mag / num_pts; // Find unit vector direction diff --git a/nav2_route/test/test_path_converter.cpp b/nav2_route/test/test_path_converter.cpp index 91c8ec749a1..d8af7736cc8 100644 --- a/nav2_route/test/test_path_converter.cpp +++ b/nav2_route/test/test_path_converter.cpp @@ -163,3 +163,15 @@ TEST(PathConverterTest, test_path_converter_interpolation) poses[i].pose.position.y - poses[i + 1].pose.position.y), 0.05); } } + +TEST(PathConverterTest, test_path_converter_zero_length_edge) +{ + auto node = std::make_shared("edge_scorer_test"); + PathConverter converter; + converter.configure(node); + + float x0 = 10.0, y0 = 10.0, x1 = 10.0, y1 = 10.0; + std::vector poses; + converter.interpolateEdge(x0, y0, x1, y1, poses); + ASSERT_TRUE(poses.empty()); +} From 0acacb031f53ff1a9b512766a2d464c1c2793e83 Mon Sep 17 00:00:00 2001 From: Emerson Knapp <537409+emersonknapp@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:23:43 -0700 Subject: [PATCH 32/51] nav2_route vizualization marker rendering performance improvement (#5452) * nav2_route vizualization marker use sphere_list and line_list for rendering performance Signed-off-by: Emerson Knapp * Fix unit test and break out magic constants into named values Signed-off-by: Emerson Knapp --------- Signed-off-by: Emerson Knapp --- nav2_route/include/nav2_route/utils.hpp | 158 +++++++++++------------ nav2_route/test/test_utils_and_types.cpp | 10 +- 2 files changed, 82 insertions(+), 86 deletions(-) diff --git a/nav2_route/include/nav2_route/utils.hpp b/nav2_route/include/nav2_route/utils.hpp index 9ccf65eb41e..04deda14bec 100644 --- a/nav2_route/include/nav2_route/utils.hpp +++ b/nav2_route/include/nav2_route/utils.hpp @@ -59,102 +59,90 @@ inline visualization_msgs::msg::MarkerArray toMsg( const nav2_route::Graph & graph, const std::string & frame, const rclcpp::Time & now) { visualization_msgs::msg::MarkerArray msg; - visualization_msgs::msg::Marker curr_marker; - curr_marker.header.frame_id = frame; - curr_marker.header.stamp = now; - curr_marker.action = 0; - auto getSphereSize = []() { - geometry_msgs::msg::Vector3 v_msg; - v_msg.x = 0.05; - v_msg.y = 0.05; - v_msg.z = 0.05; - return v_msg; - }; - - auto getSphereColor = []() { - std_msgs::msg::ColorRGBA c_msg; - c_msg.r = 1.0; - c_msg.g = 0.0; - c_msg.b = 0.0; - c_msg.a = 1.0; - return c_msg; - }; - - auto getLineColor = []() { - std_msgs::msg::ColorRGBA c_msg; - c_msg.r = 0.0; - c_msg.g = 1.0; - c_msg.b = 0.0; - c_msg.a = 0.5; // So bi-directional connections stand out overlapping - return c_msg; - }; - - unsigned int marker_idx = 1; - for (unsigned int i = 0; i != graph.size(); i++) { - if (graph[i].nodeid == std::numeric_limits::max()) { - continue; // Skip "deleted" nodes - } - curr_marker.ns = "route_graph"; - curr_marker.id = marker_idx++; - curr_marker.type = visualization_msgs::msg::Marker::SPHERE; - curr_marker.pose.position.x = graph[i].coords.x; - curr_marker.pose.position.y = graph[i].coords.y; - curr_marker.scale = getSphereSize(); - curr_marker.color = getSphereColor(); - msg.markers.push_back(curr_marker); - - // Add text - curr_marker.ns = "route_graph_ids"; - curr_marker.id = marker_idx++; - curr_marker.type = visualization_msgs::msg::Marker::TEXT_VIEW_FACING; - curr_marker.pose.position.x = graph[i].coords.x + 0.07; - curr_marker.pose.position.y = graph[i].coords.y; - curr_marker.text = std::to_string(graph[i].nodeid); - curr_marker.scale.z = 0.1; - msg.markers.push_back(curr_marker); - - for (unsigned int j = 0; j != graph[i].neighbors.size(); j++) { - curr_marker.ns = "route_graph"; - curr_marker.id = marker_idx++; - curr_marker.type = visualization_msgs::msg::Marker::LINE_LIST; - curr_marker.pose.position.x = 0; // Set to 0 since points are relative to this frame - curr_marker.pose.position.y = 0; // Set to 0 since points are relative to this frame - curr_marker.points.resize(2); - curr_marker.points[0].x = graph[i].coords.x; - curr_marker.points[0].y = graph[i].coords.y; - curr_marker.points[1].x = graph[i].neighbors[j].end->coords.x; - curr_marker.points[1].y = graph[i].neighbors[j].end->coords.y; - curr_marker.scale.x = 0.03; - curr_marker.color = getLineColor(); - msg.markers.push_back(curr_marker); - curr_marker.points.clear(); // Reset for next node marker - - // Add text - curr_marker.ns = "route_graph_ids"; - curr_marker.id = marker_idx++; - curr_marker.type = visualization_msgs::msg::Marker::TEXT_VIEW_FACING; - curr_marker.pose.position.x = - graph[i].coords.x + ((graph[i].neighbors[j].end->coords.x - graph[i].coords.x) / 2.0) + - 0.07; + visualization_msgs::msg::Marker nodes_marker; + nodes_marker.header.frame_id = frame; + nodes_marker.header.stamp = now; + nodes_marker.action = 0; + nodes_marker.ns = "route_graph_nodes"; + nodes_marker.type = visualization_msgs::msg::Marker::POINTS; + nodes_marker.scale.x = 0.5; + nodes_marker.scale.y = 0.5; + nodes_marker.scale.z = 0.5; + nodes_marker.color.r = 1.0; + nodes_marker.color.a = 1.0; + nodes_marker.points.reserve(graph.size()); + + visualization_msgs::msg::Marker edges_marker; + edges_marker.header.frame_id = frame; + edges_marker.header.stamp = now; + edges_marker.action = 0; + edges_marker.ns = "route_graph_edges"; + edges_marker.type = visualization_msgs::msg::Marker::LINE_LIST; + edges_marker.scale.x = 0.1; // Line width + edges_marker.color.g = 1.0; + edges_marker.color.a = 0.5; // Semi-transparent green so bidirectional connections stand out + constexpr size_t points_per_edge = 2; + // This probably under-reserves but saves some initial reallocations + constexpr size_t likely_min_edges_per_node = 2; + edges_marker.points.reserve(graph.size() * points_per_edge * likely_min_edges_per_node); + + geometry_msgs::msg::Point node_pos; + geometry_msgs::msg::Point edge_start; + geometry_msgs::msg::Point edge_end; + + visualization_msgs::msg::Marker node_id_marker; + node_id_marker.ns = "route_graph_node_ids"; + node_id_marker.type = visualization_msgs::msg::Marker::TEXT_VIEW_FACING; + node_id_marker.scale.z = 0.1; + + visualization_msgs::msg::Marker edge_id_marker; + edge_id_marker.ns = "route_graph_edge_ids"; + edge_id_marker.type = visualization_msgs::msg::Marker::TEXT_VIEW_FACING; + edge_id_marker.scale.z = 0.1; + + for (const auto & node : graph) { + node_pos.x = node.coords.x; + node_pos.y = node.coords.y; + nodes_marker.points.push_back(node_pos); + + // Add text for Node ID + node_id_marker.id++; + node_id_marker.pose.position.x = node.coords.x + 0.07; + node_id_marker.pose.position.y = node.coords.y; + node_id_marker.text = std::to_string(node.nodeid); + msg.markers.push_back(node_id_marker); + + for (const auto & neighbor : node.neighbors) { + edge_start.x = node.coords.x; + edge_start.y = node.coords.y; + edge_end.x = neighbor.end->coords.x; + edge_end.y = neighbor.end->coords.y; + edges_marker.points.push_back(edge_start); + edges_marker.points.push_back(edge_end); // Deal with overlapping bi-directional text markers by offsetting locations float y_offset = 0.0; - if (graph[i].nodeid > graph[i].neighbors[j].end->nodeid) { + if (node.nodeid > neighbor.end->nodeid) { y_offset = 0.05; } else { y_offset = -0.05; } - - curr_marker.pose.position.y = - graph[i].coords.y + ((graph[i].neighbors[j].end->coords.y - graph[i].coords.y) / 2.0) + - y_offset; - curr_marker.text = std::to_string(graph[i].neighbors[j].edgeid); - curr_marker.scale.z = 0.1; - msg.markers.push_back(curr_marker); + const float x_offset = 0.07; + + // Add text for Edge ID + edge_id_marker.id++; + edge_id_marker.pose.position.x = + node.coords.x + ((neighbor.end->coords.x - node.coords.x) / 2.0) + x_offset; + edge_id_marker.pose.position.y = + node.coords.y + ((neighbor.end->coords.y - node.coords.y) / 2.0) + y_offset; + edge_id_marker.text = std::to_string(neighbor.edgeid); + msg.markers.push_back(edge_id_marker); } } + msg.markers.push_back(nodes_marker); + msg.markers.push_back(edges_marker); return msg; } diff --git a/nav2_route/test/test_utils_and_types.cpp b/nav2_route/test/test_utils_and_types.cpp index a6b9a7e37e9..25703f95ff3 100644 --- a/nav2_route/test/test_utils_and_types.cpp +++ b/nav2_route/test/test_utils_and_types.cpp @@ -159,7 +159,15 @@ TEST(UtilsTest, test_to_visualization_msg_conversion) graph[3].addEdge(default_cost, &graph[6], ids++); auto graph_msg = utils::toMsg(graph, frame, time); - EXPECT_EQ(graph_msg.markers.size(), 34u); // 9 nodes and 8 edges (with text markers) + constexpr size_t expected_edge_markers = 1; + constexpr size_t expected_node_markers = 1; + constexpr size_t expected_edge_id_text_markers = 8; + constexpr size_t expected_node_id_text_markers = 9; + constexpr size_t expected_total_markers = + expected_edge_markers + expected_node_markers + + expected_edge_id_text_markers + expected_node_id_text_markers; + + EXPECT_EQ(graph_msg.markers.size(), expected_total_markers); for (auto & marker : graph_msg.markers) { if (marker.ns == "route_graph_ids") { EXPECT_EQ(marker.type, visualization_msgs::msg::Marker::TEXT_VIEW_FACING); From f01ee785441f73e0a713bf35e704e0eefacedc53 Mon Sep 17 00:00:00 2001 From: Adi Vardi <57910756+adivardi@users.noreply.github.com> Date: Tue, 26 Aug 2025 21:42:30 +0200 Subject: [PATCH 33/51] make Blackboard variable ID private to the navigators (#5466) Signed-off-by: Adi Vardi --- .../src/navigators/navigate_through_poses.cpp | 6 +++--- nav2_bt_navigator/src/navigators/navigate_to_pose.cpp | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/nav2_bt_navigator/src/navigators/navigate_through_poses.cpp b/nav2_bt_navigator/src/navigators/navigate_through_poses.cpp index 5b8bbbc5b30..9b8435ac4a3 100644 --- a/nav2_bt_navigator/src/navigators/navigate_through_poses.cpp +++ b/nav2_bt_navigator/src/navigators/navigate_through_poses.cpp @@ -31,11 +31,11 @@ NavigateThroughPosesNavigator::configure( auto node = parent_node.lock(); goals_blackboard_id_ = - node->declare_or_get_parameter("goals_blackboard_id", std::string("goals")); + node->declare_or_get_parameter(getName() + ".goals_blackboard_id", std::string("goals")); path_blackboard_id_ = - node->declare_or_get_parameter("path_blackboard_id", std::string("path")); + node->declare_or_get_parameter(getName() + ".path_blackboard_id", std::string("path")); waypoint_statuses_blackboard_id_ = - node->declare_or_get_parameter("waypoint_statuses_blackboard_id", + node->declare_or_get_parameter(getName() + ".waypoint_statuses_blackboard_id", std::string("waypoint_statuses")); // Odometry smoother object for getting current speed diff --git a/nav2_bt_navigator/src/navigators/navigate_to_pose.cpp b/nav2_bt_navigator/src/navigators/navigate_to_pose.cpp index a3b64ec2cc2..a9495645858 100644 --- a/nav2_bt_navigator/src/navigators/navigate_to_pose.cpp +++ b/nav2_bt_navigator/src/navigators/navigate_to_pose.cpp @@ -29,10 +29,10 @@ NavigateToPoseNavigator::configure( start_time_ = rclcpp::Time(0); auto node = parent_node.lock(); - goal_blackboard_id_ = - node->declare_or_get_parameter("goal_blackboard_id", std::string("goal")); - path_blackboard_id_ = - node->declare_or_get_parameter("path_blackboard_id", std::string("path")); + goal_blackboard_id_ = node->declare_or_get_parameter(getName() + ".goal_blackboard_id", + std::string("goal")); + path_blackboard_id_ = node->declare_or_get_parameter(getName() + ".path_blackboard_id", + std::string("path")); // Odometry smoother object for getting current speed odom_smoother_ = odom_smoother; From 76039388943b5fc35b5b6199cfdd002bcb796e9d Mon Sep 17 00:00:00 2001 From: Tony Najjar Date: Tue, 26 Aug 2025 22:03:17 +0200 Subject: [PATCH 34/51] Fix dynamic param SmacPlannerLattice (#5478) * Fix SmacPlannerLattice dynamic parameter early exit Signed-off-by: Tony Najjar * remove comment Signed-off-by: Tony Najjar --------- Signed-off-by: Tony Najjar --- nav2_smac_planner/src/node_lattice.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/nav2_smac_planner/src/node_lattice.cpp b/nav2_smac_planner/src/node_lattice.cpp index d9a7c8ebfa5..7922d353d19 100644 --- a/nav2_smac_planner/src/node_lattice.cpp +++ b/nav2_smac_planner/src/node_lattice.cpp @@ -50,23 +50,21 @@ void LatticeMotionTable::initMotionModel( unsigned int & size_x_in, SearchInfo & search_info) { - size_x = size_x_in; - - if (current_lattice_filepath == search_info.lattice_filepath) { - return; - } - size_x = size_x_in; change_penalty = search_info.change_penalty; non_straight_penalty = search_info.non_straight_penalty; cost_penalty = search_info.cost_penalty; reverse_penalty = search_info.reverse_penalty; travel_distance_reward = 1.0f - search_info.retrospective_penalty; - current_lattice_filepath = search_info.lattice_filepath; allow_reverse_expansion = search_info.allow_reverse_expansion; rotation_penalty = search_info.rotation_penalty; min_turning_radius = search_info.minimum_turning_radius; + if (current_lattice_filepath == search_info.lattice_filepath) { + return; + } + current_lattice_filepath = search_info.lattice_filepath; + // Get the metadata about this minimum control set lattice_metadata = getLatticeMetadata(current_lattice_filepath); std::ifstream latticeFile(current_lattice_filepath); From ff80727eee2bc5f3d70a849d9e821bc01eee757c Mon Sep 17 00:00:00 2001 From: mini-1235 Date: Thu, 28 Aug 2025 02:47:03 +0800 Subject: [PATCH 35/51] Replace last pose if only orientation differs in Navfn (#5490) Signed-off-by: mini-1235 --- nav2_navfn_planner/src/navfn_planner.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/nav2_navfn_planner/src/navfn_planner.cpp b/nav2_navfn_planner/src/navfn_planner.cpp index fab5d3f0fcf..dd5ac3f3e5f 100644 --- a/nav2_navfn_planner/src/navfn_planner.cpp +++ b/nav2_navfn_planner/src/navfn_planner.cpp @@ -353,11 +353,11 @@ NavfnPlanner::smoothApproachToGoal( const geometry_msgs::msg::Pose & goal, nav_msgs::msg::Path & plan) { - // Replace the last pose of the computed path if it's actually further away - // to the second to last pose than the goal pose. if (plan.poses.size() >= 2) { auto second_to_last_pose = plan.poses.end()[-2]; auto last_pose = plan.poses.back(); + // Replace the last pose of the computed path if it's actually further away + // to the second to last pose than the goal pose. if ( squared_distance(last_pose.pose, second_to_last_pose.pose) > squared_distance(goal, second_to_last_pose.pose)) @@ -365,6 +365,11 @@ NavfnPlanner::smoothApproachToGoal( plan.poses.back().pose = goal; return; } + // Replace the last pose of the computed path if its position matches but orientation differs + if (squared_distance(last_pose.pose, goal) < 1e-6) { + plan.poses.back().pose = goal; + return; + } } geometry_msgs::msg::PoseStamped goal_copy; goal_copy.pose = goal; From c057c954a343af2a337b61d5da464c53e9fc9f58 Mon Sep 17 00:00:00 2001 From: Tony Najjar Date: Wed, 27 Aug 2025 23:05:41 +0200 Subject: [PATCH 36/51] Fix duplicate poses with computePlanThroughPoses (#5488) * fix-duplicate-poses Signed-off-by: Tony Najjar * Update nav2_planner/src/planner_server.cpp Co-authored-by: Steve Macenski Signed-off-by: Tony Najjar --------- Signed-off-by: Tony Najjar Co-authored-by: Steve Macenski --- nav2_planner/src/planner_server.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/nav2_planner/src/planner_server.cpp b/nav2_planner/src/planner_server.cpp index 81752e25644..998220d6533 100644 --- a/nav2_planner/src/planner_server.cpp +++ b/nav2_planner/src/planner_server.cpp @@ -428,9 +428,17 @@ void PlannerServer::computePlanThroughPoses() throw nav2_core::NoValidPathCouldBeFound(goal->planner_id + " generated a empty path"); } - // Concatenate paths together - concat_path.poses.insert( - concat_path.poses.end(), curr_path.poses.begin(), curr_path.poses.end()); + // Concatenate paths together, but skip the first pose of subsequent paths + // to avoid duplicating the connection point + if (i == 0) { + // First path: add all poses + concat_path.poses.insert( + concat_path.poses.end(), curr_path.poses.begin(), curr_path.poses.end()); + } else if (curr_path.poses.size() > 1) { + // Subsequent paths: skip the first pose to avoid duplication + concat_path.poses.insert( + concat_path.poses.end(), curr_path.poses.begin() + 1, curr_path.poses.end()); + } concat_path.header = curr_path.header; } From 2b8cc4d140ce12469f0828d9647002c88e831e7e Mon Sep 17 00:00:00 2001 From: mini-1235 Date: Thu, 28 Aug 2025 05:08:17 +0800 Subject: [PATCH 37/51] Revert double spin (#5477) * Revert "Add double spin_some in some BT nodes (#5055)" This reverts commit 4e8469e5c9b33c8cf987ee8e83e70aefcbb34eba. Signed-off-by: mini-1235 * Update spin some to use spin all Signed-off-by: mini-1235 * Replace 50 ms with bt loop duration Signed-off-by: mini-1235 --------- Signed-off-by: mini-1235 --- .../plugins/action/controller_selector_node.hpp | 2 ++ .../plugins/action/goal_checker_selector_node.hpp | 2 ++ .../plugins/action/planner_selector_node.hpp | 2 ++ .../plugins/action/progress_checker_selector_node.hpp | 2 ++ .../plugins/action/smoother_selector_node.hpp | 2 ++ .../plugins/condition/is_battery_charging_condition.hpp | 2 ++ .../plugins/condition/is_battery_low_condition.hpp | 2 ++ .../plugins/decorator/goal_updater_node.hpp | 2 ++ .../plugins/action/controller_selector_node.cpp | 7 +++---- .../plugins/action/goal_checker_selector_node.cpp | 4 +++- .../plugins/action/planner_selector_node.cpp | 7 +++---- .../plugins/action/progress_checker_selector_node.cpp | 4 +++- .../plugins/action/smoother_selector_node.cpp | 7 +++---- .../plugins/condition/is_battery_charging_condition.cpp | 7 +++---- .../plugins/condition/is_battery_low_condition.cpp | 7 +++---- nav2_behavior_tree/plugins/decorator/goal_updater_node.cpp | 7 +++---- .../test/plugins/action/test_controller_selector_node.cpp | 3 +++ .../plugins/action/test_goal_checker_selector_node.cpp | 3 +++ .../test/plugins/action/test_planner_selector_node.cpp | 3 +++ .../plugins/action/test_progress_checker_selector_node.cpp | 3 +++ .../test/plugins/action/test_smoother_selector_node.cpp | 3 +++ .../test/plugins/condition/test_is_battery_charging.cpp | 3 +++ .../test/plugins/condition/test_is_battery_low.cpp | 3 +++ .../test/plugins/decorator/test_goal_updater_node.cpp | 3 +++ nav2_route/test/test_graph_loader.cpp | 1 - nav2_route/test/test_graph_saver.cpp | 1 - 26 files changed, 64 insertions(+), 28 deletions(-) diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/controller_selector_node.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/controller_selector_node.hpp index ebc29fa9721..cd52705f197 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/controller_selector_node.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/controller_selector_node.hpp @@ -18,6 +18,7 @@ #include #include +#include #include "std_msgs/msg/string.hpp" @@ -102,6 +103,7 @@ class ControllerSelector : public BT::SyncActionNode rclcpp::executors::SingleThreadedExecutor callback_group_executor_; std::string topic_name_; + std::chrono::milliseconds bt_loop_duration_; }; } // namespace nav2_behavior_tree diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/goal_checker_selector_node.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/goal_checker_selector_node.hpp index 820a1a7f71f..0b9d9f6b43c 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/goal_checker_selector_node.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/goal_checker_selector_node.hpp @@ -18,6 +18,7 @@ #include #include +#include #include "std_msgs/msg/string.hpp" @@ -102,6 +103,7 @@ class GoalCheckerSelector : public BT::SyncActionNode rclcpp::executors::SingleThreadedExecutor callback_group_executor_; std::string topic_name_; + std::chrono::milliseconds bt_loop_duration_; }; } // namespace nav2_behavior_tree diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/planner_selector_node.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/planner_selector_node.hpp index 07026ac64f9..269ad96b13b 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/planner_selector_node.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/planner_selector_node.hpp @@ -18,6 +18,7 @@ #include #include +#include #include "std_msgs/msg/string.hpp" @@ -103,6 +104,7 @@ class PlannerSelector : public BT::SyncActionNode rclcpp::executors::SingleThreadedExecutor callback_group_executor_; std::string topic_name_; + std::chrono::milliseconds bt_loop_duration_; }; } // namespace nav2_behavior_tree diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/progress_checker_selector_node.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/progress_checker_selector_node.hpp index c58f615dbf7..3ff8e397313 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/progress_checker_selector_node.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/progress_checker_selector_node.hpp @@ -17,6 +17,7 @@ #include #include +#include #include "std_msgs/msg/string.hpp" @@ -101,6 +102,7 @@ class ProgressCheckerSelector : public BT::SyncActionNode rclcpp::executors::SingleThreadedExecutor callback_group_executor_; std::string topic_name_; + std::chrono::milliseconds bt_loop_duration_; }; } // namespace nav2_behavior_tree diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/smoother_selector_node.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/smoother_selector_node.hpp index f752fafb913..6d438262a58 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/smoother_selector_node.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/smoother_selector_node.hpp @@ -18,6 +18,7 @@ #include #include +#include #include "std_msgs/msg/string.hpp" @@ -102,6 +103,7 @@ class SmootherSelector : public BT::SyncActionNode rclcpp::executors::SingleThreadedExecutor callback_group_executor_; std::string topic_name_; + std::chrono::milliseconds bt_loop_duration_; }; } // namespace nav2_behavior_tree diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/is_battery_charging_condition.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/is_battery_charging_condition.hpp index 2c95e1e880e..d456d8092c1 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/is_battery_charging_condition.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/is_battery_charging_condition.hpp @@ -18,6 +18,7 @@ #include #include #include +#include #include "nav2_ros_common/lifecycle_node.hpp" #include "sensor_msgs/msg/battery_state.hpp" @@ -85,6 +86,7 @@ class IsBatteryChargingCondition : public BT::ConditionNode nav2::Subscription::SharedPtr battery_sub_; std::string battery_topic_; bool is_battery_charging_; + std::chrono::milliseconds bt_loop_duration_; }; } // namespace nav2_behavior_tree diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/is_battery_low_condition.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/is_battery_low_condition.hpp index d66eb5eea72..de31aa1c4e0 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/is_battery_low_condition.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/is_battery_low_condition.hpp @@ -19,6 +19,7 @@ #include #include #include +#include #include "nav2_ros_common/lifecycle_node.hpp" #include "sensor_msgs/msg/battery_state.hpp" @@ -93,6 +94,7 @@ class IsBatteryLowCondition : public BT::ConditionNode double min_battery_; bool is_voltage_; bool is_battery_low_; + std::chrono::milliseconds bt_loop_duration_; }; } // namespace nav2_behavior_tree diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/decorator/goal_updater_node.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/decorator/goal_updater_node.hpp index 2c6d69895e6..61a909c53c6 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/decorator/goal_updater_node.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/decorator/goal_updater_node.hpp @@ -19,6 +19,7 @@ #include #include +#include #include "behaviortree_cpp/decorator_node.h" #include "behaviortree_cpp/json_export.h" @@ -110,6 +111,7 @@ class GoalUpdater : public BT::DecoratorNode rclcpp::executors::SingleThreadedExecutor callback_group_executor_; std::string goal_updater_topic_; std::string goals_updater_topic_; + std::chrono::milliseconds bt_loop_duration_; }; } // namespace nav2_behavior_tree diff --git a/nav2_behavior_tree/plugins/action/controller_selector_node.cpp b/nav2_behavior_tree/plugins/action/controller_selector_node.cpp index 860f1ed12de..eee98bd7861 100644 --- a/nav2_behavior_tree/plugins/action/controller_selector_node.cpp +++ b/nav2_behavior_tree/plugins/action/controller_selector_node.cpp @@ -33,9 +33,8 @@ ControllerSelector::ControllerSelector( : BT::SyncActionNode(name, conf) { initialize(); - - // Spin multiple times due to rclcpp regression in Jazzy requiring a 'warm up' spin - callback_group_executor_.spin_some(std::chrono::nanoseconds(1)); + bt_loop_duration_ = + config().blackboard->template get("bt_loop_duration"); } void ControllerSelector::initialize() @@ -69,7 +68,7 @@ BT::NodeStatus ControllerSelector::tick() initialize(); } - callback_group_executor_.spin_some(); + callback_group_executor_.spin_all(bt_loop_duration_); // This behavior always use the last selected controller received from the topic input. // When no input is specified it uses the default controller. diff --git a/nav2_behavior_tree/plugins/action/goal_checker_selector_node.cpp b/nav2_behavior_tree/plugins/action/goal_checker_selector_node.cpp index 20df1bcca9c..0b1fbae15c8 100644 --- a/nav2_behavior_tree/plugins/action/goal_checker_selector_node.cpp +++ b/nav2_behavior_tree/plugins/action/goal_checker_selector_node.cpp @@ -33,6 +33,8 @@ GoalCheckerSelector::GoalCheckerSelector( : BT::SyncActionNode(name, conf) { initialize(); + bt_loop_duration_ = + config().blackboard->template get("bt_loop_duration"); } void GoalCheckerSelector::initialize() @@ -66,7 +68,7 @@ BT::NodeStatus GoalCheckerSelector::tick() initialize(); } - callback_group_executor_.spin_some(); + callback_group_executor_.spin_all(bt_loop_duration_); // This behavior always use the last selected goal checker received from the topic input. // When no input is specified it uses the default goal checker. diff --git a/nav2_behavior_tree/plugins/action/planner_selector_node.cpp b/nav2_behavior_tree/plugins/action/planner_selector_node.cpp index 6dd1806d67a..8dc248d032e 100644 --- a/nav2_behavior_tree/plugins/action/planner_selector_node.cpp +++ b/nav2_behavior_tree/plugins/action/planner_selector_node.cpp @@ -33,9 +33,8 @@ PlannerSelector::PlannerSelector( : BT::SyncActionNode(name, conf) { initialize(); - - // Spin multiple times due to rclcpp regression in Jazzy requiring a 'warm up' spin - callback_group_executor_.spin_some(std::chrono::nanoseconds(1)); + bt_loop_duration_ = + config().blackboard->template get("bt_loop_duration"); } void PlannerSelector::initialize() @@ -69,7 +68,7 @@ BT::NodeStatus PlannerSelector::tick() initialize(); } - callback_group_executor_.spin_some(); + callback_group_executor_.spin_all(bt_loop_duration_); // This behavior always use the last selected planner received from the topic input. // When no input is specified it uses the default planner. diff --git a/nav2_behavior_tree/plugins/action/progress_checker_selector_node.cpp b/nav2_behavior_tree/plugins/action/progress_checker_selector_node.cpp index 71d0b308e2a..458c0253e41 100644 --- a/nav2_behavior_tree/plugins/action/progress_checker_selector_node.cpp +++ b/nav2_behavior_tree/plugins/action/progress_checker_selector_node.cpp @@ -32,6 +32,8 @@ ProgressCheckerSelector::ProgressCheckerSelector( : BT::SyncActionNode(name, conf) { initialize(); + bt_loop_duration_ = + config().blackboard->template get("bt_loop_duration"); } void ProgressCheckerSelector::initialize() @@ -65,7 +67,7 @@ BT::NodeStatus ProgressCheckerSelector::tick() initialize(); } - callback_group_executor_.spin_some(); + callback_group_executor_.spin_all(bt_loop_duration_); // This behavior always use the last selected progress checker received from the topic input. // When no input is specified it uses the default goaprogressl checker. diff --git a/nav2_behavior_tree/plugins/action/smoother_selector_node.cpp b/nav2_behavior_tree/plugins/action/smoother_selector_node.cpp index 1e7768e7020..7c17a97561f 100644 --- a/nav2_behavior_tree/plugins/action/smoother_selector_node.cpp +++ b/nav2_behavior_tree/plugins/action/smoother_selector_node.cpp @@ -34,9 +34,8 @@ SmootherSelector::SmootherSelector( : BT::SyncActionNode(name, conf) { initialize(); - - // Spin multiple times due to rclcpp regression in Jazzy requiring a 'warm up' spin - callback_group_executor_.spin_some(std::chrono::nanoseconds(1)); + bt_loop_duration_ = + config().blackboard->template get("bt_loop_duration"); } void SmootherSelector::initialize() @@ -70,7 +69,7 @@ BT::NodeStatus SmootherSelector::tick() initialize(); } - callback_group_executor_.spin_some(); + callback_group_executor_.spin_all(bt_loop_duration_); // This behavior always use the last selected smoother received from the topic input. // When no input is specified it uses the default smoother. diff --git a/nav2_behavior_tree/plugins/condition/is_battery_charging_condition.cpp b/nav2_behavior_tree/plugins/condition/is_battery_charging_condition.cpp index 571cf10d64f..dab7e019dd0 100644 --- a/nav2_behavior_tree/plugins/condition/is_battery_charging_condition.cpp +++ b/nav2_behavior_tree/plugins/condition/is_battery_charging_condition.cpp @@ -27,9 +27,8 @@ IsBatteryChargingCondition::IsBatteryChargingCondition( is_battery_charging_(false) { initialize(); - - // Spin multiple times due to rclcpp regression in Jazzy requiring a 'warm up' spin - callback_group_executor_.spin_some(std::chrono::nanoseconds(1)); + bt_loop_duration_ = + config().blackboard->template get("bt_loop_duration"); } void IsBatteryChargingCondition::initialize() @@ -65,7 +64,7 @@ BT::NodeStatus IsBatteryChargingCondition::tick() initialize(); } - callback_group_executor_.spin_some(); + callback_group_executor_.spin_all(bt_loop_duration_); if (is_battery_charging_) { return BT::NodeStatus::SUCCESS; } diff --git a/nav2_behavior_tree/plugins/condition/is_battery_low_condition.cpp b/nav2_behavior_tree/plugins/condition/is_battery_low_condition.cpp index 628b31145b4..57898da658b 100644 --- a/nav2_behavior_tree/plugins/condition/is_battery_low_condition.cpp +++ b/nav2_behavior_tree/plugins/condition/is_battery_low_condition.cpp @@ -30,9 +30,8 @@ IsBatteryLowCondition::IsBatteryLowCondition( is_battery_low_(false) { initialize(); - - // Spin multiple times due to rclcpp regression in Jazzy requiring a 'warm up' spin - callback_group_executor_.spin_some(std::chrono::nanoseconds(1)); + bt_loop_duration_ = + config().blackboard->template get("bt_loop_duration"); } void IsBatteryLowCondition::initialize() @@ -71,7 +70,7 @@ BT::NodeStatus IsBatteryLowCondition::tick() initialize(); } - callback_group_executor_.spin_some(); + callback_group_executor_.spin_all(bt_loop_duration_); if (is_battery_low_) { return BT::NodeStatus::SUCCESS; } diff --git a/nav2_behavior_tree/plugins/decorator/goal_updater_node.cpp b/nav2_behavior_tree/plugins/decorator/goal_updater_node.cpp index 2cf6031ce19..d5b67155d30 100644 --- a/nav2_behavior_tree/plugins/decorator/goal_updater_node.cpp +++ b/nav2_behavior_tree/plugins/decorator/goal_updater_node.cpp @@ -36,9 +36,8 @@ GoalUpdater::GoalUpdater( goals_updater_topic_("goals_update") { initialize(); - - // Spin multiple times due to rclcpp regression in Jazzy requiring a 'warm up' spin - callback_group_executor_.spin_all(std::chrono::milliseconds(1)); + bt_loop_duration_ = + config().blackboard->template get("bt_loop_duration"); } void GoalUpdater::initialize() @@ -91,7 +90,7 @@ inline BT::NodeStatus GoalUpdater::tick() getInput("input_goal", goal); getInput("input_goals", goals); - callback_group_executor_.spin_all(std::chrono::milliseconds(49)); + callback_group_executor_.spin_all(bt_loop_duration_); if (last_goal_received_set_) { if (last_goal_received_.header.stamp == rclcpp::Time(0)) { diff --git a/nav2_behavior_tree/test/plugins/action/test_controller_selector_node.cpp b/nav2_behavior_tree/test/plugins/action/test_controller_selector_node.cpp index ec1abd18e59..9f686ad2ed4 100644 --- a/nav2_behavior_tree/test/plugins/action/test_controller_selector_node.cpp +++ b/nav2_behavior_tree/test/plugins/action/test_controller_selector_node.cpp @@ -44,6 +44,9 @@ class ControllerSelectorTestFixture : public ::testing::Test config_->blackboard = BT::Blackboard::create(); // Put items on the blackboard config_->blackboard->set("node", node_); + config_->blackboard->set( + "bt_loop_duration", + std::chrono::milliseconds(10)); BT::NodeBuilder builder = [](const std::string & name, const BT::NodeConfiguration & config) { return std::make_unique(name, config); diff --git a/nav2_behavior_tree/test/plugins/action/test_goal_checker_selector_node.cpp b/nav2_behavior_tree/test/plugins/action/test_goal_checker_selector_node.cpp index fb4a40c0ffc..0067a5bd7ed 100644 --- a/nav2_behavior_tree/test/plugins/action/test_goal_checker_selector_node.cpp +++ b/nav2_behavior_tree/test/plugins/action/test_goal_checker_selector_node.cpp @@ -44,6 +44,9 @@ class GoalCheckerSelectorTestFixture : public ::testing::Test config_->blackboard = BT::Blackboard::create(); // Put items on the blackboard config_->blackboard->set("node", node_); + config_->blackboard->set( + "bt_loop_duration", + std::chrono::milliseconds(10)); BT::NodeBuilder builder = [](const std::string & name, const BT::NodeConfiguration & config) { return std::make_unique(name, config); diff --git a/nav2_behavior_tree/test/plugins/action/test_planner_selector_node.cpp b/nav2_behavior_tree/test/plugins/action/test_planner_selector_node.cpp index 3308abe5efa..7043d193789 100644 --- a/nav2_behavior_tree/test/plugins/action/test_planner_selector_node.cpp +++ b/nav2_behavior_tree/test/plugins/action/test_planner_selector_node.cpp @@ -44,6 +44,9 @@ class PlannerSelectorTestFixture : public ::testing::Test config_->blackboard = BT::Blackboard::create(); // Put items on the blackboard config_->blackboard->set("node", node_); + config_->blackboard->set( + "bt_loop_duration", + std::chrono::milliseconds(10)); BT::NodeBuilder builder = [](const std::string & name, const BT::NodeConfiguration & config) { return std::make_unique(name, config); diff --git a/nav2_behavior_tree/test/plugins/action/test_progress_checker_selector_node.cpp b/nav2_behavior_tree/test/plugins/action/test_progress_checker_selector_node.cpp index c7c70e742e9..eebb6c193b4 100644 --- a/nav2_behavior_tree/test/plugins/action/test_progress_checker_selector_node.cpp +++ b/nav2_behavior_tree/test/plugins/action/test_progress_checker_selector_node.cpp @@ -43,6 +43,9 @@ class ProgressCheckerSelectorTestFixture : public ::testing::Test config_->blackboard = BT::Blackboard::create(); // Put items on the blackboard config_->blackboard->set("node", node_); + config_->blackboard->set( + "bt_loop_duration", + std::chrono::milliseconds(10)); BT::NodeBuilder builder = [](const std::string & name, const BT::NodeConfiguration & config) { return std::make_unique(name, config); diff --git a/nav2_behavior_tree/test/plugins/action/test_smoother_selector_node.cpp b/nav2_behavior_tree/test/plugins/action/test_smoother_selector_node.cpp index 002610472f4..50238346741 100644 --- a/nav2_behavior_tree/test/plugins/action/test_smoother_selector_node.cpp +++ b/nav2_behavior_tree/test/plugins/action/test_smoother_selector_node.cpp @@ -44,6 +44,9 @@ class SmootherSelectorTestFixture : public ::testing::Test config_->blackboard = BT::Blackboard::create(); // Put items on the blackboard config_->blackboard->set("node", node_); + config_->blackboard->set( + "bt_loop_duration", + std::chrono::milliseconds(10)); BT::NodeBuilder builder = [](const std::string & name, const BT::NodeConfiguration & config) { return std::make_unique(name, config); diff --git a/nav2_behavior_tree/test/plugins/condition/test_is_battery_charging.cpp b/nav2_behavior_tree/test/plugins/condition/test_is_battery_charging.cpp index 18aaf99f8ec..fb27568c275 100644 --- a/nav2_behavior_tree/test/plugins/condition/test_is_battery_charging.cpp +++ b/nav2_behavior_tree/test/plugins/condition/test_is_battery_charging.cpp @@ -39,6 +39,9 @@ class IsBatteryChargingConditionTestFixture : public ::testing::Test config_->blackboard->set( "node", node_); + config_->blackboard->set( + "bt_loop_duration", + std::chrono::milliseconds(10)); factory_->registerNodeType("IsBatteryCharging"); diff --git a/nav2_behavior_tree/test/plugins/condition/test_is_battery_low.cpp b/nav2_behavior_tree/test/plugins/condition/test_is_battery_low.cpp index 0af03b9afaa..a5080593117 100644 --- a/nav2_behavior_tree/test/plugins/condition/test_is_battery_low.cpp +++ b/nav2_behavior_tree/test/plugins/condition/test_is_battery_low.cpp @@ -40,6 +40,9 @@ class IsBatteryLowConditionTestFixture : public ::testing::Test config_->blackboard->set( "node", node_); + config_->blackboard->set( + "bt_loop_duration", + std::chrono::milliseconds(10)); factory_->registerNodeType("IsBatteryLow"); diff --git a/nav2_behavior_tree/test/plugins/decorator/test_goal_updater_node.cpp b/nav2_behavior_tree/test/plugins/decorator/test_goal_updater_node.cpp index 1766dfbd418..33cfeffc5e7 100644 --- a/nav2_behavior_tree/test/plugins/decorator/test_goal_updater_node.cpp +++ b/nav2_behavior_tree/test/plugins/decorator/test_goal_updater_node.cpp @@ -43,6 +43,9 @@ class GoalUpdaterTestFixture : public ::testing::Test config_->blackboard->set( "node", node_); + config_->blackboard->set( + "bt_loop_duration", + std::chrono::milliseconds(10)); BT::NodeBuilder builder = [](const std::string & name, const BT::NodeConfiguration & config) diff --git a/nav2_route/test/test_graph_loader.cpp b/nav2_route/test/test_graph_loader.cpp index e76fe863314..84b544a1be5 100644 --- a/nav2_route/test/test_graph_loader.cpp +++ b/nav2_route/test/test_graph_loader.cpp @@ -107,7 +107,6 @@ TEST(GraphLoader, test_transformation_api) tf_broadcaster->sendTransform(transform); rclcpp::Rate(1).sleep(); tf_broadcaster->sendTransform(transform); - rclcpp::spin_all(node->get_node_base_interface(), std::chrono::milliseconds(1)); rclcpp::spin_all(node->get_node_base_interface(), std::chrono::milliseconds(50)); graph[0].coords.frame_id = "map_test"; diff --git a/nav2_route/test/test_graph_saver.cpp b/nav2_route/test/test_graph_saver.cpp index 4abdc0b74d3..bd69a9bc112 100644 --- a/nav2_route/test/test_graph_saver.cpp +++ b/nav2_route/test/test_graph_saver.cpp @@ -143,7 +143,6 @@ TEST(GraphSaver, test_transformation_api) tf_broadcaster->sendTransform(transform); rclcpp::Rate(1).sleep(); tf_broadcaster->sendTransform(transform); - rclcpp::spin_all(node->get_node_base_interface(), std::chrono::milliseconds(1)); rclcpp::spin_all(node->get_node_base_interface(), std::chrono::milliseconds(50)); GraphSaver graph_saver(node, tf, frame); From 3c081db0cf7508451cf10651be79b4566729a40d Mon Sep 17 00:00:00 2001 From: mini-1235 Date: Fri, 29 Aug 2025 01:27:34 +0800 Subject: [PATCH 38/51] Add custom window size and poly order for SG filter (#5489) * Add custom window size and poly order in SG filter Signed-off-by: mini-1235 * Fix linting and benchmark tool Signed-off-by: mini-1235 * Cmakelist Signed-off-by: mini-1235 * Adding comments Signed-off-by: mini-1235 * Add blog Signed-off-by: mini-1235 --------- Signed-off-by: mini-1235 --- nav2_smoother/CMakeLists.txt | 3 + .../nav2_smoother/savitzky_golay_smoother.hpp | 9 +- nav2_smoother/package.xml | 2 + nav2_smoother/src/savitzky_golay_smoother.cpp | 95 ++++++++++--------- .../smoother_benchmark_bringup.py | 9 ++ 5 files changed, 72 insertions(+), 46 deletions(-) diff --git a/nav2_smoother/CMakeLists.txt b/nav2_smoother/CMakeLists.txt index f65d18fa2f9..a48a7166d45 100644 --- a/nav2_smoother/CMakeLists.txt +++ b/nav2_smoother/CMakeLists.txt @@ -16,6 +16,7 @@ find_package(rclcpp_lifecycle REQUIRED) find_package(tf2 REQUIRED) find_package(tf2_ros REQUIRED) find_package(nav2_ros_common REQUIRED) +find_package(Eigen3 REQUIRED) nav2_package() @@ -42,6 +43,7 @@ target_link_libraries(${library_name} PUBLIC rclcpp_lifecycle::rclcpp_lifecycle tf2_ros::tf2_ros nav2_ros_common::nav2_ros_common + Eigen3::Eigen ) target_link_libraries(${library_name} PRIVATE rclcpp_components::component @@ -157,6 +159,7 @@ ament_export_dependencies( rclcpp_lifecycle tf2 tf2_ros + Eigen3 ) ament_export_targets(${library_name}) ament_package() diff --git a/nav2_smoother/include/nav2_smoother/savitzky_golay_smoother.hpp b/nav2_smoother/include/nav2_smoother/savitzky_golay_smoother.hpp index e18c0b823e5..1f56e8440e5 100644 --- a/nav2_smoother/include/nav2_smoother/savitzky_golay_smoother.hpp +++ b/nav2_smoother/include/nav2_smoother/savitzky_golay_smoother.hpp @@ -15,6 +15,7 @@ #ifndef NAV2_SMOOTHER__SAVITZKY_GOLAY_SMOOTHER_HPP_ #define NAV2_SMOOTHER__SAVITZKY_GOLAY_SMOOTHER_HPP_ +#include #include #include #include @@ -85,6 +86,11 @@ class SavitzkyGolaySmoother : public nav2_core::Smoother nav_msgs::msg::Path & path, const rclcpp::Duration & max_time) override; + /** + * @brief Method to calculate SavitzkyGolay Coefficients + */ + void calculateCoefficients(); + protected: /** * @brief Smoother method - does the smoothing on a segment @@ -99,7 +105,8 @@ class SavitzkyGolaySmoother : public nav2_core::Smoother bool & reversing_segment); bool do_refinement_, enforce_path_inversion_; - int refinement_num_; + int refinement_num_, window_size_, half_window_size_, poly_order_; + Eigen::VectorXd sg_coeffs_; rclcpp::Logger logger_{rclcpp::get_logger("SGSmoother")}; }; diff --git a/nav2_smoother/package.xml b/nav2_smoother/package.xml index 339bdeb5586..f193809becc 100644 --- a/nav2_smoother/package.xml +++ b/nav2_smoother/package.xml @@ -24,6 +24,8 @@ tf2 tf2_ros nav2_ros_common + eigen3_cmake_module + eigen ament_cmake_gtest ament_index_cpp diff --git a/nav2_smoother/src/savitzky_golay_smoother.cpp b/nav2_smoother/src/savitzky_golay_smoother.cpp index af23cf30995..90180734e5c 100644 --- a/nav2_smoother/src/savitzky_golay_smoother.cpp +++ b/nav2_smoother/src/savitzky_golay_smoother.cpp @@ -40,9 +40,39 @@ void SavitzkyGolaySmoother::configure( node, name + ".refinement_num", rclcpp::ParameterValue(2)); declare_parameter_if_not_declared( node, name + ".enforce_path_inversion", rclcpp::ParameterValue(true)); + declare_parameter_if_not_declared( + node, name + ".window_size", rclcpp::ParameterValue(7)); + declare_parameter_if_not_declared( + node, name + ".poly_order", rclcpp::ParameterValue(3)); node->get_parameter(name + ".do_refinement", do_refinement_); node->get_parameter(name + ".refinement_num", refinement_num_); node->get_parameter(name + ".enforce_path_inversion", enforce_path_inversion_); + node->get_parameter(name + ".window_size", window_size_); + node->get_parameter(name + ".poly_order", poly_order_); + if (window_size_ % 2 == 0 || window_size_ <= 2) { + throw nav2_core::SmootherException( + "Savitzky-Golay Smoother requires an odd window size of 3 or greater"); + } + half_window_size_ = (window_size_ - 1) / 2; + calculateCoefficients(); +} + +// For more details on calculating Savitzky–Golay filter coefficients, +// see: https://www.colmryan.org/posts/savitsky_golay/ +void SavitzkyGolaySmoother::calculateCoefficients() +{ + // We construct the Vandermonde matrix here + Eigen::VectorXd v = Eigen::VectorXd::LinSpaced(window_size_, -half_window_size_, + half_window_size_); + Eigen::MatrixXd x = Eigen::MatrixXd::Ones(window_size_, poly_order_ + 1); + for(int i = 1; i <= poly_order_; i++) { + x.col(i) = (x.col(i - 1).array() * v.array()).matrix(); + } + // Compute the pseudoinverse of X, (X^T * X)^-1 * X^T + Eigen::MatrixXd coeff_mat = (x.transpose() * x).inverse() * x.transpose(); + + // Extract the smoothing coefficients + sg_coeffs_ = coeff_mat.row(0).transpose(); } bool SavitzkyGolaySmoother::smooth( @@ -62,8 +92,10 @@ bool SavitzkyGolaySmoother::smooth( path_segments = findDirectionalPathSegments(path); } + // Minimum point size to smooth is SG filter size + start + end + unsigned int minimum_points = window_size_ + 2; for (unsigned int i = 0; i != path_segments.size(); i++) { - if (path_segments[i].end - path_segments[i].start > 9) { + if (path_segments[i].end - path_segments[i].start > minimum_points) { // Populate path segment curr_path_segment.poses.clear(); std::copy( @@ -100,68 +132,41 @@ bool SavitzkyGolaySmoother::smoothImpl( nav_msgs::msg::Path & path, bool & reversing_segment) { - // Must be at least 10 in length to enter function const unsigned int & path_size = path.poses.size(); - // 7-point SG filter - const std::array filter = { - -2.0 / 21.0, - 3.0 / 21.0, - 6.0 / 21.0, - 7.0 / 21.0, - 6.0 / 21.0, - 3.0 / 21.0, - -2.0 / 21.0}; - - auto applyFilter = [&](const std::vector & data) - -> geometry_msgs::msg::Point - { - geometry_msgs::msg::Point val; - for (unsigned int i = 0; i != filter.size(); i++) { - val.x += filter[i] * data[i].x; - val.y += filter[i] * data[i].y; - } - return val; + // Convert PoseStamped to Eigen + auto toEigenVec = [](const geometry_msgs::msg::PoseStamped & pose) -> Eigen::Vector2d { + return {pose.pose.position.x, pose.pose.position.y}; }; auto applyFilterOverAxes = [&](std::vector & plan_pts, - const std::vector & init_plan_pts) -> void + const std::vector & init_plan_pts) -> void { - auto pt_m3 = init_plan_pts[0].pose.position; - auto pt_m2 = init_plan_pts[0].pose.position; - auto pt_m1 = init_plan_pts[0].pose.position; - auto pt = init_plan_pts[1].pose.position; - auto pt_p1 = init_plan_pts[2].pose.position; - auto pt_p2 = init_plan_pts[3].pose.position; - auto pt_p3 = init_plan_pts[4].pose.position; - // First point is fixed for (unsigned int idx = 1; idx != path_size - 1; idx++) { - plan_pts[idx].pose.position = applyFilter({pt_m3, pt_m2, pt_m1, pt, pt_p1, pt_p2, pt_p3}); - pt_m3 = pt_m2; - pt_m2 = pt_m1; - pt_m1 = pt; - pt = pt_p1; - pt_p1 = pt_p2; - pt_p2 = pt_p3; - - if (idx + 4 < path_size - 1) { - pt_p3 = init_plan_pts[idx + 4].pose.position; - } else { - // Return the last point - pt_p3 = init_plan_pts[path_size - 1].pose.position; + Eigen::Vector2d accum(0.0, 0.0); + + for(int j = -half_window_size_; j <= half_window_size_; j++) { + int path_idx = std::clamp(idx + j, 0, path_size - 1); + accum += sg_coeffs_(j + half_window_size_) * init_plan_pts[path_idx]; } + plan_pts[idx].pose.position.x = accum.x(); + plan_pts[idx].pose.position.y = accum.y(); } }; - const auto initial_path_poses = path.poses; + std::vector initial_path_poses(path.poses.size()); + std::transform(path.poses.begin(), path.poses.end(), + initial_path_poses.begin(), toEigenVec); applyFilterOverAxes(path.poses, initial_path_poses); // Let's do additional refinement, it shouldn't take more than a couple milliseconds if (do_refinement_) { for (int i = 0; i < refinement_num_; i++) { - const auto reined_initial_path_poses = path.poses; + std::vector reined_initial_path_poses(path.poses.size()); + std::transform(path.poses.begin(), path.poses.end(), + reined_initial_path_poses.begin(), toEigenVec); applyFilterOverAxes(path.poses, reined_initial_path_poses); } } diff --git a/tools/smoother_benchmarking/smoother_benchmark_bringup.py b/tools/smoother_benchmarking/smoother_benchmark_bringup.py index 86baea19ce5..5506b8a1f74 100644 --- a/tools/smoother_benchmarking/smoother_benchmark_bringup.py +++ b/tools/smoother_benchmarking/smoother_benchmark_bringup.py @@ -19,6 +19,7 @@ from launch.actions import ExecuteProcess, IncludeLaunchDescription from launch.launch_description_sources import PythonLaunchDescriptionSource from launch_ros.actions import Node +from nav2_common.launch import RewrittenYaml def generate_launch_description() -> LaunchDescription: @@ -30,6 +31,14 @@ def generate_launch_description() -> LaunchDescription: ) map_file = os.path.join(benchmark_dir, 'maps', 'smoothers_world.yaml') lifecycle_nodes = ['map_server', 'planner_server', 'smoother_server'] + config = RewrittenYaml( + source_file=config, root_key='', param_rewrites={}, + value_rewrites={ + 'KEEPOUT_ZONE_ENABLED': 'False', + 'SPEED_ZONE_ENABLED': 'False', + }, + convert_types=True + ) static_transform_one = Node( package='tf2_ros', From 6c3005ef89fac9c3c06b382eff22d11e03a47dba Mon Sep 17 00:00:00 2001 From: Steve Macenski Date: Tue, 2 Sep 2025 15:13:50 -0700 Subject: [PATCH 39/51] Adding 3Laws Sponsors Sept 2025 (#5505) Signed-off-by: SteveMacenski --- README.md | 2 +- doc/sponsors_sept_2025.png | Bin 0 -> 192041 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 doc/sponsors_sept_2025.png diff --git a/README.md b/README.md index 48a40d00e21..7e85a8225c8 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Please visit our [documentation site](https://docs.nav2.org/). [Please visit our Please thank our amazing sponsors for their generous support of Nav2 on behalf of the community to allow the project to continue to be professionally maintained, developed, and supported for the long-haul! [Open Navigation LLC](https://www.opennav.org/) provides project leadership, maintenance, development, and support services to the Nav2 & ROS community.

- +

### [Dexory](https://www.dexory.com/) develops robotics and AI logistics solutions to drive better business decisions using a digital twin of warehouses to provide inventory insights. diff --git a/doc/sponsors_sept_2025.png b/doc/sponsors_sept_2025.png new file mode 100644 index 0000000000000000000000000000000000000000..655ad1ce3cdc4bf2850e1d514ea37e93ab28e450 GIT binary patch literal 192041 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%0Dtr9IMY$nTJEZak$QJsLQaX={CS|%BWn8(6 zMpL1r!|%hZ3vJ{g4E}x9${nvVQlBE|(giRq>x>bt_kIziB?{26R604n2dIJL(jK6m z$!bB~+^j|x4J^?ww`#L>TimlW?*_cbuq5dy(J3^wgr0&JfrP#x1~$G<#>SZ9Xa*eE zH29a*K<_FG23w?js85LXXXk28mltLgMoxo}Q7_LZwiuujFSB_0_?g;v49<;46H?IH zu#$?*yAOulFWdi7c>5`a2*UJ$2mNJ!Obok?B+?0O=ZQt|a051DP5Q^}b>M2;P_!J* zBc_HUE-zRhX=~p3{o51ol`eXexlDJd-l)m?$aS6RWj(8_Lu;6~y!xkB>^H!zSAGG` z?V@E|nY2E9(@RaBva#T;NipAs!#D6XAf18E&)Z~w42ef4FKg+h4{+OwEawud9X^#J=C z;@(UD2#|PNx#cnerQl>yPnu61?;2rN2i5TjPkJrQbnyXN zZt+sgsz-kHN16pJJm6e`!+~&;R^3l8tTijjZmn{ILJRv%>|c z0Q_^jbK*xMGP#9T)RD^sz=7({XeucHuOO)|YSa*D5b*S5OuLh%M z7g04A`Q5LnhGSAxUmHJE%0&0wG|Ib?VCpE~Z=KS}TNe_yh9=N>r_V&kp44>MQL#Hj z79j8;4fGE8RO@)&@COfXRbNFT`@>y(RCnj_)ll7oJlEFT+Wq{ywXqUO-^ttnBJ%LGn=fX z-iPi_6VKe0>b#_nYz5Eitso)*2W}K&JhD3E_k&t*E}5w=Q>%K(acj_OhCX8x1g2I# z0cDkFs?|X9WgOVPPIZD#SWOJ0wOu*}Vjj@`L4$Y;muqfCG7m=1OvzjD+vrY%T^)lD z4V6~J_vc#=rZBC6bvIp^b#6>hV>XTfcem>UsX7a2Z|74H>Wr~-ej5mQ|J1TO5JKY; zi|Ij|NcV}VVx%_P$YOb6O)^=~)op#1tk>gtD~jmOQq97f1MkUF=T8LnQ=1lKeUsXW zXzYQZp>zb;N~TSlK|cx#36O_2atNOLMUmpqH*PC=(U6D$2I?gFDH4M%qlfsm5B2*6 z{Q+Hk(!<=e&UHF=29OJUxz|&KgYNLTCWw}!XgL#NUsygXXCZ=!(18lM7cVN(FFEZg z9CehAM8PFV3@?D|zh#8nc$(Sp--$$dA1BAdpYF_$BeuA%1>70IF! zhzxWkcb9LwzKa*fxpT)(wi_TWP(LU#-u;v-(V(&?&Kab1WAY9lA(d8f3Iu$x03DX> zLR*bu-y zG=5Zl(cN51pU7GI?a9lJ7x(NBm=70=-VV@WSx?)(W@Y4~$1|JYOI*($u^RM&*Qvaf za+@nuh8uI=VxVFr6j5I-o?l%1c+W41d1$MkbmzB+3eWkh4zXrK?n464xv#cMX<8KW zIs22dm1Z#wW&iZ+v)C$PaFccidjsB_BHopuAwcSvPYsTCU&-P;`JS-1e(S{A zOvCg!vz&(!YmvM`?w(6R2(;fxAIsV%Njr1%^DPhp)>ayPq*Z1v(XjPjz5Zz)i>#j8 z>>pFUZ@qf;v9;SV-NvSDEPDP#NJP3q_h+Z4q#3vjkxm%7?ITbjn%G)M&^a>DVa+K{ zz9#EOXyYDnji<~3^H~{?apwM9SNcOXvf$DP^38ebJ9tnMy?%1s@nt19(T(aJ?1EO4 zY~0pP=k8F*)1eu`=!6C;atr=z@ayZwIVASw9uE?kJ>l4C1@T{Va^Gf=uDYNg<4oDA zwX|ziyON4_Jb0KG?5@(g^P4nk^6ceR-8X4sibenjk3J&Kehu?={e3%Zp?v)AqRDQ{IC;b!4fJ3bo z0?(zhKxnc$Dyk%+|k0qmiH8}E8+|5r0Jr*#xOKKtI0J<1M zdDv>wV;eJt!=}8K%uX}ltT?Hc43}QJ?-D)8b#WY1Dn~H@Ipl&I#jG1-C7PZb{*JpL zxNo5&;t8)2lk$-(=Mkw^Dqx$vskoo>t~S)+C#t6#_mcq-E^uy|@fI#WvbL48+<0N! zT&Z7wXN8OphPbIj9L`{%TQKQ^FBw;f73ybL#=K}^s+`F47O1MqA^v@Hb1d!P+X1Ke z_c+X5D8|9BNsx9m#fV!cK1Op%dK3|wpVbtdnFUNf254LMxi!c}__Fd!Y==P704FNoqjFGtS&cwP+`c1|#YcO7 zGLIyx$3p9olG$z>>_VK9%?-ylkm8*$1deUwpQvz>x)+_Ik9U$keT=1&E@6}IAUj1{bXbCiHU zK2yLDra;7f#>`JYYMrYp9&9MDA$Hqw<&Bia5_@5r_r>Z9j7;}Zlr$j(AE@@BK%*0Tq;@4w67-Vi;;1QKk>ReSpbFM`M>!-RhX48P z(U{1{LVW7G?_DgZR$mgsMrA3R$62)Q&db^!6`RqPSf!q>1%_C`Wm|9%_ywVkAeteM zVe9hcMDEFkp-Cn0(;7lsZJ!)23VyO=n<+S{xBQiwmvkcbz9gfC<)t+lXQ-T3u(sFtesBmbtYkhO4}=! z5lrRETbVTm&aMrzYMmAl%rxY-KP+n_4#!i*0PM3FD&=>40ti5 zeB`!SXi5d_7qN27!wZPp#pCvY(hr+_I6MM~#`Su{P9r#h7ib~0du#38WPhEswDtRB}9DTBzMOo1?%W8&XN&A`CV7^a^ix1|HP7ieMOAM{PZbSaL` z1tr=Oy?g>>PIp}pu~G6XR37GZZAAF{;wwY%{g_5^>$!J==*;vxheO!xFEvkSz;DaC zCXnrYMv8)E6gm6>gE!Fbxi*`O9-_x(`6DZyT+D61d1cGld;X8BUd8-m6^a_fynz>& zEOyvtzsfz624t8xnyw&$b}6p`$cXQ2tFvOIu2-axU7|{RfwH^pj&*6(A_LM+{zY7CuDkh`y?Mw}Kcs~8r@XTeIdT%9RfB%? zzp?)-jE5IJ+(e~^USes`0tMPwx9{UN z>zmgSQ4fSj8uL|H8*`NLx30)na9_{p;pTxkyZ{<3G@*i+GV^YZYfnz@RY#Y6wbhRn zBrD2;?}(c}+2jqpJWHHsQWm)#MB4N9VvWi;LDD~@H>=-_m8P=QMofs5U(v;HwKL+P~-A_0NokWAZ{M@QnWJmPWC{&(r zzX)-XjAt?^aNIF_?en|L&lHb(@PbMn452DHWd#E1JzF{O+aRyxN%~7gk5R*ocYhMD z$>N>8@Upsp`LU?p0HS*hZT|3SA+Dq7Z69kFZjd3XC)xwBH)Y(ReWJ{vhAp)F<(Wnc z+}wF;W;Yb55i^j#dEf^T=z{iHW7Yc}MVDQ?74%~9CofQS|2V87zt5f~y&`tkq%cGf zP=V&2B$?e!rmx-4Z% zWNku5l}^E~I3unb`&tR>;TeKeD&iO}`lc_*yhb|FJu=X{G+RJ0o?_^NfwB>7N#PQr{qe zY|NkcaaTF4h~s*=hkB8dQ?ltD!S=T7+lfRHl7mQsJP~>X`5SDHHcN)dk<5i{nVDik*u7jUTBjdC>Yt$uB|G(%MfiE**`VGr0d0m)00L z*f%swB!Yy(&;31PSNp72XJ<&8@TU473Gc2A1l89vWzzhY%dC+f8B(zJ139E=tI4iL z-3sn*TnW>3Ff)(Kpi?nMW0LRssR05f2CWvuTt0_{6c^><0_BZE-~Q0QK@lCFESDuw zqi-uV6_6GHm}|%Vr%k%}d_-#*D^Hv$-0&D<0j(AHiv3-tg6H|?$X%ywAO-*~gzR5% zkjJ?Q^ikNx2i`-ZJOtkvbSM5Wt#k$OFrZ)x)~=2i)BF2CJywUgeds&pEivz1iEcat zl9EqG5u0#d|L#jiuQYtNx3@9a%bmSpdV9HGe_zBK@?hQG0VvUS+L_8h$I9~N*rGd)qq5v8I3tgN zc}hZ0b>rU?4WSoCxVnC(c&sam=rBu?sGraHzw?y(?ikmz-|L3tCQjn!98zTj0^QAl|nvI-u}$hMpR?)?k?vM@r2JKQPd+g zuZk1eS|ya!9|X<-y$wSC{T-%&3Pw7I&nCs3>Kku-o%-K%R#OW|*~&A)@}ibF#n&@) z+i#aLT*3+NScLuN=TY6*R}RU`%3O@+PaLKK(-zbe^ot;`cFLH~ z6W5kq0-dAJ^KPi-UqT+~Wfkryj%<)Rum*Cj^~w@uW!PWSDFi>xx%(T7dK5<|*Yr#c zn&9J+g%_TS^zQKIY>OZ}x+4f4hwC!|B@aQr{MYhV{0P^lgwy6JOABnceur$Q-j4Xc zjC{@`G8Ib&os6ZYWzAfZlfI^iC9i)w*B(lp)nuoivIP|G$vrze-TBo5d;=J3opmO) z5ZDCl8;S`LUCkF9d%7v!k`{HK4b1;2A3htHDLtwDbl19?KjE}k@J*_d2KNXML58<@ zU0DD}r9)}exxLp7v0B%4c`^Q;4~o0kvwhfG$r~#5#qzGh=r5f0RtL=++*a0Rde3*y zj-{pWN)iePWSfYjh73UcnT0o%d)CluIlrul=8(k{z&iT_P_admv9J)mQ*aF7OyJ?J zm=fw2&a~E$U4Sp|#r_}$!_zdMY08`}pOHex*+fuwk8AIOn)yS)n5!sjXn$4+Nsy$> z0^u%bHQ*tFx!2<>OGNrx=uWw{`Sb^h|;*j`S8_WI-dzbu~Zohag^e;V15 z4(IsedFPSK|0b1a9w9S7nl_$}N6Wl5Zl8xmM{*y#6^#%|qU|-V2WLO}Ev71}jHxm-EWJvP^QsAh zaGg)513XjDTk~i$6;NBl^Q6#KvXWqbFpNj2{d4i?Vt7 zT-nC|X@#*9AB3Kg2-5Xi!HZpM#Dek`{UMyB8x-qyO}e6^Cf}XC2MLMc;+*wjpSAmM zX05&YOrAJFFawyXOskcd@#x^#^dgudfP}IUJPs1>w*`v%KkTs8Avo#v;?Q;lb3z!|?nOAC6{3D`Pzu!+z`HHD#O8RcC zapOp`$hE?%J$V4USPt6VRD>@W+9h@LZ>0enCWmjBVkvS~1ZC-L7>_Q{Vej;aExtTI zVW~uJ&2UU%JN6os+6e9cK4Cu_y|Dt{qSh+v+I+6yG`&lqZ!Afkofj8G!PDI!lM4vfsL z8|(e?XUK1GR6im?`DbVQ@N`F2^&O4e?Z;X{|P%ouxN=>}P*Hg;x^=v+C6h-Ah9OkD-e>#k-i zh?IRUChog87@_&L*8yvX5T`sk=1YC_H;y1b2>oj|gWTm$CZdQutiG|G-_!pAmZ7}7 z_7?M?sa2gs=5SuOA{TP@Hs%7v;R9nk8yPV3(Zcp#%Q-O!xKLKRFGm18 z$X8OJXvzMvfRW0D9;O9dkuqV)-|F-u|wg`8RnAWp=z+zb5}T<6mS9&tByQ#Zb7xwj2!;v2<3C z6X%EHU;E&lC?KIlffDq;i2DFIT@sm(&}sxv!dCzN!twCEo{ug8|HniH3;)bcaweFI z_TRH7`YyN3b~$L_9l2}id9T-&T2!$KI*9H9{Y}Yd{M2hb#7@2toz5Kt=aD>#K~SJz z$mzBo0X^?Ov!W7={cVH_Ku8}S1w=?FvyMD2*TE*o7U-@>E@{%BMVG+s38PWio~Ddc z&QoHp|3_a$SO@5! zM`r7v3_-(6{opcV3u`w2(kn5FL{mkIbv`ZgGKaB{7-e&&e=uKTV$Qh`G^7YMJ&N4s zDtp_s_LJG(gh&^?={(R?|eLt?kvh zVG1A21qw@99s2$~7tpc%qil#I*ck)t4JGpua8O8m_gusXs}IElT%Oz#lAN>H+V8Sh z?_G$RI90d#Q_TvkV_N;F=KN4``iaAqRdY*!3;GA!iS3b|fgl3g8A#_xD1VUt!IDrN zkcWahR}&s>M>>Svr{fkAEekfe>wm2 zv@JL2va&qY=nmRTGvbDw?x5QNxvjk+dswZAC(N|T|DUkYR<;d z5c>qRQQLpi>Yl6NCbfvp7gz~A9BnhuWD82}g~ZO`)?aU?1}7b$d3LoVMqFee)M{Rg zZ{#WSrCOK&)c1p!L6)}JrA`82;Qdn5IV1u1;i$@_`+A2k8U%q95R%%zN;ZvuqE`8_hchwlj`?UY0+HeyWyC_w z=zxkrM8rP6yFvWx5@@jlY~cnzBWn>4ZE=B(Lw@qvR=WJp>2S*Ni>36kqsd-0zseH& z`ANC>j!Flag5Q-9>xfNW!KJbkZ8p;{qjoN+R?BzU7q}3*gf&KYv}71J`ywJyqfkFf zt}q8qbLFq{dqKM3Q?!TcC2RLwKj@Af@h?J~|6d!ZKu`Cpww^=i#;+S9CuRK4K+ycB z!kIA-#Ln5?iT}-5ka9=gWK&7G;O+MwuD9PTW|@O0?zP2*yBWN zEHw|T>xr101Cz(Vjwyb?Bq1n6wdq+|(rR*`KFE~T=!@(H3_rr;VILTeWS;^Ihy`pOkvZOTg-Ri%XgL{^LHYMCS9t1gAnb1x;hCy+pYX` zv?v0x9d)FQe>12$=@RtElC4+w3)x5)Q88jo++>+DIrm(&^qy| zXA*>si~q5}BDd~($pqrPx;tr_L0PWru+I=Kcl~S+YmYjE^{wm>d8%F9Yi_2q;YO^CGkdgnt0TO`pgSs*Z-$5b%Ev|7i zHlj+xo3OOU9!DTlguapiA6R2~^I+IQ!_|GDlxrQEd}EAl4ZZs{Se|XD6K=^Iiu%*^_ zOz)dH#j>(U37~HRkBuuZa_%)Mm1G&lM)<%0=LY^-2?n4EGHc_T)K$TFW_juiy}ySu z9dXM~ZR!wk+CPY*h&{DSvMg)Tq_fQO8#od>>T1xs8}RyS__;(iO6zi0EcKR%|K75V zC|l^horMI@%!Q^;g!>ooGH6TpDEPZiiH$=2#9ovAVZhLAT2o}0!o*3H&W7F0s})U4Ff0)T{6Io z0|MV3&b{Bg_s`=GJ^S5z#j~FEtcAwKLiUSYJ(|EKQgL7=|9zaztcU)vMqE|ENJcB+JVSBQ_wZFUk(vw;X1JA%xSrdGq=%7sNW^c6tf8_N zyv&w=EPUJ@+Fa~R_p|>dzxQOx=+?>Dm39)dFVE%jSb3=2)HN*6C5sL>GTPjjG*rHr zg`^wg?-e@#N=i{P%?P#q+lu0UPUXebt3dMH-vDL_q!9COpTN?Ao($+;x~YRwNX{zb z#?}^Es%UL7$EBUmfBkqYpYcl}`QF*7GaL9=pC$?jvZhj1lt_N>)kIIVaPJ zSHJ$OpW=@tLy*5*9$9`u39kh-eM?tO8?UjwrcH|_(vV^)4;*v0hQD#zpe{Okd}l%u zOaRug0O7cqc;dH~9h&J#H9R=l5V)<_H2;t8Ns>E$G$}-$U^ zi3;h*KQ?u|!SRM4u{|`NVi2D%-qbZd7;E?SulLUctc)vlcIt3nDJpc zQ*S-fR;?vL=bCl~{4x5r&0a#o;fUlW&xU`MsDRsR5ET`CQr$_S<26%T`Liu3X)dR8 z{y$6`Tjh(r*SA-bV~?xDpN8qiJ^!a%;@A!wX1wjK-wgUAeBwO781X$lvoPS)d#v1( z8}!7!Y_gi~v8{nZEnnU^UMLw1AX_L01Rk(#F#UbBC&gc!bPA5amSVf{R`4kX=$rIG za3r_mOR~S_Uj*yUm;2o2wGS$ZKV|@#^50p*@<^kaA{Od|7Y^Ej=~W<2n)PI;{u6|) zHx9Q}tZaSOM$H?&B$JjO$v)WMrUc`1OfAk`S07p)04}-V^ourRq#&UC?KK|~X;K15 zZgaiaAlGd_3N@2GHR>Z(xfIKb1c~dN*r6F0}tl<8FA_%S{ELoM$Gq_?6ik}tDy5KcIe-QAStEu=@GOF&^h0-q)YMEH6z2VttJ6proAy}d0 zKZy#dSv-=7S43+9e+Z?*(rLg*?3^}%S$sD3maMchN&bmM>F6~&^avvx#pfqk6H}6n zb&MfpK+8T*^{j@oDaNBDDD1!_?pxF3h%*5kRFD4^o%-IF_I`e`48%IFS+`f%q|+S4ciM?oGw^*aXga|F^U zBPRnG!q9`ecYqRf>=(K~=BY*w(x+!cZ2IwG6Ft~Ckx`_ZAefxPV9&R@4GA1GCGGi6 z?qbY$`_+twUvc*G^g4he1=$p^w^zq7?oSD!)Bn75OF8MlHMF zVHNB1=?5dhs>X4P+VS*!i(cizg4yrcT;Iy1S2^uA*r2@TG%!Ck(@G$o*1k2QQ>NqN z$#1deobMmfUYYc*#oHN3K0#JZV1DUVHy9-IO`gl+<-)9?&9!tRqIl4U`mY413Z_j9 z<~|a+t_x$SAwwUMm4T*y4|z@?LOXN~|B&Qk_TrdVIUSt^KE*$9q3E)-jg}LQn!wII zcft0n3*m}1C6v$eGojxopej@1IqA>{4ry`%M|`kc7M6@lQ77@tY$}>O3izeSd3zt) zcXtpZ@@j#$5V#b@I1R?)M6PYgajcJsQ!wR!@W}mlG^Cta6nTE_9XS^Z6T_lHGCkx9 zc=ZxNzchYY(k-Q(({qM>9G+@f6mD3P^WJc6$sSi zs!YYs3r?Bv>O3<_Fck{M$&I&|Pd*C;t=;EoRMQxmhm+C_X~FB_KA?ns+U4}YD3%sE zz1rpX6Z|_Y?REMd)f;c@Yr)c7H6tazCvUAD#3=&T1JIh_!N|)Wmz`+m6t1P0@~2wo z`XXWz2C4_kPCFySD=JHBA|d9-wk1`}$Z>xeova)9D(ne?AiLBc@6D6)KrIPTgaDRx zGD8#wGc{*5<=_FiBqc#l(lRElN2xv)Tl;5 zD=WL!es`@Vw+ETxn<=@fi;p5r1n|&Ynw^~eRa?KNxp#2r#afjVDLr*3``8zjI_I;i z7M9Q^PWx?&xivDV6ES{lBt-a*h?1D~k#o`hNK$xi>x&nf)}CM#ppC$i){5YN;sY_h zK71sw`qy!leE9i=nXqg&e%lLU6fc+{A&61kkk*Y;2@Xe5$RYi&m)JTKO|bYYaSjLw zm_78gcwee05|H=W)TwVto3?v1$)*Z+bd{FC(xM)m*LIcU=u9Zhbf?u-TT?KW!E%aw z=Q;k$#%?)~C2c7k{uWGW{iOBc^b3~ge-U2L?{1(x8rrnpT34n!Sx`^87VdT>c1Md- zCaqM_865x3?}wHJpbJyCAEC!Xm+94Sy`Ul0>UJtzQ*$MPn5En}5o-~9k|AJ2+NKQs zUKJ2s^4?6D@XZ=;)?Z4hn4E({s-d*V+7lc8aWB+YnxHOH^{QuIpC6|iT}w=NdQ1$M z{{O1gV%(&N?jP?9Zn{2ek*HExl4i$)3Rcsh`LjVH{9#)E0u;y$ zU|%$~ja_?tJ-hrx$2+(fhi;kdY-`T+SUQSKehY-FDef(f;U%1ZfJ1wfJ}= zRX5B98R(vnNR}}HvmSV}hth%hX!w`k@fyAP>N3Ip8vzFyj}NT#eabK)f9F${Gs;M% zPIJ+_f|B)a?WO>5=8kPGQPq@aNaH*2>JQhe!SKZ9s581Ee^i59kla0yHqv2H#ihOU zE@p(gDHML4`Ln_w4+sk*{K_!LJM^`w?mpUEGvvxl08CpyZ~zg`YoNOUenej7cI!>a z{}d>9`am)~;-Wq=S{p49u6ceiKlk#J{rMgnxKVQ?$JkA6ZnhixqF z7DXsfXJ@dOxzWB9TT-Pf^3^+^uGPKxG*M?|D@{irlcfWGr{+T2&ZyV}856+`n6I<}ybN{tM-jXuCO@9yC!@;As zQ+W@HKOU{8s1;U@lpe$y0?G6DN6cH}9vpdTx6f~d4QMg(e0%dD6wB(Lz_Vn2yrmc^ ziOO#DCVq0vH}3h*yl2AQ@^XW#W1%;MXl9yjbmBKGfZX;9ze^?hXw>YUVb8WwfhNht zT0SNiOdl}D{xh2F|7kqp!;?|8#HPRX-jb6a{I017N76~ZPWn()4Ur=gP<#(XxX@Yd zRqqvg4GFpRY`5&bCK&zT#YYrtq9M6#aE+`42*mxB@%UXKj#@@InK>6XvR^Vf(jRAU zoui1>q*#_$+602GtYFzK`5P=V48+wy%?1vKC&_|-oA34HpUcFid?;TSB@ZV#D{dmG z1%S^)njJsJN^ocU=Lq`QQ6%n8TPf6hF@Y@@&aT_8Z%P+-bnLnKX4vvs!pMqF#vF}X zku`ersm~I_3`p;uYHz?~@QV^i_mi`?xMMOOc={HFRe^|1uGSN zYQE<$(e|R!8NIYqawMC-f(IEHl|ZTSAA%9xi%%1F*`7B`DW3RCwK=*=hkwJ~CQ>S# zcR`Tge{R+W7m>>q=>#V|Zn;CB_+QrdArKHm3&HG9Y<06Lpc|Ic zY?IF2oU`3gPcwmI6Z8ugygm@e`^J{R-=cE!CQnVfHcz|%ieImtO3n`CW$tQm56w_8 z)1^(8b-H+vn6^&@l6`5Ih5Cur0hW)9*0dmzAnw} zeTB%4DBFwcxNGsqq%8v1aM$18Cj@bX)WLq&0Fyueb4+60sSkiEFU?}I-?414COuvm z`V!BWB!jo!7=8RMFWY-KLD_B!QJL`odp?jhn6?$X!PM)~*_lvyTv6)p^l0INNP(5y zYSM6}6=iKwjFz!{pG?b@;7JKSV|d;`d+?RL{v#lCkDI7meRS~-9-G&$C|bsYIF;l~ zG{5QvYDFJD$#Npic_g;Pu_qivL)yqGQ1HuSM8=n#D~oC6-kA}Y_0xJ#;wT_gP(@f0 zZB<9P8!5Tmn3;|167rUBb9~d4&S;~!DRZ(fBl)umJ?m4jpN;#ayp9(un-Hko{x7S2 z;K5Eda$83}Q@**otqQg<1q9PeMTqZ%JN+m?ff2mfIY1thY?VDI)+ZaS$?|zia$KXe zv9L{YK3Y$+629n^Wqbd}S$f1DJ`zbMH`b1yfUQ*P)OE)xgEsYh0PNpBFF109^n8}w zlay6y&gL`HVWOc)frgU4LHwt=@NkOurBl*oY01TiV39Q4$t1)==o?2gle^&TrC4no-^_{zG}fN@HSR>agV%nx6ze zjGYj~Y!M(At@-WHLnvvJUK%f?BYTx(k-qoHvd=9AhUKSE`TJl=j19fph+haZ`z0Z0 z4GecKmQRbl465{bqkNL^b$Git`44sgW&Uj$vxTxx*MXKWm3`AogxL#>7SHRPB_0}Ih6l$DV|tz|J^)hM)hqez-VA9oqOzGWf!XK(;d(g~a$Hfm5V zj@51WU=_OFcf;`>XdAH)gmV>A3sY#OM?wo_L;xp7t@Mhyh9bF%{1{g?)X~!CaAU*w z(b9K?OZB`uQ(#xRn39oq(qbZBatdSLBA;;7)1hBqGTd|8v!kUtpIRHyKYYeV{J)8u)AO1VBr8W>db5 z4@&_U zE#ApzU&K=&_3lVhQ$jMuPrlM_ui6W>a6L#e2uus&aSIfeM|>vyOjdnAV@W8MU!1;W zC*}`9Ab$r8?PW)ypB+eiT5oMiXhTg9tzt>TkK)@Sl&O3?PoNSN`Ob|8z@p$fpb2fG(D@R<{8}E#) z_lnj4S9=N8MLxUKdTb}XD(TqT^EG?lOWhGYRO0o2lX!}(GJc<+q2ZG7Kca5Hi$ z3cmcj=Dia!sUt(zjy&>>_7+9)CZ&$@-`LJ1S$37(snaP4-THr$P?nW&XwRitoqNv@*9_DJnS+C3(z7Or(sS{srp?wz(1UcY{^G%ib+Q}{PKf??sOdMigs;4tR>3A~LSHp(o?httH?A{%{rARn9Zf$QZn*4`aO?{Ir;%{7f zCVLYE*Oo<#=>Ivw=KCA`!V3||XFj7Uiz4#bLSlG8cuuvlZsn@J&LGUVOLs`xuF*gL zx>p5v?{?qRT)1Q0%7@d04;?z9XT%r`W<<^4dU=tEQC&;fd{)A$uO?g=%NltWm7c{6 z;Y2yK6?P$!$oOU`GvD_2RoZ>Tc;hxdW=UB|eX!Sf!ww40MbJ-0H)MBvlJ;QJu~8dC~( zYW@5iHZUViIfx3?E;61EjvMpmLHyy%nJ%pgz|-SnqP~S zlA>qt3mf&%BLgHLLT0&Nf$`ljuoKLqJp!`lt@S2ch`#0h2U!A*jgH^o0S*BpnI@ig zU>&j2F4XQZ<5a`L_7U%t5lbj++>mKz6ymN8h`yU>*?>aSMjlT9%tBlWWOy)82%RgjtAaB2Uc%q zIjESFS_Qw-fqaD9(~!hRK~2}nW*o`gVaoka)?KLP!VCD71@*+)>B#`@}YBz<5`et z2>ud9x^A5D*AeR}!gIegaOv;^G!d>f+nn%w9LW~C3N@Ac7-nM@=m%Zm+@7i%*r&g_ z;M<}9F^*#JptpvGzopLoap@zfXNwhd!`2I(mLy6oRHY%d+ETfrd zx#!pY2iLsy(gqCl^}IEjO*$Xvg0T9aV$2LKDohv{6=z~7XUh{(Y|pBOgiI_6gp`u^ zuTXO1ZJpSwJBmEsK&682(EC`zi&KKy!=QizQt2cHOWD=acFlKOg6(%H9v{HT{y2>v z(fM0obk?0(w`^hIJ!jv~r4^N$lSmSYTdIiA<9jUzIjJ2*@`wGA=sSJE7YxZL`&D1J z*$HRI=1(-UD|y9pu@(AEV`flVC&sn0YDa@5=2!8jHdPHIOgkvClH7>NRflY|ej{MTPRMJXKO{c2o9#^1?YpA~P=Gvqxz(p_C@mt46R zoko^CJy2f9u&NqIepBrJ<*sz#C+T@#`{Rxitr&)ne!DLW9`GdTBnx8JW^doM+{oJZ z&N*(?GX?GUd?!|hcY9Bg`zZ#o5?M=5w)RXXLz}l(AQ6kU>>Xx~Oo$_kGaWoYrnW9# zeY>d)Qie>`N20^>NqY~ClVd|W!G(oHSCot^!i*)Y@D;2o30m&n-)1-wZjxndJ#J>51zufGMni;=T`su79(h5K__daJK6mYzHt$2XuB_{#5=Y7$qnv)5(l;S=)kYE(Xq7K zX`i;UypR!Y-I*CJ^>Wb+oWqVT*2!J0zB9QmYIA2}r%GA1jW+CeiA+T|>g`DXoO_#K z0Lkq#{;4br@u^6k_=>8~66evqn8UP-4PK5@D)wJB5sbpZxeC^e2esbS<~!&^fjL%+ zTiqyLs%M!zVgsw=gZSqWK`zpk&q0C@LKEmctaeV1g!WE`$j~W_lvgqaO8+<>z@n-f~%Fpp=Kjg>X2%_}pj5BG@Fba8%O`h9n^e zbWj8BRmnNB%Crmk@ar0tt%EmyXBEQ!p+6J+D4&~T=$mRPoN_J95V>0F${y3-I~Ned zG(H-c)}yPZzi8AHQn%%#uRv=6#!Pw2#D=1mm(xO?QO0@GQIMVkIwswN@cjYgJ zfBu{jQ2W=nTpN0j&2p+!a*>qqVc}+hu_J97z22>?&wkz>&zk+bL4NGiE1`&FWXp;l zU9q&3%R67Yurl?lb!acz77Lt1QrgeWxDhJsDp#x6yF@y)w&324U+exgWH=$O8Hy46 zBzNx*mhTZglja!{C=NwkN%zHXADbAj70r5h9NSBd8gdP;4E~*2yTm)!L$M`@+u7L* zgp7L(($fu8SDn8dU-7&OBJs~25wT=t^xVtUw_Cw>k_6syu1)voYOh|M#kTxV=?hSj zjewG#y-qcH{mQG^gtEM}^pk#xFzW>jrp0d4v9I%VYOwwM3ghtw&}4#kD<9D=wezy<-uNq&bj_nz&`!K3ifI1h-Qa7cXQ(chp zf&Mw|-o>uzp`@J5aPu8P;tFLY{hc>%Ra>dE((mbw1 zPR`@(xKECRmTT6TYTdgJo6v9Xl=-m>Vx&sWLLydw_*+4K>yKIP-daG4?t)LL#|=8# zYSs6Zz{${?*d1%D(>0}>)Aqe80lr(A{b$EF)Z{K-$;nR8Ic}OM;oKjjT)f#`AAbgX zvtaPdx0Dn2=F_NHKjim|=!F@Y5kXwbTMlZ1mjKA9HMcmM3)MO&j^?z?jGh<8rg?A$ zn1&iQ6$?&E2rCl17wfp@|0XlwPVBQY9`Azal{OLSH(Sv}FQzDtcNmFf(x^`-iOM-p zwEX3oP?C-capu=Mv)faTt#48#PumV@nP$gT^(+kaj31>`O>gU@=!T#w* zft@G`3KiM*IojS+%@-o$k@gnL(>#<*9}lp&R1eo7cTX2YXXZ8j2`F=e$;UXDMa5A0 znu~sQ!@%^#SdM>+uW)qHsAtTME%Mfbim&(jd?dIeu*Wma@jl{NsphE|`SCYIZsbRp z5%+B;t*7nqKY2Cbka?W0*^Ubd<~c+yiFD>RaE?yO7j}ZC+)ejZPBr_tmtxN)>_wAb z5RlpIyx)o-A2*mX*U@J`4|DaxgxUHjCXiC;XX=`YGq6P1#4{wB2=r<0>yc1k*1$^n+sVIhQnjWANUiERpf64I z*v-wY;=rc~0R<{eUat{1J41|!@_;SY7v!Rd32}(?xA$oc$HIpDCSuN;1CpEeYqVr* zey_I6{idQ5Et6Dbo+qwErV)L&m5QK1Z=PIZXS$4fx9NsD+`7#Nr`0#J5LjjniKv&C z$YF+m_fu&A+Ao=$CrkTSGHlE3@KuiW@1|UuS=i3rp6=!{`1gT>nOoWIcyUrLiLV`Q zIPd9b`}u%n6F)E>uB|^^XU{J*T*}6ta2ZxU#aLK#*S!V%%_yzU{pPt&=PmWP&XvP#v%QHYJP#nNt+h%4{! zvbVO|;OmffX|_bS5zW8lR-=mG?J$9r!mDHO0M{ft{bYU`L6d)8&0aA2_l_#9yj$eT zzSFrgz=t5|R;kY+WuU#BlExaT9jqQ_?8EL@z=rfJ5TstvCi! z9R=YvU3j&Vei*@0eSy=rP0+g$OeI}Vn(8H*e3&m~Ig)!$#lm7OcxJ#-&HO^hYy-4rGrkIZ5 zD*8H=b7e&r9nK?AKuYc7izKsMXtFVGD1#~))g_drD8G{W1O~!q{~8@zo|L6V3Hr(c z4IIg+kbePXUP+u(mx|Ej4+-6mtDu3Krz}XuoH?28T-&4J$%9)tA`ea%;^Z5P4pB3X z6d}?J#vX7a8{%(C6Ii)pWx&1dNhcP~HzFrrez_KjACy4f1-e`NKM0aEMY<%=b16tv z!15iwJ0Lm9<#pNS4Sw#}c&m|KM$K}u~(VlrU!im;Eifg*RuC-o=yUUECdm5OYs zR5z|X8PD-DPVPH*$X+_euc?&&Ro`{r{7nQz7%*ru|zU6b{=_lq(!i(<^ zQdDfFQ?du>M>V^!!aso>$65}zSM$$$4f?jr=8?Cr@iW4+xKrC@A)sm?tPjBl)8!Pp zp+$P{xZBA~>qtnjru)tXlJH6=ws?Tw zIki{iOC`g2BOVkk*DZPO*Ol<>Ii1gb~6My>_&; z?*srSZ;SJ%&uHWoYWs%4bbz7=E#>dd6MC_rX6WL}d&RymX+?&&F>A4#&?Zm_1%E*s zZc43d?yE`i?{kMB{y;Bk#_g`-(C7Pf-WHM&j_Tt^`r83)>(R=Owd+_QFHOhHM)DR@ zSXe%%^X;Ghh%kDce4_sLqG%8!w%5oHih2iNif=gJU(NW-AT1b4blWj)<%vPK&3<%<*Xliz5z69O$VJ~G2<1HF1`EW`RC^33eRu{o__Iak+7uAxOC#AIEqpo zzbe=6kR%HX$2_%!!1nPRYZ0ikzxYJTDh5ertqIg*a92B6%U)zGpY+>__q(hM1oh9^ zw3Yld{NXZf%8=P8w_^h%rkj3azkfG#hGSm}N&s$#$=>v{nTNK(9?VlKisB>X_l79} zbaP>~O2zozm$<5EuFPkdV^`U6;oQ9(x4MHC#%2~xE_2VlM*2MD*i5rf^v29+c2<+F zYwDbb2LP$+%2B;nD4DrW;o-OX=pz%hB%c1Rr(y5AC6m$OuG~}takn#N!5t=_#5sL% z$ZYifR{8NTh?ywI-&yD>dDcQauS*_d!S>>#xydgG~Ue#N9%>$tjpltM4yame2!l#kuM~Pd;zv zdB``2tsHz^?!cOra?V7yW}^@!c@Z+d@LBON*NR!fDg{JJ*KD`M2>{KJ0{R6ftpj^1 z*4TG^3K1Glce-`3B5R4<;Ut;lk+NaH?itxn4`MBoma3P0grRod_J>MLk<8HGGqto9 zl?NYpKcE{-;|~H_)y$TOgLKeh62_JL>Z!XT$i20ogwZd39+u}wAu;y(TMhmmZZ}(u zObWYE($&MVZO`|l%MKFkRm<|KHIDel%r73gI$}Hx1xDOhWB~vxwI?mg#Ik?C^%&D_ zj&|_M+{`4^6d+gvreCua(K)k@kh-Ai9d^lc@n}>^JD2z+^OU}x>@q9ArXL#l{{8z_ z#B{Ucz%OtEh08C3W|^FZ9*i~qnhSHGc2ydet{LMo1Z3eC^cFAm^b>x5+L@Y~^d6}Q z)AC%DC?g0l@N*sepB$107o~n}3bM0WHYTfF?^_yw>Mq0|)%OQCs(^~LliYs${qmVL z3?qgs6PmVXNi*(#oM!deC1RZ0$F^BARNty&j7HXa=oBrKMi*I$5}kgdX+XO2d1%iV z_Gq$4{zu8g^>Ar}SA8|mC#NkQ$;OPyC>W|k-ZWz8z<&C*sU*eMW4CyC)t-CA#Q4;G zgSd{I7EOfEOpTOvTCw`-$|t!>SJz;a=j46j<=?gzZY_@8q3isc9*3Vb`l_7)iR3o2 zfO@`E&zfXC(KDJ5cN|!CK`A=o-?l{bJq6GFX*#L~&BkW=^Q;`FKVdT9tz_mT|CYb2J$hT%NY6(XXy`ntruoK^Nf_ zO=?gNMO{ggMtEr6d-c$IYuWzXx8y$HrG^*S#np{@}18j)F$gZU0^}C}9*{ySp zB+HwlU>HV#zgR}~qt(SU4PDxqVIGkLpq;S&$hLA1^HH*oyZpjpg`2O`vyM&=g`oVq zgW+ta(K2MU{zyH+>Wt?++mmb&u7W>Y<{7X(pQIaJ4I9ngo~O@W?D%Mz<%TlezM!I> z{e!C9Zv>f{-wt!ryxC&CoO%(=NRLpickbJtn4BzKND$d$kC-hOSUySLGk5*WyDvdx z?pl}@Ds6|<^DRmzk-d*@a3vSL?y{ch%72m~8UBpliI*9eV+UN655ZNvYgxSW^&P?= zn$X(WGb*q%y{G7LC3gYQRN2M)Qc8zkv%OutQ60Zo?h5+1XU?U-Mj zf8q-J&YvvGztc!m?MpYC(c}6f|D{exws!`|?t9Ayf#Lif;121tMhB>*YCGTgGQgM2%K7V}ogdm= znBHWoBvo70vOXdIYVH>!&Feq(;kNGCPnqdOzxupZz%PvfHB5_;ebq7Le zZ^vYJZgwcv&q6?3AT94-*H^f1nv!z9rUJubk!>D$2s(V|8tBI}k+1_i$?7%>Jf{1yl6PgcVxzMt?-O9D*rPYhJit}48D@3!>)-bPd zzqV)XUIl(<@u&tVHU5FH-{;P#*C)|&gF%8WnyfAC)oEMnOp`PP2(kOhSsEG_7x9SU zmAx-bX>o`J)`asTTm(@KGqRWzTD$}I?6t5MQ+1frUB-Hy?V@7Gg%3|}JOt(tUmst+f<&`Dq~`hve$# z(3h);DW$1S_YGb>=4f7VTT?{HZNB{@?=l(H7j^CQJCMgX4*i1A4XK0te3vp)ut_f% zW4ETq7!Ljugq&6}n>(+%bDw7L>x$AUtz?q_hFT^>{4h2~8ju)x4YnT+qZca>uRBcO zWLWxl2rnCDOhH6QHDoUA$B&jqT$zWZ-5JO)?;PEb@hhg^3eIiakixggY_jxhBO($G z6noXx@?82hp|v04Z>ajb{B)P{GDl#680mBTW{xh+PA?mo=A?xhV)x4d5lCeu zaTR*ve@f$(gMms`{F)fiZsmmtfzrV@bdzUL9JQ-6=%8$$S|M`R%FiWF)W4`EeWy=d zVAf91*9P>w7#bLIAS^E(BmE-jQ5s)5P}$e1F^wDkYkH zJtezw4ei8T%ght<(ROw9L-hwG;1)w58i^uJobM;@6zN-z47)GLsQcB3VaDTGRn>qkWr?Y zQVvc?5$5ouo{O#J9rlg_9IE&kz!JoEfK zE1t#rmpWt@%0ph?AhTiCw=EW2G_Xf|mWrx!I;&H8h1vS5hX)n{-n`fhc$d_BFx-xjbGBS;s%d0qN;!Y77NQw z;Y^vbrwBuP^J>kU&lW{Twl_4DgJxei?WsU!H9J)8-5iwKz=Owyz8wo_%j=75B^1Rs zgz&vX%4&t{yKEWSu2u#H`bF{(ntwYtZG-HjIhwmW1M7+8o7ILWH#8#^6WTa+a0M(w zv27FaXo5LQPM^J z1^+qL@BGyoDc_Sa@_5sJ72HXLk$3V5GN}X9T=s^_HU&J1rro=W;|;cEahgvU_h?^G z3#{-@$gysBC+R=;h6Y+j?}{Rsc+XqnCeW;iI$96*cgZO2Y2(D*pMj!DebGcU>(3ZE zbZs?}l}`_PX~ho3()8>u3|!rFI^8Yskl?|{m>Ij3^m6K;0qBC~gFtE?NYc9UFBh&T z9eJfLGTR=Y(exv`ZwfK`4lyiMUG%Bqsb;eU*ib7~Dn#9Imbv^gd)T5By@pc~!nE8hE_ZMzvait5oEwmnJ zA*?Fj$n^n4K)XqdTi&~VIqalPI1iOVVYE#Uszbjd?p7nhiCE9Jm~&RSUh+bTz_oaJcC#@CCeUT*@(X%LMa)STvW=#vRC*Nnt0Ue0e zmwn#5^S6CqLd`0Yh|8=gU*$jGW`W*pUI$u^DVhPcDaZh!CkqcDUKq7A# z{b=N8yoDZ1mcr3vMaO1+v|D8$LXJUfZ8g%l14|o%xUz{OOoSBWvMy3xrX%TWR47CH zBiEaHLNc5C5J+1y4O^&#m&o&#!q4t!ei5bJ%n2V%wJ%Fo+TRcV-!B_=pF*{B&WiVF zpYLtQTS)^myqj{+-c{ziWAf=dU-`G0lK*ct#Nyux-22yW_Z7UwQsaDWo6V^T_~oC; z^vci~ggS)5GdD8P8X!_kJrg8+7`iF1wdGw%&ewfN>71wTS`f8E@9wqq<;0e79>h?k z+H0$mO2LSEeY?uh;;c&V=k%xL3VmN~XrKTiX-2L$d9$cBN2+sqogS z?H7~~b^>O$^8g*qN2vywYGffCyru;>eyMAyp7XPHw>|wJMW(bU!#+s zk0cVw{3fj%T#5nI?puqiiC-IYs`ukp&Z9T>x)&mOf8PB6w;jDXz0+7_7dIYezG@gu z)#vty7xCr$o{J|BKlmP{;BF@nCxtq**m-@}NFE;^X6;L(-aSm#;H7j>ZISHvJPoxM z(dr-oI&9ONW>DMh)*7`IJb)kfrwA|h>74P>sr1w%Y{m!+< zIJuTt{H>rpsW|Q|S6IkRIKCAw_CR1#8?7nRB;x{{wG5!+@5N z;w623;q0?{o$31~|2Z5Bl&QMI&Qg;vHBgr1T1amuCq7|Cz{qSQL_O`IwQ|~?i)Cg% zo$l!XW$9cG^_F0Ow}C;WMiOaW$o#rHc9;7YT%P>>!l-i_fCMV9!Q}*i>(+>~%okfE zGu)BXjUs05FP6Alo=(QnZPC%Su%qbay*y^7ln8REzUW8rm;Kw98@)vLQU|T2!c6y} zOls4lxc_9+aZ3q%%Su4Ff&X2l@j#3^lY9ek=GUC|erd@0ay;FwKol2Mx+qv*XaQA}kVU#YKzRf)Jm7j}dFg8zHlCWy zC-=pW@KXX00%7D+m$4`ziM!oJR_lJtEd|TywZCS?Gs{~}u^4OF8EpteA41C0Hz0Q! z7U6S3JAMB~yeYsAO-=8{#kxIJ{QN^ILVnWx8UIrQQrzSJk8@9$;s`slO8YDLLWuWu zzHe5kJiLr0<7S=;@-4e%_QrbIP00Qg?g#;XCB%g z5THy(J!E8GdUg6DlPHxFFOX=LT^Ron$D?n#Hz!obk)NUqyCJU@aPysiDUwX;+K31H zH8LB=NkG+DbBAt>Wl*?ve|v(&b$SdOWjs%ImL6wQIRF3jwO^wWijbX~ai)rtb;N_r zYy6FBbeAq0O>}r;Iv)hqG?Yy5*i2fli7Dx>w0rdK(FkL9V!7ON%S3I|83=2JbNU3d@3 zv9_Yls|-%umGN|oG1><5x?$6U;#(b3c~9@HE7W{rQ)vIsIo(=r>~?Gm%GsS)pD@-K zzt4Dl=^zwjYvR*8-`M5!7xyE>)`sl%yxg>jZNB3qP0!;#tK1C zxwgu7Lisigm}R0|Dp19YH}~zW+9&YSp@aU7pI&EdTP?sq$UjtC<3>HUiMxcqP0oS> zAo&%4`DKUuZF+CtOF@b3w`y`3#TQgAc|7UuW}k&u1vC8JyEXI**08bR6^X(T;Jx%2 z1uS7$0D`408X@=sf}2CEv8!SKct#ees_ByRq#J(C2(jBXaXr_W<{7 z=x<{eW0};x0w!4O=;({dFOv(KsdxY1Up^$cXx@huJ~~~?81$5opw>Bw4@6vAB&w|o zIrUfhoM|X#8*MUe-3nu_QAzm~awkIb8RR3AL7w~9&wNo!TpKEwakgzBqnVyt>1jYr zkE0H$3wc5qg^+uNda)-J36`lfO;v%S=HduBtd0b!GbrX-xVXPL2(c5B*+l53EtWgy zfXs{n>kyUv&(tifVzLH>Ai;kx6|a`6ys$XILZW2Q{nne) z7R?~XWwNR5#Z#U$Z-DUkm6W&NMWKl6pSy+%wiMlTf9@UKI!Y|O*pl>>JwOYie33Kg z@7rRd-)u8OuIgj!U^!hpW|CUb(C2Lolt;JKSDBleKgqDkR{=Rlj;nD^DE};WQ^(aI zZ_zUGkGs0G608`7YQEne$anwB(Dsl!O)dceam64b$CoH^P!6R#(1W#^n31 zG`rknwF`ubiY4;H$=do;3lS@{FZ|=b}#<}R*-2*i@MAO+H zdMJTTWz$!eE0!Hx+e@;vgMl$B5euB_k_MZC-nRHQI`nbqkx^I)z=iXUU0b;=w7Qsa z*l=-8Fh! z*56+m=dFa@sjcJ^d^^ZW3(e{n^7N!Ry@&II=}fhy2`3WXrDT#IZY1A@QK@2HON1#b zMzI+I(%dmr+>0=oe^%s{gq7UKF*15+M>aJXuO{{Xo|S>4R$_B3pe&DLu|o)Z$1O%3 zRMOzAULdFEXE?%4@i^7Ow1~T4$ZHEcPP`ys-w@Aa!b-DXnO~hLV9dEzFh^! zm9A?_6Im5GyRn<9W4XQS06J8I!F#FJcRzXNhlfnzQ|Z6Y8}&E)HqZrz-&l#J0#XOO ziq4e-<_7BgCpNB=W}f4J6^{ixS)5j&-Jcw6ysJjv{ks>MERRvu1)XBUP|Tl()B@U~ zSS6jTqT^BSPwC%7{?<$)rTkQGMsx+T1@rtm)A01{BAOoBe--62n}y_;tQ3hj4F`qy ztBP@Ba8}~lZ6JwYXz}ww!SmgR{fE2b1O}@H-M(E@aJX!VXLs$8&<(Lt&*ne!En?_A z6a6#ZU@}x|xn#BQF+T633cHM%>E>r7`hA%<3EW z!#DFh-|r-S{9y3ZAJ(I;qB)qMlQixD3LtmHn9n6G2H2>#L?SP7W$}a!s@oh41p}(a zZh4(IUDPjO9LRRwAX;$G$~nblxoYpcYi34_AYYHLYKq;SG<>(jT zn|kW$w8L?{d~QqqIyZp3cG%QLntt2D`guJuEXMk$9lZK3E6Xwc-_XR-9Z9LMi$yWP zg?hfWxEx84u%foMKG{n~$vw08vsPO0k7Q`+fj&;9ljy6v)G1x38*0z)QZthB0>wdT z_rTyf@0RHRLg_FM|JG zK;0NyUjjhh)(6-MQxoyn!YJ4hr01$c!i=b!P1v~;Hkz}dQf1Qk}}nr|QnU!mc? z8ulp5*IuuwL=4U^Ag~fb21HxP^y-E3PAREg(7S6&VZ--E`zZ{*+Ofxxp_^JuY1o%0 zSFJvGml>llRoCRQ_yAgB52H7KobrpR+WA%FeJ?)NHd4PKBE){U`RWYr(8;#h#*Xa2 zE?3llE7BTBACQV;CtVu=%>;irK=v-Z#Z%2GiBLi6;m*vt84;AfOv1rpYlQeg8~XUr z=W8eWN@U;~zG*0m5!8Pgkk_as4Jd={e0FE%roXP}IstvCmR!%q7*oy{RB2$<^tc^H zc(3MlgWqk~-T9IRu%mN#eqo{L#*VxZAY|3DJtfW}T({#@_WGX_Wx|{PkG=N}YbxvB zhMjQ~M@P^BLHZ0xZz@f?qJVU1(u>l&A`m)`sHn6FE!0G%cOi5L$S5^HB$N;-~$AAQP}t00C<{34a?tAP!yq{fe6GQ__CSQvqh#e+C&jU|`Etm`u#kdnEoY-3US4)3MCa{M z5k?Z5jYm9D-v@1hMxHV~)RN+v0tSbUj^YKJO;FyudSnk~?hOzt2bi~{^f&Y)y7@aU zL1#YXnFz|HQVQNrFIl(x;rhpX_S(ib`P^i z+(b4R%3D*HQm-IC^|$gfYNhnETCP~{H2OdK;8j&?lop+56Y@%c$Mtp2li$oM;uk{g z=&Gs#@!nht?$=iLfum^n=`b4K2@pxQ=Y>b@xVx6`VGZIsS|#a@73Vob8qXb3=nggE z;q6yeEo<)bl(=??QTE{IZXL9nOF^7m=^5Zs-CyeY37^T_f$0iW>CRQri-bqqRFq7$ zU5_SG?TCeHbV|S~>)2tXyEFeDYL?M`CM>sfaSeTm$tr!m#7Cf z(H(suZx-y9x3VRGU~#p(tHp-_){L|ZjK^aEA(%h$5$g$$hQeDydp~s^?fsK%Y$y}e z=LUHnmg4s8T;hWOypG4I{EgPfv-`tx<|JGdGU{LtLEk#mvOC%&eFX) zZh6*=R-Cn7Z!iHnCMUN3Byx`lMCUkF*j9c5=$vsIH{mIKr6S;Tt(Ao#xNkD(>`=ku57ZSrCp7GkrihlVd;lKCodd_VA$ zZb3@Rt7@zIJVQJ=A?#uj^oO+rQ7yuZQT+L3ZYjfQB<6!6xw&pPVE#kh9&sSyMk$V`oxUeHj5h2cGq<5g}K7<19 zNpa<%ui_xkT3Xn}ZgYc8Tn9aC{)F@TXByzm1UB8TXT97{9!AH{4{*YM8KGI%KKwAn z{UHHli1}qC$fJDKa$m3a4mO{teL>uNM2cGHv$K|ti}F@^Zc?Kub2%n17h z9m`v3J%4VULl>+19EkHMnh!7Wi84TeTESDI!1)=RnLDcwq%z!1q@ENMeWzpztJEL0 zaB=*++**2+L~}(+iOX9l(K&(scH19bZ4Y0zU-u;dYZC_nL;}#+t5T8HQx(S6cJvnl z)`FrE*V%?S0`bdhc-<1*krw3kxy;eRmb7QvTFPlYs3pfy^(oE*Nb-@>yYI8gjfbKI z+a50JE-S@cnH@?D;7AgnU>FGQ$CGP7)AqPc~QPXBgT)he*?I?M)Mm#l?f7 z9~R-ADro#bNS2EC=KI>M?An5*yaQ6nPdWg6!}ABhg4yMeSCAwi^TA7&-*u}TUzYNw zA)j6VPVI`H@9>1w~ zbjb(e%31)7GH_)Wc7aUuQTh7GMVhf@%l+2RpxwvEC~!SzRxlbRZ^PZ;UVDF#Rn&1s z^75c^jrn|Hby-eK-ExFUzNt0ZdvyW3tiLPpD>)b?M^>8 z*vnfdJo0||KsX0TcN`4YGrPYSeLTZSj_n%{U=o^y-10_MGM%j=;SyPD<^AJFBExjv zE!^C_LQ>6=HHwqY<1BhOLXdGvxB)o$WZ!lBKm4%PKG)L1o`D()iT%wzl9AV6EQBMi z8txd|{$g4;qWl}TtetbgA#knd>b9HOu&wwMrg8-K_f0F|UK7P8_7~o@c zxVEBz4;az3daqU$C7SMU(D3^bF`r2BYf@8y?g54bFL3XnI^~5tw)>8`4HjDdVfQzLq)Tmd0A^son4`M}=b^3fJ>87 zqDPfhiwcK)SiOMUOqzFQtm}%JdIlUZ5~aTU!+@k76JrF?=N57|L5y6>PM2qcbn2UW zUiE9X%Oj3z2Ko4=hJrdl_#88ZziB!B{l|mLAj6Hh(2-9eGpA+lt{%ZHZxBr(O!d`J zeVh$skv0_0I@1uy_#+14LRS`U^Z#kVrMvBt$!H~P>teLi*tW1_e@*m6{*Wa+YVEN% zApWiVBEzOuL-rScGwBjX;=d#l@ZP%7bur9N7Y`c1)o zo;_7uu67hRc!#kD!*4 zcY<1Fcxzumj8{C-9{wda#B^+$lQ&8MGBalyDqX2MGvz58{hiL9sp}Z~rM3RD?aW+U z8IZh`aN?owp{H_r+gdzM@0|EX<{FEU@Qy;&{}8zvHTKs-23*6Vkd=!sT1N~qVwz6d z3a;`EtrvJNpjQO)hNc15;>zTlGM`PYfK0z!JU${;Dct>Y5VtH=HAK;8>j*va2?%J` z-mRL?9@C5~-ACmq`|p?@MEd~w7R=;WJO1e0SJE&@!EbEZ{X;#pnNiF5YJlVGNHRH$ zL;{VU<&}~-&BJ-+_am_@9sr145Rj*HVM$ffTs?h)Gls1R5gz3>XG z&0L~+R!f-X?M0$W8dz}-tQ|}R6n~MvAv$-K^O12@?>b^C_-zeG$xyhU1`w`Yy6_-6 z_T!s(6eyU)jz$gv{_m)Y{<>G<+Kun2)ul(dOm}PUYCM1bd}@l%T*+0uO{eHh45V{e zl|uahoMyJtgR@HcE6J&c2@0JGV8PEt#lCR)%Y!#7AO7{ATcEr(Nu73k-CDfMv}*xS z@#6HTBK%1@G#dS0>mF4@5DQQ8WO%8%Hta9-$hS+7&H?~t7zkN>+3^|;SW(Hmr}#$j zQ3(kh)GJaRg)!jje4SCd{+~BhOLiAi(Z46ES|qf6&+_Ts<-x)Rv`bi*h|}G(-I~>= z6xYI%OY&S%-la*;=X@TiVpE1K7!tjEI(dm6`QI4#Ck}?52&e)-I{8%vle4!+!-=O4 znW6UO+*j$bp%s+$y~t-TZNsxOdhWMT6na0g?lgmKlM52(>~vJ6#9Aa((|}Lh-c8xM z2>O6lzt9&X4NalUm^zv>eV5LPl0S=z9z54zYcJXiPJoK`%FOe&P%Qq8oa>e}HpHN$~QU3BX)!FoTxz5!Xp;K5>>tahw;U?e>`N z?XBXE2qzi>My*oTR;N9eUFSCQ^I4xC0a;cXp}m)5tt-+8Saelhq^x&+_auRJ0|29! z*?~;ksrT=%|H;yJxUqD7LJRzQuG6tts>qb^s28BU40fE`3p(K!#e&JQwC=llRnMQi zYpDEiDSr0+43BnhIYkvnpY4ubaRWR&CFzlX&p3^Jt2y$ql+7rVE;3{Gp!mSWwJ3%8RE3$)`|Yzc`ohhHUs^htS*J5p+sFDr8ah2{@mamsu-<6|d_Jqe zoF3us+z{0D307qp!tx2t+1CZKhV$diUA7eIED(NcR}6bH-V(Bu5!Oagr~TAWz=PQK zC2DJhtaFMnTFcU%MI+()UTPaXnGZ!D5ALx-Jf&gg!8fbr;c5ZYwKv8e*k*a&K52< zl6!b*LQC>?Q0y5pFYgfGJe2G$81Ru=7x4fx$`PrYi;w2sg;OOe^9&q&e);K8DX|=5 z%@Wlb=X|P_ktxS=tV3c-IvCvT?(eqSKRNn&)dyt{Dqx8PJtQy zPrb+77SYQmw>S-iBJmB|8zqC}RKo)oATaRo8vpO*^|MJJ|0BConi=g&$y4y!Kg?MB zcNs{U+2tE{H9m03>s&5P#vMl5{<}n4@Vh3^*MaX^?`&_Rro%RJH#OW3#qPh0gI!<@ zkYFwlcViD^Fo<%2#Kqn{bYlOjTt8FgshU%V++CPOcWW5M?TjR2(&(!Hv&w-e69K?! zSV=tQ;Z1KOYQqr0`h5_CJ~;WGz4f`*w0d$YU$LvZ7GNBT+u*Ex2C zublx>WB!@G-(3BGjK#Xc>>1Jq$Jw*YxGh}M)VeG#{t~sFEh;+Nd+NW8rfZ`4lUufM z(_lOi5m?gWkU#=M08?e;;bqI1Y{QdtEpqxp%c(IJ(`rfS#;gt0LZHQ=6_u@m9CRpkt9rU z#-_%9o6IGj%g6)3YE~u<_(NOh8x#L+Wq@csz{J*`Ib#kkXgh}xN4D_eTnDPHQ{FLsV{mN~!KG1$%tm6N8cJ$;H*g&Yw zU@uIx%{Xo@J5KArtXwW&Qf328jFn1S%*NwwasjX%K*g>yI1P2Cx&bSt+r0a3m#uL( z`@d)D|NZ6vsu{S>0cba`lufp$pCC7B3cmY5U$BM+yCbB68S?8`#v|4!Oe@Fp7 z_4&O-l-{jERH-zB4Nm?3^v{q$cGaQ3tId`Jk)-EzfjZ}|c0DK> zR_TNAd*Wk-GPN;39aT{y$e*0(bGat+^m{Gcz6)w~F`zxC@14X?08-|`-r_N!fumL$ z&Z;TL*8Y6-Y{-!Fse#sO*AH7i{4rT(y^pZnCVuqefRt*c{eL`ob)mHaf+pr2{rFoM zfY{kXc#exp19s>K3U$~ksT1`GAFR=BxMe%oWSW!c$IFxUcf*&mKqV;U?*jR8>4Uk4 z{TJBS*dR)dKqKLhI)rInkzdoy71JCbH}hX}75!JrRiIZdaEIp6_wwH3vwrT{X<(*q z`prqgwZ7={>W2SDp=+xly8n?q$AU7kUHCnH-U_BMn*4pBe>d=d*&hcmgvJkP#nml3 zaq0FK|1|wc?9xnCE$SGlNQ9*N`d7)%*G$lY z?|WiDIM3br;f4j5p7pI}`hl6P$eZ?Y(#m!oJq}9LyiwL+~N!AR|Ic7T}@B;2B_7y*Qq>7;%Vx z+QtIc7u_aQXm))q_zmbxl&)Xd{f z@n%2zdHzP}xL%sP2m?+&} zjHwZTO_4LwSvi?uTE7=7zNF2UZo)$!{Oj38nhiLtjPt$p)1S|o3G#)6g&&%f| zjYqj#PF(;8KK*mlqqBO%U5sh)_fy{{rnP_H#8k>Zcg*D%aTM+N{*>GAT*FU8CAbdn zx3qg-Ip8=kLzYMJxTZzkK5F&YnKy3Y#Tsu8Va0X9t{qC?Bx?)_-;`Wd%w)M7Re4Oe zxXg(zymJVaUZ)bkWtECW5bv$o0oLIx9XnQ@_$2qw^`}n`F>cuE>(HpxfS>0^_Z2Cj z-$y)g69_W^=^o0&$Lis)Osd*{3i8%;*vtCg9v7hJRvLc}@59{F);ntb81>FSZ)fKj z?zRX6T5omJzWKlY3@JZsUGh7We*c$b&d(&Xqkr8=JhSY2^lGQe@lOBq*;L0{U8O6( zwidQ&Q^fz5j^EWYJ9@Jdznbp<^7I8TvGVZEtY6y_zyG}QpA(w^P7tvzZ$A}e5M-?` z)?#4xeRmG0{WEoErqx0!E0LeWKVn>6WRBW+{Oq5xyZwL^O|w!D3u`_Xa{O2+skC9tu=*PWYx!-zW zb7C6$oT2i)DW9MIX^OA$riUs1z75aEq?ZPI@oI}3#X@qxS9^i-?(%6b-FD8?Ucle+ z@BHy}aFVq%mntU&>6)aeYm)N47UlIn8;f1&UZ*489vm!LIHT^!r`|Y;Qo%PxH0loI{w}+bfnv8RrEoyU z+F4RVRiAZHO_j1T;4ac~{(H;te>T9o+f^cLl7eF~5%{MA8loF%1p<-A_~`*fQ4VVd zSp}Nl_eIas|7W*y^Cnj(&q+79;!}+buHno`VfoI|48GCBwm%62NFXSF`d*?|#U06N zK_MaShh{E9!0nV%)%Ht*!pEF-|J~40U*0DJ=o)eQ*i0NKQl!Bxt3Ml5q&zxb+mwoExZr+ z_wn%|>b`m4(k$s>@;ouoe3T`0%#?q++4MN)tWxX4qHMv)hP$nH(CJE>LHijwDe_sc zabH=O@BTi)B<@!=1+f6BNb{|}5X={$y(rvE{ z74?YzzEuz3>lUqiN0MuFnET@+#YZUw8V3gl?4k~gKQhZesvp(u8JypSvx0a3a$QhS zjPmiW98B)dr2IzYmHg2BFtK^x&gz@`#!(aX`AuG-M(&38!RDn+_d1I`VjR+VVQA)5 ztQjqaE)n|KqRg+mNTSJhE;ubpEvg6zyr`HVmX!G%6ls@gqy(W6uf*ncC@&)RCDMY% z9`%3nvAK9wTx>o%z{vcfjK5Ou z)a)zISn;?xH7PP=$f%{ZQ%^SPaBKJ0&y{vC#PByCB9KA@-^C%64=0Bz~YkhB}KZ0&6>5Q6> zDFjZ$W0tC-@(#?8>%%C|A`XOHD9dhL+vB6bMsC7e*qRNp-iCX5P-YYx8TFoPw!BiB z6(7=l5x}ySrKfl0XHq+MaklN(QekJZ8`k7=_$?xX_?yt{6&_)Abl1{d?U3}no=;( z19tXa9}kjG4Hk}!!tV||ALtt(io%j&q$z(4%nWdfL{}&xJzdyXwdAnHuq~na#;(09 z8?QQDNdEEG_*?)ItaD*+`u=bzuU?-Aj!*xD?FK` z&jfI2u3(-~+EZq(H1tn5vYUug@kS)|8gmJ;SRE;H!zDq2qm7Glo{MCT|3i|1&dK|2 zL7NSxGi9xMG+xVa9z=(Y)oH$9+`?NP2a-?#8r+Z%Ax33&d$2f$LP0PjRQ1Mh+b_LvAsRA_T|2Khzq_5Oq3R!pOfz} zfR;A?$$Wwa2D%ic)By}MKFR6LcGE%FN5%D6<*U4EViboG^3Dx#Y-VaGpzM#`F@AGr z_eS$MsMIrJA$PR)LzqEdZ2Yfulj4g9jF+2RD%jZ{jHgu22F#UvLf51AP39@Ba_Z#~ zrBF**G8L+>E=3|PK^N2!`VVdFCj+9I+@NU&>d~*(>gC!|)0wF+JmVo}hyx{t4%b|H zd`ape#e(05>?5KhF=5UoS2cS)H2;VNQ!ar9g9R% zLhg07284g<_&ucDK_`#C0L~D>CGFcxcE?xro!Ax@)k%xFLixF5b|gcTQQ1_d1B~kv z4v=5A-bsX9r{bxN;$z0>?lQzeb)ngG^{kjxRDdOVvsbFnL6&TgrQ0^*>Dzr_nD@8= ztXMD=TRfW_G?mb2q%IB}C*DUemEv4-O~^CFBjmI{y}lnKxYMHc<&a7=xJNz(Ub3jW z8OwSb`jiZB+)W?rx7QTeyI3NceNeK3!}>VAh*P5klwf#VWa|_-lycBJHnJ!l9y&xu zYw$B1%$b$?ghuY)Jk)FfhkZ8i%S6uV>+8n^@*UgyHi0pKR$oBP7CUPisY&@>qCktQ zgSn7<#^^7)ji@ur`Lm^eEZW{j0GT)`WkvbsFNwPd5A`V-PeoGw5mej0Ej?Pl~Nlx(GP_-i@s?}ct>xN}b&NNJaBzp`NG3uXj7Xtlww3MY0&H04BkR!T z2U8x9S|MoiZu_ZGZI`M=3PaQgbE7C`Z5ibA5c6bVVJVE3G>|BomNuu$H3DopyA=Rm zWViKF3N)3XXWJHWX|)ew?Yw+^_lJJlZhRTYS1|qBhO9ykFA~$Lzg7Yg6N^>Nox+J| z*-E2EcI+;r!nTWd9CS+hQ%!AZ6PO52BWklae)M0}dTaP(|+AinngOaRb3Sx-P9!-lwf zMghli%oL&XG}o}5bhEG9lD^zlSXNSg*ukb$jZA6Ly@Nct^eDC2=pIe-i?a?*aq#u4 z4(J&>1>z+!<`~A$Eb768nmyll*~2T;M)ntIaravd(Db7+jSP+{ct8d+D}{>$H&2nw zW#pQ87H12pSNeZTLK48^BIaqUvOs6tr1*GwBW;^cK?lz?v=mld?@88Hz4E3<8RcPk~zVnm{FnGazwv4Z~{=?8R_ybovey|#O2rz!NYab zx2{jcm1nxcKkBWYFfuT~ELlt@-m0~vx;M%Qf#(L4-_nwtidPsJz5U}^FdntvLmaPo zssKI7&I52G#3RYAzxUBj^P>xrIb&tw&A#8tQ)VIq;H)A$^vq^MS7tHBrdB$2Pl{`} ziGJ(JOT#t3I;yD1hFK|f%g*%x=ts@zzxx4d(5Y-+^|xhl(}O38WTkGWNm`({X8Yc= zO4Kvr9lW{?4%R3RHDZE{4BDe-13awNfNlWjH!*P|U+#%FWob2#{{oHalG0z~rcVnP ze-D=M?={vu+nl8D>eyE8dI8z4?Z{2_m(1Z&X|#~R1fpa6OSgB5Z;hEa9N7?$p$}Wp z;9l(8I_HOAhIUC&CJ|nQCZ)rHesr>HSMC*gA%-{ZP-6|rTeN}Y0nUl(%oU&y*EjNW zaxThporZWL-Ml?KK8{}_==UVMeDO_}Q1K%m%_g;r9F@`|D)9|*Et#(f*gKb1gM4&l zFr}i?IBxPf&-v0(anr;@yY;nq=iNzyi^ii^e88l_+1RrSTQ;${@IF!R?%KvVDC*U1 z`$?ME+Dn61@yb^P+MWS#e_ZnVj%25*<4Kf<@lJEfYd8HCn^%ObrjY%B`QN3;VuOv% z`mAiZ=X=*wN4egrUJ{$XY?b3Gs3Vao=imKuWX5-!tzGY7M!4Z9I*8M;i4o9;nvm=7 zu67({9%w!b-Jhcmw7Tv)5iGUiK(qXec_B6@t?I}A@cXHUS zLftGMIsebd?H-+wT- z-ne@pk&)iqhYIdPZ4A$`5;ud1{qYAM542p+AZ9R7TTRGan$?Z^=pN&?ZGtGZLnkfa zpz3kW>of{#<-NOh#jIv z74%H^O9oaq!ptT1>zhP*)PjV6M8q!m;Vf*TRp*4_%#f%>Z0tNQu{KDUvn zDO7&cR>IPFyAr7`@I@$aw1F;bB|K3(d*abX|Ln+^k!xbteBvRrb7J4HmE@`&$o}im z;^OWJz?6(eA#i{~5?t(3NQq~Jr!asut}0%>z8eBhvkP#KpGrA<|5}MbE>X7 zx3OVSU3QEn4gH+~G6E&U-aSrTaga>`((f~Q>90_BiDwy zMI}zQeDzy~-aDUaWWNo0zt1%)KAkSbaR*R}hv-g%_*VrDg%f+*0u9Mn36?*iOJ94>$xt+GAQ5XrHsE`cL<$L8*%hqu$P|BSk{D|x{9D)ENt#6A2omII;4&qrL;s!WL6qj63+Bk8|#Fw;88lMdp@J%(Bc1~R-UvaHX z*1WkEFDP`8o`n`DhWA z7o=(R^{iAZM}%4fviXkYhkp0TNHzm);A_tHt5+wJ6aqhEg6n5lQ1F2jb%}O!KQ1N- zeN_z!OOl7w-K8>@k#z8RTwBV16vB>#%U<1pDa70Cb^Z7$Sv2+9{YgM}T>*r%s`9I) zXO#IvD+CCNh#YctyK?fmiXV2Y%olgxUO^Z(rJCMQJIbv&pKKs4p;!`v&v&(m8vnvek_N(;N3+$|UL?JN>2c@Pj7UsGHI55gGcN-FRfY5WLbwX~ zQGT-_?S?umUVou3^iqJrI#0C6I?Ng|SNIAS9B&O|sYBk7vr=C>2MlIFT=~0OI<*)0 z=?&-%2Zyk?&vD)I(^_GB-lF|dg1uymk+nH_?UGRbVR=-{=2ezXQ#`Z7jppOqy@U14 zQIH=pw`L&k#y5BPY#W|vG7~AkfLos!n73w7x0SK^Op~4F-8++UbKzUf-(qbPtVHR5 zpvO>!c=slSSNoQeNcf$YA2A&wEtthtw;k`eolVB?EX|#P4f^Tj78$wq!vn*TCuI1| z48jK^N>9NgvzW}SUI~-V8PLghr-WjiB)NoyBK`3N1Hy`)4Q1o3hj_SrF^A6nHw{nW z<34>KRn8u=SoMZl9d=V~OY&mTWy4z`P8xx=`4J{+gDOMcb5FfA=S< zMUSSceExwz-9|Q`bvDkgFfUhPk*eAyll5~FA634M4>T!5`LSk44uwyOPT`v7q#r)7 z{_)q{el~MlSzm1-IUWC@GA=p%BU_wKWnZb|j--h&U=y7rnX#%v_iTa{5T&yxw&r$c zh!9DwT)a<7(QS<@6h^a=0XFcC`r@dbS5fbM#m%kINUfUCnLzUk>*>XU0KcKav+8Q2 z{`PW*z*es(QFLO#Vp#qnVuAH$1*hX0LqR>tgkHXbynos&t=Wkm>s%OSTw?WCbpC+6 zl_*KSXBCxfl^L85<9D5UUsTKUXcYno@I&jQLa{tAvQ5C@J&P<=lG8q4!VKXDDfs zg?lkw_d~9nfVT|F8JXY8+1Q-z!Mxs4S{~_=Xt%h`pqzuo>2%1Z^v!mA^OT1PuIXD` z#vM}C*Utd9#UcHci@ph*%LvOUf}sLexNrR`xEZ);Yx`Mo@%;vS+K%<$T4!m9ORURf z)JRxwgM9cOIxgs?Lv$?>)#|gO@-7a(BT$Ima>sV7Y1hP$1}I{Qdf7hT72~Tj9BEhg zlPWp5o27;7cTOs+7)4}r++w`;hgw8LDr$szL(%=H9=1CgS61?61%z91)!XVH5RJlf zNhh`foRas{N4mtn#faX_ZwkhHf@_QnWWD1-5=%d#F!9uhIgY#jXQ(dnIcsr7<^mMM z6Ijn&1^F7MOU)r#Bkhmyp#(>79=+3*IyZZOUDPjek$%PRLj2n2ZvQ%~E~#NSDYOT0 zqog?OgPxiKT$$$S<^01_V3+xasQJ{ZrO~My015tL|Egh~uagZv^qML+vOO-w0m5y( z_kNmNYt$+nD_W?4}#P8hi#T|3gr z{H~GZ*59WYW8|{ELHr; z?MPB=rEs$6Sk>Ww5RKKE?+cDlR)2&Jv2>MLxg->kLyz5(>1iL(v&xxcmNI8WjW@5- zlnlw{1}$2bo$*qDmle|d)19+5+isJ@)be6E?)<%L9%%~h%#4T5E@r06W^b1iS9g0O z^Ub_VFLtWuPMM3;@iPmAzIB>z8o(3vWiSo+)5~fviBw30vx)(K^Ru$uUpa=G@2$K+;Mu>Pz!%$B4>@?|nPu!p|82-FJ-5CAI)T*N0)!PH zlQ>=xm=RY10qDXkA|H7>R` ztfyygmlm^?eqe#>ou)AQ64VArtP9)riqbSSfaAng_mVHNjQVCwN-zr0S=B-U&g)~t z2C&lDej|15Y`V80q~#I1>pP_=E4-~`M+(IXO-`EF;~7qG8Xrd{=gHJG#JnCC&DAOE zb;eAJrPUo@IX;&ph47Bl**ym zjnrT*Io* zfc1QLnI{OiY%~yCjaH1a=hrgMQSXSALNtDi{%$~+Rp6g z*ITby)|VRTPa|9z<^8!2}v=Y#0 zlnv`WkXFYpBYhiM@pa!a_agtLlbnx}Rs8y?j@$3<8^TQ5bp*pp0F!jj?4jjgyJ@?X z4i6IE+yUtMeVOJm;Y;J{&9>&hUIf@2U|tIi%C>8#*X51dJD3^V-?cQ6{=N(O2b&~< zwRo}w!_P_x(iUvw$cvgN=ZFXfywo~Iq=EP0-$LlW!RrcF-(uM+<7;P%mSWybvE7T! ze>t$^y*?{1#H7EtZ9!^(a1Z07X70el6DbeUaK?KoTU}=G^*}3W^JkZ1jmqk@j7jAv z%@cYm@;gqq)=T>$qT}Ic${U;6R&R-Kx6zNXxC`Qf%JfwXx+=HZJ8LAr3dE-taz_4{ z$KN!gZ~{JWLpQh*JsaD`4MMCOSV!ePb%3F^dh5C|8OR#_#!6t{s*b{g8uu>dn$KL* zq_{UNBhVF3=wFO*Ni}xMZJVJag*&WVam}|2MelXGUj$gRrx4nzpW%o))_ZE1v7P!$ zC^)fVr#$9R`IB4i_+jXkdMSiR?iAh7{$MCuQ4kqgGSSg&hgJ}&186j@*qXW3tF)@M zRyXK~jgXiVz{lm>enlz2~&^;g(TyCOnPW*V|o4 zToCOSUv-Drc45D^LD0wF7f#oGD_4h13!NZTSY!11M1l)m8ZR_~y+`NzO0f6!7nO%l z^P(bJa%&kP3fAqZF6Seih5JNnHSZ5wWki1@3TJKn7SYn6+>oXYmm4k1eS)2>H8_E* z1#Vky)Qs`*NI}!^Sfeuk;8HHi>Tx!|FVy89N3A3~rnd~mvHgRbkRe=3%x@t9K-|R5 zEVG)`+#h1*9Z*i6Ic@bbN4Y+S(0-Kb3fN&HVsSUl=n$*pq;r^2ZL(cN?vtG@dO)dKfK?>hZzF8^7f#)?VEqetn_>ra~F%lny6b`?l?vL z*&RTHGzArT5VNbJh_V^p=|CdhfNy%2)NK?J5#+$#vqs&<9>R5FyIaFBOw)ofz(YOe zo-X~P$=inp0QPhf^6+K@D=!o@u}lRUCyB+ntrZI`o}a|(t%JCR4@OG{GZ(a*^=PDC z&QoUE<}y>k=puK=0+V)4Q%zsf?<^nvfqgi?#*K z!`3)Oxz*)A$Eu~7j99(!SPG%>gE2B%Hwec{>M*(+j<9bb93_NT#u|_$OvTzwonP#Fo4$1JoDF}Z!I+qfkq+@YNG2MT1-7G0Q}G0Z^b2S zJLTMmjCzDuHMCdtIFrl1+~?sCp9IV3ZGbML9nZw~YycCjnq)8(f5m0%gG)?%kk*y4 zW(70TO-i!qO6q15LL+;N!w_WN^;^;}NAU@tAWkPEl%QuWx)w1wjmqRktwh44JBCkroulQvJLho_`w|F+ z!=;$66!x2~e0C8S=>%ZgeWtK0z?}VTCreWbW#ILDy@9CpYfXa6Fs)0&1Rn;hI3;Yq7WHCSqQ3$(eHZ`*RlYm1tsr5p?e zzM_X|->)&kpC-sb z9Oj(M>-}fdNSuDDSqmb|%z2#zU>ZeN%_uA;x*I|9H|W`C44lbIopSx9+tsO=AoZ2c zu;a{fzf575HY1vtq~ks%8Cx5vy(Mp@Ub#>bTn4yyQUS;kHSxZ^&bJ>BLl&~+xnd_N zv8vNK?8~rv0aso*UyphHwJ!vu`~z2jZ{;Zl6AS`ARpMpW!eb0uoLdY*uZa8Q5v;lw z#DnEsH6a~XcU%Va;i{L`p-X3D*LR+{+pu0=Tda{1Jh{@Q7M{dzWYo^swO5abwdZh* zE`?sZi#`J^S5#+p#u9Ecxu0Q?&OIjox~?X3bY2VmTd=%L_BOkYIqV$RxoIAc+0H?c zesH%^*UBEZYNw4y&95K%Z;r{T$?M0?yQdi;tvZ_&<j@9njBJqIC3Ej%Ck8)D|CIizvc2>9mDKf4^XSi0}kyEQX2N3VSb z4`MwF6fpWM)eyEx;##1c@9%}){JC-_TTaoDcQsA}UVSJFC*dQze38V=G{ZrOKc(rT zN1tF%tR9qaVvoh7EYoC|1z}g1RKAv61s9a?%Q^ePn>};a0QLM;tt)j^t=-)cO%{Q8 z7(>|mMFX{dq^%Ws&MysQI9^q@Xt5dcNHcNiu<5ANF=Yt1=y6GIJrW7}QHc3OF zUoo^ENh0}`SfQYfu<-0lRF~5*gq*zABv0u#g_q-a)iwSe9+0fZP zx5#L_ZTwZmU*e*`HM|pAa>8&2G{OTJOS25a5<>{4ezlzlk@RvV=HrgL@UOX+T@;Xt z>#I4(lnaJThs)7|TB;#`R6^=qqjHWV!reH(R3V?^heLa{|F=13QFU^r|HKPt@OJD%-CNR7Ra?;k~lS-UFTx19obWa+3HjnwcYFT!E0dv|8`1|+sVf+ zmbs~sNkWCip3W_0R79=alx@G!UOZ<;Rv{!jt|z$cFp6LqJSY?^NZwfhgYO##rx16a zy{H>!V0I6J%In$D6|a%%4;u@}>Kdl}eCP&heiJdmJ~7#pe4^Jk)~B<0D zl)~TvoHvyEl$q$+=7kv-eSisY=XE_#rs^vArw}BxvV|mb5N8##wv$#vT4b2NbqufF z7Q!-`MU%2(J6(o$L>Qfpg>hN2LWS(dwnc=Wf($p$mssJvIIe@#g>A^jv-;acJu#4h zIvZvOzz0=L<*{Q4Uu-i|?S>c1T%4=}??f?i;TM!1$t7tT`~I+WJ{?P_;Bl>+DS2yN z>k8b2512OPi2X>TX6#rgTct|8f|@SrHdP;K*l*RKb9clwO~brr75vdm6iHm0D{4l} zt9%$AW<)KgWIlI+>O+vewmqwu_s{M_QAwnMgjnwqiIQCw&s6Jt%;xPqjNXo=_rFBP zUn*w>;D#-L>!`iB+SM~DTON-ZY*cBTBQ^P^@h?i%srnIsIJf8qJsKcsafXH)Fkapm zz4Rit)zF>-!e5bqZ)WW)x0Z5q+Ch@%o?*HR+e{I%D!YYkv7*b|ffhNB!031OH+TBY zS!F`DnU@gI@NldlM@jyPEum;13;{`fy^AW_wt$|C^uT`_I4I;aovo@X|*`5Y92 zYFCrjO1Jqes@TBv%xC@Z|L zCD8sY>P4Vu*?f4FNfgxArx*zc(8t^uM1IT~v^m$DK! z81qKmHfVHG<^;UEsP)ewt7dIe62q_7`2#T)(VP_8trF>iG8RQy>^Z9EixBPre8t@IE>4s!*r(Y1G)x!A zCZCwej6%N%kAPOXedb+@jh~2Cy1%<)e&km+o3-X6XNO(lBpV%bW!gD62Tnoii?UHnQ`^=>IUv)re(r1x7BS6_x8j?!NxLY zUoIrC1$B{z0PBg-us^u}BBqu9#9Kqmq2Uy(V8ktT5`E5(T2OTLbZZbX4-#wjX@Y~q zL_{(xKV5xGWObWsyO@L%W;+hw)qB|*R~3`zQ#e@ql;~@bpYE#PlP}KRn3Pk!0pDy+D94VTZ4Y>EF3S~JoQH8GzoMjtM7Qei4nf&D z^-HpAZkH`fPhz3==EAgdTUPGF+wOabK`=%mAGNCr^fb1VJGfWmBw;&jaW)bs{y}A1 zhH-qWdw7NGL>RJ78Fyo~c30@fo-%f$z)p0kV0kckcAa*;Sh4BL(t~K;qb=p#wZ+a@8EePbscdAC9WVj?7WC%Ko8jgL5h<^@ zVtLp;>JZ1zv|<@zlw1c;qYwaP_X7xjnU|Nhb^{QI`Nu>>y`m6P0yHa-um`{L2O&QY zp%_8T;xCt)FCxg`g@XTZ0KRn22KC@w$BJlkW|E0%I{j&YU1R)g^^05eig(%lEMcv}a2ZX^0kir`@}es%k^$O-5F_7$~B% zG4i@D?d;pUA%iK!gAQ=C2=cM85gb3SBac+035xIds@)uwku%)U`@e$m)0rFeFYE5^ zT1-OjO~KjKJ^SWE>_+lL9hNM9E9ldK+cOyc$Wwe>I68;d=^c=~-ak;ypy_iGytVcG zTW_&}{v%e!1+#|0$5(rq`f9r564#gP`?jpyai)()s~ks+{Y9Xh?2-H4n{Qqk*a-8) z-N&b|d`2m$iO(K?Z@-LbMz$|-8Ma96TH0PTxe9hD3*zmV;E$r+_4_LsyuE>xRG#jk zABZv;+15_)qiD!vkZRXD8}Z4%1@2oerUyARZr=S>hZjCkY`3j4WKOxGqMLYXK~@c_ z6F?!WWigbPA3UnFSlOU0#~#BsLihMnJ61EWTdosNHXjLz4xn>^Tw_qhnvApCwt3!N zp3d>0Mo-EJv$Wa|dgw9?q}VQa1R!1e<}2C#vN$am{PVL|1Vo)TNrO2IO3Y>Js#Svyvgcg{jJ&_p^q+#Cu{7I){gp}1_FtM*1E)4KthV}dF7b(N< z5`8mPV5$O1cXLCodvaS%yE8#v>_&RfWeX9nviymD=%w0sQ|u%B&4~4ULzn&SS|bjF zWElPFPHMC4+m-$|8P2V9GJz0aLU#ylow}Uq694?xs3LT~+1A{$I)kz8YBjj%hpnaf zyEGiSp1&FB{15dLq6^HkO+|l4YIxH4=e(!)hZ7UgkvO^F!|=x2qrd{3^|~AFRwA5I z&E0^RQdXfegNH@AE`2t`EFpEnQXj0u#l((*>edU=2JN#hoou7an&VVC*{=p=K58x3 zkc>#;2eu1Tmpc`c%)1F6xAd1Bt_y?{?CVI9jN8Qppki1yJ9cH=8Jza5XaLou3A40A zubI=fHa~NWd06baCQ;Z~1UZz!K5vv8Vif8J%+KjY#BzO*V=(4u5uv~p0DOVlCR6LG z8_LO$bixv22Xv!}4d9D?U4Y8yw!B!e?b}0W$jNugcxGIGVd#Lv21*huS$#X&WR}@d zs7qW68)P0TpgP->Za!EA{za#w`fJ_OH^9y!5UIp}AaQm)U;}ZPlgK%$dr9w{cjeh@ zNs=3T)m+;*1~ua@aQW_R4&5j*Dad2q{eEuww|F~Cm6-rK$A^d8W*`)!xNF$=-H>9$ z*KaaVW#vwpb0WgtmQjfyv(buucj8lQLLpFmOnr2zMcT`}&s2x(Ht3g`>x&-P3g-|N zy6%&0%A6?mdqK`@mnT@~xtGWpn;!h-93tIzyO2z>tmp7&!}s@^146u&${vMlrIuhU zB~zhno&^6N?ZU@T@sK+lt{=*Rf|?jN(7DK@MXT*nGvB+QeQd$EcpDbfRA?#dSMqoJ zPxR%74uTU=sm@8n9WFJ8fZ&P9mXK9OG|ubTUY}-CP)^t^Om=sB#>B9tueWcq%Le%= zQKwCnQ%vW|V)@AY!@U~+!y$rugwf3mqQ@Z`KJLF_u;UupA@BG3D$+@*5_vOQ0}y{M z&yew0FWE?`{5j6(Q*WF0cdJK+`SlV<+O$fJC+jmUuo38qq}kDi9M?EFPkRN{HQVTrzuIe+{d2i!D z#g2{XLPB;g`Y6GpdF;acLoz|d@5nkOErM0_gU#T%%*;7DFo(*8$Zvh0)d%5%yc{8& z_YO#5s!CajE#_)nl84XdNaW_qKUc%uy;t7Ltc%&>PdTW`u@T~AR`%_G_a~VfzHPhX zZJl;`-aPPu!*%Ei_Wfu7Y@Wpm`5#+&+awC4DjpK)dn?LaleMST<8XKf)dqozIRjS7`q+E(MO1psi#HliH9(DegCPIH_$R?ZV_Pt z)*_R)4+w2w-G^v{!yCg1Bx%uELU?(iwK}wap7cOObF$Qwf@6%G=byvSTX2p5cgH)J z^{kr8qfEhauB3zRKHaxcP zrss<*Zt715}BtWBL40R=-hq?y)R zsv3}o$ncc`(f2owIcoatm)^21O7GJ-h~y&E6?a2|t^x+N5+NB`LM|Omw<3HKOnlps zx{dh<7+LA-DlphqDH)l8$7#l%HVc5iwM2ES%T=&fmDcXn7htUEr%JU$(~ zyb_D!kg2G2x`ExYPnL+)TIGe8>$dv=y>NoJshk~IbUXay+Jz-gA37elZQVCt`wx%T zYr~Bg3_!cf6PPZ2e0XK7*0EKe+VZt02^QSS|GsMCGF-$l$q?P%8A>`N=2(cMtXtg4 z|E}F1Y5SIc*O4jZhL-(|Pb*^KO!rshI#K2mEY>_u|Gd8|Gky@$O zcyt{`;Nz`3DU8*A+UuXpFYUnL8X+6)?P|oYIDPC<6(U1ZuiAiv$VgO_O!uI;_3%IB zdJ)So>hc)|v-^EZ_ey9;dS1H3upN5!bMj}_o(>SKWw~A3TQUs}k2X&;&pgZnyH<`v z+WJ)AVBH@8`ZZXPZW!s=Kd|XzWEM6$&B|X#hPP|h_{(0j*Ey?fIu?xl84H3T&plXI zJbbgXS9S3Jrm|J|1Ik#ynsR!dZn#rL797;n1`Gh5$sN7)^Ilb$0e{@D32krWb*Mdo^+ya9e3YsboUAqIHlV5fkJUqfzSmp|wS{5WWPejIm$$cW{bK zihhCy4HR2x&2leUxLNMP9C~JRh$KEab!`QyERM$?A|g37){&(4cZ&kDKu*fCxBwQ$hzOog)uA>C z{Ikx6pyf;KK%fo+ZG3vGARBvp6a0D!@_#|yw7)lmI1ZO4|3!75b@G~S%fat zs7FS$==Qd?5;l095xr8%4sTOj2>fq|UcM6+zCC+c;7ETAL1w0;Xdg*h0!~hyrTaVB z%|tjsP##u>@~q!Kxv`VZ0{UDh;6$tZ{yp;mjjg3@nYT)X6%dv3CfQ3?m5qFNy*@12 zLTUn4P}ue?uUWcmkFnoHh_!yCv}U5Ar^&}PQc=I5$`U&hyF<30!Oiu1O!fqj{FH$^ z#O@zu@N=$Gw_rxM_iCD1U&)$MUr*&q%}>=$O=x=YtaMYv5h;7w^a8nYkw*Z z+FQR8l-vYdh)WbHFd1>pl{D_3CyZrW`^`rO!3v@p%qL!*nQo;u&mY@K-s6j)ywhSK z%?jss0lQX_R-BavMEyCH%s?;I|ye4_UHF9Ko-|Sr_Z-a?x{}A-B&8& zCR8emT5FgtMb5_u{cH%Z5&vWnR_Xw3P>q&oAjbMIBd%f_;2kJ?ay$_ zL9DxNBLpOP#B~zC zWw@q>_g2KRLXu$SB-Of_L-Ty2K~O9f_a_$q>V9j@Ma1^Q0KkvF!oyZ9 zC!FsZ{K%xb(if)G4hjC2+xY*P@ppslSzlzr_*j@lu0 zlj=;RWXnce2q4@y9@+^)^9DDn+n7udDv{)#&r;nrusVEx`e4 zMl?AE1*}7vX-PZpd3!s&cK(zC@NRo%4zvI(maGnId3SQ%DC>Yxc1{1twT$ZFb)7>7 zWdyyimz{2`0JNIFJ-b;-A7Be~jV4P?V17ww{nKlw7RL6 zbTK&ch)Q-XvKKvr(dly!++2kW)%s-`SWMmn-`DM()91r|HlqsJev!7G`9b_! zlaq!n@r-%8sXnOGnt;u=3+Q)ca``mRy98taGv?{{F!?wR7LmctU^O=Bu5{fhTKE8Z zB6r6hbjRf|4gyEHGU-@-^y>_Qb+}pv>+3uI1x(3_^xC%Gt4AHObBI*FIt{PyldAPZ zKbTWQthO2ltMDv@PnyFlp9#uaS1s+M1j*P95&JitNLeMk^rCN0hIzBe;bTYPo|!Dd zEk5TbdiMU9HCJXK4a^VjEy}kP-u08rKV1$gdBH%Cy!?;vY;Ip}i0Z5{H}AUiPHZ>X z_)wk>Mfk-|WH7W+0(vB&iDnme)oXpNpH8C2eS7>9_vw|GheYZ=xjLmPoDKZMqaWx1 zTbhe)Lf5z};Hv?AS67LsW|b zC40Tq4pbwEpP?OOO@%h8-kkDH;Ci_=qse8{#XDuY2LEx<2gL$a?qq1C83>3KC}T2` zDOHFXs5Ij%CJn<_Rn>HqFXIi#u6l;W&}bn(&o?Ch-n%9Tk0kBFj)zcc8XTsj>$_D- zkq~s0rXumEJOg{`h)vH&pasG9p985ue+jhgwP{yS?oTf`LF+&7>W`dXl2-3k6N@;`5V){5pWg zLEeM@t~bUJ1+^KSAO_ppYbNj3@oZXw#fiSP4u-WJdEg6Yf8}UMk0F1S*8ND;=)DN! z?A?z$1Gucgj>tUj|MFHZpt*jSWeV~=*m8p{^09zU>tF+E&mhC>dL7a?v7dMI^UFrO z*PtWp$&vIK_4lXEE4@;-1hjlTYAyrUUJ_MhJSJBaWx8b8~wME^k!I$q2bp;yr2l|TwWVOtw8wx1Me|wE2 z?^W5{>73v#ni3qmY{_Z=s-9=r%OCIGk33RzK1BBfg2BR`>ic`U%f`WAPO9r|xo!da zx(Fgp@4zRkmgQdfD6Fatsi~pMKGC@PsiAEa6!cSH{tc(-G7UpG!n_TBa~-VuwWDo!GzAED?f}c^3Q- z`qWu)M!J<&w=PkTSD+^%NrdU*4Nz6Qxynomb4)=Qp_Y)vVnFL|IqNBGGzIft{f!B_ zYx0vrQD)7`wg_CiIOvDPB%ZCdAX41d{oYBHa)SQEq&Ks9^BhzW**~q zfLe#DPDLm-`Mh;Lse?%DA<=-0N=_+h)Q~?j2V58clyPgovv?y4;@ZowK~5vsAa`)o zU3*TyhecX60nMe|?xBH$S?Dm+bFxPEhw72J68%=YRD3O9r8ZMavi`lm`(lC${huvA zrRlVpQ#K?WQfG^g(L8q3S5jG$vT3?kgiH6y?OFlV@rZl47>C`J074b<&v>uUP(?8cp| z(LIFSvO9Nq>WF1|Fy^+V zU(nXS@H{ymOWq>3Hx@Ooy~(dJqM!@HM$pIEVqw)8Z^q)DF)qFH)im7Y@5EkPOL!O1E_hASfLxZ3M8LFq4%-^pdvy=p$QX z9DGIX+;lDyMVFO5OWGp;6MV6^9{*t>Ri+^hFmR$r15;A68KxkSh0COWS?SO}d4FSU zvjq?|wawa3$C#Rze;O9#00!vfyE4!MeGnkF16b(1_XTPusrSE$=JFIb=5{Bl6jmzq zA)ayr{kE6+b(TA#nd?h*2^$?AsJdE4hC16V%TM0KjCBy>?;4nHA0{(Bh?le;f83iW*UpigZbyH4zE3W|b%LeAp;x+& z?jd@)LCXhl2=r-2S~7JF+2AEO4&w5*(Z~myknpfNEOFd5qA7;3t3Z9c&`twspr5e< zi=yu{zeTUagX+S@8%;O4-)WB4a#JRtT5M1MsjOFMV^zw7umFwtT~*c0Za?{JLGn^ zRWJPN*7xKdRCk{q?03vWJQRApgd*uI+^WGB$df1xFLs@+xj)I6=iV^?th6B!I?6=k zP37DHYu%AIS8RbQ=M7|pg3#z?k%3p+q&(H8j@JK5zxlg}ReNBejrNqKcm?o+L-xP; z$^!BA&Ge7B_{gB4Fu7L+EF+L^jsr0#W9bhw&`0;4_|Wgp?#wl%&ej2w(F*O!!poIB zU^~|Kzd=oH)rtAf99m;w@zz_#y=bQgR)laxrOIu?V94>jTHIXkhN$&&YI6@EC8>XQ zy;kFg*twHC!sND@y@usa@`#4m0jNfBr558qdYD;p*M6fN0d-IPG$PT}YPODsD%%=i z-Q%6OnQ!nd1Ku5Oxx(Ek($y5uYn~PVQomr$c);^so8nnrN-#IhR<1?S03Q%h%8ttV zGHn$B&Xnyo9dmE?6idaR)2@6O0$oHn0r$1kx(vY^#2CSP&04tb<{p7-9D&S(%*>*@ z&_*!42q&Z3f77JyrUmTA7ZwCN_UNcdRGbr9aZBEA*4V@uerTJW^E|gYcSvtUno6>N zI=L@DcU{N~vC2Wx%eR5u3IBG_Z0TM!@xJgcY<_gQheWC$OqK+}d?dQb5@VJ|;T~Ma z&6l;4MDJ{T`S;Aa_DvnL)Z0Xp(1DffJ^0R-%^F^t1G61br~VtI zgH2OhS>_dmC;tm&$bUjf?dPeMCu;{dX;=3*1pc`mhvB3LT=gp6fP=;DQR~)fy|#+9;NyhU3GtW++wwp;%SpFI*YeDWZFe^Cs}=?_s#O$ZQT=`#sCl3 zWHqdbB@-39vCTY0D#8jYTlCmFw<;hS|8wvncK&>CA;ca|f4GycsO_y=@S*sezJ--# zZ|iNFNRF9<9#FW{h4I`}TDFB)eJy_M$ClhpnHjxn)Mwmpbs3OV#I^D&PE z>c3>$B4K3#UhF-mkn3vA-`L~o&}=Av^~-AELZ1`aXlibwptPN}{Yak<7&xUVRnCj= zt>10OnZo=b()Xkt8&YofH3U{9WTV@q&_xGJ{YReHs;>R_z>GpupR@@KO&|p|36Bhj z`LJ!YCP!ViEO_57Lk~+3GOO3RHSD#dEb&RuE<3m-DjYLfffCS%^CWj1_#No!aC&E5 z*vrW>S|VFg8sCfRo>oRGl8nMF^@*->N~miL^|8J6JeXTjHhnn+lVuEn2i`8X#sAAt z7yvli11NR>cE=VHBmv8p#JGS;mkLCO4cPmXXHKP@J~{O1lm9sBYlEnfEtdg#XnN7X zIJ-Ne0$_T~Q-eacbf>49oBO%lx&7=FE2DDZCBBUHd~Vs-7Nl=eO5-r0(%x)AAGDS4DeC*Dm#9{=hmgQk9d%@Nnn&;egELsCd%p zzwS0KFkR?H!H75((d;R!3gzb0eWEw5cI@=kv5V)u-cxe|r*0!Z1PLeVw}W(cn@=wm z|DJ4A9>b{?jro^Vf)m{qV~Fg9d%hIawsZvCZ~B(8mB8y4DX2e)8SW&^$jSa}xaM)s z0j6{GN~Pz(_&{Q7L{63n+Pu&jGUh_tp>e*!p5QWb1$e{uBWxVb4xSV+et2u<4d`e< zO3E&Ke{RU(VB;}t}F z;{BKv=6{*w165#MRZ2QDk`6A0{T8!Txz%*Pv!WN|fzujElbraZ54nnb5 z_V6C+4Cg2o;YjkW^klD8XjbQz^`ojhJ{7_9c84`H*Wa}aKn2)#Lzvf+wf})XpI^P^ zf7zz9?H74}*`+%A+k~k;FI5HW3L(q%2=M><4vP)bDX~XbZXz>3`zvLG zD|#)DI%u4cGS&YW<^ihn01cr%!Vyi)!o)N2^0|S%yS2;Rcs3t4KI&gqd+y6%A2v9D z$&jt<;)O0JvIWvw`Q-e`J3oUq;j2ushp%5*vOaOB+$c(2ncik@?|xgtMEi|%>vben z0YE2~O-GBj2$V0v-PRnqnu=n@ zKA2{0p2QEVwC7JWTywz}-bm#FMxV3Ip(?laiHxG+cQ>d1)p#uw0IMUKWeE3!yd;4h zHv({?ie~a~bFT;ETcInJwjQ2BJ*)C}%>YwzzIBGt+r3nfc)G!%`ziDjV0U-YrDoyN zYOQ~B+#+Ify?1;q>~ZG0+??2VQYKA0y6c?8!iaur)mr8@q|@wM13X%pHg`O4HYdaF zy>N~so|xomt{R&dSX^584E^Y&I(2+~U)t{+&rm}}JPNxEYg z2{24Qbu%Si{uF00O_9N#a>w6)^9&AgymE}0%(ZKqL<)|Pj?~&JL|Lr#3wLpM82aWzM)NYKy3&NY z+gy3c;eAZUXW?0}*Zt0k#9a!)D%E}|R z%0=WAPyadbs%}8ihIf5*UwbP4EB37IqN(?k45Q1EPaQI-`5@zFOODmzd1Q%4M&nrV8>3C1&;5~X7Ke*uT;i(jH9m-J(^ zvz2=vU%>32!yi9b<#T#GyA79%aQtwbnCT0z|1~y6Nb^@QqgWc;PSmfL&@Y=;MbnDL zK+F}*$9(Unw9~odO}(Z^C`^3uF`H}+s-EP{I~iumHu~ciVn;}{)1?+fPPd%huXigd z4eE$9(v=2Vc&q#(s3tYTHLd%nTPmL=S`#2Jbp8ZQtEneOt-cZNnFEK`5}l zC3zjZ?(F{DK=JXMkhI+qHnr1tP!H_!=FaDcYoi~5okzVQUa#U=SD!FuLcQu5!iqML zsy~D8@}A;>estan@_(c>OlDi>`GkYoCu@3a$LQv)sQ}-jOczc%ujmHx3D4SM-mK@M zwbN|fbbRLuY*-o&uzMWn)#gY#8YKL-6~U}i$O^WPVC%P!Kov|mjCbyV@!5kwgzZiBUMxEpxjHn54MTS`po_wvc&Vlbtex`ZmFM7OCUPZf&~e7fCyVlfEow+WW;K-#aC-> zoHLHA(IK~UxQgg914-nQn?lhwvMlqce`+U4atPp!Zv zK}rL#1$7EXD^}v%)9yv%1)o|`b3HJ^$B|gUt98zXcrq+Y#c*%oK>`y&qa3J{^DA7Z z#^SD%^=b0u_oHAqQmbblRwKr~;%}a^_e}?hB00~2Fbk4H1;f{?kC>SkDR66WHv|Yt zhOd&8Ch$z!h$P$1p=={7i(gI=Mlt--^z6FNQ%2yUD7Q)p)HC_2&%O2(*rXSxMRtT_ zgD%1b$37QVLDOq6oLMjyq-p(24RP8~>#+v&ieyiM4#hV%8qTRbdj^@6#;=&;JZFum zKNMRi*C>@2bL86e-{SJRQz*VqbQa+#OVT=Y(qq`Lbur@CuRJAA>Vorxya1&s16jg8 zuqkRffqA~U98ciN2Wvk>i8=$I6^M z$$sNus!&Y?Zqvo11p`8f94lT&rAEqq%3PJs!pGkrbOO@LtIdf|_%#u2Dj)ywm!Lkg zq47k^#4nLGUEHQSQrYmNn5I<*5K>zw7|rR{V;Baj_$rdZgBGqywjW0npIu(ykffB> z{K2#c>K{5ERl8ko>h=J;&XcAzU6@fm*{%FUw|6SMwx~?44!$5DNg=^bO9AdrJ&;cQ z+{sl^d<^fSu}0`5+C9`ieY?2N^l40TT#Sh0dcd~JWL@=7mw4L^`&x3^u_*1Y)PohfrqUQfb^pi8i);^E4QTBo>*CmTTsNnn!{^wCJZ5+8;12 z59a-$@P!jOi%90L-?7^#@547{^y(YG>BW4`|E%PuaOnu~N_-4MLp#mrR_a;lf2ZDP zI0n2gI1%~W*RZ7ne0Hm9=vXdEkdz$?5x?S9NC7Vz`lKTX0*C6h*d(>h1~-tylrFW( zws{hm7Rf5lM%mxX3`7ld{Y(%dn1O;da40fk&ncdkGjO)h3XAyNQ6FKhEvMp&Von%e zCwLf3G~@X`b@wFtLNMYR%f9tYi|Hkv@KXM9dbvOt8U-GDlb?zdmW*8sS>3aQ^Y8F3 zm9fw=XIQQes%j1B|3|>y0b)9lF7dO7( zPKDRsC15?ptufS-bbk1^(h9$rIbD{mM?AT0CRd|Oi!=QS&}#gPJIj>k`+;X1n?nh| zhB}`Eccw-31~$=N{~maeXTdJ1Yo^EN;oUW1F-cdKPrD-F0M5f2iSW7^gis4r)*ICn zxu>g*g(fXk6tK)>H}Ix$Y6?|l2eNc004G-z{Ml})X68)3eBU-nM!HA#m{zL9EX*lA z;lY9tKlTC*G)(Haeq5S6Au0MQMXkk~BqqVh*=1<4J$G~%{4eu~D1_OGQEE|sjHZgD znzX*f{Sz_zFlu&mWj%!^!oTeY73WOIKtYX9NPfni=9>w1v8PNSM}DPg{aEXYk$!rG zt^szZ_}Qs2ni5!FMdS!(*+OVXNtq>im&cKU+511pK!6Z=DD+mSYT+^;vYEDRzN)Rw zLq~RVerq}<;*=4r?@gk{DFW`0u;ciG=B50+mll_O7cB-+BJp|f+Mq27|c9z7@ zsS!Jito{9{f92|ED$m3*T%HhA9qiel*13@XRJsZ6@L(#Vvpr9ixMVR zYhNR)#!Qc=a&-CFhaW>?-4QFFajWxpGtqdsv@8(FaFvp^ud72PZAQr{WEo78?WtYHonE+YyH*&eWy*FPL#sBZFh44Y6=qLisr+?eli(i{vBiamOQcCzdU zE1J`D-qcj~K#^edh&imD_1hw4ZHyZ>f?a0XkgIm+i~dU!R;FBx6-nM8+#0sK@1PLe zKz-E9Va6XCV1{9M#(9J$u@&UYkjUpM&nF6^yPv@rmKVv~4FN;^V{Y_4##50{k-y#^ zs!!iOMSbXe_aX>qbcvcXce1@3r9(4iY!|#b!SzyC2W%lGOw<)!f8{H)awL>wk3G7- z$eW2WogLyeu&0@b7BM&fHmGZ01C!C!?;HLOpdvz zrTCge$8J@HQIaFJq5yv{>Dc!vBO5n3l~Ln8iN;mlk(b#^H!qDh*J7FXt?-kVKZ{T6Iz=w{F^BwvHY{Txli0n@JnCgx z{Qk>*R`cuV_}26TyfM<}Da?wlT@U>;q>a1l;yo{==)m?0MOEa)^}&HxbL|9lo$C5L zQYRnQaGrLZdSL9cwLVEi;YU*D$4r2PNEhiafCI`+8km~GCHeQw#@F!{yu>Z+G~w1O zhS9D(NbUE|*4@H+2~BFtt{^P~mXMCx=q_1hbKj|%keGVD^oWHX&X|5nv%|q5qWshD9mh{SEL0dcM7JB|?VOp}{4|%2#3vW%Qg5#~S8J=nM zhqx5NfO(ni?x)1Oqk@Dot2<%5&9XPgyumDIT!ttO&Ge zq8&wJ7BbD>)K+&Ya1M{+Jw?2p@KYe1=CFG<$~(DAZo={u`K}~R;w+I9?dmw`lS(2I z9YP^PyB!{{>@B7h2oqBWYcl0_M^W%BSd6D%l>^A5O*jg?pUi>nQoM{NyGH%m>?Lltz&?slfc)dX8OuwTkQuIvYTN^6Oy=RqhI-fI#mn# z+2W``Y;l%TUPO)DV_WBDwtTfVIiyvf1plEI+=IZw(A{5rjES0=HyEx{xLo4g(wOZ6 zf;dM9e-5h*H2hwTcTKBcXay0Fv}6f3KOoZQpj+9aNUAb@E1D*^MvCh)_Wa63Fd7Md zRUeGW*_yf5}W{ym-tBte08kw45I9CU$}c2qk~=#I4d;=3CaO7*j5&9~9)6 zh3DNpA_4#oF<$09ud{FE^fcx@X5>wS8|hrwDK8&VPfWI$-{P#mp(E zv~;f=0EG9G0Q6udg41SYvEd9COf)E~Cjs)IbOE(;3)4rK?U}+cP7#e$tUkS++fXbg z{S{q>vm_pc-;!_jdgq*-zC7Vr?b9c`i)5;(D7StL#w5fUEeq9xMqWy@BfnxsQi(__ ze7ko{s2y7g;m}Ip7ZhbPT^%VZb0X8IGF|KQjI8}LQBa=wvaoFU`8E1f@cM*qX~^PpB1 zps7m52uw}$fKDABb?dcrehNHx5y$A)Bup`O7OapmhyA4|0JcOo5z6*{3nTI)_!Ruz zJSRYe1Y3F|_>_ErEd z@~MX&3Ogqe)iS+g`AV5}*#Xv{j`lN+I@9un#p3-awTh3Dz`zJ&FYYaYg{vz#Ssl9+ zykhz#^qDN}&e}3=t@*;12lHN&RnuC|w+2iJ3^iplZijImD8v&{%y9oUK;V7Gpma~XwG$iBXAoazU3Hc0vms`&Uj@XELzI`^AbDI6J zne;r-h->_5l885fzFyxQmDU)IzdQkb{eJd7ud@nAp`u?o*PqD-ZSTH~Gq+<5we)eb z%?t5jE3-xe!y?GkF6P@$(DygZ`1E+?HpwW{pG2-=~sh@>G?E>YeQ_WLU(Mv>#rf)r^QQF`z_v zE0ovTyj*kN8%J2(1EI{N+)^c-uW&p$J2@Sf z_Ps|a8L~bXRe?`7e{?VNROTLV!bU@`r?^a)e3XoXLTy$89wY-h6$*|zjK2;r{5#oC z1HFcccO^#&9mL2^Q#nCC?Z2_nP8rg`@~LE=zsxr%uIzlMFLh7t+XeWN{f;E(nQ^Wl zUb9R2k7S-;he!~j!Exg)rw~w6-pP0ob#zH;EBe6HZ1S#;;^>@)@#0_D7B}yMjNO!o z9AX_YfY|=$g9ceA?GTP80E(QK3WjUhPl4`VJDeM9duJh6MqUUammxGwNmaPHfbz)2soR@nCC~2964m-H{!#;v-3Er)tP1VdK~f6 z_06R14-=e_5T-viXguO;?*8OC5%5etJlVVN#<1O?D@^j&jAf`S?9mxm6dI5JsbGmp+^BrwrnapTneja`91m&E|4ZQ5eOB7P zh&P-uwZKtE>ce@BV0yUiVmwcptQPaO194{Q1ma|nok_}pc^}8=*(klkAl~ZUDF=#- zyULKWa-}=*viJxW@mB^2ZT9xso-s1Vhr*xSIN_02tfmK=_5|la{qAYNev8=c5`_wExa4d7#&= z=WS@MtXL3L1%?SrNNGO(MaTsDcMGzVhDF8M*VaB17KTSjrG}ym`ZTzkY)gLvcPm~8 zwE6uKjqf5%A@%iTC5}i!myQ?UwB2azHUAE;y~(bx`zT^14IJD6Av~Gs?RHjnh()_B zGH7V(UYy)C6kut9VbotcnLrprH*t#fW|y0ra-K%v-!)+hEb_akrmQt@eIk1IxfF7n;l zED81hDj@ar9IOruIKqJ%hL1r}lA6f}cmCR!_GOgpuvFxS0ho0Ek;{iy%_#t~oUk`F zTYAC^VhfQk+8LAl9kKWI!+50_sUmPdmFLk3jw#qvjl=CL_HK`qy2}YgbOA^c-2G-Q zKw+Y5sY2!17_Vm=7Q6`|dbihDa+^L_z!igoB0#vQ$8p5)+d5`e`Lcxtkcw+d3 z_k>-DrwDrsj9MB?g8zTv*RW0xd&!H!sNWAgrMN9!ve}P%-s2AK(jRO>Yk!OK9`yE? zZ)MZ#^{0Q0TTS-td5Y1{R#mGv$wl_Bbv7gN?0~nN7~FabWMQ*7LFU&HlvyHG=3wI(hri_vnjyYG=`R!fRork zQ2iQzRDBqakW!91{k;l;N6iBCLt}{*<3#zw=gX`AjQ7kX#hX9~ z@Yq~)*$^f|fnh`DbN1yLn_AoE8edQF;g{9bA4i-i#ZSfYVL?aGl;Bp~{sQ2;Mm>S% z9UGi#y`hsj*?r3I9BowF+^PQrb-zUUC+yIJQ$^D54Cmzz*NYc(^`{}oFUxfRyD{?< z&4u#|Gbc6Etq6-h&kH26dsocsuSUP0f3e-pzEK@##_IBSMf?Ay=_n-n<9S&pRtDPr zCW*Mos#r7D(N6*SeZ*S}xL(s7aw|3O`AjB2SNuGE@+-07l@r}#G@h$`o-lpX@X;w` zJIPefdJj1wa=-hgOQ<1WU<)}%N@~7l^+_3p^E2(U-1-SsOvFfB4JHQS{9_>&&j1yH z-l4QP@I95lt!U_`3ti;#4gXO0?;pd2fg%BVg~D=qM~uW*E3NdjK?T zQHbi)?f z`Q38DzTIml^fyp3y21p6!z?{#8%2_b7>%9L=Bn|3lCf}h5EB0o!yv*&sgj!(g@U81 zThCWRdAp%&So=1_YO^AhHtGG<_+qezkx{Q7NRs(s%v$(wVW?cK=d!pTnatFJzD z#s9%-HGdVpZ;Kyg98dlK^Js|Cs6o_m6RqE7FS75}8S;2#7|%oxLebNk``9j#b=A_j zqbQU)7|#$v{40%(V_7%ud0=Yy-Wbg9maGjpiDe|X@`t!4;iee-mwj8bnaQFan(p-Q zSu##_vw5(U0|k=t9Dm(iwr@+j7|o^Z{Y%92emw-|mHhO!{m7EiT<%G;@plk@TQ{3B zR?3Pf&_Qe-xC^l$39;3f^Y2zIJuZpE@+AAgFDfBZy@E;+@;i%>pR@u=I=e zU*8HwahcA$*p}xL(nurUBbw2{Unph9k4Kbxp3;oric(J%bD3kUxJ8DH7|%Wsk6S&J zTT5LR#$)@0{c2x}SmXZ9+ir3bUmOen9uM}6m))G8^T=0G4Ydt;8|x(9l7#IIqH~r; zFW9BGL`siMq#F9Of6Gm1K`6d%U~)cT%t4m^#Xx-`Zif}MnS)|1jW4bA^{~mhzl5Uh z3tuL8EeSpQ%GQEI;WVb#A2)dZ_NHisDSUq-i^%Znu&u_%Rd=v)rPvhHrgjR~ew)WT zXMa9)_b#Vym!}@?xXUCpdl}^k#O}_4yu-fqdK$iBta4-YnC} zI09Q6d1iXClsL4&L@xtoH<4tI?ISi)*$y(7pc;@gXL3N zozF6l;B>gEX15`ZmeW^$hCf< zJhL?S6EMBKyvCJTT*x;nbbPl@L0RS^`Q_%!0@!>X!Ls`JOr3l~F5-)LYqwp^{vRD% z-f*b6X+=Yi=kzuG%0$&mGg;gUf?cn{Hgrs{ue$|BG-_XO#ZunkG(9b3qX5(mpo@Ux z>y3riXYZ<&S}6*b;CxY$OB}yF`Rw8{?&54uXeNgJ#Nxza9>sr|Qr7vpHaJcHD%t5g z)duWkMbmZSH6r}Vqx0x|q{)GWr9|b||ytybp|d^_5rQLEBas(X$sR%#F)w2Ln}S73WD)9M`iZb|{wr zN7PsNMcHj_BhAnfl7n>1&>$t9(n`mWBF!i@Al)6p&@K9iq?FXq0t1LB-7!da*LVAz zbKdU{@P~8nz4p5H6>Bp9`o+^`wB`c5M=5qAGrtuRJ|mKSfaNGEmR6ga&bxBY0wS%d z==vAd6oFBzeb9n*i_XvrP3yil;z~69Rr~Vm_1!EuXU?-T)l6GcT0U>Azy0DQHE<^0 z6{lm;EFWu|cJLPa`kw_Pf<5jRn+_hVjg&vT-ADe~{rKu-2yI`}+GO?kc~*bro)7Y3 zz&9=ao~c^}-@TjID=>0(mgFxSXAsdY`lwid*5q@UF zRos_@sc?WXPUC=#(?>$5Tx5h3n7OdEB1lC(AyMrYv=a% zTwC6~)~8;JjE|vUB}ltXom*}B_#5sH;&{9pxdX?4KS>gYqHtiB76ZHqL!|_$0Q7PA zXi04a^pF9{8BnN><4{?(*?i5$Go&wY-}?~@()_Cn1ExW3JzA2d_t4)(SsfteC`FnI zJ^2)Myb_S2!!6qcE954~t^D`iZ3uX5W#PN*x$UCZYqP>%+x162ayg@v*aVb_>Al7i%7a5$ zCUD0|{{4a&9%vZ%amnO zxId)Zm){bzCDdjfv#yXePeuD~A!%?ss@KSX+sMU{q!H(>F0#o`~NG-ktV*$_!~8BeCmYkFfc6+7+A#RjneY-T@fo$T;oyZpIX66H@7Y6J_7Cs^5r5cb2}N5A(qt0xgr0-3#8m!zlNVvA39$SwP#a$W(E4Vx<6nt z@nG!p1HO?quZ=UePSt@n(8!haaL~q`2Q7~}D+_5Nk-4wwNJvOE zkj}fyFSA*DPf+!)@0*(e7YpvWiOAB7g>O@}Q4_X9!gt95G(*MLQF!=uQA2&y-*e(W zAK+Kw9LAYfY*Qci5>*xCAv+9*Xzl7SzrB4)((ks|9V6$A+T)w8v^DP_*;7Jev%vxS zSVvVEwNj)@)0518LDc6Reqs52hfn>A8j%l1Nsq5DB%(Xch*l^9d7X!x=%{ONr#>`B z3zur41S?m(x&W#Uma1qc9*d0kTs@$^gfZ{L#*N}LA`~4K5rSB*nn z>t$wF_*%tl7efKyV)zO382^<*h+p=B&D0+Q?m5=avn6`Amglek{6R|nSS+%7F$#dP8i(-2SVbMhFv2t0B&RPRYN&ewnHAz!;eckMsS( z8?S`wul3_It`?c<^wG5=oD%mEK!;qkAt?AD9Xz)JI*NEzWQ&c|6lHhl?>?2I$|+J( zG(r0neaxP5LjaNU6z4{F@VGjSZPabu`CkDvb-NSQeEz7K(=fRRA2t4=`Lw^=3jO1F zft9a4S9^61;B2Z9?Sh`gW=W2O1q$n;-Af&1=50AHE9lsI+`lB*b8Clook-jIOB6ph zWZYVKJVfVlL`QKg(P=94IiaiTo)m|9N%9v6xs$lzzT(GLcP_iH_}te~z6-c%Ql=Zi|F((0AAp?nulOh&{h&PhQQL}|9YfD&RIgUi z*gubsnQELyd<9k0;<6T&Z>MzfKV@{IeK9c!xv*4%6z2ZHDTf!`?#V_Hk#Y;wH|?R9f+CcZT* zBaTRO0f^A^T)6)KeSxtpk8ZL@f}`Blc}5H|sm=1zDfG|Ii|v6ElBU6Q&*wcwg3(S# zgjJg3?1Wh&SBghS*2*7ZLZjs$?d&uyOI|z4!>cXLnBed1>Jp+WM@$h+al#uyq*m}F z-^}z?^W~DVtIvE@LgfGL6t3`vR44&z9WqThYpJpx*tOCP=%XQ{Hzj4cANvRUrF8wi z=pwXRm;-#2F~(Y`|58kPm@v4McbeR1>#9)$V`%=8_Qf*_0u=YB+4I3@p030uC3m%v zYsTu}8r%7U-yOkikoFz-j)&S&3S-h!O1{Ad*N}Y|GhDhsyX4&#YL2;%-=}|J?tkk) zK6dY&)-ct9vm@E4A5Q9bnv_S!p=C6jqgxUjj;{8S3J&h%;{0?ysvDN>lTyxXY^K&v zzU40n*b_>K=cs1h+!LK3O8#Cw?)%yb*ecr*e`V-)Cs1>_G;@@T33DXMv4Yl$C39H?`n=myXWYk6K zKP~g$`^yv)w>hyC3)f}pff?kj6i+r_8v7ndYt-MlW_r5NVcE_G>uNqa->nYc93Nv| zFA!h?0NJe~l!tvcxpQOf*qDnlfp{-VDuUs-BP{Ej=c=`K)3@Wm!&vAK^Tfc#@vRPH+4RP!sPQ}p2dbhp8$u(7YT%W@p%e*^jtSe8 zZzQCG83Aw!Qh$Wdw43I-l1t-SenUoIYK4aRG0>DZp%{Ko%kK!>dN-$UZ^n9YM7c;M ze$I@#Y|JPDp=xXlM3Ry5(Oq`s`o@vot@N@GDCR=mU?Ee30!_KOz~|A&p?4+!xyV7H zR)fN1bXqpT%LzEU@qn+e5CD5K2;&RM%l({GIxaz*W{w5sEflnvmDpiD?tVRW?B=+5 zK@vbW%SB0mlaRT{DQ$28;kjoXuau37+|QvP>C#P)Rpt^`LEptqG|9xZk( z-&PhMLOO!Q*FhId{k};r@!3;6$g0k$52d*K9|Sr(KP^0s@WY}><6XgCw$O{t@+xSl z^Fzh*y1OglqdqWJG%7#)wo-dDpH#Hgc1lNuexd?@lGI(s5ah9fdN2EQ`W-3$Xu4-W z)=gjlV*e{8fFQKJqIXm%5GHXVO2zoERey-ybZFs56%*3N6M;?NVX9!9Sh7-n&~MVe z^9nGQd;9{B&r`7-8ZOOgI)9suR9|R)BW_lg?fn7xGc+aRvw}|j4HsJ61`%zQU%sGa z_vk4&tU*63UpesihDZ;&ubar?uRi!kSA>oOlvNdgaaR0{D^0aQbT)TeIj`lv2!S#I zhYFFyK%X*rx~eG2YSX(B_^Rtokg$00ZmTj81#tGCk*s8r$T+mw;}@V7;aLVqw+csh z6PFTZZ1mcp@@CxU+&kaibw8{rk<9*-pFVe6h<9uJj{0hzm)f7|(kyJLXgj7cwNtzn zmwO}&dQktnW8}C-s&9(C3&(wkprxaB3V0!VNBI666n7E;UCSC4#B7+SUWJO=oyX}# z{D%4;bByUw>@@w+NWKZo-&%(7`C?n5y2`!$_)JBDzqH!d z5K|$ryvRoF@jUKscn|hy14RjNiV6XQp0?luQ_|uM`R)-S++_qAngJe&*ZAe$B9Ysm9;B6h&Y zvE0uq(2Zzja3pc0xTZ=jI8hY$QF{IS9P_Y2Hre~*Otc$Q$t(-uc5NM z`G}mOE9`gh#!x+{_GmHUi$R=A;4q9eP7DLQ5Pizlh^E9z8E?my0V3>x{;16L(r&9~8h zn`Q7};C%EuRrQE^t*UqQ|1j5FGUjnS?W?ru4R|%Lmkgh@g2YbK?v?^aUY|7L?DS5I z=%@4PY?muJ`G3^0tXs`1Uva)TwojC6!(q2eENwg@3 zLtd1wb2WZ6=AabiIG*cXLU8>>_ZL>B(}gAadUiJH{qXY-VwVxgo82r@a{ueDTnd1o z^(D+;!aCNVM^XV~PF1W7)IOJF!LAOqwIRmgH-Ps;(eN=recnQ5DK>Z7*e6*QKNyK~lh5RtsFC;*R`% zPiT;LLby-fm=?+brNTrfhzFwuoCKK~%VLHL0(?RNPh_w@6SSVBni5GVQ27b)#kl&Ae2$@{Qb@BAI!QX-R0Adt&a2Q-&#%3W!=!hVV`$wJAaTPwOCCw z)cq0Cv!Vb5ZHji>E!T}tJcJzgDbjo(<;}?+C9ERbBN7()%NhQAXe0oxQ!{+}6fcC- zTQcXoSJLp$DSl>f*A0Hl>WA*$GAe|={s^I|CC2G@0Y4oQH;Z&b{Or)NO`gj<0ekf~ zor1wBSfzY;gjF^quiR-VdgbQ?-Zp%EspAg%d7NSMdGA5M#PV@=_1gP$C(MRmWbPW- zbOIu*nMkGid$;9UZxN_R}SJD#h-XICPn%>sllZy1~9sbcPdQLj|z_2-I>!P`|qOi7g0>Iy6w2q4= z-Q!;g>s}{K1QB5^ru9E6D_C4%lYa9iv5#DtL8pW?k$%4pJD6TN= z`!7HL5l@&-L`kfsAAHoVx!Q7c=w!Gc%qMjIr~e7D5^uXs4Z-2-y4EBfK~w7@{VBPL;T`_m#+cUcUYQ_FL)Qqc5G>LTByF=O&!zYF;wf z#Q@+chT@gX4#FdqBQ2y&BpxVM&wJBEeWjP3ec3jjIk0q`mp7(#zy(PBnKdAk z+unM7&b1V1e_ge-fRVnmKonpr0-rwp>BIm4O@;N_bXkPPviGKddSE8EDTTSCJ8@cN z*IB?PEo!G=VC)2=)sX-HVQw`*mv@tC%5TH^>oe04jFTk=+)E?GP#1Ed*&IeBDQt|O z=AQa&q>cg890v_r;Jm%0bQE3K5)bm2Dx6%pc`=pMN72=v#}+rlVRx>-ydctZAgf8U zWG?K(-siJXQ~nF@xMOjHZ7ye)6u?v;FlHjK!Gpqo*B?)KY$4HyHG+i?%m?I5bWGhi zi8`lhrsTI0m|uUvulYm?dIh^id*qK#{5=_raU41=Aw`R>e$yoDi{Vj^pBay-P$Bvg zy3OoFZp~$7!v?RpZx2nfNIQ}|eR)|h#*0yiu$oe5-y&VZ&^|${;Kc2htgMqB#0_|u ztsVb65Mm>N_WXswpZBRB-;O9ag9=jvI2Gr0rziPL!vZsBD6y}TEvqrdVK{u5*KJGu zQB))=clqxa-gfDW2jx?JMBdPCMqE$2kgFQ}s(MH_@$^s4tVPc|{d_9@d`FFmi4vu4^ASuCz9$C>z;o{PagFzxT4KLN_+Et2*o8Pk755rd34lQT(~PWqXOXf zP*Q@bh+EyC&!2A!cepyk`lI$`S09}SWeWbt8dx$o3L3>Y)*KJ!ijaS&U4Hodvazxz z2MX6p;{PX+MRMsb9CP(K&4((UoQx!7X|k^ri&)qHRSHW8@SWmtGHvn7qvHUcVuH6& z>H9{lU+#1r<}7^Z5eHZC`pZ=hzljb&_WZFKyMZ&eAQfGSqd6~)Ec&~51qh7PWEiFl zda%Q(RwN(i{+{BSOj<3Szvo!@^8_j;=3K4gjW+)ZzaHFQSnl|5SYnhXKzr*okaE9U zIAYKLYZZ=){NBI<>5gV-=(f8TPV3C|qJd8S;UHF(m6bsGbX)WNhotIbf#~GNyN6x2b%Q#ar0Z1mW2}8>`K>)uBnfuM9Lbaj+D+gs-^MBR*X;1+6#V?RA(~rWOLgPwBPDy+K(Du8zx<5L`TW3T-N(Ek=XcgKN(-iV4Y~CD%u0 zYuTUX&^wL^Q)oWnYu?f82C{aOrjq28(huEV#pRAG(zcD-@h*1sPCO?*H;&Lp@WJb^ zp!0r!OffymB6ECX{fcQ?zlo9OI@;$6&F1B*f}i%P3DLFWM)iC+yi2K$=UAr8r%Rft zqhHp_Ev!@qxNPH7@IV@$T#2#F*MacXV=okQi)!I zn!qDz5sTj&rniBIF90Y++l<^H&vf6D9srXUSgp+rbMJoXiyTE?;SM&!@&0kVL>+Ow1ogUbUDZ3x_pY{aFzcBU9#c=? z)o~1UC3(Y}w5}w4IgH~S$}-~&3FXcdWcznn^{|k(2&Smbj=1neswdP>$e&PI8eXB^ za+S!~L-TV2Zvr8=IdC)j(QgF@#)cDc*5nOFO4By$`ML#k;gR_73qOP(pGNvM{=V5B zt4o}Lb-eww>c}HY%yCr-agQe#u?G^%%aGNO4uR1i&tSOKLV?W@(XwOsG4x7usKeZs zN~@<#XcE6Fc>4YbE(38z3XU8L=WDY01psCh(;`fm^S?R;|B_O9bdaxd`gda_Z!=4! z_t8{;gQ~roWWzA=3LhtrUPEvu9B&M0{D8q}lV3;Xm1oH1mH7kJyjli>E3rP-^jevg zvQdr2+xqm48vXA+?viN=tDEylq|K2Gf`0zMD{aD>jETEoDZ%SfNq@9xw{vwW_@MQ3gq#U=_LPd znk?iF`h8hQ_yDoi=#P&*_DB1W%S^1cWBTXT9}bNGhqB?%AN_D9d*bxV{g5vO(p*X z!9CB;Bd<2CkkC-VbITLlN_!axLKoZE!IL>#3(x1Rm{qEc2Q6pl^NStxZ4V1gMZLY= zKg2Y@g?+k8#}6}xshhhv(Bc`=cVyTk!8Sr})=Q zFMPV-82#+_eYhX9hiB|m?N-Q-$C9|pi%fA zU5wqdwp7+vt*ycGLMgQ^E<2SYMs~jMRMO}%soW>kUsa?oS*C}lq*@`DrNhM5b&JABkYSCDph92gI48tPCr~dWbz{R z&#tzx)GqUgy1B3VG0TAVh(E@{u}!2(EtvhF)fo;%YlS-qg*T3^_xv`~GQTGE!wXqp zyupECD+tjNA$u0J!zKUa`>?|wuQO^G-y@|THb)je6MiiH+!vxXbqKuj3N6CoK=DVg z<>y!u+Xhe?27FSn+>nQo_kBL>hJ@xkFNx4r;)NKoPG;&5xCbBZl#VgBO|Oz=`^iO$ zewf^e>579ctlO6ex=%YpFHM4Yr1dY{!dbt4Lb|*%vv!ag8#-_65&#JVlN)r;@E$&G z?AqaMCM_8w*f(;&ryp-T~2|8E%^}BdwU^^HXZU%uAOxdm@T}1)Km&|L3abtpaNx6*~vnSMFJ_ z2VExYlW)V7J+r0!z#qIy`se=F;9EdH_uQ@!$bWiFm^h^DTwRua zX}xUU#Tn(7uT+hL?GT$n$1)4nU+uRYH5`-?0c}4X9CG}*i;(k%OxcLW9FT6~6If!SsQ6bFtM|=Yjlfk>;*)*l z449*JouVrXA$9+te{ z(*kYH;Kmd)%RgnqM=1_mreQMml17@_zrm0@_Ap*D_2ALs+U$kEIdYtJQ=Px(Koz!+ zWV-6@`JI1`{fMyx{*AiemLNhhT|rnTn|p5-cf+kxGRrCheBw-wVr`@Ln(u!rR`9-f zwnew#VI3nkvL zA(}ecHf9mLxiy!obmifW}8fJ*5O(lOd3!2aW61W^|)r3oWZ{?Ts71Cm(y*|1} zD%Y|5w`FUumj4Z(o2f+7d}VprY#G8~WXkvV4mgE*i~x%6LxY$&>5>zq`Rg44Yej@p zCpcXECo7Yvaf3Jli3;nHMPRAy%cP$kG9w`A6}F30-e&Qcer^x6Bs51#7)bH{x*=|S z(E8>=M4O&(zXfafvp-bg}WDcBu=T}gbU>u816ybS_;lQMj^x8~8U-T$zbHMj0>K%kgV6y0s z(h)YgsI=*f0x{QhieFU{5*Ty+EVk9CbMLB#&gn;=`Mk)1ol>~z*<&6E{R|KlCMQaa zB)HZRc(Whqn(7Q{hbKl52wRNag{?*}bk|w4aQu*3gm-X#Z<974gcx0hKyXJw&I18y zT?GhXa@j6mG!#2JR7OX&g=&NcG&e?DN%GDmgnm&jiEG~$w9G%(^cro6bQR} zo|r)UTMFJRj#?kRJ3L5dw(yg%FS*BNCLQ=tgewAAMRu#>T5AL%=I?&~V<-0^YPk|0 z7i?!>xbPUZWdQ*jN-xb}^?fdYdO8INrspPIEOHN;bI&okQ)c-+{(cZs)s(_FbMQcE zJET0CWYrp*9C-3w?1^Ni$8Mh*ZX3i;Fz1p?8qf=~0vhiXnv#CJ{6;-8I!l-oWkS(+ zn~1)SP#{wlrt^#Zeg8DYB_wN2DkBtJ50`emv_^S$vU!)h`lDY_b!jLCm<}GS8%#817`_w?A`lxTUNS5Xz6REp zUtg(%+YDI~m(dIz4}F4dF4By=`S^99nP`4NJox$Q5%O*Krz+%$Ir(CW(EKi&+FC_d zdFjZ==D?~2yL@|5r*{G8Zh=_@mYw!RR(uX#9(MFKu(ZcyGAUrxFbjl&XmYUnOLe1Q z+2rO^MifcANRrAI|%UYc+M$Li_5DTI%`|dr7!DhwEHPbS}a0 zRPfKV=lHJ5u#j6j9@K+3%D@6MT!K3&9OitwX+D2dpcySCM6qp{H4rMJvlqKxJUjIE zK_t(iVi@Wty+n_0yUvG=tk3wMC%xKiR@NNTxJKRKrRC&0#!D?(3T)k{`fEoE;!nJ8 zziC}~(g^r>02>*i6_VxHD9u_F67~M}Epci8p(vy;PDFyBR=_bfM4(ei<-k&0+v}Ze zOQ3}WE#Msz1a^fD7h)DjZP<3nNkv>JU~GVl7sr#PVUNs4ZPT+GDb3c8DuZeAHX45M z7wFyPuT=DyIg{(=7+Sv@J3X*WDADF#dhWSy%7L!zxo~+taZMHC6lkhFjfp>UD& z)zd`PN`dBrfliuB_RhF*%}UWb4Ayk7`#+A)D}c(R!4%$xn<2Px%}F`FBzZ{BG2aMj z^>E7BhJp|4p!u9JFnVI~4qS>C&x&+KFoA;f?ffxZ_G9e+0K`jv^xmAjV|&0h@@F!7t73}BZF3!%NmK)H-tAuw5Qx!bY<``6>-oXHYk3MCOU%6 ze?_a1A)5EBm9S`-g`A>Z4)+*s?k+kg=qG*aOW*gL6E?pYs;ymtLmgZ1T8eqR&4 zMvP$!u%E`MYNTifP|EYK^~;6&A2&a z@EGEL{w`j#v6&^3-b%jY}9#IZkW2YFPoCd5Z8`P0Cj zF@EFNNNl=?IKjA&9^2v42fVh&rjt zQ$tMuM1$?eLKVsyNqN%b!T*fouUSEgA~$31!G>+?cy(midF45Zc_RUcVlhUn8nY8nCc*dcXllBiV62}%GK zNk25S7OPOTM&FEnO$gu=(E!o6kAGZmrTtxlqt(&ahOT@+S64&o!Sn@?J#kp^QM~K> zDe~MM*XifrSX?EhpUp3(rnzlC9hg)hA2JsVQhDOa#6$hUHeylE)nNw6X|F}q;uF*N zjsYfjl|xhqw-LNGvvb?3qb1|tfps*#D^Kgkk-=e5faXDyq0I@OL-1mz<#MH+{3YZhXD&;U3I-4b4)l3)|_TqFlD zj_skd&Yjb-_)@z7l%n9#3j*}{>~r9%Dg7GNX08JKhS0|D=;W7Io;>UR=BODPLYJrZ zAP%vOiVG$Z3mC=naE@(7827MvlC+$1i0v^1FQmw5`@oT@9eC~e+-IR*QInJhaon< zn4m|EmEr0JU11li*70a>N-u`KTBhe*bU-+JdIXhYu4#x5%!z|u;U(0yPHGD^XD(gk z!V#wVZ4{k24(%HfqcxQ`sp~{T)fw9y?VqFQf-ig75Q~*z^KYYw7ceMX<3pWAZy0zW zX{P{f<0J9Xmx8W5$s?XWB<3Gs28n*gXpfOJ%f9W^0$aff&*3YF`$~H-z$kkbWh2db z9Y^y6lYo`&!#-|8gCvl&J@xDlzAC(`8IfW?_tEA@n0yut{{+CR|gW}r5eY_$P9 zUtVjonTupe;FxKej4D)zbuVcxXK3LTJTp2Ctj&F}I$r};v7?=72iDj!Gs;C; zWP_PrX5cT2VEcg)CO%!dbr(^5ng@YxeF8kG{^?xVSUbGs-LvPZif6M?5tD?+5J__# zs-GFo4kX4NdEnaQss+^ zB?hhKPn{x0{3uQ7;5u5bLOZ97XF05WDK<8fw6meXZQ*6)yTe* zN64k%ve&ZTg9}1L>qI=w0Uq;c(&b&n>80P7*EXi2kR$p@SIXMgyN-$j&eDVwF_F0Z zB3@jL!g7ls+lsz=u1Ug$MLhQHSlf7*X?^|1eb{t&_2_SnJ)N-FU+10Db?2t?Q~Yyu z@1ymt9ooPkVS(SX4lY$k<{sjx?E9z7_3S?nVggeF!)==;t`EVj;313M`Mz^YfzZ)y zwt_H62+{A6yZ#yGWiwIbyXA$GV2Xf`!j}DI~ z6>R#AgJME{$-7C4rCdpN3NCC=y?rG1R|+#u{Z2OhN+@Jn88-^afD!JAB*$?dv^&s? z&%Yje3g~hg?SLgBG?7HS)MGxd;sKz5E=W-T$s71QMD0*WbKjngVf+I$KK*@uI3;K6 zC^-i==6G<(D$MxJ+oF=;aQ<>mq25GRJqv4e2t8@PpK-tUTIb{YS(#0-Trsy?`cRm} z58lYYjj=O938$l3)e|Ls5o(DAT;1vO6^?5T(tGZmT#h+X0tmGM8(lp<>IAfCskTbj zQTlkWwTOv2pNQ31_ti?SY21NYXXGSdZ6X4nbx<$)$Wj2mIe$ZT-GN5u&wh8Q`I3Ku zt(yR%bs=CvQh`p`0+)5LrqY=zbFh?40k%bz9fXi~mQip(1NXL%Qr5)hgw zTIsX@@XpXzB3>}jsK+7adBkC|1C7ra4jtLyC3odDdCnw3N3b=l5da{6kzzb9x_6jr zY=iQ!(CAf6GNr}j&ysKMPQ!lo0bwhvbA$&Rk&nG`J(DPEzVF0t5sjPo+%`+UL;I%+ z%N>#xHIrkbEhCKtma2$sa~XT^{HuD|#B*V}T_5MJd+ew5x6bSI+ZQz?3p{Ov^6{T# z?2|wRU=fYhYCHcd8Sf6P-U?|8q?`r3Mtmdu3l)z&3czo>U3XTL>mt5W$1}$5+Bl0s4PZQFS4|I%@he~PfOv}M5>uG;pieYcx5(@n~nh3 ze&vswnl^E89=5k|H5>#8{*g+BL6qAbA8&O+FrWW``|V#pLVUz12q%ZCn8|tZxv}2h z$7A%Ki=9i#;}i;ACaxNnH*Hb{t0-_5Qy`*3M|Et2>!2*WzWai7o06z+$5Q34coZ6~ z`E6{A*XB$=;5mAETx7MQi7#>elk|;qIn8lb*G)#jXhCLHXhKF(GxdRqw&KBIL4=<+ zeKfG00)Og$0NYZ~k$K22GxW)HogIjSr;})$!gdE`$Rix&BFRWYKAEaO^7)n(A(qn% zi%Ci&+?1InAI2)>C~YI8I%zBpZX%uCM>c+sNJ~_{a#3WpjZheS1&}Kbt5W($W`}4+srfu1kvyQyzE)$>xqc~9sa?(t8F6$k z$NsjOzd)`NU>&ZXJxS68Op%*URv1qp3Bvtev?o{ZYj^pj7X^#HtrFaVgDE%5=C9X} z`%CA=nO@ud*s_Le$VnM)p?Uh(Vv~BvO8+I;Uq$Z>i*>WnK6uXl-{~RXKEaCYMm}{` z(h-FC*M+cd-9iA(rbK~=err@E{Xpll5u$`0)Cx3O@H|h%jx2?VgsiS?xT(53`@Y>B z#l=v}L#E}TZUv8@ajg2Q#Z%J8xDIy9gTu#$a-+iRiKq~M2j_&#PauK8mQDdW1`Jcl zz&OdTEs`=;{n$oYOAo{g(#{LXRyLOOQUW~kv>$Y$b1BlLeLtxF$=9uVtPJU4hh%m( zr%sk29vOPvipBeUWr8pW;K;=)ENoveL{R1tc2{3QKB8h~1q1e2&fIM~7sn zCJE)S4FqUobG8#r2L&Gj8D$5iqNpeR3jAR74e`FZZTDy=3R>Ul)0{u|qDk!n9?!5D zecx9vOf0H`IV6bJc`Y~D83#p2o60ZeS}uz$teL|(dHSDD^6bd5ei;U4edr6q&?xO? zC#WYd`sujsTvz0Q7)$*}CMiiA^A#HAa)mcB(I;XujgC~_WgEqfh$6-O97+H6d|{Zf z`3BA5FEfs9z6e!x8!hSz86Bv6^e$zI1diBJ{UHt30{u?%V zGJC55k`>pVK^;7&K56OulAJ7pwJ*Q9%5u4yA*Z|MOPRbNBJ5pmp()J0;J!-g9esz9`BuZdF!}jCasq1p_O^jelPUIo8;D^LHu~* z&@}tP^YKim7YQ$=Jb5Eh>wvMZ{+{Av>!cbmrh!uhuYX7I6pZmVvk z83ILY+ppkA3bd#*G5Lt&r8|xLC1mU$Oc-bUgU2$fZIg5fE*)@dG%p=>B=c2WK}}LF zsj#ZwHGVJf*3fzPpRrg8m5-Cy36#iv0~+w|)jQba3(T(o-&(OGIaUtdEo8m zzwWkI8Yiww-za0fT@#rG@8>Z1jI+R8k%jl~U-BBiSN{A*MvoK$k`^l}o2 z*$9)KrO~pq(83_A^+4Pp`?ZWxY-m;Kb2oEztOhoKbU?D1BTJKIKfV){QS&_HgTNAg zvEN9!5a@A8izzPu1~S*|`n$VR27{L2U;O>@04YyG%d}{QE-rK~$(lrnsn}SKP0Ra5 zhzU}65%0Tj`r>Z^QXpa&(f>hpP!5WI0?u$VyZTam*okG2Ha1?sN1lV9#_SB^Qh4pPW%~ zQ%179Xd`QmKQ#kaCku{jN0QC?bgp^Yf{l26<8yFgIlCYm5sJOAu6%=O9a@%LaBh7? zAY%0l5}t1PAsm>wIUSW4{z+#anAjJw_!i$>; zn{sZp#dah^3^tKwo>@ZP`t+z*e50E=3oJ`OBJAR|4*BePWhV(>aAeE$f!SA)E$|1- z+$NGQZE^D+TOB{Wr(WPb=d&ZcuoO$G#NmbHUB;KW^?fV$5(&NklkAmS4Fr$0@8XKS zF^*eOy0(pGh_uf*Z(W80voa;2+#D))N@HY*I&9lIWv6kLBYjn!_tMvGZ(sd?l1g0$ z(Fpj($DMo(dbs6`y<^`^2g$g4Eho3FK%Ft zeoK>mu#0xA@)JCsxevYEy#+EZ{OcwQW=(Wx$dLm=I`8FG2Me_aSc3TdE&pp-5`#oY z>kj3YxKeynu1^POysGW|v&u+KuKQEl? z;!vjL8j3?gl)1PSU_^^o^3Oh^RqdX&ChC4YEnK~*hk=I6x_CAd`5OsD4t(BeyMMJN z97wwCFW6X&MB!tbkN*Ct`l<;j`I=`ZWHk)#30k7ek^~}G1Oxkc@woCSGxXX1BDJJz zWYz@~7;{!I^TQn{fzgh?Ht^ys`IK_X>@y}$0%?+nCVBCwO4w2MKX{5oB}ofuL)GucH1b zv825mcyt+)dl>odry$EtUbFlhS4ZOxB*yag$V+RJ+xp!| zxI~a(9~b7AzhfW)*+|@$#GxxsQ}}{L3;e~DZ?a@50k93nVh>;QK*afmUIG)9qM)pd zcM}n%Ak^-zulu0yroZBRgmPrRtmj~IH7X$#2;Dr0Oc%4HTi5`lJ%! z1rUP$ROf?()Z0TGr;<8N6^z$fQ_|LKxc=?%YqRvV&SQb4`yVDnA0vSc(*p!vN#rL6 zPPQ1hpMnxNoQK;2NaN2frt*g@b?x%;SUGSkgnuAo`n2EW3Oyz7K%#uyegr;Ybr%GU zs4Xq5bNy64|i+_bEJD2+q;N{WQ|^XBK%J*f%!+Qh{zNX zo9edw)oJlwOaCk3(SQy5ySH}L762%fAwQpfg!J8?^_R62uW0|9$BtEzXF7gJY6Bhd z9qeO2IcPtZ#L1m&qfTqH{~8HQ=CDXQB*d-mn7dyAf)q?2TqXcZ%^9^`vW>C55w>Mq z%~2i!WxkFyli9mX8yCPBn%FG8uwc<6MNM#-c`w)U&^_mz*QI}7@LVKFb*MbRb%IBD`Z?&6_bXm^torBc%SXs863LO^Kee$S;!b zMOR)xPXjZRLj3xOHZ`5ZGpGFxEqQ@-j`1v@AKw*!a71U%Eq3brD*i!~UYW ze=t%qY8k#?g_G}sVQqVyHknT2b7$y%H4w(aH;2U!lQj2%3^U9BI+eqlOR&t zIlw?f-T;v1QzY_NlInWd3l-3WwwFL230=i}L9;rOj9YTelHDAJ+K{X!+SxYD2}h>+ovF?{f$Z+} z|1y72PPiX8Q{qJ#;>T8Mp0vTE6so+o{A?KxmGdqxum9RjOd?ZD&pIlku5)wR{2+Ix z1tg`bbt-E$q;OUAYk=)j!K$6h?eiItmsRUU|A{})v*hHUKTskxp|v`yt~s)^rKhrA z$|%-}%P?Wx$)WwQPn4BIc5}MvC%UxS$%KsRln7|oV@vFo+m((nq+g);G`87bz<2M} zxz)f1$iUo@ra4vtPhm3Ot#?Df4Lu4fEc4=teg68c*7t34`J|oHsUU7tOWbP6Wr1Uc zf&*_hp^Jfkq2INsVd=qJX%>VP6@LgD0m?kgGQqm2c=OV1xEoNE1ano-pjvhn-0j5Xinb1>1X4m;J@!GE z3Mg-mbRtzO#(8M0$GhVdP@|U}mwO~UJBwK3sw(tW>KNy>R?n&h#nR(3kPIWa*B7nd zM*eNx_*2opT8C-?1->_tpvIiJM@b2d-_CA^BtNM*qaNol*UM;rhj&n*A32nZzgGi08Z+BthV8 zM1`N5%do5-$ny+apVxlsjYhBe*)>u$(-uQ1&h5>KENq`=b;942LWr z5@==UBpPaRe2QNJe>56FAh>sXtF{)P(Otb64c;fOl=rBqGiQQU)S0PUqwByVb$!Qo z@7TO#)btASdlEtwp)B&g1-%0IILAOiL-awjcBQ9v|Jvb8!iuL_d8iRD_f9& zY1;Vyv@DNfrmCJC7!E44c8+fjqD8A{{}~w13p+6$S@+ET)t#Vi>&Hh=vcjZ)#|Pv8 zJb6T)oZa#;t<0+6@#&ke?fP6KWCP$_78AoGzdMki-^>iVP60F1o<6O1=2-`8!MeBj z{~vqb{npgdg^PHw5X(_1NzFXN|hxZ?U>+gH=CZ%RMzsn8t&JqhU8olpJVDBtB< z0l2jL$$R}BM7X3=;w}SFKS=Nj^UW!Z(}2`Y;D`2|RS=I`x~9ak%^3zuKr|Ldl__cQ@_r%xu9y7-@>s@J=`It|RIG;MH_37$(rs&7)%Qxl| zgg=q|-(rMmH4_*t4ZGMnoFo ztm4)+6#kQZi>@kfkLZ!8E(J?_lKt+Oa^aE2OOqq?anBOMl->p`A<2r=;}39H<(e;%8Z47a>s))@Q7RPu6obDOdg?i4JeggSy39Alfq}x!qIsr4*JOYeBLk zt@JHUkNKS=3yx#UWx-kD1h+=H8TVo8$E}VD(& zaXlE-UH&Sdy#mwe>bpH=;Vd`0+j21Yt@BM8$t3oy>Rh1AAbo!XsFf@ezFy**7qG9y`=S(i^L>B!L#){#$%G(xQe}gk=?cmDHrPVQD{A zF;YUilvlM>C&CYV+u!^tE{`39Sf);GF=?`cVc{!MYRO7v`7NzU2)B}exwBa#Cj<5h ztN948R6g>~U!P2-kyy&CCon_nA!-0?vzow2z>eO4%~3M6@4s<3i?t9eb8oPFo2pjS zTFTkYnyj{YOH%_0((+V4Raf95%gFgERa5(|=IlX&n0BVAe8YDgT+Lz#x+>6u&RxSh zjKJIt#2+o2Eq(|`Y2fdfn;PF}{<3}+>h-O#FAF3?aw*=y_C^n6y;?GUrBS*G=Kvt`?I z3}?p86?jVTn9cdIgp^>xj0F1ED_uU{H4lq42XBDz!Rg3TGZB5&9rUuQOIwhfMdskZ zwRK&21#ug6&UR!b6;BgM)_ydld=S(v_1ZS;0BJv~WQ9j)y8PwpT0lk62ygsP12`YZJtgjd%IT!l2N1K_e zm_<}p65QYwCg#=4L~NztPq$V@>{L6vVRIf+0+X-bV15Y}i0{nwxhL^(BfqT0dB{y` zepXQno!m$|-Lfb1bG@~_I8`yAPrz!VZ|iyO>uAd(ITc$6sxq!4v*A!ld~N$E2~V@p zmgjcQw)M3!uI!ypuIliuP6d-Vu<$B3Y8j^1ZCM3pPyuV{7pIQ5_7DI9D(@+&F|M*N zl02@gB)@c*tZNopLxPrD&pqrE6bgB>aa=VH4pJ6^$|x zv0=qXZX#Zm@4zxR&2z7L-Cfz$65GJ(s})1?odzl_cJWJ8!&8+>T(}{@7PqbfRpC!p z=C$aMoy6Fp4^wN_ZuWnJ5em8SbAHuiPv&`nO<$`_6zH*0)a&wFw z#Upx+pVDSG9AbRZPOF)oX~b@a6Ga{!!soCxhyqV2Z|B@bgM~)LG^a+3D+e%g&qqqO ze@1pE4<@ps>nixE&x&rVgL(uY-GOp?!DFpd{kfQ!Bxjm#F#Y!Z~JYi6`2q!b>Bnqf^WwCL_N@@taPk-nxR#g_48;3V7;!GI8o zskA+4(*nNqAU_gTwzN@LbJD#2b6d(K4@A4vgT<)gQ-Mtr=7fFbvQ9ORYSWS^3>i{j z%UDR6nYAz4B`wG&)`G{s_du>i(;@U-cdsw%GR&vGiEWYj%8ZDs((Y3n=US$jJg(|) z5A@CCr4CCvgA*vDz`>FoOC~V3N?8wGkEJJVj4f>wQ@{*MN&qxy!-BRK?*Zo#kP{hd zCv8QY;Cc}FD2;SB>I?}xp*4S+?+k~a>3mMk)*J5*jrDErB?z@8;L$l-rleghc6JQw zRCl$nV=SNZiutXjFDKa|aO30EVLF5svwa-lNOH28S`IJTZ9DA2bzCm;#qH8l#U1e;DVLYyS*GmnT zHP^P2VW2(L`Ud5MSC*ENtBtm|1yDqvOR|JlG-U#eUn*V2N(7gK{ z!~)6Q8~ONM|91(ZCpXZN3--dP0 zV{Sz*F0Om%lGf6qXt#llp9=>lathEB+_DKsNBuXap7Ne}B%s}uPdK{Aq9CKZ)Ty*h zw^0=O?!Exkso5PoHGy6`-XIfkA)p%R6kSgK6T~ccLeSb+PBNq~OP?3v1s=01M!QyT zzM-`7@()gV_Ev(JRRDIGIQlJ-4l#=?Q`O9t+n1cP*vj%}5YbdAXU`x45v2 zE2q>czQ=*DRE#qYINsieL85EPX}rC{TWh2Kqe2mmFCI( zjp5xNh(mVhM^QN3SNyaiUQeh!Q+Jr~dG5J@?j3qmjIHm$^1Gs$S+b_EN3%!bnEV0K zJNA}9x8RX+adF(D0B;W?#H^7b^`vNAG+{z0yE$vR|7Sg_U0 z*B~+=Ht;(#RxfdORX0`5r%KAmI`ht3773=T{pnOBx`nY6$TBoWBjJAN$QfLj1;=rXx;!OvhGXDa{Rc(H~>r)Acfhz^S<+n0gto z4sdB@)s(0hI9S1PqGd{4M6?;=81tRQ>Np@JCEZ{?og14gYt=%8tJZzS92XH;*Q~7h z-0Dff#3YKYRB%S4pZk0tw@N=zu^DqM_5>g%$@2h)w)*|xaR5>Ph8(H%KuvU*-I zyxeAE5R4-rt>HRExN5talOB^C-dE$rGcPRr{FXOeBL^@*dV`TULq1mWiVpEBp5e_Q zKSW%oL()rRd&XYOjYH3-GHUy5qSJP9C$J`M(B*qY0wAyvleCg z%=?oOuTg5r$po)Fr>e-0w$zTB)HNuW(SvUDKf{;4i;L^ULvBUa6hOyFId{N##uujM zRv#Rs!LXvxowQ)kGM-XKsCv<#flV$5y2UatH1MZ+gI73Ty7cBFvZzY*%dmPE@tk79 zB!1wNH~k)n?D6K*RILE&=u~H>BA{GxmX{GT-ph061YDKUJvJIN-(P#TsV?C9vY$t2 zYdazNYRURr*lh9K!OZ3>+Y375HL^HI{T5loBCA!?4HA15a+Iq#Jd#x(CCj;`M6tFd{5 za3S%VjuC|%yk-riG~CK@`j*1s-wWvJZE6wQDY=ZA0oxIrYhcboNEqbaag1+R^g_oP z7LGd&=wsut#6J&9l)70u_@OJa50IY0hUm9N2Y;w_&x+zt7{KP*gd%EjY5JnatDq_O z#q#u*Ax8h!O&=gjt6Q$wLZ)dM`6ZiUa-YSE^9bXpT_{?dK~<%SUsE#8Ki5rA*eXpA zIBy)n2)qy*Uqs{0w6rtR8V+TK8yaGdX&1C~wFNw9RM@f?!kU#i;2|$51*qqIX~!l5 z{-}2CK>l#dUi`9#c+K~)BC^J4yQ}Q8w`~|yoG)Euj{^UX&24(Ub-DB%0U7iDRXdbI zuLqh8EUU7+XoE8nNLW{}V6Ku2ouBnVcv`!&*1N#!O(HAtOa z_S*VQC#ApYmn%7(b|LOgY{-tD>%EJY0C&w@M1ZFlxR%@ zo%0I2xw!t~;c@LbJqRQwgifeNBWVOGZ|2@iR+Q0=5M>z2TOy5-2$IK5141=_*v1LJ zQrJU+Ip8m2EjkRe3hZ1vD`cL>H?_4XHL2k?tgD|7l-(e?%UUOzi!UovYgdp)71ek1 zyo34fD{e#$GBt+HDeR%{b^+p`HxUpn?$f%_PMG0SqfkpnZ?fz5Hx#;<;e|ECYZbtC zlOHlqTtVz=_<0rr>Ak@^D`4x(l~azzGBzS1I0!VdQJqDu*e0K2k;VLQt)Y8{NqeZ8 zc;L*4(JS|=HP2AwZ+iqTY4S?YCxB3-gwVJK3CVrm?R{*DoP3Vp{@wpQf__cG7LoZNYCVMR>cT+(w;P zk>LsA2;Ycu(R3b(H?wzt1!}y!J?T2AJ3w5TWEWxUlBic!%o&WCT$eAQl-Q!R7Zkfm z*lrLQSLMZHk;ogFHY7jtE1``4DCAN?ix=)y)R3?KZn?N!%vXwDrQaY%n)q2FiQ-O+ zwu|*2J+UQe>N0$XNt+qP)YHmK8q2B;V}tQjNmnD_b=)D_?_`yV`-hWpV;z-CTh!&jiwqe%K_J=k69lT^stUMuJF==H|Jl z30^x=gLOe=beE-Z3=)5^|z|_7&`-mRF$_Fh)d^ zFDm&3hI$s!p?9$N*%(r2Vh{5p*2ZQoBC^#^OGH;tH~POlVmfEMhm9K z#I={Qci!uB1R~!rRn$%tjvCt&zpNElV4TVhSY4*ND%$iT6SUD{Tf<;Mi{r78$wOil zGWEoOy4nzdU0jLo4zU*XRRsm)`ZXu6pfaqgC9@9je%9y5lDmS1?^Bb3Tt?3CL&-uAPeT(Ri_b6{>r@OF!CjTyXDwZvLM?+X&gj=)k5o^!|yY&LLn zai~A~I|{oRdqPpCKrmf7+yq)s`~aQmQiFgMy{LJfZbCA!NJlk@Ziv)EinO;r0U$%> zDa*lI!-FmqU?Z%?(7Fq-n@6T1fj5bY-Z(P0rAxffeKEhzzaAElC;CW#L-dOVNaEux zt5}eqhSskm9{1f@QOi;JK~HLu1aN(AJzSKAuX%$DLBr%&SH8PzHZWINn?J>|+PRG7 zXDalbKe*Ji5k$G$cbjF!WQt;z8dq9?*4tb07J7vEHj`M%&ly1p#h zp+4!umu_`b?{KukJ7+4xX|pYM2qP=;h6N5w@vd4CA(g~*CapPbNZ_Zs2TSow|Lgkn zWfhjQhus|$b-}G?P5Z+i!XA1O4HKlcnn%VIt5^BqQx+6RVw$4YHlfaj$mXtb=k?cH zkHii^Cd~<(-?c#alf8Ro`GjUEOIolhq^#~bwW#wDz!j?5 zQZe`6>oi3*yDx3N0vptff)&o7@Y;HD6+y3S=fmGH{DWGkVM)6dyn=Xp+`jqO8-3nv z;Y8V}riHKG^57ss5otX+>R3^nyF=#St*-o%m3kn9OL@H_?sv67kKjH}($GBrd}JG$ zfH69!m!%gQ)<)R+tk$8uH9QiLmhFTexV6QjB_&1wOv?3$Fql0dwJy*g3SI!XJEgD<+ixu|9SM8B2>+C!(S$Cj7$y zwgf~{MYe%WO1bx&49jeYCwN?&QM!e;)^Dn3+o%JP!SI-nih(qPSMF9~w0X1GA83U={&MY8rD17O!C3zAnvrA-=N zfIxz0=eY4D2d}sZm$Yicu(!{(drJ$*S=ad)7~t8HtAcC6sesHRW+@M}11l;ICPSuz zZ}-*4<~f^%i#-Rad&g`0*lOOb_64%Kpag>|Z(>O6J~Q5)3?wbJLO(t@o}9R2q%Er3 zREEO@#Yq;JeJCEK++4yh_G+%PAPqDNegLU2VDe$dQ)~I#TsUA z{4I0V`+;`h{Sw31-6ktiWB6Bh$IB$Y>jv>2Hpq%{;k=0Q7`+{_c|=-!cqzTuP6-Me zN34mgAU&N>xm%6c!q0BkMiddvObv(BzMhIz;T^QEz<`+v4nkglsolg!d?ttYR-HZ^ z_d5Z;YyU}i;297Gb%NioUxu<9kL%4AUvnjTRMX&t4K4WUXUT8-u0fAgY>d0qGVjTc z;cH|x@M}pXX+iq2o6q3lR+QPH1nppG_yRhYa~7U#PWPxzEs=eGHzyu%7KICa?Sd$_ zAOxs?96)ex^kBGaIcMXml_NKYBDbcMz00|39 znR0eeT6bQ>qR7L>GZykmn>8?o*aLl{Zmh*2^_9x9W|rLyCDmGvF<%FM@jm-~5XRzO zb=ql(VC{S8m<}GeiZ-&CD)o9Lq{XfSIx+d)1Zj6#cV4rz=)6Kff%RX_Bz%rENQ-Ie z&NS;UbpnU?|RGV-7C{{b9NShru%0^_q?r9wz3!S#mP*zWE;(5u9!;g z5!(bu+Ny1?N7R~Wv`?bJ z1j!}G$bJ&PE?g4TwDu!dI6`)uBBL9doqyTlSzqFi0C921EwZGDtDyGP9hItD&!E_k z7lmVTC^D{2u@(}Ynm(gj6-n_eQpDoU@y>{H!*JO^-P%t-(owJJ} zkCC$0e;7AUWop~8TE^Sl=S^eY6e*OipIQ!Q#8fLZs{k4q&m9mF!YE&(-2URpSRvQIe9zl8f&=OQ#c=)W;=&f|P98}i2knoxMmm45n0J+)Nz9DznknKKwys&&A z{>b|+-O;YL`jUVf)a9j)!S-_)yAMUH&*3;c>6pHCeoPsb z5><1*Tr_aQl4NLfw8H>p;_4A=QPGGBXz2|wbQpmYFT&H_tn7!SVrXrzwgoFCMYJY8 zlpeD461->gp>6Iin%ZO_XHw&Yxt-*jRbP%$)VuyKFr%2ehRrD_6vWhewd1^zV_x>g1(zAgL*yBai*mDu|bZ#>855XHY1cFJTYO zwnJ(D2{1?F=3F@M2Ri@@`@FQU-S1~(Kh6EXwND*9++lG(n*Ajpc?e=FLEt$gq&4-K z(0$Pg)Fbr1M6xgl(jKKOWFxYs1@*a6*>~8x*x-N*C@oXgqh8aSSYn4QmuKBmP+3oHhwKTV9`;FgJ#mAGld;4FH!cIm+ zzH_Hazg`(Q%_jtn*QE5s5IH7&ip@lMoguZrtwwH2+_VeF-4(wnje=3NL`-Cl_81>HekJNHr0 zNUAZ!H)z)b-lv&|EVL-veVVm~(|5@Yyo-n*)=o}>hdL7AA4cJ1tL0uIcMTkCYt~z+ zKnS=&@H!B#=^+r?EMu+vc9BHOcoAH_ppw(57A%xldStO1tXVT=*zFgn&nC9P4sVU? zAPu{wYCrWU(+WU>#(a>rHm3hgP|fZ<3e$X}rT^ldEiaRpwb|!s0665prDg>J^|*CJ zz5bAT)?JnVXtKG3m8>u(?aUdg3wNty+S?I_;(5PfBg$p@C|0l((T1O1%R-_qSrqYk zW5v4G?Ys5o!y_&_;(&KkO}2q4)7y}yV@Z!nyg)Wy_BBm~rHqHXD^Vq0;+hcgwb`JT zob8BQkcb+z`Ns*tf7uOVf%LgD__7SCaDpVm=aOY3Y^F864nVx>vQh^QhfO*-^nS6C zeJ*F>r-R&Q5@={#N?5qU{|Y|a6=7-NB++f4aUoVLkN4+1F#_pY9RV-Np3qeh!wsS3S_q*{ zwcMjPs65ajl)5}~e8}GT){y($b#Q^XdCcYR@bNJNJqpXU1k6_3JLNRJ`H_?;7$wI3 z)W*ZV*CO5KDv|-<&~D{@e$8n?(>Lb8Wq9Q%;InZ_6&Cb>!XI@~x+U@eCJ*D8_ic|l zzdqj^aeZ9CouA($EqV(6me8|XR4ub!N-BL4S^E$i^Bot11nmAGb__nz?6{#RbnGHP zLSm~;u68V%3B{5wdEH4Xt4fo%aA|bcgX+D6kcN%R7sSwdDFG`MLR}QH5g~bk23J4Y zyf>XY;`YDO;w7Ns=;t=}Q-VH|?d$M(G8AdHOkfR;lv%uv2@q7^D{~u$!I}p6VwaHMyWmPS8&9d^~$2 zAuN1SO1tR$(7syr(YC-=Kf7bkCMBfqLvK`TC*cTU(K(0F3M^)vZ0Wm_hi($cTOHlK z0!caApY)%MTH6ccNFcEHcikB$-#{KASQ@c9;59OQDF(F2skSf6nU;+i0ET21F7I(V zhNiVw$~I;&Q}|`s!pz|^2lhb_vV=A{+|C~^zpO}373hoNCmYnv^KO_~hRQ3iXB@JZ zLRYqK_tqUglh(bM#$!KVtgXn1tIaUDgSz?7MB7DnmwU54n^ah<_!Z_OvoXDFFsX)B-wy6)ZC_VZHiF_@2KKY(u*iKnxN+_aHypcb; zhn@YHz+{}&jRnvBE0xdYS`oP5B-CbZnKvKh@IZ^kri$j!F1YoP*N%SS#q)OO(jRm_K5v91S)I&{))0lW%;_hqY5nJPT< z-@~n!>)*zVv`ph=*T~^l9BRR{3yR(aKoU<$L5$7M&1^Zd9%@kBKL-1kQKGs|Ckvab>1GSB+tj=qNb#fX7`Vz5*gjmu>*QtTW3HLH?m}3>zbeu zQmch^*k(Q5G#7b=kZT*`%1hv7%d*U3D?*7}dG71P`#=D-ZOVoTn zEBopLhsO3;W?^Fz!4(RQh@J4^nRXY7FHY_9*i>W6jQf$bCl66 z?RBi#b#3`F$>U3K_2*#P`cHe!Bs<11$Dgwq!U;1ws*M{Ki@5l2RYN0NWu*9^A8p>e zM1+e+c3*pk9;KcL{t-EJ?WJlr1TB#|^TSVo9+4}!5;D3WqX6Bg*7kNg(QN%L;mDzN zYsW$4gThnqhzz!TH-sk~2%vW7NUUoJTDc)4wnX}&`7_nTQx%bWd}hNcU`1tC7Y=fq zZz}R;J2*&1aSdJtToNvUdDp+*+H71P-Ajn4(JR#(2h=AJ&qpq4NQD#mo~l6{WNrAf zm3gX(!>l;6JS@d#e)H-(_bD0|ur=Wuxsy)F7edA^cnV%h@J@gRfQ!0Xg9OJPTl+a~ zZrX$UwhzBv(dnwEn`*+(h06(@A3X?HXDR0wj>rCMneLQybNh4zP_L7i6@vWdj1`Pe z7F#4UB1f;SVT=-b#^m)3tR&?wA-hp_u@>YGH;u-zj~53eD-9@rB3nGJiny~Zfdn1Emyi>X(cY8yeAbT^8$t z`*)>Fm7Q0RH?Oo;oSL7kJB+71dh{srueV?#DO>A0rifTdv2&uz@KN^aoF5?a#iJ~w zw8%`}GmYmAPu4q&-;Sr3=AVgZYqcbKuX#p%st)j~0QSFmbDjniZna}o z8=89kc1yfD{q9OrUF1|m^VD_b9pY@*BhKa!;Or4V3?SA$JYqD26Dx?x0sD-qk!+2u z4tpw9$9D?7)IC}6Ak;oV?x^)0IkYxB!>38dt!>NhCoi(*hSj23I&uFgj{m|20G3PB zNC3=5ezkaCUafkEwWrJ@Q841WwRf4FV*RLl4El{9f?P#xlFPCMV1#)v3CVc{Evj`b zw2!DHYk;(t^`%-p?0_l-9HtywkXt9imoBcu!9a7U;+E&>9m#{`?#48#Z0S`y>NQZ5 zn5QMJ!opm>eh>vS^Rws{k*QHw5^4=3#BVw*d_Vwus`@Ze+aEE+ zg&D#rw=Rq^d+-w}*@sM$ZyHfCC4{0isb;KYzfyYqWu>p*0V6kXGNJ_+9CuCvbBgfv zd&e5r6>q2pU^56$jO z`8>C)vGs5IYFRR+n`N^pw3#$hZR+ksZT36Zk)MftF0{;5FNe=wWk6CH^eL=7!Nu4* z8mFvzZ1;mAATh1MYRXR9cFpp4|P0njpz;@#@*6a`wK-n6^g+z}qv z!0c5Y()Wd03=(H5FMq%a$I&1KXkEOhHn9nwTda_jGWhVXYQ>m^y+GNS;x-@Rsi&w~ zwlCh5D3qDwXNR|d3ztSAIZBpm;rK2A;(2cqKa+;ibpW8hF|nXJR1;U*^0k52XKByK z*}9Pqy!JA{e|3LXe>n=&3mm80mwiUsYkh~;h?DbO62}+_#0R(FOwGdB85-8c*D62dd5#Z zg5Fnkt6^J9XVQ4f$8hZe+76hlO*5lufGWb#Fta%SP_NzH1pFkv0JolP|8C#N=Y)OL zZ(FSq@YyU`>%d&YdCRP@i=f^mE9m3psjXBv);!(j-3iN>NO6#&R$*t!aCosCoZyxz zM@b?ZxWm7dyAkk6N9`O(Z;lk* z%`LLel5P8F92(KE112b#Oov>IzmxhhUaGCIGV(R-)O{i$K5?nK4>&5H*E6$x%MrgO zK(sP7^eT4$_T5TWH;Z)+WOURGd|3FIoN>PvAC49|+NRVag~A-;nO`%}Zo&;JIy(gK zvU%SB8P9DKcs~wyitdST)G(+d+FnLZS<$N%{5f#~Vs|>~hR6*`2@q*z(0jFrpNifW z_twpou-*9t$>;JL z;}>j*ZTYPlH1r{wgS7YZjscDO5d0K(=v-*uop|~=PFMm$-Ex*09sgNT{T~?XXc;A} z1{P3Ckiin+2~v>A*0$)vk)r@?Vm%*L1$)_FhJu9D-2G@eO->eww~*p3Q9yPiPnFPe z>0|osr}<5Wrb1--HpP8knVFX_{Ma7%OrCbWSy7Y=;x^81Y+pA6Awz446s86qT_h`G zHn>@kz-PO>nGJKw!&~uZ)#5E~s+hrRwC{TwW8%Yvj&M%mC*ftKio8Cl0C{-vr*cUe zBx@X?egzR<_pQw)@i(V!h-K=A`xf_SGa|bIJf>r4Gn-a(#k)2UVx%!_Z*FjfP1}KuRma4 zoM>Y2bL#M%zpCCKm#(z5>BtknS1+^-(O0e!CXLRI-oICk3tLSF-O1tO?RjOuz6rcvaBWU@vb%R)HiXNCV7*$Mkfs=xNA|qXSN!(%a)=O zBfo@~OD$xtge8+t2oPVuiye`?&eEmy{_Pw6Y+0^o=l_7ZQ*y< zX7mM6+HAC287$#=IYvRssNIhrUSbDjS@0P0FMgSq0xeG-3WGSlyWiZb=QDl(RN#2> zcWe9F3ae%j*H@P&s(6Q`?3Y>mnycoU&tUkfqTB$wg@CpYV!pn=Ey9YrhQ>c!TE8x6 zy3lqs>d->-cOMX9cSD_H@zh=^HiWo-8-yIdc}$xxDVmTg=_Wk)`IBg>s0GypRZk*M z^SQun8nt6l01`)IW!Qcwik9O#YXns24u{2TwQ`|k%Lp1fgdKD5Mw%eyYo4>6LEBcn z)L+1%RAZvRsrHvHy9>*xzE`octUd=A$tmZLZvazRElve(Q-?S4q@!g3fo1PeZAtvt zzwEz~zmbtK^!9=_(gS8?BV%;+8Zs2&TAO^y7Vd6@ODZFJM!$nyD~j(Er*Bu>?A~ws zg9n5(Rpz$jsU%xmo-Bg|Ck|X#sveuYltEZ7qP-5f+UA_x4?mC<7R+cGTMq^een8ke zY`D>&WrkQDkc5XxkyS5)_^k9Xy$Bn}G3bxZzr_V`p}1UI&hEUD=*;f9$r=uu ziG|7@u=fW_$RGeNQ;wonpNw$jRL(K)(C6};ackp*)rS&1^D9cXn*oBYzUgTBZV7s3 zTP5wBM6mxoG-x}WJ-U9Fvz`+zJZSss!rvGrK+WJv-wcn4IQ)6eze7dH7xxzeFRtc0 z*_0*+M5Pyp6?UohEo_V~d)2s)aTs zJI*VPtxRufs}%im$*Y?+5=B~9#M5-re}o4<_8)jM{6ZK1KXc^zYXXp1VTH4m9Dr_| zE}@)ZX{i_AMN@%1^ZG?tJK&*`72F6fywxhlFgD?Dz_kiq6;jzy>iY1vgy_=SI>&>e zuOH?89CGg6+i~KN1?R0N|E*F1d{*F^?dKaiH~Pu)12zVL6)Qj4j$HDh%DrAO#=8J? zBpTxI$Mzyq7XiU^a*rrFQM;f4G8{~ET1&}u_m$?O%(BTf8iLHJ3Wiev^e8WzadtZB z?OR>Te~RwE264W4QzC0G5G!V;SU^=62S;POUWlf>UJ=2k>JZpDk6+d(^OQVuR^dxm zR<74hkhgSD7pWb96YbE${6Kh)C#OsUISVZMK>2O$6<1D85*553`Ks&m-xastb2)Zt zhm`la_tACwdA)paC6T@)ZRvXtoy-@Hk&+x02Xk(>EsX8gD*_ww_AC#_&20bTR0Uyr zXMC;9fIMAad*wx2r7Tr`Op+aWhix50!Lr&p+xXa&3Ha-R2D!VsXN~?TS^b(C`j*Kq zWz}IZR*NsG?~d6hKz5|`cDZE}?#-qZ#a%nIZb01Qv$OTp5bUV($7 zNC83b0v9MHgFS9Hs_6ydy-uIrr6+5Pp1uM5mSRKQ>{wVkKboWClDT{&zAKOQx7h|r zY+PKec^OE@<%NCZAk0v;35GROQwAONB-O->*iK6&h@i^OTE}^Jz@ekyk*;e0>j4Cb>qKXQX+xGMLg*ePU6VIXq$rs4isxpf)1sx%s=8)Mopa*rCp^@A)x9JE(`0?M}KNMhWOw z>C9ZEwM?jyZm|k+grFDA90uH#B&#+kh6_vt5BWy;`In1+WezMegPk2ZA?D}D%uJ5U zzg~%)!bzvx72#O@=p=Ne2BnzXa=mdeBlv&icID=ASq(&Bm3OHmCfVH0$|GlH4(i=_ zhCC{#Lr6Mt_X5^kQ=DZ{okD(otxKPVu+x7+dEBCmiCk0-{sttEgwRDRmu{xnXh$Ry zze%_If*zXeI3ZP-%H(8_1*17GbA*mdn@&?Jn{K&=#CHE(VINoT2&K_wkd?KmJBJf4`zW?bwVL^uN>4 z-wP%ukz4U3W&iJbTwHJc_|o@#82sj@!FYS7&|{~*J8SdIFKbu{ z{~EJ3^t{5c0r+o&6|V5&wBwmG4cR0l+;E&q*^Pj{fe#{SO}3Yh%ZL3-3b5`el%jX$S0U*Jp2d^TA(z zygl;IliDA!jN6nDAZjNiye~^ab+BK$-TA-!IsORM1tbpIC9?alY9V0)$A9&61^KV} zhzhlD`{8$+TJiaqD1~pHa@x4~>!wbCKl)#peR)@#j`+Hns@G>cy0m^4AN^abF90Ur zf1mhx#}oK)INP&K{vxM)eHUK)|Gve&^(_24_>vVgzGrDR7=I3&wL|px#!L7)f1A@00M8o2 z4v_151Bb&|tP#=MXJ5}nA4sO;{e(21Vq5>V9$XJ}UV`kDh9eC}b-Kvw9ZbB@E&2wX z{$~4UnMB4;&GF*9@mULleJ7o72k36Ai~UEey77~flkZ>4OYT4U_|I#d8^68O&L52) z0Vd#Ao4#C{x(ZwL@PGd0EUe|lb6nnO;BWg*4*p|EbOD$4yX`m7uygyr+AM#Z{#PGb z5QOGgc1p@lACGkrfOB(2S<8jwkzc#cAdXOzn7y9wAM#|iv*umj_v{B4oA^WoRvaHW z?sJv^rEpNE*-9~#`igC8PgykS40{{55Cc(|M2StI&dnWfk?~3UL}~_4Vj3GG;_4;> zzcQOksGJB5yAriS9bt4@Bl#m`M{aLt=Oh)5UAv|3pS&X=>u(k}tMjo~EMJVy9iaqq z+$@V+B?_-!qlR*!G-G@@l6Fk>8>pXT4#5v+(VKP2ms)3s_wZ}XKv}y-eO#vxf2*vU z(7gb#2xVESY?3Hge} zl<&+zm90*6+79^=_oKGGP+nmylL0fqiZD&w z;ST(zGqBLc@Fte#y{*tzeD}Fe-X*fub@!coLqgCKhi8LxdZs=kuSc<+KCc@j@b>#A zGIt*A_a!tyA~DBapE4$D)Y-p(=snX?^CMb6fk`Z!2PQv@UvF3ln<7k1G&_v@puUqs zqPEo{eW?gZ!^r7(ZaN9q9XIW7feTIva2Gqn|C!Qff zn#aMvsF+dz(QaVmSs*VIza@Fa=FOdq&oQN;OoI-V{$qwwkEVZpymYOpFQ6K#-(Fk~ zzOBk8cTim8f#u?sr!j>v5&Y7POcJ)0AHlzezlKQHJFJe^3%8Sue`2UQqpoAmztdQn zGA>(8v3J7xO;X}p{Pye{My#7vYg|I}2TYHCZZt#728L?lP7 zg2Ec{5uZvkut+wS@ShF75R-^0TNySMxS>4L@FSf`#c@z;tl2dd``&gWnE8oP{hfk0 zSgcW#SX1GTp7PDb#3YmQ2muuk_A&6K%xPwWQ%f*w11d8YHOKtPDx7#k(+>U}%k4a$ zGBDL+xw)_6yF*lrLNj-?s0k4?R8)#wiHUwi{!!d9Soj(InIS6VOJ-J2Nkqh?i*+zK z36rat#cZki5iB^eDc4R;H+lPM#|nUKTQ84o(N#Ijr{u}vXzKS!6NziHEsnqfH|UKc zRAEiME36kSOh>UloHQIcwW#~!h@b41&s6mZ(zfj6rvU5}=NKpKV<=u7lmnV`s`|7} zecfRaOGh@i|22teqsD}=r=3KS^GvR?+CH<`<`Zjln!6_@GSp^7|8O+|^ zwAzX~e8YSABSmVvg%V|#Ohcp<^asG)WmdRY(cl=CC z?soO$SpOuxv8fjASg8e<%QHSV=|@wTl>y@hrqMF=A1%>o12KAm z4iCxRe&Q*Iq0nKkS8u`v`ST?0`u}7c6Qrfi&U&+|Y{jrfyHtJ7N$p{>u;6HUcfIBEy(VTeC7cVIp%B6OxVKL`WmAD^Sw=%StR?n4#$U1>$|9DC<>)?5d~I z25ts2ko&UOFAgvyjS4>MrUUQ9zsBNm zGApu|^fP(I!5Nh*F$eTYeTI=P{ogDB_rjcl~VyOXFNz0tTK?`_r9?26`vuUDJ*h~@m{yB2AS z)$$g5%KSW>^sS|6i|r}to!ai#xb{AR)fe;n2 zwwdBhGQl3vJnQ?M6c){&ydZ9{nO#`_y>)%GPWErrxSJLalY|wN*5^0ifk4R&g*Ieg z1TaZAQ*7BhDeC;>*|ct&@v6Mwfr_ND@p3}`5X<;y6g6oKa@?Rf+W?{R6R}Rl$J`5Q z^e4IHqoY3VbJc`b=y(JejwuaU;zp@V=xru#Et3#Un#xs)xn<>MhIaMW@KhN2Oy7=(^Xde|~#SGYH5Q3Vh5lik~ zdT$8GI58GAoIMhDX1maHmo^UZH^9hKTPvEiXf?C(-#PZ2+kL%THWmZ7P%V%vv_IGF1k-O~==?M8bOVOjq( zu?}it6}-Hl+Tic-oDY`|cx*-58Pr+DwgGGz?qd%&Hna_~#{^~^-No>arL>cZy4K~c z6*7cx0j}4lUAXz6YHe+;vP=Me1%$a2{3xW&aD|p!oD|FEpJGHk+gJR~Z@WUxkvg2{ zGfr!Xb+6JEZ_#7Q^C!TvX7^v!J7$&>4T|=S;=xXo<)#=C+c>0llMLz#+FS9^B{DZD zZ>>_ydavfhy@d}LXI-s1!Xf{X4Y+g)Jh;B?le3wb%;Bu+eCkJ&K%reTq$gsjubfJK z(;L)|Y}vuY%CfFP(CZFRp|J2W@-p{Ab)sH)vb&Wq{4w=AHc@6tuqb|>cf$kBdY$)_rdW^7><9%D;FJcSmo+6ln)RBM+z7(NJBOcima!W=2%Bl= zZ`g+Lt1Zoy&6M%{0|`Sld#}2#KM{IVSmlWSZJpxo9zshL%CTu{YeOWGW6NFVLXZ-L zT18Jm6Ppdrcd=Q=WUX3GZ-Vpb99m1TmS!!Nce9K5rmWqXpnAeVS|GV(r5?Y2|K3`` zS>j>aeR)L?Jeuf?l{GpPTwF3R>7L=g;vsW|=@Bs84pfdCb8 zqIJf@0|VPIxg44j1sSb-EQwnbDmFmW=b*0wypuyD4wirlQia{9cYkVI3R_ z-L^5>DyqoHfL-*yg-C1&M%_AZ?3b}p0O22VAU0V@xz)7+?T|7XbB_^Pqsk~;9EwU) z5DF5F!xC0A5aqSrROW`yT_X5k0YPASO=_*acXHicG%0x}q7xTh;iOOwK`>of?yihq4>lBPScuc2<$+k{dy;%DWhQoaKN zMjkZz@vf8P#v#3r7n=wIPXUvI6J*v#t;#t%$(?Rs30Gd#uI*)DcWe~}+(U)RfiXNG z5aPZY>;{M8wY!`Cu!aU^sK}ylMLiL0a(A5+)+oh4>t>(g`IU~3COu#-tUsUn+W?h{ zf%25%PS#SuPZZ4gn9e(6lPzEX%<>uk8NF}$_&&l}aKb4#qel=|e&?+fCg8P82}l^V zO*t?pQ8l{T;Dpshc=-={Y~0kX<3>Q^2>ec=)-qSmEH&u1D-8RwIVL-Y!1O{gkPQ0+ zz#RCkfRVWG#1I^u|x0f>N7R zoI_Rqz`+ONB-{Ml>W!KX7$F7Hz9K-hH@7UEU;2sCz&c}vBA3w^GOFbDDrSoSw!Hh6 zt}%s*HJ~hwdLF1;ZJpa;k&R^5Y;vTp1)2z2QW&U%J3?5$K@3=eGcEaV;1FumQ>+Pe zd~`ytw#J#|E4szvI$H<$H-h~J0arNKlvTtUi+c->(=looRb-fi4VBTYvPZDkvSzm@WmA{FBVc>Mg!R#Kdg(&kL01zPNge?3 zOJejmyekMg!vz=BImM`Js$3_b=Ng0Qs&hW=A#QJRwgCPb0Y8N?XQ??C*Od*|Vxv)Q z4bCqJ<&JgbxWwCsBdN;wE1NAuay-OUy!R(AopIw72JhUSg(y&$+rFpC8387KFwq2N zG5AU-iI_)a2S2Y^bgGz}5v%+mpcx!ajtd?Q z&G7BSK~K83Z2C8aL{Cj;_YwUoTQRVMpnwB9X^y!sjxhotuBe7%9RQCWBO*a@XWGPE zjc1TeSswBB0&Wtj1qcRkvQaTCseE`Qz;JH`h@+$gVw?;E!C>RIqN-z`h+mp}s49?y zBqO}*Be&l4iDjgW<}0R1NM#O&w`hdvsov6Z*V`9gObB+x8==L4pk%$HVVps8-!;G& zr4tT5_z9GG=;3GF=#SJ})}4@2+x0+AUN*&G>fL=q-!Q^KykABc1pN+I+*54sHSDlI z(I&XY@o6kIK?89`&}Pt{8Srm)e0n?i;V;~_U4jS6xRU4_`^(bYKar)HW5zR<{)k!k{oR-a{uut)y?_w) z7WJy02vVREcfHl6mlk=9|FLh@C2B+KBPrtrs%?B)CMf%u{;0ke% zn3J7CLFU{IEzW;Y4CIeM^FBm~ak`WIQC^}oKA&KljZ}d%MApzZ?hOr4X$kJCro@gh zqD*HHwr5(_$Q_)27VRDoVp%@xW9lbxyl4vnI7)->3Nux$kY%rxV1gQ6ItpXSRPQBA z75pft1Y`N^ITor^O{VyiAaBi(P=m$a;4oZaB4 z>X=3SZMT0&%u{H`M6#v^Y z2d5>@(W3nw6#lZ6+{wK^n=BrS+4e%v?SKP9x}gGly@rVLxo2_2peFd9BA9u=OEvsX z^$y7BGuVsvM5uIk<8%@&!2|+~h`MmyBKBkjfr1KBZw-2^C?a*y%~umI#zuf~6m8LJ zn=Ja{7zIIA>zb20OYUaF4z8!TG#z34-~ivl0*|2w9D%j>l~#fXFMQ|pHk1s$4j%R5 z{yM1nn<37dl*XbDp-ny`)38&@HqPbT6455k8|{^T1P{TJEo@gXxF}VH4HYg9rK$s} zT;1!wgeN|pXV`;hU|3Q^&zdA!Ub5w~HIgn*vFbBB6`Dgi#a{raN}_*w+t^T0jHBni zem9jZwq7?jG$9sGKN4!G1P%>J3HhCp_5?X`IteE{qtCkUnC^R|@q4SF+uyndKC8ti z8ZG+5YQAt;V&TGX&XkRj?jT~`vGs4>OV7yQNX9k1wxG?Z!}At=UFhx%-y696{q;V=I@HT~lw9Qzz#{+;Y%O%bo>c^Si;NR&4WMBL{xX<=}**h<>xJD()r!sf)uAA(XQo zX1{)Caqi6-w_{w9R;hQa9~R<`y0PFhGu~2{VUj~+aQaJ;Ag?Fs(%&!Gdr(~{3ThbTQjbPW0E43S|=J^=`w zUmrc8X`1XEb>?}4`6Ds$9KAu!8wo@Ywi|INN2WD{kj-vjwNF?&)5R`SxGG_s#5D znx<=CmrXBsqRx?%eZdR8xc_+-oC}m8+Se?f+thFBIs5jetEk&QB;a393re&#l_tU; zMzRTFb?pqQ-s8M;-vOs_3EqL#L0XJl|CJ&C>#IXN$+lUd$b)yk|EBM>OE^O#aI-8P z+spGeWj-hn;r+{sApviUGn47xE{Veb3Cma(*|}2B9$nG~5(10XTd%zn_Fs3`KmuCv zjS62fX!s(Iy;qe9|1G=Mv6KD3{pocRNT6@s5xdq0cA$x9-$;<_Biq`bVow?1(1#nX zIsh2Z z8zHKIz4vy_ip?YRtET@;eLtTSlxk}l@Y`j(V_Y#BCm|tb5^4AcYoK4>B5lwJ$w;=H zzWT=GGARDY=6#}y6IgTYAK?Ah*W0h}PW30-j-R-!or?qO4=xN*i$@_*9v2#fQsDn1 z)vwS0|MOResL@#WCcG*)P( zcV7BeDKpygVs>`6_fi=V*YqQHyq3M(BQft~OI#D%+!$)kapB=L1ii%M_wwO0pB{g| zFJ<5}_~_XHVi@Bod6u-}GM1KdO3r|!Duy?9wGuR#WB<65qKkui1>jn%DJhzFAtHFU z5p54#)gAVN9J{F_^Av2>4}?JGnIkJIQ}phEdDpUZ{GUDzdTeu|qM~`$DjxPw5sMRX zIhGk`p#r~%T7AN?S;|tXbGpnLy-YKyyQ|!<>%9?LX6D_9A~ie+tbp_# zqRI?;{@{>ZN0!S}j14_n9-1mTVb*K26gzdJCZP>&A|AkJ60nM;}->-zVUE-O4;c6q=SEy)qB@E$OM@hrP<`YwfYL=cHPY?YEQ-4 zq`&?E^s?Pwg68SgrU5k?@6v3}DwCeN$tT{sz=NMopU%|0(YJDu{LEh==tA{+m0g;h z2{@kmUycTtiVU^>YP4}%KFl1yQW?l2*?A>raS2QFO^Ra0=_#QcZ1j!nEIcjR}!M{<=9_cVjbJj@w{3|?_qFBUb8+` zK>6YH2{HM##;Byo?`?a#h16DP~Y@L2SivCjK%-mN({E<@63X-&e+uflqQq~-KipgJtc9fFGS7whwbN;U!8j|F~ zLt$-Npe&fG%Ry_N^{Vgu#`%su8+7zX)fXMwV!69GJtO-VKLLT;Y$=dx8s@7GiZ+iE)M7Ld(Zgo1C~{Dnxr7qzR-66E5bJ6 zO@=c!f04lDaJnNyPpT93Q-U<9GHV@DTHB`I>mRif2u7xYWPnxu@eptT@`;>n$Dn}c zSyDpd<8PNobK7ZDrQh9x#19G>J)_fKzez9drA35uzl9ZxmnX8M2SIN)mN8q zsgC2r;s-as$-CrFSd}z;o0ZmmT~Tf6v(a(G>BPN(E$stg?9A<7#M+V==dv$r-WANP zhCkty?2HiaP%QBI@?H!oeJwuv6R)_H zZNbuUPbq`beJVHC0F+x)t4A7fxyC>vhMzQSHnAc4G*)&vH}6j*4zq7O=&ufjaS6pz zHYaF@Y2EwpzexCJorDrtSQNh0RzwRLbF2L3p8a2mA{Lh2J>;y{_?h+NT-(y#ehbUv zs&`ES`fS9u7MYwKlWNUUypF`lUMssRgQIC`&gsubus278ZnJW|4v?>hJx(j3P|QDi z`$;dGrxv&mF@yy*JoHHUtmB~d8*$UcOQoO7tZ)Bz?|6np`BMXVBbnYAV}7?h*#c-r zs!%Xrb&!tT^{gD;#4BB{ETLj&9|k@$y-9qG;QKHA2}apP5=z3iU6u4;Cdmetd1L{O zlL|Z6rrV#Go~?#F$%ELJoJzhCH;K~Fv+t+`_dLoxc*063L_|tT@n_Yk;?A_5-waGlh()_PN#D9w&I#L?G zd)?L@kXWrkMlb~P?y0iwa*W;U6e%Nb=lrthgN5ifhZtU5&aa$`RC;W7Y>$5&*PMX< z&Kp=cZwSaJ>=htDJhA5wRm#Zz3QD{wL?yOPjy$lT-Qsk&YlJ_Q<|A2qwZ=6nF!O2Z zlAZW3l&WQ)d`q~N?19h#7c!P-2U&W+akx?Rx_NGUR``VvsQ%dvzpx^d~&d$Z?DIZEWXsH0l%Fn%+Ehb|` z4p#ZmnSN_aelw|6!Z8=DUh;^izhc`BqvP@zJ--|K6k$05SRF)W1pUElt zC;@NvKCks?{pGdOiv=dxF0ja~Q#!I&c)e2!lGzZ4o=`;QupVbgA&F?5;!M&<;8yW0 zV@b>J>2wZ1ch|cwRb9mNLHO4N*BWOFe{R$Ar&&T>fs@$& zvGH85nm69HPLbFtK;KDH$%qlYOjj zi@<%&8n$vA!`Nt?yrsv8Gk`ewEH`CU8Q!&jF!&+9M$sd+pFfNOR$E;LoZQSGG90La z$ghU|-t(dsZ`M1tX0C^g+4dO{@v!8Vv%8CR4xdgV)})jdDQIb3^NmcK?F~gm6Gu~B zW0FyaS%yUas(}4l9w85A%T)1xwCO#D(AQiM~L{I1O%0e!NH_OK?!n_pwVUFx?bn%9F6 zTJaV6csZn9uVom@Am_{x!;W@UMP~kR7toX@CcIj%pv+7D4dNM)p%_J)27HJJ)mZKd zqSDU9{%HNWpoUHOy`+>i%*C!$Kzv(PGDROM!oo5Ql&k(NJ5W-(bk|^CO{!DlP*>}7 z=(Qp;+36NirL>u^dlmRoiQF>4b{shMI{u*TrL4Jv%DrJmN9m(;_{! z#a89tZzm%bW?}ej3O86AvW`|6u}5>rotiAM84l7pRxf%TP0U}iIT=?x=%DyMmKq!C zIpi*@_lJ?{b?Q5w9WZW7Pkwz^G57}6_tEPH-{Kqt3lXDJD|ZSDuiu>zEsMSmnexh% z8bgH~??SoVj62Sf+xJ(A@!!J`w9o|IbO<-$((PZlSCnd69G&M85_arHf}!%cQe~T> zCX?|lRbZX|F7B(jb5Bg={7xFKZ}JMSR!(-zJ*#6}ZqYYTb;%u-DRw(dI=?5CM%tM? z#5oStBfT-d%P2M(X=D`<)H! z7aM)v&O=`P@UNuYHzEa;J=mA_sSZ{<*_@Q4TY4hEAepf0+m`$-iU8_DDV}g3SQ;6C zAU#3sk{{$oS6o$;TArI0-B346=VQse_N%Q8T9i$29h?%J=k8+@SJs@Jto&S~ zQy@($nd!#^G|swUj=Q**TE|4S{0!}TPs_rWLDu_jb~Wa$qM~{vFk%t7izX5)q|O7X zBC)HjE6R*CTNQQle1-2T5DvUsl67ySOG})!*UM1UK!fTTL^kJQqdf*@#byV~PA429 z<(ViBvx|4F7hk1auWgag@E3o~wId}M-0!oMe!or&7fV5u;SPS#BT)%vdPpRjgYh-Y zNkce>dW2;N^6N;nM-7slqU>?!H>0|qw7BTYT5rPk^i>= zhK@LfZ!3?V(K*hw>91gURv-Uyd7$Ie+*t6YNsxSk--GS07Tf%UcPN>~|g z-*v9E0}&|{SEo?7vyY2KloZ%sSqibeI>_ts3f}hO%3!AE1kVjBHeR(OAvPBxkU)33 zY2F!};2{o5&Z*FLyGbSS_V*cQV2FoEg(k@*%}q*c=;E>-Q9vjzNpf=PopphUE=m7Y8 z@dNbNNNKZog;V6sBT;^BNks9Ebdcm~&gMHKXkg(to`K19p<$P?tbzW=7KjPn#Nyg` zjKJ3*?`xcnQjRUyBYP)L#$27=Tnv2+sk$o4JMx|##qRF-39)pqc~?`i(<0r~EKEaI1rycng*6)wz9182zw_F&Gb+2iixmyjsBSf(p{aX1OVJhcCRl zJ{-*T8S9=*KgzOr_g7E>0ybo)NEA>)ciZm%Pgnsf#93i7FjS|E^u|P#X_R^A{d*v9 z*VBSK$B4+E?si^L_Cs`0902J!j_iG?3-_=p)p&{WDt{)`g*-f{Ra0T)y>WKBT{kt= zS~MRAH(bZ};u6!P0BACD(cn03Za$4D9;BJcZ+3pDy`a@NrCO#b*3;577`XZPaHCMN z&a#*I)3q}!xud_j)-0!!7E+!2p@%Kq>_v%;)XQsSl3j6QdOZrnn)3}arNvYMi&WaVLG&>}|~)0{M2yBaAcJW+l7#J!st5*mhev_Jji+k>I2Kbeoa zgJ9NrvY_*#)J{dZ+M2Fkzhb-9I9cvUqi`}Y(83V`W(n2)SJ&OYCp*{6yBShev)aPP|ZL-B!rWz zy}Pc=ECoJY^)c|AWM%W81}9jCpuhUus`g<;F}tYZ!%@DEpHsa3tP16E+SvuI`J4KE zt(qPgpd!&*FECl9gni)>{Zd_@pSTUR&!~M zZG~D^eD+Un;QfuomiPB-A}MW?M{dukulcHtPE;2~_s!1{j+%C1-C_X<4Yj*-+qQDa zN8^)p;hQ~fVH}$R6Nvi@Um%=JLc}iJWKk;queJ^r7G2o^F7V>BSElj<*ttST9EW3N&j7MkxG0W21@ z=UEiAa<9zCp8I&30R^4~z?V8loRmin-*o~rsJTs!(~Hji&NPqfoxf(4#C9EdTC}`nM0AdtU7{ zfj-K$E*mRkhXF_oe9k+2fpR+v5c+8qGJd!$GzNKdS;(X6QW?zJVGW-&P}Y8mOD9cJ zY(6<%vlANZ+F8SfLTEdl5XIXpC;wz7+UvX1m6nxP{r=oTyO=c+?NDGcL#{1;fs_ zlGXUc;&@ncU;K}GXoGB?D!2X)T2ue((hHzi@>`yPV+}b1Lpl*XASpCYswR;3tMhI5 zD~DS9?ripk&O(NPP2gm6n;0(>6^geUJfHSsp5Ymf(W6`$?Ae+yMZEOCqM&%6(={vg zZ6ovTn%sS0F?zI2jvH_12q0cs?J=VU2cI)#m*M~xNdG&c6pLDg0TCO~hVb?Kbht^5WQq@83=zM^#_-RP%@YMq^9=@E%RR2;m1oTd zLd)x%%@$@8$*7p^2c`i{{1iT2*Xk6!ZS z=zjJXF3`ok(F$x_KOv`N#R>Tq`NfOauuTm=-@T4QZHeP$uC@%0-+@lQi@%!9sAfQ` zQ)J5FlfHZ^lB>`%*PHXqDbCSKrzxzV2l#qdy=%E-I>#10y`$^9+FmT6_nJU8gcwNc zKpN!kS^xw8<}hlBv1{Fdk2E3+aKb_wN2a{rZ!Nog^j2wH_|xG#;=7H3_X=Rw7V_oK z!~G1#yV@YnO{hb2zCZdMoII~*DMzdw@J&xG3kG~0QVF1c5wDM6gL|u#z1pK+`Qw0) zC}KDO1b-8USk(U~e0Vyo_(ZBx_|W5)D>b0bcZrv&SpMunUdc`^2wjMOYOTFlr3JI@ zY(2jZsT@wD!oz{z@h6MZsX!7<%+4#~LdA`GlN+ z9-UGwG}_CCj=`kLs!1UP@jDaI^BTQ;%I9oOPv0q3o?fvWy^qxl`1Jn!oR5gK2121# zH0!|g$PPoR*JI#Jai7g`7Qqw0s@lMNBc#k%)Dq!P7z;d5jN%P*PDOe=ZL0RT_Dk1xW4wJlp^8 zpG0k`N`t2z!B=w~j7890TVw7XA%}XyrLR-p@?dD`mlfF;ixctsu((NN_2}d{Xf-)T zHe>*BYe%9GVW!D3AFc9y+=j4LN9AX-0@GsZ+qLKXq*m672j*Cxgk(m4ya^8a+PTif zf85jl(#lRn=$u$GQC`@54OaYPbE1{K*{NBwkQfv)TKgzChUNd@D#vE;bf-x8;gddm zs&FPqvO@B%g48`h@zJsZAJsDdTTkGQ4H|tfTk2A<4&mE6Qt|k_m-v-LZ(G0(FS>N5 zjW(vP*g>SHn(uM?*a6TnY%9nrN8^9xys9A7)xmW|3Z~WEB*7w zxmKfylH=9cy58tf~pYoCkjrH|!=4;SZ+HNUt?;!y`*!Lt` zGeT`I`VuIqV=xS%w!%HOFddrjhVBv-DTehvz7)|Oaz*%d8XwWN-VEJ@v{MJuE?me$ zbv!lbkB@76^OF;oMj-k1eL4jLtS;jI3}u{u%Lwz;a&&egU9BpDSOGcR;ff+GzW;>; z;`4Upf1w8;-F&<>eA^5l9a*_n+l8R@z#gs64|kgh-SP#}Jew8p{cYn!tRR+n#WDVb z+|6OEPNUFoRKF4DrJd4C>$fwR)?%~x_uJhyH+UT_lHV4=pe_f|Up6O#wu`nE2C5fW zF9p=N)E}0-9mglBe~fGc{S+MC>2Cv25)ShGutk?}=_y=!Q7XO>eJ(#b?(S9Yq_|&m z5B`zeedfHgucf=1-Bgl?CI*T51@f*0Ss*xv-QWYVHEtjA@odF9rY4ftWx?_r?_D|( zLZtIh;-4?kKrCaV>q`2QYIEd9c-{9W#hcX0mn%qUAVAhS`cuB8UOs>9Qb^5@{vUGl zO#d%;t|BC3mevo^MUoB6tZmun#y(9>b@_!shCil1*HR*M)tZlXq30EURbWIX`^NF{ zGa84Zyfb9wJ$GXLuW~1#MEb}?7x7}dkyn%*tq|YF5&fDw9?V8yV}ePXB`G7$m|v6U zq;d8-*ONzn(mQ0$kK6r|PlHBSWiGlj#L+)+7L%uZ?0O!asx$#fq46xY$xT<%Gu2UwI ze2i-afDk3k0a~**vm41Lq5XWO#e%k!{G~nf&NLofmNh|h^WZFx4gAZ!L?A_xLt7{M zkh`-)`Mmkl1lp>N_>EPc7a10(&6b8zbVUNEzaO7xHuv3m7yPVXyyPM#Dm3VPUqG2F zpD=Zj=V$0tR^)B=Q*+7P`>*WyWt7%9|Moq9efo#mJ+m(l-@TS=0x#VxwTt9=_HbXx zFv0BWoM^cH=@#t6*n0wBS-)L@-Uhi3W=Q_tCP%INeX7}4dUan4q#eRF)L76801_qw zNSKtwBp0R6hJ||Si8vc}WM8LZEoL?+N}n8}3!lFj=cmV)gg02LlbqeyU^J*~)P*uN zdN%zH_D%j*CM8YyLH@Gq4dd_jb&VUZAKYw4%^2_*;FM%Y@@smCOJj zn2S4d^3wy;2M8%31uC-Y9PGDw8A!-IYkaeEk91|JVU77^Fbm-BYpQde12GP8VqH?G zr@n~!P>|XhECWxhDJ2{nU4@zWbyKGC5qL-YALp>?jvV@@R-!|LxcYAS^Gz3edH$O6 zKJ&Ys2?O|5{AP<;&qGqQlkUg~OVYL>D zUQl_aV_sZ=%N2x%3?v34)Sl@IxE*sbc#lb13;qJQo@&WQYsIOTPdKM8b7P5~26w#% z`IG4@r+@Ok8LD0EHTpBgCupgSau#cK!r|;-AJy?{evS#%nwP^2#$G&z-{YS&l&)-W zf~TjpFA(ZV0W%%+p)8uM*{^cC%C5oghg42wilIUOTb^0Ni#mDr!j zisa~zI{l9_Tk6h|M4K(9wcU5aHRrzs&66kbYAeBGYrPe6%BC|IQFCNhU9x1Wh_BbI z*yc`QiH=Ff^P+8JsHdU2ov^|#JQ8!q2js8aTr0mL>u*VT$AiN51^GOd&&HR{aFKfG zf})rxS&Mh^Kuq=TpU`uaX2ppO^@4c=;(;!k8$O5Kc$Z&!+5gmj#?yW^Mj~5V`sMSk z?@Ge`tJjP1O4I|3adN`lHt0)$p^1?%%DyW%qaXh>Iu*-gLI4Mu5dF~# zaP_xOMs>pUf^WEy=1lv4M%5()5%?Lilep<%J?ZpzVNO>Dx%JS}x{nXWBkyv7eneSO zP)6WdR9H3~wdfNy@;W4%9U!~2vdeom+oK*!Rj`UHn_GeXw=f<#7J6oKc^|snU-VZ4 zgS&Np%C+6;qpUZd%ip78`HIspY%GyZn$9N}@66ap_E{DrDwNOl`ly?izK?4;DzEG$ z*PET#){>Sb#NYym4MjCRDOa8B2jx7a;W6jxcXG=45oo5fS+4ucr; zGg-V(a#ByprPy5^1PB{Sp!A5pjkN#w2Oji?h;&Rx2}8@0zNK8x7=sRP?V693E@@j& zqUyX2J7hH#eY>v48UZ-gBV_vQzq1ZgYvpq#%~Ix&<;j{cplrud(R{|Z=7)3D&E#kQ zDcRWd*|6J5E+rW|Y&bs@Rh>Swr@Fs4AQ&i*4IivTR^#P&i+gthD>j7|S>V%eJ>+fA z5wG-un^SudPb5~)4%h+b9)!9zg#~C%e~9h{sA6InAA|Nv)X*d2A=_1wn;cA*j|AM& zwz^!t*9ZBJ4q5i3#qPib^L9_=Np7X(b;ZQJ!K89TVDvHD+ZO^a)hRp2nC7K~>##8% z(ks7U@XY<>5ciZhwFgvJJSNBzke} zv_)p>**(piv(J=GTP9!FIW#D?t!{iDO&q&yle8{ai2h@TtzT7-|D%X;>qBwSJ2~DN zS8<}edc!a97J!_XPKK@Tg}R63l(0$fj3=0jk`IMBIg$q=2^H@meb+Ab z2fF(4Xj<~qPxLx<37_wiOw>5xMwK9@M8W5!e}S;T(FLR?)`bTlB|}@sdr9ea7_?eQ9|8?pF zAfQQ{y^aeOW`#_TiaJ?YR<`WdtWZ~5yQ#mo_nlZOkVb7rgs;_iG;ZYu;EA~KLI?WzuRZSq1$R(Nib}-{9}P^Bse31Q#I&K8bSRB3=ll_RnDPJCW`a( zXTSFL_Kph57#bS7*ZA4CN8-mT@)ZgB_{WT_VPwd`S;pQrE@h{|I~d8r^5E7cs~YoL zFX{RVAqKB!9sY#NIov2EzA*6o4|!IiQx&Jo4(9=J7)yzCT!&_VTBa)JmQgkizD^B`dZMs?842%cT%#b0f z(oCIk^T2?0OFq~ujv%?Mj_#YCON1>F5?kiA|48_Dm=bcUZCrGoBqVrXr2EIrYVIfY z=^C*R263^*jGPbFW!|>2TQm^+dW_4F?g7|fU$$FQ+y0xW8@3vXmNfbL*SHPMxdK5B z!5Q}XHQ?68yiAy%WIp1h#qvEs(Dt(*dR`!8Zz1f19*J&SUgsQUe9N-*4dxBN%qFcmkp(!&^k zqwuCZc6@2}Or`aznEVao0005&T5eyok)N5ZT9&z!Rx!K&(@Twp_2RHV9wKhEoq`jR zzoB@Hzi&t-<95vVzIESz=5h&)DmS6IW>ihs#!=d(M)^O{%?~Cb=NmiTEgYduBqb;N z7$-P6T%hwEm=9gJ5=QDFWgk59I9@64t(z4e)umY;iv|J0bwV z{^|%eZa}W_4IKGp?k#GLJMf*C$m@O2ZX3Bb-Mc``76VF9?jH-Lk!HHkJnPXo3iKO$ zq4Zi%J=nE_UMAp3z6>S>kKJ%*0dur+gsg_UFYj@uHR&7#D_RCum98Ht#G+sAXL7|( zPP=_}hTjPkImwg;!oyIv+%hJE&QVfUrfy_pG@}Fse;Xjgr0F;is3YXJG-4fYKmH^O z!AbBA)dC18PxcRq2EU@^T0P|(d1*!C>+T;)F@JwbaAarvM@?XoU-jznS@^9J*O$(} z(;x;bC4R`4ukyp{CbOXm!`b>_y1`Gr%hYciyYbUsnIAQbS+D{MyK6gVU2!qw@d34u;#)NSdx%(5Bki1k7uWgrMM|$&v5ZX{s zv1fc5Tcmltp*Q`SAxOqRAkHBBBg%fLG2~*p8sD>@du4Qlc)@+*j%}os$18n+UCpI0 z+o(95o2LHerfOrV1c!HO0(~YS{Hiv~E$P+_L&+{6sa*DIeU2G^_gaV7!O@H8yS;Nj zNr0py-Lq-2tn7(pp6>VA`!Qs&MGf`r@_2pLE7A*i1eMrif)iI3TgEUFlf7P z_aaz;>d%j!fbAAFv<=hyD&7Ib9!zlL_RT^zGi2D#6V`FST96Jh6`W+vQzAw6VTLo?iA0&807-#S z6W2WVOUiRo78`aifKc=k-WQe^UTwK>IL~PcK0Igsg86>On;ZRN`(*#l zdg=5t|9y|_>D9qcQk|7}@OD}r#O*6cfbfmkWYWiKC>H3nRuh>5q`f2Kyd4ZY1e^l+ zr>SDo?VXP|1Fg)zF+*d?uVgLQ3yW;c*?rKjeJy8-*AHgVnTA?@MebF3bT^?)Ewir* zu%cb51{z+@9@8S;vDdb_O<=Q?56I|-^27uQH3UVm<>PM>P2J>oz`Lo_SDXrhKFFM> zUtJH3I$lbs2oMSPQ*o}pIQ^yKa%@i=l=<7mBcLp(ofp$Y(HlPO2yUkDw`rp`RoaAt zSI-kTOO+kjy)3(&y^Ag^39z9Rizr*Crw`77+QuK%hVKQW?V0q`DRC*Wb0Auc`}Z8H zo8_$}Y_>LO1(OO~uE8sMdZXLEtxxg;!&AIzICh8u@WVe`oP%RZ0@v6#xUZAiXEenV z_DmnqF1xYfV z!otERg9Rhk*1~EzUa^O*&kPN>?N1)KH`bgywXZrU25hv^=QR$PjH_eG;I?HktD`J! zr&$-;*;wtAUhy>FE=}afF$&h7{X9h3dpC3|NK*I?Z{0(U2*SlZRPI6XYPnZcbswZ$ zD8!(ZSP4DmX&lwxI^fa!nv40V#ca$N*p>D!ST;_IdR%;-Rb&=&DOJ<8;Q**vM6uJ^ zo7W+|(?4ktCrK5lt`LN-DdC=7YOSk%rE!k&jLvoB@TyLnZm*`kQ z$_h}?6!$7+7)Zh$EVdULj{5kWuao1QtX?BAerT>vo#4DhdAtc1M31MuomRaQe0Dj z16gQ&-q_LmMdX9HHByjRt_=q9$Fib85hF-8hi!?;zCAoIhMgAF)@}Q;p)A#1%GfpE zlo;Q~BUp`<&3j1iah`Gm)ugTjFzyx@cTbBE(i673UpuUxncZUYFtZB58njUzaC%_j zwPTLJkK@M{ncJLucw*Rnc_r+`LJx`@uiKztzChA zQqILh=NKCfecF*D;eHV*_(8X43xf{SsGevVz4`ldV`?VP^Akwsu6KvPAGug%PbU8M z0`}DHX>Ew&_TcA@?J$^3uq&hH5a5c~Su*sYfZcQe7%n8YmH&szy3Az_A*`NXPu3^! z%N9r9WjcwQn;Xu}*3gpXs$<;BbA@GyuKUMY3qT8UrRL#$?j%A~@Z7Sv)U*C(gv_YE zOR!J-gFoaR+OOTjBcaE3-h0wv~Bi zOxmp9GL_ryMmdA--|i}YTe4DyqiPgV2h`#bC+qiv`ptaySf>syz=xckod$^`1p6TS z3=iu>wncG^z(@3q^0b|^=41Wn%^-qsMf7#~f>c;orof!j@U3H! zj9oH)nlu4XwE>Q6$@IMJ2c!k>D6D(QV{qGEgKYE{-UWzzF4I;1brT^5KXlVta`rWC zEvo_<(g02rce^10O6AT*RBpa=mR+Ax3wV!<931$gogZ%Pd3o>Z42P9(2C|6W{tcUb z;6G>c-E=7UfQ1DBL!XMibP?4I4aX5lZ6TG!!G-9ksHgy;{o~GY=hNuhEAZRfA%>q6 zU)O#Z#FEWiYPx1Tz$`<;|5%P3{vqkmO)88ZszbZp{ccG0*li&)cbd%Osq>L7@kgom zCAY#9>-O6V+)5ov#1n&Q&e|z|;x>3|Rdv@20t>xhpIYBY#D4)t|D^hra^?pw4FdO! z8q}ACdtU%uw+RnwhaasCIs%GlD};4l_P>|X|_eiLrr3D1w8wGW+VnN#9N@?X&%%p zcXp{JL#Iq8-2jg?zN3Ar2u1TBL2ph>+uT_V8Kr7GZ}x6%Eifric<2oyt@0OKm~y~2 z+emlLdC7HIoSQnM(-P*J{TSjQKGuHOA-Tfr&BAkq^}gyss@?uGgC#mu(0(}QoUQq9 z#V`TGY1^_4o$uuEOLDHN>(8*I$G|m@mS2kPdB&S>+c5`y*}ef>mhqxb%EtxUTC@j@ z?h+MmDJPsja4oSj@nUvT)-=DJ<~pA`9xk390tR&$PAf1$P#+KG{dv#r^KmuR9Nl47 z{-=JdlCM82xYzJ4uXf+XA$ScmO3@_)h`8PdWoq)r!YsYm`~qq2o9A4+*r}Q*OJ7e! z)4JHeK;K)K)fHSoTWOI(LiT!26#*!G148NQ6N&Ee^+2SX2?U88M`pf})tekXxm|!I z>pO|(nHUZE6&pW&da2aUEr0trAlJVKB>d@Oz%Q(Ji>Kj3rP`&JJ~S2Y8`-EhlqyPY zC5bz80t5j!IVTziIoV~wZWTZ?2(qIcxwGB)mNO^oC$ z^~dRDSh;`KldK5A@C@L70(fV&4T(N`_e1yWe&~;^H6bn_-b8<16EVwvAs?NzTI_ZEIu%D|6`4nM}{EKkWJ2f-0SXZip4wFrgSefIkXAm0;E&6ebrWrk8xp=k z4$C+B1qfj9>&%=2GzE(awjxs2EjZ~XeG!h9!RGMrQK~cahBPSEvBJICjeDAkf8Uv^ z%!cX-^Rs-OZvT)M*xoi-?Zk|bUszp`78!><6+|z$uSvw9 zEUsk0T2{d$+CPKvPkjZ*@7Wsb>HUEs_s2D3W_>JQO)hxWsLx#=U|=&Gb)uJc{E#6m zEWn)qVK7kzupwh*Wo2E5+}4sW27CHlCnf+MrOS&hL3Hdr+$-N3*vpwn5^~d3%ojle zMNfuKiUs&ba|_p5WOi-Z%7N`@(coX+WsSZ|UBg6CFT=#Rv!sDkBdU{C+;ZuoDpMuY zEweVy_$fOd)R;y*MNLM#HOXu}>R_~8nzEmLH57e9>}7yt3Gul*NkXzdYO`IuZ9g^~ z*Qa{QMXz!Bg?pNNfTceDZts=C0M>=^#GBkOlL4@579Y@t_(|pqaKtB|?Ui6lSJQFY zv+mMk?M8RteO59wRx!nqIpgLoW7xO7M?nQrQ&zLImFu-#%JQQ=9c{EQ<&`V2mx4Du zUyX*mgNarEWo=T^%&TPZ=df{E%PSf7Y#Fmw=BdQPH$hs6uYsxY_(=<=XQb;TmE}); zOJA&*iV0GrQf&Y>fn=jnmX=~OC_qZNTgAo(%L3~CW!>uh@Ep{hFeNJI;-Ti&bqsD>9J7~Y;<`6K85-+exDBYertd(C|D*^h^?J8vY z*z_Uvl|9!e`Eg=Sa!LxXm)x5*x%bhHiHxNcWxsCm>8V6d#E`*KTTOpm5%T|0^__ui z{@>fJR%=vIyVR^zyNV)cZKag9_Eyv`YQ!wHYS!M=EHzSF2(e<58YO0GB%wwSM9e4O z@9+12p8s=RB=7DQ_vdrYea>}V=NyWS_O!j`)QzXU>D1N4uh{>(Y`*0f)Xh_tbvs4|bWWSJ+A)E#yT|?>b+?o=(uYK=he0~817U;sM{`NgQ4-Kg>PrybHLW{ z^Wa(baTu}5uNUs zFDcu0<*Rzt#8Qjs_+e~;G*hC{Bwhx0NeQQeca{%~7Y9{(NzBpDdTzKBOq|_8UC62A ztuempuy0ga^B$gMtaWpqPZ{Wq1S)oB$++O(bRre_M8$J$%g;LGg>oF-7d-k#(W!*b zB)ie1qtgPvJ8m^7h!vdA8SWk|2I+zkF0pZhwmx%njIDwd+!W_PuB5dl8$g=o=%zEd`oDf)$faM zH{jdu?3O11BUX;ASC%(E-6l#fH(p+x;X$>Bu{}E-;04RppQErPATeKOdh_N@{I^|S z{TEjyQ44N*3lE;Jlz}nmvo-rhR4~Ixs35<9!mA)+0r*XixOoSw90vBN`c_yb@pgHb zKW+oF=l3zNb(O(OyFdg&OEDVc)}DXT>)N>U=I_;mw&1+S#+%C5*}hq5l|g({>Zf8B zMxY?L0Mw+gyCJb%>T`D7ln39gAoOvFH!S89DrAOn6wN&K^WnN=0eto1MQNA0!0e%2 zkZ`XD_DZM)eb<8|xRRk_W=L|O2LVZI+0;#amAlq*v_KyM1s95)pap}LhCtd^ikAge z?U5k+t0VK1`=UV?Or06~ZBdF}D1CP0-}%i{UGRtY*Xu}%6g)q(B{p+-U!TsebV1M^ zj7TYs!Cr_S9vIBuw;ITD9KAoIw0WEvVw*7YzF;ZGs`k{`86W0gJOzF>tY+=8>1tU# z*NH?%JGZGVH4&BmaWY@>{SBMi8ynkeS_?p5MkS-9@Nz?|BBGDH_jBiA8)vsSFV%7c zL)IUBR){(6eD32&wa{Q2b@3WI-+#<&K3djH3I00nW>@}p0A>#D8pT%<3(g=nEA}Km zVvR#ZWe0ujU-0+-$^7ul$KMuz=jA*vEf({@8py~Y33ou-|7=p1 zh)U8%6<0bfnShA;-c@;gIufI@+Z0VO&ockkoH+q3kjY9M6Io_mWp*DRb!hP)iS$|X z@5KH--+mWe$Rnp{84z^6P(!ZYupn$4{xTj^wll_tMoon3?u0&1QmPJ!+!zWH&Pk4) z436`UC(I_|&vRymj?f6KCoQqMcs@|~CN+bHkLJ#+6eo^lu9&~j~8&T!7WH3<|qQKT+sJ-+QPhV$88>qml2ou)fR``ExR1ABKD&QksQzIxBd#`kIc;9 zoC)>U|6kE|f%`BIV7_T_zxQIUc(f=_6RJ@X+al~UfgDgECaE%AEwzW28BePw&&h}}r!+W5NUukiKCEJ?hukV&m>#wHW zj6?=u>>m>{Km&uA)4;T_wCia*3|M^nqK5>;DrDDxp|bXiz%k=5E-IpXqP&y`U-LclJ^LzD>XbZ9QBVg01WmmF^?jZ zp7et@$?P3r?dvx=GNDQ1ot@XEWeZT1#70|l8k9-ZzJE%n`ur=OE#m?w?H%_&t3t>D z$zZA1AD$nm1#Fx9yzznuEZrYdTMG*F`TQR6dfpX{v%XiuHXq&5o`rzK`@I|A+h#4wv0$a82b36Ixq%kg z{Ui*z5CxMKuo>jXzU@LdIl1h9Y0C+xNB2C1D`MuRA?LoAU`Uhn=J=l@&})G1QEknI z>1AGr8s-zIGMHR}DbQ0M^bKnnP4FESBGd_^O;!L6xtTl|UjU`7gK@)+0%Rm)y;r)8x_A%9Bkz!(zacupMCKuj4vSvJ5J&9+~gm;h~Vdu-oAX%CkW<9NpE#rGzD zw-Hj0zgNlUA?v{{xx~b`MbPmT2dnVHlnK#q4h8>1LEtSe4kR-&Zvpg1HXOJ4PA%l* zteAKAZoF4`MZEh>XH%EX6+2pKI%RmSr{F@F_oG3_y$q{nnti(=YLag7;mK{Cl z*+8T$RA5%0D{*1(ia$r7(met{;&H%qX*O`SKHFCm>nGv{%(Y$d_N-n#TA1r`^!pH& z2IpW>MoFaK>vZQ0J!zF8Ce7Zx96En+yh-1u_Xudrk2G(KlT(!0B@`&dTkfy6MxLf* zFd5rY++CA)TOjEXYiGvAthXW2U+@O*<>tChWqHq6=DsXCJ}8UonN2y0o4b4(m%wD5 zx*A?+_C|*+AJjX79C6DE_~->cjgX3`EP&%$M%hN}J})eApH>|Am|P3}uaq z>m=9{iMB*p2l?i9!bx9fxWuHvD zbG9()yWkSw{By>dV}VEwn zaDq=n^W!!zeIeAH$4^=&4$cL*0L7-TP7xXzHVT5Rg|)p zqwu{Xqr1w&)Ru{&;T=Je!a~@h{r`4_m-dI|Y`Vbm`p?>?d(oX6zK@r@ilUHNlzD`iF%n5= za$_J62;@2P7HYaEtzVm+IwGb~7@31>ZRKj@zncZ*soKkfbb2pPNz9VURiYQ>6#Uqj0J2?5hp3|Pp?3f2hCf$`%+%|sIAEv1rkeqy8>*PDNT(RMyH>PjEEu?i*J^t#39`Jj{=CpY+9*B zkM{y^Yp3syS);?JbcSEq_nO28vv7Qde*6$&Yj9cWuhuY|4x$_ivFZGIf-W36Hb5kL zfcHjWvZ8-2b!dq(%SH5(@zQM23R~_k`BLfCJmbX+s-!bgSgz9}`0$#2JGg69%^TQm zZ~$H%n_PaonLm3ysSy7A2}~$zcukNOfYuf`=PK)>mObLU@_~UWHPZg9x(S--f-UxI zi-Pp|GA%C&R9BQWs<(YBIM zb$_W(qF148{~S&m5kb4Q!3zN#r+t#KUZ8|#1AInATDEJp0=A!>ospt0KGij5>It4# zy5UV9TZEBFJ}V3-@kU0Hcyp-2dpU=v(ygqJt!aDC4}&_;p8wPyh7fHNS1qMDWLY5+ z`AY=u>yGu=;|Kt#K^=i$rOOWshKCnj=l7Q2Zv$pSGFqI(0O(xlr<|Wwhn@Ka(p9c1 zRn-@1<$mYA?fjL;U^4>*Dekl|tW#Wu{^-4&aG^a@!R*%d)(0{!naq$npu^v1wJL zY+P+gmWVLjf>MX<3}LiG+&OMb^0&2r4&_q=we0X7^y@N%$XpaP^6PI>=QY8JC z#6v5_glbH}fAziaP)mi&@1O6xt|x6A-}*}&#wC0j;2;?M%a`NV4nyOTX4?fVigq~D ztNm_ILoSfuYRM3pNlhv+CRgTa9cLhQXkZUbZixpIqIA~bBs7n|NdW~DpWgu(C*=p17!aBHU*G>j z(J%TPl@!Y77o@TZdKegfTm_n&tymNi)`llDnvQ$jkQKs3oE9|h16R_*#f5>QDG!SM zBPlBqR^*{bEN>M}K+QsS|pV{UUXfUPBek%^gDOQb5Sz zS*soA?jd9%i1(ZlmV302y0-li;9j5xRzmTonK58nrR|?5QhqV#1nBQ)^rKhB;Leuc z{7L1A@ula&t2@u}Ic{?300+A|_FTzMLW;c@4*p2*e#HkffON_jc4wR|g&kbcV{hEG z2u?h=#ki#1vR@NT9gpibKU8zy_6{;K-5SQE&p9qF z)y`^40QBPJ#|PZ@)r)$S(hVe%SB?e9S1U4nCkebt=L(MAXK+O*%WMDYb19Ci{s20i znPq|~o3@oA=5kbka1wxHBle%~W?8v@EtFI`xRbp+%}VM(aI6=*VjCW)7rabJbu+TG z3?aF=qx&6$=^Y*y$?d4?g%*Op2J<9f1z_!H!DmCJrfILvfBd)+g}g5(wEt|wN%XE? zRV#p}Q91Ny(DUB>A}g4fk@GD+nHLk0XKNwA@=IHyhL1l~Ueo~RGXjRcJi_Yu?PUQg zQCX`Z{O1t|NB5==B=8O=iw8$x7u?=dC=H@2qc$m_Wo=8SMW8#AV%3}05aRM2cjPU9 z*0$$ZUQp`_A(6z7aJ2m?ey6@^$^{XJh`^aK-V0M4=skjxw|O6WZZfE2V>~f?0e5fe z>Xo#)T>icYyvzM9WXlD2un?(c4hDHBp8(hPqFH{Vcb@tEGxqB|^mzP+PD*jm8d6tp zY?s%IM*rzoff!EN#U;x{3;HYOySsOXC^Tt!Vq@4S$OOJ=zDoX}InFi^X;Ht<$#Q+@ z`4G#^b;K2ZAqS0i4s>V>3~=cEeFnfb__bTSwTi6Wt!?Q90dDa@y0%wVCa!UcdsrQE zdokIUoFrPF$ydT{mDOl=O-Gc*-&nP(mvW{Z+x16v#n(%j(+G~B(P$~D3l>^}MxBPW zyOWd2+}L-LEz-YHf4T@(+CoOJ+6~{H&AgVE`F*+YV^AbLcMB^$(H2tcUw(YgmZ{_~ zxE)IW_N*u(o$+B$WCbi;=zZ|auK+jW>a~nhY(kpVyQxo?!N8&Bi(XRR`P>Ai!Wl0^ z$>X5O$>Y0PxgOtw>thjLV@gY)r$RmZ5du@Ml^$rjI6df8wTN}o>1h7rciPev$0|am zq!lV&Km+|tB_=NRY%|MT{mc%w8pX2v`6YM%y5XT9E&_4(Iix@C2kphJzg*(cZlAvW zbloTHtDzjq@HU>h*1IL+@%xwlVzzV_&)?6ZfaC8&Yh1SX8CTd^B)kFMh2tpyYKfM# z<H~6z%A{Z+4s5WO4V-eyDP`zi???yHesm5+Qr*$-r)CbmqO&d;3!linoG2I2J! zN>LVXUG;_o%S$^{Cei0R4yelE;3L00yJ*gZLPo0%{7TGQppbR&_tFrXGet8t4V#XF zSC_AWxtCYAC{UAL`R3JrtF=VxS*>L*6Qs*^VEvvevtWV#nSx!3_y=8);QruOa<_w& z?cfs~W>*T@p#|R-wZ433!d+W+=s;ipTGq8F=^m$yQAJ%3xF8I-r(HctH2FK~z3Pd& z0hBM-d$XI}g6Fn*!BTZp?Jf18&|1a+YNr;Ud!?{|XbzYb1`J3fNe)O3Tr3%TEUEa! zW4~RYg*6%f(k`%aeZw@3Ql*DJ@zv!#(5miKGrpX5jPmuEZ2O3xS*ZE7bUCKy18Nyc zSsD>mDs7kT{1spE@?<;oJu8i=PwBMB@;)Z_=+-hXCf}+zrhv__&#~#_sZZv9)aHrf?gVp_% zG=>)K2At;p#)exBkYFi-`gdYc(SNpe$FDkJhwdDi zG8H0uYKF0Z)$f;=W`|`5>BSfB`R2<_5j{gfqVKUiKW%#VxY^3=If@BOrS{&^{yE1{ zu3w*R$h{sA^D);tHGJ>T9Vk(=fcag1(d2G~uDLeEM=^WsT&nam^@x0I&Gur*7L9>X zSeS@C5g8_tq)Z;!%wnr@`%pTw<8G%Qv$jZ$Itf{h&CL7@^|J?P2A@*BBSuHcN&nfa zXTX}ryLmJRJpFB6iI;&^H8@k-t;NAW3CLB<)9$Drx~JWx?N^*QN>+m0FHt zb~Cu%s-dr(7bFVnwZbLUmpoTjXm5j$LWFwz^WCJgpYUHrH`gvsx#feeR>Ii0*alWK zhG*w7q&Plv8XcE>(UCtaB1i4FRl2Fg27v~--+4AI^)J=8)$0Q0%&El-?>U_04p;G7 z46dp_re~)0NR90*>$3}P_Hz5OOw)A*Y`!Q&{K7a5b5q%T`*Q7GVoUX>OaQC4tcn~U za85)?u(w$aLhUwAu^u_5s+!HU30VWPvu;wKCIB|&59~{JHf>cZzi(D`a{KfwS@h*5Hb@cqV-uTH zVO8Q!M-uL0q`X(uSvq6ii1vdVXr)@N$8!U@*5L;C=Hnk+nFNG2z3;p-qGw;Mx9BXL zeZWmQ>>(|EQ;K~aq-JjQ#fcPMa6BU+ql-In(LbzYtW_YV?XlE26mFK`epRAOe}l0h ziu;G){%Id&dogEPc+8mp(523{G8?6?iO7LSEHTIFos<&`A*I#ZQhgZvFx=VvF%}83 zKR4el_LyPSb!N!QzA~CdhRzy$D#Uq?bOeKD2+=us$2j|Gy2aZnA9X^WkDQm)v13f8 zOVV(0xSdToho?OER?2q0M`LJtGA17Ty@GRNAt;n`;p^0ye zJn6b^*G{bftl5PpvMpN(9O}BbhVDbSy?RvZ^dHsdm;o3?)}@#97+HgTLk>$#x(gTD zz2E{a_cG<%yu@1kq%D@EB~R2Btn4f0;zl^>zwebpG@O~TuF4jzw$FSOcE;~bE751t z&P~OH+(@3L#bPA;I$Jc3n4orc^JrVr*(b%ks1Le_>M1o{I*O1IY-x~jJ#L1V8sbF$ zU_46pYx-Ec)Xg)CbUkgkT&>Ty&ywDS;UBLD=8EH84>YIrg4O=QqY|}Y1wx86DqDg6 z|FJtk|G5dY0Ll zJQNOL4@qzf!;*cmw?uLrK6VGxey8=HA{%YQ|25_7fv56y(qu%fCty(7!!LOQCB!Jm z(nnl?@Y2Q2LOIe=aRf98y@VlTZ^5xd zM2udRJMKL=+9_KVVO#- z6#e|_*`g5v@{p&apz;wsw#NFCZ}_ zi0omYrEEr>q*z|LR9>(xDE3}s%rY5d{&Ac%ap*66FJob>-LmiOaHd!wOAX0Sn?-Bh z_S8kz1J2XYpSB+!*Zq*h8?hYG+qpPxyqZJen}dGsAOJC*B>$ya8C zQUn>Pzpu*W7`A{e5c^Y>=Z;7cl}+KBAN)`aam<69zA<&OEC>jy3KpL##odC3=ViYz{-De_ zQ;%q=ARATreVmUw&hu%}O?Y_WoA!?Gu~b6WxuD1u*#$nAHWb8UHS5bC#Jl{oX@=#d zX>?P%zg0unfCxxG==Ii-cAX8Op~A@I3Gvc6xt?Gn(R)5J=JkQ&N;23G{}2y-aN0nA zyF327(S7thwH~XqE8_d&0RUiIWR^2CXBNeCv&HWgQ;Q$Uax*J47pW+w{4{YONoq%% z&JjJ%7HAl9o}n=mv%7n`Y{p9lQ-r+MqYGsBkmahluQdP$ImRg5_T}{FT7B78ejr}Q z%^O~21~O%)a4ABlUy^-lq*E3NHy0qwpzw4$<0Kcn5l9xNX!?i}dR`Dit;8Ti&(9z( zNLj}i)|TPdLWB5BqcylA1fxhZX<_jxElkWH<#rb#Q*-WL5J{;mtoyR>Rg2)!RL0tO zf}o@te^LTAZMCONbxHV?ZpSd=EzDPqd$@ATP%g%qijS)(d5#*o|dLj9} z)waQmZ9Xco;72 zm^@>ODGyh`1~ogmzKie*GIAqC8e{7M6yTM(`Tl@qvBu}lznd82P~AZrLoR+|P}|bm zI6+(ZkLt`F_RkDUlZ}7b>;1Rl{|N2mccUH{p_Vr`M#qw964nmZh-GK{nwVRT>|ZfL zvk3K9T3(wvRtd#kFLgmjA!Oz<$t&}eU8P~3bRliKZ4harWbx+iaHwW8x4l3q*B@@~ zwM~K_%HEdrgbo&4!%u$&KO-$(w1aK+(hQi)y-xJcwG^W~tuim%4@_)B=vNOo-w$Ic z+PAGsR&XW_j%EgaGS>TJs`}gOq26wETptA(HP0vd6IgNZtd79vrZU)SExf~d{Gbp} zmq2zCnn}9c>)zJd$JZ0ibB~8FRE}>vu4U_ z3LuaVhiXihi4ojIGuK#NjkM}K6lY~MRd83D{TxB&DAAfxK{DdPo|)BoWbQk}*N|U& zyg{#T9aQ}dk5@Fseov6Z#v4PxXBCn&o1JGn2ae*%GfNkIvwTP!W}a-p!+xBx{b@m@ zO#VP|7~kBh(GAIi)OpOU2D#edj^As+i)K%Bl_#N+o5SC{d(^;%@!7N=U46}McE*y@ z%j6O>9ZWc-XDQdm6rQcqsm_?*d^AZbo%vGg;v&fVZ^APf^=*^ZCbBJ26|vXjX&uk2 zV@Bud?O*}14d6&{^akU(Y8KC<5r0$xY)dQpyZCBuZm8iMH7ke>t)dW@tC%Z9FQtWH zmd=?)B2cPQ*um(==1J6|4lr|C!#rwz=;A}Pu>bgPTS<7+%XIvUwy_)LaP()WfRfPh zB?Z>1Uk}0P6yd@grG3?8oSc>5>8~D*+H1JitB67#Qey$@-6zWit^znEArD3&TW`2N zAD-MhN-2;CSo~`xApPgox-h4ya)q6Yf8Y%LzIx)O*4h35zIMu%^cc>)obryFSknXA zcIycclmGAK31_@Te?V^r59O6s&#MOOME?7mKlbO9i+0O$@>JEN`=k;@4yy&`JLm4;1k+elv;t)Mi#t;71_{c=jF{LbW1AdvB zo=154hr7!E37f*|(EWvYV+UfwX65(sI1g-I)p|}YU%N{npi8sC?R@6<@}yU|8?ddC z2PGODJcUai+V0ep{L(4tg++Z2evuMqS*D6;vjFEGUIkJt7#d!xGv-BZ&F~AV&>oSZ zq=u~o`D2du?!a0qqy;a`HWK9Yf$_!?_dsyrZEvm64q@p-&%FIex1cz4UP?gq5@1Ba zl~wiEgh8js>NWWcrUgGlvf-ig$Q^4GlU{3LkBavlsZy6_6=#=wXY)Ms z_@v%=UpC)E zJC^cdmUXL08|wLT@_Z$h5uim`bTp4iejb-K4W&7oMW?gfK5oG42RRY#2&_cks?y(5tY*44; zWF(4;xH^;0V!-SOgX@pgbS>4Kit^XLq?Nbmt+E!U92IfKVqdmVQN&C3FR+pZhMXqM z>l#fuo?=B&JEZb{p2%OmPSOv8s)6^s9=nm@t4ST^zr0lTa9(1cDwOfod+pei3dY{O zkip$u%i7;APf~GJ`=AM7QJ!*p(78$bkJza+=!&YKC%tH?eyjWE;0{Uri@+!n zZ|Zh04nOn!GyknBZ;p?nBMJ&m4j*8xIIr6Yaw`2vrw!FV$Ja*(90hJkh;g0#d7Gu4 zM?w1NW}ob^;U?0S)_4D$@WZ%UvPJ+i!?Cg#QpY~1s`>2I9jK#nn8>+b%-)r#6Eq!G z;1igSz~#6!P4IGKQim<lGg!;VkcJ zErT8nXhEc7Nj7`-GiDqnA(iL(2%5E(r`_!6NRAh$o;vp) zJDRiD=F1cwIo#U-S?AGNh6MADGrI0_J9iBYt$zrRw5nE29Nzv?V|0&*LGeVV!5C zk*+sSIv-Z8Su@UBKg)4to)fcgvy7nF_(>=6oI<$HXqX zeuQWCo4lacIA*a4yrI-PUh!y>?HkS`9$A!q6lG;TXw0gdM)i-`9pj&lUvZ<_rdB6P zBA?a%Jfu_xzQk*!o(lufVE!hEU7B#bw=ik221kh3m0;!+G-=Z1i)PO)n$yQl?VVM4 zzjlP$?i0?X7tX!UNrg%W+|3}VnHC}M@eyvB*vsQ24PR_4_Qeuwg6WUI{W8W>hA`cF?5?&#Vo6gT{AN{h*eXlLhXhB&VZhGI;-A`TvC z@rkSSyi^iuPMD0RuEi*Lysfhx4dn$>X|YhWBa{8z=t)nL{j7KEp6oDEbos?nQCiB? z4(2+Z?!&fH&=1ghX7~E@A(PV*qRI=s`w9lzSqE_Qq;86SIbHgabS1{Xhu1aq>*v-5 z>uAMdin|xP_-e!-sc52KN+13l+F(FIVNvytl5}v=o^dSKv24NmSU$r>N=tp>h zWVn6rO+ONsY(@<38KE)<^;Um3v8lgTL`u)#;38G`Sdi30c-k!!Hpj*Ky#uG04WI+h zR7i)c$44KTsYma7rpy;u^)!e=#nOD_EqBa0*ARj{Nl|K z&iKGFHGauD_03`A)g)jhBMaq8eP8IjzXRix+VYvRtBM1At02MilY+KO;CaPU#?=g_ zjG-Rjq$c^{#l0L~4OC5eXr9>1>FALI--heCLAFP!1KihCmxk`e%PS$u-F)}(L>BCf ztfxbDwfZRt^PM3FG~6+(D1-k~Y64#4x6I*y?v&NvT%NA|B1l8w0H^}dZ-wO=cD>`u z`_Q8C2&kdQ!zoRI=sQD7+rnqcI-R!eXlb{5)(RKcyC#nP*j*3%Hq6o-_(g$-RqT)H zwg_(ztjQJzf7->~+3^$Qqb2I#r9eV%S>?3c4KDCYdyq!0eIUN%6f;J47Vk2w=PYR# z<~Wo%tijuXe{8q?td7K>k_wgjc$xx-M z$_7!ZlHto+rf6tdL%N3GcY#^jtz=lSTCWQ3sIF_V6zE<7kuI)&u}#ZC8}l$5??^Tt z$ga)Km+iPyWqO5t@z}GHF)aHl1pbJyVO#XwaD6&M?0u(9s-G$lHK-n+JDsS%zT$BI z##l^qe=JaI{*2*4;dYMhi0OEc4<>Y| zt^|nF=g50=Y{ElZ#wf>gKRW%~IiY*Ec}LZuUuuoj=1W7Wi^-vSsqKw1>UZ7Qy%)3y zSUcXvR)vF~;cQWE&C|C9@oL83ww#trC=B8cGEo7hO*8f{Ha*ba_pt5QT1AXXk>$nU z$auAO$VML0OR~17G`>aL1vrv1vEs+M&NSgmXSM9_YHi&+ zHC&%0gT5?%TK)H<{Ljw>wmbaLFs7sYlG~f!SPx6F>5`K5 znX$F{2d+k+dv!PV|I`+@nvZbvDla`9+ggr!Je+*lrn_)Wud`KCMBF8ZDQ$|kyn^mV z9E(tRKLbA(d)U=0`R26wex72*oiW0wI}1wly=8F0DL>vOmCRLnX`r(HXmH=Z>b}yAt~Y0<2C?OQ`C;F+OnuFEF^$C%3wd z=5^q2+0{2rVERIV#zaaS|R^FG^y|ZFjtBa*6h=V2&Kka zacQfGkI%kb%bzEU;I^--f9;IFrT)ZPv9Q)$E5&V9n`B!Zfm$46g=Lt$mM~h$6HCjktTnmk(Sid|{I>x)c+4p0xs%CxIm2x$J)}H_B_ge|X(z z?L9MKp)mEMbm}dS{foZljs?AQ4*ioyl`pONmZ*lwZ3j$GcJfM-0073fA2<@GL?hq6z}ySV zvAgA;xo7-jf=ArLE23KBa&_^`j<>g1lVk9uSJ1u3XEj9X+MZ5yn7I1pb(EG|a`uza z4xu=O*O_}WS}@Sp(q4J*TCY4r&=lg0hcz8GoJ~JmGrPHy=J~EYYIB&X^fEd1{cF|j z5?BGuByQ*HVa!^4OfVxX`0ncg;w%~a=kl$u)6NxWOZ|Nz~6vKL}yYP$bXqfe2Rm};!W27^a-rf0RALIytG?E zB%1kAC#jJxQo90Hvb|~$xMY_vHw~!o<$nTSa-)o3<7NN1RAs-oA3^9soY)?dLKZ9_jxhniF*vw-8WF1tEEM+D=ceRrtF4qaXVy2AXJlJQd>X)SU#7uWV-+B{t%*T3Zeoj4)hss_Ua?*9x8+$biU%L9+!e zt8wBA%X4xCc!8i327}RRR*R0@tdqsHP^~X)Xq}XFanw@lwn_mpN6tad$ zJ}gK53{fOTA4u88CM&rczv}Ws!t;oYf$0p6FBW}UCV$X#mB5SsH^<>2ZweF02sS6P zdGkMW>T<=ipsAfDG`D8$^i^9JvIfS)b9M4q%omrl16hk?l+SCF$g`Ky&svEjl=?#T zpTj!GEGo0_-x+1J8VU}c)LEuzk^cNz8mQu`sXHFB4N|7ZUgfwsQ&oua)LguL`WJ;A>wNek9-Y6=&rf-wm#K%rpvN<&taM5 z%B=Rpg=x!8w-6^>CXCK~wVk*S&CdRr%C+LM{@`96-YJwW!{T5xKoeMu5YRPoRX_S4 z;%cvgmBeoJbog~Th=Iv-MC6)G3RzA*A*dh;H41;TEffYs{In^Ygxr-Gnm_U7<=U<9 z`x;IKXO+>BSzG1fjZ60Ys%{-JWejR_wlPb!_S|vlNp2?wj3bDOo1?y@_MesZ)Gt_@ z7$d>BfQUp;M8OzIe?V(HFpb9%5Q-cVxWP%n5W*{HeNrb41x_;bhIOhI>b_PASQDjh5Aw*3~& z9~_yHTf89MF!b zcujbHCf)y)AT{gbcVry5vxCX->0Q-?>Y6_9r|t`@F9XBhF%3Vx$xX>h=^i7{z{Vkj zkThQ7BobL^tp9X098#;%mvzcQe@g!Ks!>#mQhVF4n~!JBa$7!Tpu-o zHGs;IA$$>bFXyU|XIMnzrk}4P{$45D9rZAvV9gYKdIp!kkmE#onyl!h=GbGW=O;Nt z;;fSU+K{)iXx4*k2)+7-q#*-m_od}q{;j+bORepXgwEwSvJ8fj`|mkcx;c!jLv$V= zzYr%57oDQ9i>Zqc@u#vXIq&e2vFr7sqbm8KSv2eJx()6}ohBloa5f=BO~C~#DY8+n zCvgQY_3_z{Y#M$S#2o?0xS<2wWPJ-#eyOxIm|N|QV`EFmi8-EKQ;$`z?d|=?yIfa^ zG+$hJi|R2gaPnH#P~FZBJ7^y(zskyP?yy;zv<=1czvQS(TR2e1R->fryfMOy9~GR^ z**VSNc`EI+4V+4Q{(#s9POLomQW&Y-BYHomJTs42ixMApQVnqBYkZ1@sqR*6+)E{$fiA^Pa?u(i{v9I}ZmG^UMKYF}o zl-?2EJmrbCVU2!hHBS}jPQ&V)Ic5cYwJU1)kuT_J(UcOeZ%~BgzU9YbW}QEltYwfl z+X^eo@;4$!wUUHPs7*~$6mI;F#tHy9y$NZob&e;59R%1ZM4g(thJaoPe25Zl|0xNq z+~oBTEtI@<5|<2Pt_hY!&gOPr2{OIB4nj%de!3`7=`c1Mp}$j$pM% zNm@)qAtm}gI{YE(;dNHUyT83IS@nG%HhJ7leYAWdV5yGpd0M!+XiLkRAxZKl@CWIR zE=zYY=XQ0JgAoHdlszFF?yeunXG2uC<>OFqYE*8Kc*pQIirt+&f9k+JY<9K;QD|we zM(juoN?3SpU7BZb1lsI&9ankj`I;~B?D}SwT?8gwTU>KmJo+-2*Yrz=MT`j9`1M6; z9Q(`iWh4FZ7V2OS0%t!(@w&9P7sp|%Hqf}zww3T}D_(pA{=#il*me!s=f2^T_KA^V zlm`~|6qacE_tWj;k9BsOLw+>AzXl+)t4-{&Ux`^hb2G=Pv|UF1ND~LtQBpYhJB`d# zTIADMCW%}j+UNW4bW=34;DNT0!9A9l4NQA71q^m+uBR`YB}=WytAZiMo*f2PuPis_ zE=wPZRozor;F6A1mc`2l4+bB8OIec@ed9&{>$)xGy7b*z?qd_n$|MEAEebMfXvqO% z9jnITnLTGpf_mncgyyev=r{kbC=TeQ@5S!s!!cq*3*Mh;<$J{EwQhbk9V$Lda8{{$ zbLpQHsdz6d+QqW{{R7J{0wru7Z_LQu45{OPo#SJP5r!~BxBLGOQEwR*WgD%3KO!h8UDAy-qI5_%2rAu3H%JUAAzjir zNQ30iF+&a=Lk?Zi-6i$j?)TXHKOZ?}zHqN=T`PX)dE)?Pb3 z8~%QSvoWtPQKB?M{9}C@PWpt^z#0Ts5CRf3AcDHj`D@^#=$%J2wI5Ei7Mk3I9%PdO z8KdGN&@3zX=uB#b-NXr^h0GabF4)?!TjAb!$v+^ujqB7R{~2Qjtx_cTg+vZ_xFI+w zy?xrQ5N=ANzp)Xzm66zJThw^jQj=LW2jkQ^(VU#yjhcSlk$tOjlO*lQo4wgN3uO+D z&KiBt)xE2?Q|-o+{31>M6U3dIiI$s3>F-3Yy%rW1?Whc!aT|Kq$slvyf>x5KTe$OE zXoiHN2=#cR%pa~9x|0XJTWA4kkpgL`J~jJ;wn|s!`yOw5y4b=BZhDHY*A)A&cPmtd zrk-`4S}U>`gfdbs+j-@jJ!W4}gzpQrX70SVIq=dyXfE8)uz{WkY&plaD6VHK8@pKY zxe4B$jP!^I1t@2bZW`ws7?#5{_|HXTUW3g82q-LtZG`ZiU8`*}Ml_l-T%3u^mv6ge zye;EB*wzdU;*&^!enPapY{J)JzsWmjm?R^#reCgU?BjKz&0~pyh7e)1A}d~+oivWn z@ee+GDp2~&^#4NT5tD?!jSTe}{VrsX%N&gznstiGz*tN%|6V!adRob&ekP<1FD(q0 zrmcL-?N`L{TCcm6;zJ0DG`c0>N>tn<>~~?I#TS8mbVg#p_w3}1*4I=^N6kS{IIrBb z0EPhW6g=r^@Ehhph2MvLfAoaL#Y6*c(}7qyI2BC#8xSA+u{AD|wVm>G>ais=B3Sl| zNu>w@=CHGXVnau^oC&k22Pe4ErGvb?G5v<(BbZ=cL))YC%i*$8m2yoWdRyUrA_P%C z=2IdOxK7k&XJ2wCaGBmIC2DF+2vYyb$rCoVtx*O!MjOUaXBCKloC&4I>9xI6JpVgs zu@M@U$?fnhZLWY)K(x!gWzF=qqRmUee}cIWHL-8F|4{64Vi7;F5_{Axhj4KjC~-?+ zW!u=>D$sPFE_dT(AG1G&%?~rTfgDVy2s2BSTKxSgGZ@okq-KGSJgA665Ft>B6N7Ta z^W?;V#Cjj2LyjaK=ea;Sel?U1Oc&x5brNu(l6ppnSQSLEd?ST_>88Z*5{ikaE`E?0-N4ZI0?CrX zIE-4u%5qI-oXr|t+`vX7`r4uD=xV2GF~&UN^TA1_Tl36xo1?iG@Ao};{Wew=X#Y%+yGTCMRti6y#blix^)hc`HX5k`nqi) zGFdy}zyBQS6lEko&!TxsK~wZ~)TFvBa)j(>t2e292y(emEk{G9ps@m{3(pYc>x}y2 z^+X}@)H#jdUB@{M_|Za)SaK{okOW6Pfnn(WCDFSpHY|GCVsL;?d>T-#b5St-hd6gY_ZIHDVzCmmYa#f>qz5Kqq)pg8iI9T ze_$5axO}LA5mNZ53wbOhsoi7ZA=MbWXeEYqvU{idaA=~hd}4QNS=JvMNM!n+lg=TH zTrRfQ%g-D48;}a&%2Py=#>(KG<3CWQAV!Z`pz_R3cm~hPY5k26`VYif#dV7AX zo{s{mOlEv*^~H*y@zl2`&b^ksikg|t)O2?*AZyT}WJ0t`LtXouZ>Y9=$6G6`DVU>c z>b;b?b+{UiO#C&U<~(U|%{iHhoUGp0%X=wb0<-wMkwPdzW^D&-6%v+Bu`Jwe<;Q)T7r& z!V%16!w`;+C0(^h5uKgsUwP}Ko)cvVXok028k473N2hd6Uc8^%=4Bn9TB3@ZELMIM zdCB@1lC>%N_{Ze#N=TKo0WtJYobU1T_8&0hLumDWX}>Jkb=ztrKFH3i#9RETj3Z5OvSMO6sf)F zSU^{y9VAdYp>~m&_{b%|kA~eN_~%0Ctc=1utOrv>QP#dvcI@l93~OE@iv|7*{tA;A zPG5G{IN7=Acv$LeOW`iXpqH#YIh3@?;xo$3*=XyJ*^~9Gij2CYy-v+s@)~lBB3CPj zDQtaMo0>Z2#WkUPezoGg?5-ll7H-9VE z$aKHZuX8qZr!V8AD79O)xo5rM_w4wPXHHGtkVUfqA@Mwi>o1Lt_zH|X>^W?+wT67s zonXjcM0gDw#JIk(VWoD^#c?TeRpr<(>h?;BqLZ{vRp(cwKoepHc}5*kO&7UnoT|84 z3nR4%6jJx{>kUNietZD^L>?QN3;e3q=%X^X*=aClLh75eX_q&CN$8pXJl4RN_hAwJ zzghJqcCJ4=@AvH_#2g8h_rK_{iH7;fg+#rqf5lf1?GC1$O<^UUP?ybrfey$W(w~j% zMh0yNPg>kGVVx&3k2tneKUcRLwK(FW?6Xu)I{>LR##tT!h+w@Cfl*xG4R`I;>J)(|nJgE36|yGG8yDXoC`t)d zPU!Y@jC}GxoqJtasat@#z)Twl@d`DaeA^tYBL@B|X9If6N^UI^ z&&e+gw*+V8PIAkf%6HiT=bVuK5K3l1dM-t)gj&gert_=u$?bJI zM~7T~9X0ucd2DI)^=ik-;AwF}nw(&}0SftvWk5k+`XI^bNwUXIH|ATMdDG?2$w8bw zXMfT%F9>X@OA&(5r9Ho8Z+ie5H~%l+=3)J71V$=atH-w&p5MN0ikB#UMb=SetK!Cr zsCyVXgn{FarbFI5s_h_*qb^64*g})JWEf0FJ~_VGm4V0skB-_zeX~2k$b=b(I<1{h z#7Ur~Qj|q#Z}o-l*i~iBbokBRl7wwXPWKN@4#X6D!bGo@Y6gGWomPt}HYaf0-)~c3 zkMk0FCTvh_Jyhxhx)ba2Fw<>*q|U@nn8y~Hyj6j0o1{7xt**%YcbltoDz4fd|KfPl z5WbbVkzGPG3!j@dPY%!$XWA8Q*zc)cS_?MoqjN}YXrrdq4aQ+?4TpIyp0<|6-<^|e z_9nO*4*$PLM;na0k1vU*!r#02#el<;G<)bY;D^*&UY!;1^^cA$#CGGm0WsS0eS!bBO$yYmyQq3&&l_OoYt{32dnU(t>%wsL$5 zuG0*s`tSSYvVUscX8-@|^M6mxd<;c$v#i(qx|e`15~@Gx*E7QL>aT*yveLP?ApKLI z3^$KJ{44^thO7uLKsjIUsdb7{sg7A??3s; zmX$5ht;J4YLs+~ZGKn<|G)K3ImfO@Tdr6CNa*j*u=>|697F?2E3kC<|^@_6mso{?D z?9zzcM^*w;!Y(T6B6URv6h7*e_M~2f-&A1rV%Dkh&U)Py!$Hk%B;Hh^QIq0_*YKnF za7RrAHZ509scW2?YO1-Jk&D(yY{tpO$?ZNSjDK>cr@)xHT!oJjJv7HxSdlH+*&PRrSA04q_wTv0V5V z+s~hzpm)NCeH=&-J)Kf{{5;0|jF?IPt27Fe4}hba4^sn!+#Y|=HK7FHkgO#tR2mm3wQ9Itzsr`c zi5I#n)C0_5XqF`06vXo&7b+G6yk50$)rCOa z(HV08OIP3XZEh9c+E0`(dIEx+H~?>G92pRUY`$sEDZjr2pL8W}eR}K-&8&*Jq*h@F z@BElV&v@dWuuRc_kB+uQ!=CeVelNO1)bKTT=(v+tL=G!I>bKX=Q138KAG9H$W)ypZ zv6bPfFr~k42{f8DrKt_@6#PMlWKX3t@jrT^V2rsDw>P$;X93{Gm6VO;alANvXdg?| z2v#QOkxsB!Y$0=R_IKB+stxJ zvhd`X1M+%@8DhJA^IYbOXBOAs26=n%#dvGm|Nh_m^035T=?*(3_2~$`BR&A7S$)y@ z?g0$GuoX>xjM0ZP5SD;A!13{Ugpr9O?fKQ>h8B>OfM%11(5KgsAzsFWIqUenCaCYy zgd{CyUiix`G-4*zZ$wttq?eDjvIEKFV7;`g5NOzUs!Cb90T05$3!OM)+0qW!6mi4pMfJC*DO8 zr%gmE7D!UI=-;~QuJX>&0G@#o3G-rUr{z87tv}tUuWFkl)XI6n$9CsQ!e$Z8ycpBKg*H9m;?GN0G6p3STdEH)-hh|9_EJP91@6VExUc5NUI|pW z7GO&iFigep7tnnmf;n_t=3TD?*o*s8WUD-aAm}J{X2_BgiLojWlFGaN;7{i*`>p|wd>aSgc&leVjvwt7X==7;lw)~-R zc5EelS|sw2<}u|Rx6JN-vU!2;(hb#0#eS5S`FmBNi>IGvhqC3%dbhr(PKswib>wD8 zPViU+n!U1`$sxMJIrQ}qM`iPo6*zfO!2 z{uR_zxsa+9V01p2VATiFyltKTzgwGuUyGFpb01amWYbst>61CybXjuTYxY0%NT|~R zDCc0OS2>S+d}?uV(J>+WlP~N{*->tKOdjQ2ZZMPKj7Gt?q_FTu_QmwrHc)vtRzl2| zIPGXp6+_5@qk}!uer$ChROTq0imt4lPIRrL#8K!M00Pq2#$y*leuCAG2@!zs74zD% zWTKm4WIcpjlB5YaCHy#%GkL>#&NLJ1`g!41!Z#E*bdqp9xmu${{DH(W)tjC&Scv^&Yv*y-P$N)=#p)B!X@pGwgzrR?nHV@Heb z`oN?Ibz9o(q3Jk33cn;s-khA$i%*&j( z@%)rp4pmIYUPAjfDa!et((-l!W}$)d9pYo`XIGiwmB@DW1obr1+#Z`&-h`#OAs4(W zm@=wch~r1gfi?Q9oMLC~mZtJ`8HSe)T%`v(>Vz;Fd^n@((`B|T~=vZCZ z8X@ZI>Z0BGN(obD-a37AVC}x1Zm2DE)!FD|<~d)Mr!cfK!>oEGF}iSa-1t)RBo%u3 z^(i)jOnSP+Ccr*n?xS{~uA+hlbY$;UHUi0mL2t==$~e?z9`-12&&*Bo(A;Mr>eX7VHfrv{3hmaZmb72&YHJJ3V-Att*++*}_Heix%+n8M^>co%7 z?putXb{Y-?q&AC%;A__uZAWMxFPv1wSwZM9(wMA&T~NSPcpPR= z^y^`9A80s0TYVL&VPSnAq%`pS<>wAv!>BxFCtz)_i&QiDdq!UF9ap(l0jq4Ccc#&x zA-I0@mVIw?k;`(7MJ~UuhC}nau3p!Bc|24ON+v1b@(p1@LPsNjFvBNlW4%JFZLc<9 z6(RNG;%Tg_EC0R8SGiP^7f#AU$&)m*321o zIMAzyTlTf43zFHR)M^#8{6@|6H&|f*IfCj*Bx|~5Csj-U3X0PgXIwh{l-v_T<^m?9 zyN=(ZIVn_NNIjYI6K6@Lr6kQ2yN9=p$t^Q@jn=*~sW}tee6H#94t4%H_k^I| z&e~7nn=LvH(eoA&uJw2q1SNlEoS@Mh-IWpJdyeskO`r|*fygHOfaS<>-KBs|={E8`hOp|waIF27lJ@qNW?j7=F)Ra{qUk7J$r zq^81_Y{;JNA>5!ZBR> z^CxKacU|argQs9x^gme`P&^<5GQ+mqvm&_b7AqXa_qO;x{JZKJuB}w=#lclj#6_?w z2?kyXER&3VtmQJ|55;`1Z`Kq=c(Fd2Pi3{8G|`Hjy*r-^Pa3MF_=(m>isOjtR)Q6z z@F}F<%q?LFNu2ApX3PATg$hg_+GDLC?^dOChUQ1$G@qRI$iBLu)76dt;@oi}QJGcw z9u6s=uX9=Uu3E84aMJQJoQ!4L+FcvJ-bpE{@Mlm~gK{)(7Yqfr<4Jj+Cx(^Ju5T`c zNu6&D5Y&M=7+6M!AAUO+9qq=BF5aWB336@!(~nXGC?17y#wENjbj|L%1MJ$izXpCh z;G)+yPkV*i>lEqd&zRm#fwMOvb!@$voM5Daz90M1r%lHt{;GUx%SIAxe`fY9>xTt& zW8&hA0(^nAhpV?Xf&(jaAehe=Lsd1Cu|CenNHjI_#$z4|%b`C0i0_hTgcmTaYhr%`{K z9j|Z6H1SeKbJY)(j%W?0_l^my-x?{FvZe?*kIUrVP>2poDvntC)%(Jbv^89;tnD3@ z+nR>icAt_-n~3beIF_m2-zWx*St#1uiqYHqMt{i5zrPk^_seI56HE ze$?P*zZsv%Sq=JA`fJ(1+q{3_rv{^T3Y6Kw3e<(v&3pfME1QHb+m&~8VEOic!q3bG zbvhhKh&Mf;zJm)Y9#(G~2?~uIR(hH9@lQ@ffCv%^r`fJ@g8_T4PDY-WwhIDNy-#wp zhOso(4*{GWR^N)EP;H0wnA>i!fVd-k%oGw+B)92!5tFDg?DCr*Qr(s^wNlBrx)3P4 zK0Vzl!MgJ8u(BhrPR;8_GFIpD+PK2IdbQLPjpEG(=8;#G2Tj5Ng_Ydw(`TF|;dNkR zIjI^OUnsDcZzdDh@*nPAL;Ij6As3d%-VV0o;m)AncT(37XUbIcVfE-%p6ds9kK@x` zHtn2`lxCybOIauIdJfUrSt0!cGmq{CUbHl;nJ-{A{sLD{_ddIr_yR9g$VV!l5hiA9 zWmFe1u4r*iGB9r`W#^aR((>y6UDY3kc6}T!mMT#C($$(Y>P9hSIUXG=E}R%MAzG3I zj!$=#xWO-NZ<#BmI`90`zsS`>*V!0}(pI}3zONg6#tK+nrZC$RHjYk{HDFL^k*mj4 zhV|uP^v+8rI1Yy%sH3>O@nWthzK5LgmpXh(9-2u`ED+CB1iT&!83|Cvd3qIYFXd&7 z|7KfxEwYjrgq5^px{~XX$^2e7?2q|#;;B1pE{F-0Sfa&}kT)XS|1#1@w!@85tIY2S znfvwS?~G?Tkhk>e8XC5W)w2I;hVZCZ??a9*H-F7IZ5MGTEH(e52gLv`gO7nEdFV7> zDwk(yHh$Reg*n1x%RqCc?H_ITBN^R~kq1{+=8KPUEGt7K_x@!_&~K6&7OWGLp`+Du zBjLN?j%b`jhC#MI?WtwyMU}k}XUQ7m)nnbf?-v=vyzta3F*E7;X+nmxt9zZsdj(sX z2l)sAWhQLqK;02O7P(o2*z@`$j__(8uuz(Ws_!W&F#nbt-V`tIGgEjIYONa}a4M}* z+q7hi;3|}`-fmC3xki_P?pC><>4c048=mQdr=VK}y0`1|fGbF&=a%>$Kx%2X@toGI zc0m9(CniHf$LZYMqLd)Fc|yvh!a4!aBjnI2$Fj;gnAxMWL{&1PUG=VICv4LyP7@>+vi4czRl|j^~LolouHkT6AoK=+vJNIwD z>Eiw7bIi70!GVWTITeh=pPhVXE^r{o916Vlm%(gfk(SK+i7s{%!0eI#;WZsQGzZ;n zd8PZ%2m2LIZ@B%p;tT3HMIAMhSI38P)dXpTZFx|cL7&~CwE@i>rzbr$%YI6D>%iay z#jHw5Z84)>b;U*b=C$Z{_^8g_cL1eHn++!`oM9k3X4{07ri)+*JVbM3y)yN#%r~wL)&hKN(W-E`gvGvrpEh=pB-6~ zf9q}iapjWmd#Hcf-<+%swUArjbSc{DN_@GC9nUfD<4ZAaoniVAfS2ah3+!5KWk4Ff zIjZDl-KWzOLw0^~V{ZYcT6a-@wTT~`dhG6U;9LY7OH=w+bny^$)mZ246dGB$ zn1`J9&OMl%S+x>~jiFXGy+Q&Kyv6v1i#Ys zSn0<`KaZof2)|IBB?^ zZ&Qlf%b;5ZCi9su^~@I(;GpZPl4;85^CPF|>k?y~n82SXBE-VZj1$`84D`GC0#0qq z+*QJ8ahLb!l9ZQifZy0Ofzi$bB8k-#kYPAXzKOI_1@%HW-Ts2w_7XF~Mhb{)HoC6j zsLR1Ef0+;J^UOA#X+5vHIyD>Lq44>An4MmN#}>=Y9zv{xUF6byd&@bCDG^c)wEUHs(JA>iZvlYE8h2r>Wd8n5Bz@T(kU{aT9nkgB@(2Bvh5>>16ithELd#~! z3HwT&!aeCVCuDH0Q9mQdMhk0T`ZkTnz1qPdMVL%DEu*hsyBx+aH*4$HHIL9F3H5S? zLvil8g0=SyxN)69rJsr&k1)Gs+oHp2ePB@z4Re+yMt5p5f3=y_!HekJ0>It+3}9!) znRUsuWYXDHRsy-Mcv!>@V_#roE0=Q(w8VwJejOU(X1ko!|GNF?%WTZ^gNTrId4w| z#%YPfSz*$?7HEmi;z2+hCs(oSnkZ_j5zzKNF2W_+vzkpMA{sG5#Hl-r@TqF`ZCjR- z`*zy7XL1@T7D(1~f9VFF?8|UP5MSwo;>2f^NaX-8l$fH}hl1uyr(0x$P^ZPAVY5I~ zUJbEj#jm?lq@h@D;fi%;Vyovw`D(e#_Ga4(VD0m)Y8&TOl7&1yzxvEAiY<6)X&P|; zwj*(_L8UmNi)sYVLg#PX&JGE=5DzT?-J~yXb(BpF2w_rvCKxgh17dQ$M|-M z819`5Nk7K)Lf%i{XQT;Xo7U?!KcGQL>mn}=EHnfh9;7Eiy*SsX_*KjMh%7?PY2jV# zdDknuq^NOUdb}GRzZ{^nwW1qgVMN4|jkY%G`*WI9?4FzTV3EM=93}+|u%W5=C?^Wa zdNlKy9yj$bdX>(O`0ljVGMVTQjkr%2ifvvgL+qB;T)TQJwMxlZ=L1kvb9J{q*A9#)z~#Q+TyzRi1?gGNzApx{?&H=8!#4_6=kZA7H!{gpSJlGH_Pc4+d{Wv0i0?G3Hj z-c)IvEIPb;35M+XS8$U5jKy0}sry$QU^8Dngc1!7PgK44NjrsQem4H~S_S2t&2}Xb zpI_dk4H+!kFW6;iX1#1@M_whHq#qH3mnNaL@GAMt%5^$qtvign_;$toPu!%o`@IXl zwvdJw227h}*ZuCz6a1k};&}Qkn6JTaMWfB>KrS8^*svSzeHKXMP`P>l=DRz}Dmh%u zoNDpOECxSCTF74e_=gDIMT{*nOn7WA+vH^Wl>4cTy z&Z5A-d{nTabc?Ht7xNvj37s=b(z`(C*|RwJ5WK&zZwG{pjLnYCP%XMo z8-tebx8p*M1XWaAFUZ#bMzy4l=p8+qU>=kMoQb=1A;a<#N|a}}lDb~8c>%Ly#G5gy z3#VL?a8hl-Hly+;L*djTc32+~cpU0>3VnlSXJr4vEb+tcrPtR_mT!Pd>F=G7=7Ohj zQim@fwkaD5d^Ghl? z?=k9+Q^;lElYL2Elb^dy2-39QW6Uck-jtPlW(PyJ{2~>Z@4MvChfg%JGj;ksNeyge z)y^MsA$e8$mO{&{SlqrGsOHkA61E2~@OxRWx!n|-g>&fL=ie28axDHhnRx%(g8QL~ zhT{kJWm6nXh;w^#ZxfPAo45Wskdop&sFu1212(cV$?Bc}(V&*`Pj~Kr1RM?5m-XPp z1-GukPLFjiY~kLlY#Ncr#rqN@Z~|g(_5G)2-OkK=jpK6!fqLHVA8Q{c#_qWq66MOG znn5)@Df(2(56uI$t$dPm_xUsCZGsqB5;(dyv5jkV-vn_Ih2UkpB4y`iCJr*74_c=4 zI88?DdxR_Zc)zdUz>2cEml<2qIG}HBrOi&sZMuf)sE_6+w}#`459qdwdTMFJ67AFV znDJw6CBnURvr9J8JIwfQr{)C;vC*JbaIWt&^!9Rr&=lf@WppZCbkbynNBdVDcY@e_ z*+jF$ThB$Os8i;lx9jmxa=Q=%3zZX217(_zR(om}w5Ww2XK@C|y4a5;A=Ew+gcQx+ zdHXzBj-S>9SFLW21vm6UECyL5L~m#9gFdb!MP5Njxm}k{#ThmQmO%fO@_xvd6R~re zfY%I2&0pW1Y~S0Nlu3SkO&5@r38lHtS-Y0Z?w}O9;andtn{DGXaw!g+Vm!vi*gm{9yae z*NC2as1rMlz$WQNz84IVD@T|QoEYefnF&z@@9F+uV^ueD{#MX*_}Ja$+uCXY2PRiA zFOAuz(UJ58X<)PBg2Vd}9}gKoTR`p51uAv4d>?dDZ#;Hi#d|8oVm$fIx8t)OoV;k} z(B{uI-3)$$$k4|dXmEK6$SA(!l?;zmNVAc$1{rXhDD{rISB_|WjrCHn@4w)X4Br`G ze(a6XELqVwVUML?}3nKz%t^(6fy{r)GbzmN3Eb4ojK7mH=T?GrJAfsWM3WGqphJ@#sWULkRelS9bvWkTT> zVcqFNV02^OzdFk*^Qv;40%QlF(;#06m#C~x44J*jI%_o>Qkl%5rdIq;3re`4jXW|v=Q-!Z=$8OrD3Gx4l2HmEBvkXJJD z!-ZEWpIj&KV1MK%794>#BQP4Ur&>C~?2(1slpWR6y&IWX+K<{W{pZs%jwLG@Rr_t} z$EK~X{N$*OqvIA6`%tO|X*h0{M#RjwMcGs8yQF}GWm9{z+?^%WG&6w}R(c*|wbueU zNjfU$iwrpk24stcEWT-|n|B-quUSazr?;FL=&*7SwbLJ;Gdl1hDe5!xyw`uDI8EOphJ zVAPwoC^GA`<(Ws|<_ltd4puZV>Sa7G<8_p)|8Txo!dlZ}(PNsAYI##LFC_ch$vOfJ z>R!(|w<;;~qOn|oWShq$Q2Q?#O|HkNYCQLP;_Z)~SiyA(l6MG)O4enWJqv%tR?pg9s!2V zt76gD(lU)4^SMd6P1zK7br@ledowIZJTezqv{-GII{PhZBw+4c8q{0OpMse7a#E}AJ-ZUJ@eIJ=zlHK#GIT`@}+ zJyivb0+(}x2-2!IvPm7->-H!4oSyO4K=b0&cCNzeuTk7_xet}4IzZtgMU!y31M%=IBi5RfK z{UEruofKw8`*zYYXHqoo{lKWPY52DI3{|`xDsCTTXT|zabRmn4-gKR`8NT=66j|VuQC*M z!;xcq!hm3T^;;zpn=;P+vC5YjH<KiJYDlG4!}^%4fe;>&sf*iy(cGC~#l9Y=`s_ zdZjb<^*NHtjFj09KlCr3?mbk!xlD&n=(pbum6;vu#B>>_k}T}whFidP(LhF0XP019 zudajhx6auvA1*3l>F?0fgsG6=~~Fc6K116bJ4`(g%Om z{Kp=(^zoChmB#fFqtRz}R4wgz3Dsu3U0E~OZxHfrYKqa&ad;Mr92VS0|$m59#{J9Qtk-+|BwQH*MQ>ykOf;cMKi4b!E=AUZlVKd$i+cc7J zn19#x{-5GC={fi}JH^zsw|sFE>GErABEXc6DE;T z_!wCmj2HL$q@QOnEe9zF5TtdY1EPYJI6y&g>gBI@+e#-LTE1aPMkw9n6q$*Emzr27 zusr0e)3>|*p=zlEjtiqq>AVqPvbb?S&{NyL!eUx~JK%S3i-+nf(?hOo!hSJXND^7IWdBY?!mKY8L92ia*D(?Ky_6M@qGUPg`(VUwh+x>kF z(v#4VMNMxX!iyk4x7PghXZfYrpXP+6z4s1g<52$3B39{NQLgXwu4W1wcsJL65{Yed z#Rl3 zKn`HW7Z9w&UFk~w^F9`|H)JL)RYPV_1~`h5V}@0+#U)?R6_y3M^b4R;$l){W^S15G zgeA2``L#{!;Z88v{>fIpo9eballir|gzu88rIB@Aklf=J!S4ps&AbZpCQj+wyk`qB}DYDj|ndsXG zn7N9I;rBdmL_VC4onMm+lfTAJ)0LXY`JO4UH18p9z$X>6_ z<{8I*qnAMM*md+jYdfWWc0JX%Rn3|;CZk>V487jp4*^1I_-HUl|0DScO4u(EB~W}( zd&@^wQLFMI zy!jIqMwiGI_((SDHB;C3<_QoXcRMa8-*|U=lQPdK!kSV*-y~RYJgNii5rqPTg)Sr*L&P9yj%+-+qtD{vX7plwmRe!RX5*X-IoGWcl4Z zGL!1h;yGRDuCI{VB)IJ15*oq%J!O4ek7cwe(oVVpg-_mjwQUeBj5LWY@C+{n_dV8LYC#MQ(*LVKz@q=f zNeOvYD|F83>jqret=$%w$m9WtPJ2eT`LzT=;>p1I;4mV<)M8L_DI;yz2#SEQ5e2z_Cwxbfw zdE~+>USAO#Ac_a>o|5zkWUctJlvbm53j(GP~xU$qrorWf1z zm)kTVbM)!xN%o;Jj}AUE)l8`dHS!aIocFujX6hYf3NKJRvgi_%=xu^8{}`zPV0ZwA6k zH4WX)r278xaTI`cloDPaKQ(&Nn7A5VF5Sd=dLn-iD0O_x+=?GdNc&!zLXqGVc#pBe zeK`2yd4x1dAoRm38cO&px+GLDn`bJ@|CzVApi$B?UcApW&Tqdv&N92W(SFt5y?SL_;~;E9d4J^3U?J?4 zlR@fkr8_J~D<{CI0@_!#K@XPnD{5cq6;qp3y@EVAz!GMBLr1pFS~vq7sh*A=ZnxmQ zCa@ZZxPy8hE2pw^klAvS_o0}W;ZoJNhZ@eDFCI(g*hqtx-Zo^4>&|Ia25e!=&FabH z{FYBbg-nG0mstEhsi$zjR$nF}`_`*#b3we~UP;eB^;BZ$rK!R4^T*$HgqitjWR+oq ziZhEoyi3&Ed4(|8%+@(4DOn-Y%*^25MXq4lpikbTno)zDG#6+t?I)%8CEodI0iacN})RYUs1pzjbQ3oop)_$Yyk}vM1 zoyF~nMLx=weK#edd*QF4#cso+f(>U4%(L6O!D1+7h>L^6H$cYuzWZ)*TRfElyAgE0 z6}-RlXa{$04xsB0JY%{%X>M*teYGCyKlDXSjio?kC&e+PU3}#TH!J=m@6o(v`tjOG z#0BgqGC6&vq}CGmdzeL=U~6c>c4WAoq^P;?o2U5D7o&h0&oNS&=N{(}IceG1 zFn!%Q;;K9I4oZyH$C8<#-$!o`Dnc8tC1(CwJMCVL`YS{&D>i!e-l|A^HypU4RtRr% zL0j-^s_}2WVW^rrI!)>e2@%>NoZYOK82Ibg%C(S_lx$2}Gw~f1@Ei5}E`h`By+Nzh zu@d){!Y1q;Y3o;-O{pBu8R079<`1i4m&Y{w5;@?PT5ggE*W@T#f?wM(?&Hf{@qlK> znId8yT($)c!_aN;$}ZJvFw7&m=uBAUzibO7GKm2PI?Y=Hk^1s`#^GG|X7HlEuJ&}W zlEVGczmJ{STxot!R4an#@;0Tgpp>EKZ!lX_t^W0+@?Du9>1F7x4FHjs0nb$6sxCwUHsOl>qp(966r{A6iyN6q)JHw zvc#BU6333O;vkhY$s;d7Z$I8<RgdR1fcs%Q| zjg3&QsTszwA!re+N7fHbe85KPje2^w-tkI}qurBgjdMPG zhm4bo%E$H#Yo}*odqx)DShHM?^z{&|-Rm3v=T79O*X8n8ldf`T!#1kfSS@ltcGq7M z^BC*Q%W4N7W(k+p#Wzz#vaC`QS3ln$c*~hMsp@ew&@kEHuy8c**JcdLMiAmQ$qw)QUpYvd!~N>|L^7V@_F&R;`2G@zR!JK=UmtKTF!M2 zVueD{wE%p>aTYcYxu@GalAx=I8T*8{CszCCNd7+Y3%{9zEOR*rAA37P)#uomsss=L zkeY3-`sjcuG5rjnxRT_TM2o|h3Nn7U+@tY*X8miV+mqxyaAD3Z_5R@gr|42@pqKc> zRu??y9FBmI{ysmO`>XQkYx}vwd*pH`rOjNtKB19wPF?Q=5f*OfHYbm)n3yIsYpO{< z%A)n84>MjpHe%nIt~!3Xr&Mc|kod3THMJYF390pprJ~vh2l+jUk!EP{iXb7j$(_2=+C0smdj_IgbkCfB0ZKD} zug0$)Jqs-ypP03Nh~2mt$plc_H*}H3(Or|8t@ud)%%Yr4YMS-2~Sh)gTPaK@z6j6ynj)#d^6aB zUB-!(Fg6=8%d6&}16*^fV`I19&^6-l@U(9WWt$nRY&)gzj!mvl#G=L8x; zETrdvXxrGBK={5rMnWv9%Evy@q#@)AhY^FT0g#o~kqb8u>qE|^rY`R#ztq!M3Ei{a zByWQjJZCp2DP!sJCKJO!O^(dt;kVNvp{fvhxqKJOpSE#6OTk$s?{pVEz~C3S>5z%_ zR|gVX8o8H>CG~;p^TcX0l|Hw!8YjT#^%y*=;v(@Xr5d4TeVFe2VLJl>v)%xh1tmdB1eFGVR~IbKQI*cV}$yw5`p zwFjol;t|DV@PgTEzbcgN;l^<{Xt|x)Y2sny17-W=(>EsjlVjP5wi>dcxu{&cE`n+N z{YOdpd=@BCOL3q*7t}D8-`oi6bHa6+_lUDcPU(AK-}^Hqf9Gr)t2oPE>An{0gZnix zjabdi38z2fysWQ3Y27*gpli&7#{E#>zEfx(%86qMWs$YACReAWDi4$sD@D*Jt&2P3{Kp&T zN6FUie#Ok%i+7ssc5diy=zU=mBw1H3UNXe1CH~L_vWUPSCeOh0)?#nNJ+*>keO+x^%I7okk#@BRJBIpS-QNJ-YxNW ze6mZg@Vu4$gjj5{4c*bN(_&yP-g=L3tsZ+=#VS11K34`u z*f+U77|}ko6O6S@mLpP*r#@{R3930>-IWfhIY~JIunDv=NrLJ@OArMGpX5HunY@vZ zT75URZRwLTp%zYi(cw4d`8ahimueW)7vo^#>gb4@go`?GX}U$SmeiNYwAN?l@0u!% zN#~CiHT9L}iOOe&id0cfs(ZF}<&HANiOIg?sIb?~t?|XBGmZ>SzEoAre9Tx{DB_kb zUL6$B_-MwdCPA!gVA=DZOB^1MHsB$H&NtpirB#>g@G^hMl)EfaGba!IQvWw%jcagM(UrxkXtZo;tiNjN*pGm6w^Ink(G*8ROu-o zXBR(2#(xjRV@&9#gCizZ9u#i$_`7bZxEmC)EXZIv?`+BG*hJKa=g&|U+;^P+( zm`$;W*>E@F{AQc5w4jHPaGtnf*c;uA1DRv=#{kg!*0b#^6S!Ei;GEBU%Y%R=W;HYz37fN-Wl54Pl0zU?| zrZ@|4*c#Br{MXQt>?YfC;sCHuI(12Tdu_=EGx_Cls;8?UVWd}npv)2d=6FAHXbomQWe)o#*b>(u=Lw6Lih|{To z$CutS=_NQyk=`90B=6;8#ION}`Rd0RteOj`C4YH33T8DVQg8xU z8~sw_j)b8@2Ysp5UAK@E&l5W?S?3N|qZdv=mozEJL*3y)tuTL1GT|8Wx2BuFs19f! z1M>7%@Y`cOW1FihsWNEY5}pODtDJn29M(7Ig)44z*Q0arjL`M6ZEvrKAUX_S?BV8gp`dq=Ek^J@K|XL#@h(b+x&#n0PfSW7VsaA01zc z+*R4S)xJDRlxyXdp9vcc4VlCoYw0qbV+VBJ($B^@0 zdaXI}Rm*9Ua$gfR%%6J4pJ76=)jcw`1b-Wl;=vhM-Mbh2>3$}>3OY#wOhn@1Xv9ff zX@7ZXX`ebec3vIcp!)MWo1+bKGLBgF*(=+xhFl$YBDuePjY)}Qn=j9DAvV+iyidN0 zo5&|{fiv+1Aw)A`y(Xe&-X)rDaTMF`^9Ox(?=9}&eo@TKYlmUVk?XGF9$e2+(NZjV zVJ4-q2GA@^zVDQhD}Snt*dCSR&sg{Ar2SO2G29j=A?`di@g%~y_ca$j9zoQGH2Q7iOJsN z6j`FJ>qv5YFLO?K*h1ZzM}dkSfv1HHK_#;mvxw9rXo{?Ed;Vqf-d&razcj5BAR5H; zX%4Xc25S;8PnA9A2VlCG%dfgMBpvst*wz4ZfntOE$BHB%6T$bP3Vuyqkgvj%KS|2| zC~xp|wS38PH?!37r8e^s%mdxj$>;OHmD@$3E}e7!>ZjmdA{5~|{!3=Y2Zg?iIKm~t zc~b7ZAbdC>T`wYsZk=6Bsr#yjV&rjI3`=L!{wk)~@qyJAP8w!#34O!wq! zRkY>8_Uh*(_?pK9I=zW5hSzp{EGD@CL%3u&rX(d(wl#NhZphPPNRs&HQYb@p5nJj5 zNUhi+%Lp)%|5$R=s-auO3@*vICvth(evE zATtj_fxFiaitMKp*(Q|-c#B2Psj-hgrTu~A|GiQ)0HWtI^0%8yY(O)hR(<&J0ZkiQ zS?$(xaI%bujF=w=@zXlq>$LQ;d)?B!aN;)eT-Vqb0R+IiDx@5olv-(ckSC+Wx*&^Z@Jagva= zmMVL;-|>|DR;t5ONs~IyN1&TM!>#0THL|2&qSt560Sl9w^0z5-%H*1YRP%YsrS{Z{ zE1QlMj98Wj)+CDz^NU9$_-AWPTT&;4TbOs9abpnM@{u@6ubo}FpkqQ=sQg`%+UIz9 z^8Wk88M*Voy{Pm4~(wD-4af@Kifpm)g&(>UoOr^v8$@$Miy8;6woP?AnV zocJL@&BN?l!jB&p_~B|8QX2{F(9b+6x?k@Cc~icjT)sz4TMnm$j0oz*k$xx?(()l2 zh6V=ofqh{kogqB;@b6yYX?>(4>W3@|x-ih(-f zx<)Q22PL{cY^c}IzfTJs^IjNl&E@P2>$pn}T<}zF#*-5m$)=#|S#WZ!gtg-ObSK$Q z(ylt3%`AD{8cEc{W7Kz5AuZ21uKAof7ly4p^bG%e^v=AWLl!lcg$VkFi>4M$bsUXl zL@U3Zl(la)FT}E8mFF*Ak#@<&B+%4`L0IQsNC&8@jJy%UxlIFVmog1OUfJ%()oj@zz0cz7 zx9uDEI3nqe=>u~~dgQHa(fo1?M}DNFu`CoH@Q!4Z!kM1FShLQlo|KLbN)tltwU5K3 zne)30wExMf^b#-;zzxwvMHNpiGX zM`U`_W|4DncJm}`S0@LPlAKZ!nzPJ+B9 zO0X@Kw>zjUF)mS3GS4)wE8x_;z$2)TNv&6oY<_u5NZ?7Qc^Wp#@cZf{=lsXgw-K07 zVuadO|hyb|>fgfA_9s7w3}UnNR49CqzXaJ(#%VWE?>;{H-a zcq?G9aO&0fRTGVSNppAWK#`lyIplSH3F~W*LK|=`UY>(RY@<*6ZB(ao( zN~z;8P{zKMhJPS7H=W$AeF$n%?hO>=*IGxU8s^2%LHOFHy^L<9w_9d=>zPC=;$LXN zu#P_Dap^&swL5CI_)aepcEIT6&S00P96>%_S;S5A}`vP>FmcZF&czTlJ zo2r`LEuN1)gcQv;u~uA-Du8u~X7)PTN3PkQY-Dhik+8|t&_9{VQMscYFIBTRj`WOi zmCq$9`P+9PG*0t{=9z_CrA>}pjOhm(GI^?Pk+ZC7(p^1W&o zl4mJwVWdyiIOkuuwr0fHJIca?CYSU?eeliWcj0`If|R*bCwHp}_haQ&&t#BdL6#;E zXj3a#N#tYpOV?R$+-YuJPmAExP#EB{eRW<6TKX%zN>IWZJTmn&ILOOCI{-^zaB7ev%u5 zBMv{bc6F(w)4THK9V^nDN^f%GYcW|)!r9!{POzUezr!fB0dc-bo?@Gmm^&L-+C>Dj zzIGoL2bm(idIl2+d77N(FtQh;ajNF{)4Q!dA3wRY6j*Uu-lx}-06RE2(vB3D`E-tb zF1yLhSi0%*;QENPKt#5B;5r;YjiAo^A`~0!5@91Y?MPUQt*A~geZvqiFzZ}C4?5Gc zUS^i*ufq5(xhXh&v%%Z(#xP>RfHE#BFGLo&w%7VmP7VV8CQV1M%CB7f6? zsCTC(A{SoA7H#3z8Yx6+4wO-S0&_(E+>YrSG6s2OO`NZEY)M%#1}#>KrV<_I@^rSj zh#BRAoybz(TND-dmnn{Pd;&DLG*M323G?FqVuhRWglr45Nb%8Flz1ZZTfkKbU@b}{boFg zSYl7dAWf+Ry0b04nXBJ1vE4(IFViNM!2$EplHe2OWv!N$e|U}y6amob%2lUzX28moqH12Nu>vbj;^m?z)Fq0vJxQzq(o$Q5p5Od#<3G>f(be zu$AR0Hx+!m1uQ?$8z>==2LWp|_x+-gQ)o)PxW*6gpr62F2}VOnolei=Y&ZgcrkkUA#xcpow+D@g)<ncf<2jYO6QT&AXKq`2Gi z;$*mfPJw$CkFK_2U@LTZi3Y7+MAZP*7bz`1noG}Vwev4;_MMQ0ob1X6?~}8a-WkMl z_lSM;s~%U6&7`_Frd$|I!6CNjo76tMfJ4yYDHsE43;U+=*?d-6Twd6Rc`A6nFaBuG;uK^{^p|@Bx zmNfxXeZmcNo!f^Z206P7D!o=0by^Gd}~+j(|!I-EGy)6D^#edF)*#j8|=<($fVHa@Ru8Ulqs>|nH zyaAA_X99>682Z`$!U~6iA%ir$WvAXNbiWe$Qnro9wOT7>9%j7RNGlDLU z2|F>3YgHo6G+^*Z=_Yb(#j(*j&yWmce8h6L!jEJCj3X+n{Pwg`Xjl%H`||5#JxnCSCRSnu)q*=71d%j{57d~>DEXObc^s!#4NYeE9`>?FPrNZL+bhf1&|jE1H(Oht=)M)@y!_m**J-N4 zj_H)Z_P1?q9UXYai&9)4bQDaVqMy$OoIWP`zB}2PdGQ?)F?DF?{kfsl4A^B%pp?wv zq2I>bIr($Bw(f3rQrzJ|%z8y5%T1b0=*Bjbyf)5==5I!9Rz)-+Yw;^%ng~yOAE(fV zVGpw(x5wh=_?CttJL+ETEnZt66)^PF!6?0A{ogyPd*n~Id#GG&7U2h+az+#3ERg~3 zj%1j&O;|x5N{8f)TA5jX=AS7XOexhfE8kOKP0Wj;g7=akM`M$=B*6WLF4=WXe7HTF zbf4yO$J^>7zr3HKRksn0+)du<<%G7965Pi@UKfBfgEw96~oX$;oQIM&@GZ6)qMm*5DK53-x#wAhfN zRtvA3uVk8c61p-;r=cDnGI}m6TqqXeH{03p>Uny7+@@Vb(}G!Q((3mI8|vz!6Nu&U zhsO`$ub{C;Amxp@gPZQI!@%e)E&P^O)p5!CPJA^zuzmR+F0iKJ{lS8t8@Akr0_<-K z&*&CuT1b-Xv|948+0+&CGM;0YImJNu`f@jEw&3{zigpB&G45&W+@JEmb8>`n*}aiY ziOkdXGuXIsW4~lIO8!Ddad&9DSSOA<|z`BA9>1Ut(nlvBddn0JEC>kmE)y z&UL0VK(D36`a4M5m!#f9B($|^e8;i=xcYLFGeb)zXGPd>ew`-Smt!iYd!c3|$uu&N z6pi(B8@c0-bCHj>^h>;gEV_<%NaBd88kSWN(~R-M(3>*^5y&H2#NsPAnIDpj9ZD_TQzm6Fpy)%ZGj9_SQ$VskT! zbT#lZl_pjM7dWt%k39ZLd8IziS*BBI@S z@WG=GhiRn9SGLJOn5H^NfvwCtsNRY1OY^Sc>o6fLBYs2m`qpX78ZQ0{iGLGfdCkju zjBMp)IH^HR`q6e2GD)(fHQ^D%YGD?hJZHjHdLu>q3qvPVqb#@2I^8hjrqap!R@;jW z2+5dwj0N{F%-#6O#Nj9HFWhmm9)_#W_-`IQaT@ib4Plu^vax~L8hBj0>+>$iI--#| z;81y8--#WokoLq$o16k`cY7S38Td)R5bY||xo=`_`EKbPlIJLzYh(c*;$a{!ZD}Wm z7Ciix-ms|Od9@X%+>U(7UZ3oJ(Amgl@~1>EBrK!*;KGSK zuyp23e}j<)t^pGqVL0X{id zSY8qM4ms>vcOr`)g1k~vQ_6mUp=P!m_NO{$uACrcKz#>_d_YQCOVp*`LE6Ky!xzUg zYB$N}@mIz=*Ix@8b}Hu#Kvow#+o+8OEC}YJYE5U)C##MESrwkVoB3iLq@Pt9@3s`j zE>q7{02B)ksioc**%Wn<88x_`n#zUW+e&&!>*IEP5;dHJirx&bwPW0VZ_Rhjp<2fR z_bXRP_5qP$n{)Pf1W;=@(QPG|3XyG zC%H{U4LCfth)4ldk${g~@s{+nCWj@tU!vO9TFHsZ!&}C)_2N7D*NbsyG(8e8X0!qz};P&Dm|a!^@^*;}U=D53lu=lP&_?rGIeX^WfFNU4veaDe}xOxRQWnG}Y;w)1&;_FAFPZU@ntM8=v#V_8=QuM6PCF z!Al(}7-E_JOw4+sdf#M z!0z2(sFx-2#YK)K-!bPdw&MevQR72S^8h)8G%YdHQ=eTQXb|0=GWWwPdr)Nx8P za;(j&)&g=2=vpNx3-DJ8V7gkZ-b2l;r%-7Bp}e?d%`i(s;FT`e=V71;)QxhUv3_AW zEPj=+DJ{m0Pii?K`Zz32@D>SiZs8Tw51RYQ)y{%9R)p6ph{z4uU}msf%IOnMDIc|eW8A4Nn{h6D z=4@Mf$oDmvXdo}!cgB{Fx0=y_07C5bb<1io;AIV#IY_{@g79W)@R+gEgZ8}huklIC zrRTR<7H@ck-RYScf)tgF+D-@4E~x7x)>8?2wqF*gIzU&Kx%&>~9JDcCC9L!qJ+$+J zXH~za%pAY!{^dzlxWl$ktiv5|H@Yg9v)&E{F)-7lxoH&Z-lM9m|x!lu>fb07$XDCFpDnEP6{qvUh_RQ%2eu*9#CZJe+| zV4cgEbL2Z42#OQmxq(Gu?Xi^gk3w6#o?!U($!)2nEb3>d$COT8QQB5p$wQB|lk#0% z?h;u^O#`zQGcED5zIEP$9}os%5;0&%9Av>mogq?k<1dCbokq@=9dzWyHAURX9~Q1~ zL2nq;bRzh(^+ zAMjzVih1H8>%nIQo?mNEOj>U=y)ILvThOF4ZWYny4r z_M_dgJ8zzlv*l*Q@tIrd)|~S;%fT&8_Q78DrS++smO~NJP6BRz7^}d#2fQrKoweHh ziW;UN{Eq2Zw8ZnX9PYb(VJpyVhj+3+G97in};z>K|UEiYA-*!m68?q<;8T27$EnX6UmWw z!SsE;ZgOL?)tN6{TJQD{(K(Oj$Lsf9pp!WFP}7|#WACqhjbJ(zN}PXKzwaV_pHIg^ z@xWcnuU!@~R{*ca^Uvz{Mbq~U7=-hid}_6#g-^kZi6FqwFM+$S3M!lBEB<}F&EZzf zy;>g;d7OoFohW~+90M|c(6WMo5m4D_QCekS)TjP0z{9zf(aW_GFL&~1Fmx^x<$Rwc z>CkQ@`G05gB|_Q}3i|b&D+uH_v@(&9Y~`!f7zg~Pvts?PU_1mtAitxHOcBUs-Wd$7 zg2=TiLjR@0??y)N(YHzwi<%=@{Y-Q})lYPvY!v_w5a72fcm#}NIKWZpX+lYC&sm!6 zU%Qo#vDhXXC71b$jv|l;aX)igxkIN#uUBY+I6%%8>#zNHU<=Vbpr97xj=X=-?ve9% zqQm6WDo(O?`hUoJyW@gO1?LT(U!X8VApgMq{5Fl76i-CG1!vRd=iq-mvh>FuyK(bC zTA)bK!_va05p_P8RAUG%;Jm5Lnsk^>@4sKF+qJ$QUdibTcW&j#dsqjtX-}-VNq-Pg z8)4^G)){Gk0f*|xq|lagL{OWH0T94C`5yHxa8Wlo6;C=CScTBar(~uprook>U&NEa znzeB3)_+GoC>&6Z1C%|)-(|j_GmH%yV4_m$IonajX94Ux+f`Z%p$6Kriunf;r+=SM z&*~eXo+fVLQqzd&@DzfD-k$_mQ){ebjrs|It;LiA1?HC!T0Z9vwSAG`f(#v60TD9~ zfIdDE+<{1G;DF^d8MJM)?q0-WnjvBllO$&$7Vklslf zxdtZc!C&bC%vaVq{6`kFv|c`~^K5HwY+SqtPDF$sGJ;Q?fAp0xEiJz_`D+VN#ysPV z|8lC8?y>jl2xdLrgg1PzYXU&hqZ5B_g*z6yetR`O1|mnQxQ08S@kW$W@(-O9EIsMBzU@-{px_ zekHVegF1aNtIi4wG^EBPU78ZHjbd0?8i8yXs>(ezaso^D&30-sI#?Z3vT(k&F(6x4RQR5?brrndaE!$TeWI#P5x zU=<8j?kycmN#@3$?NV`-i_atphsRnU{Fh{a08>@%Re?cc;a}e~Y6BB0Tw!VA)MEnt zzN5*12#!AUZDFgw=<^rZO8MH%`3n>{jgRO75VU!AIGTPwCT8|;zVIvAb^R7!r|)ve zfvAz>Xf=;-2)u8;@D2zfvYQ5G;=ZN>{K8)97Ot5R7}U-WO6t_gRN@%zKaA8t!3wf$ zVgO}1+|?8QE0ib&^X4OUj{09o5Vg6loOm?01jjDkSH4CPN;#tJudCjs9SPRCPT53> zvTYx|c=5uRKl=59_H8FN+mt8Ib^F$Krc^n?TT`3C*(;9g`r7|q>j1LN2wVsrZA@=K z6+iuMFWdO5)0)2WZTO0bN1ZKC9VPdsNK&rx7hKZ1Jvs5e&29hedRK60R7}qvLLkL)y6hq- zn`W418I>*MdWe2r^FM=sor(Yd^JNC!v&J>re<#rSfZlJkf0~Z}4*jjhE%?&^0k-oh Ab^rhX literal 0 HcmV?d00001 From 15604f23c306c38bfb09dbb46fc8a2624ee31758 Mon Sep 17 00:00:00 2001 From: Steve Macenski Date: Tue, 2 Sep 2025 15:17:17 -0700 Subject: [PATCH 40/51] Include 3Laws Robotics in README partners section Added 3Laws Robotics to the list of partners. Signed-off-by: Steve Macenski --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 7e85a8225c8..d58ef3a8b6b 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ Please thank our amazing sponsors for their generous support of Nav2 on behalf o ### [Stereolabs](https://www.stereolabs.com/) produces the high-quality ZED stereo cameras with a complete vision pipeline from neural depth to SLAM, 3D object tracking, AI and more. +### [3Laws Robotics](https://3laws.io/) provide Supervisor ROS and Pro, easy-to-use dynamic collision avoidance solutions to improve safety and application throughput. + ## Citation If you use the navigation framework, an algorithm from this repository, or ideas from it From fbdf75328b36f3808f5a20d79288cd8f7a4a2d09 Mon Sep 17 00:00:00 2001 From: Tony Najjar Date: Wed, 3 Sep 2025 11:28:00 +0200 Subject: [PATCH 41/51] Fix seg fault (#5501) * Fix segmentation fault Signed-off-by: Tony Najjar * fix linting Signed-off-by: Tony Najjar --------- Signed-off-by: Tony Najjar --- nav2_smac_planner/src/node_lattice.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nav2_smac_planner/src/node_lattice.cpp b/nav2_smac_planner/src/node_lattice.cpp index 7922d353d19..56dcbfdc271 100644 --- a/nav2_smac_planner/src/node_lattice.cpp +++ b/nav2_smac_planner/src/node_lattice.cpp @@ -230,7 +230,8 @@ bool NodeLattice::isNodeValid( // Check primitive end pose // Convert grid quantization of primitives to radians, then collision checker quantization static const double bin_size = 2.0 * M_PI / collision_checker->getPrecomputedAngles().size(); - const double & angle = motion_table.getAngleFromBin(this->pose.theta) / bin_size; + const double angle = std::fmod(motion_table.getAngleFromBin(this->pose.theta), + 2.0 * M_PI) / bin_size; if (collision_checker->inCollision( this->pose.x, this->pose.y, angle /*bin in collision checker*/, traverse_unknown)) { @@ -269,7 +270,7 @@ bool NodeLattice::isNodeValid( if (is_backwards) { prim_pose._theta = std::fmod(it->_theta + M_PI, 2.0 * M_PI); } else { - prim_pose._theta = it->_theta; + prim_pose._theta = std::fmod(it->_theta, 2.0 * M_PI); } if (collision_checker->inCollision( prim_pose._x, From 1d399b8a355b3b8e86d70d51c6e1b79d91a5c405 Mon Sep 17 00:00:00 2001 From: Steve Macenski Date: Wed, 3 Sep 2025 11:29:27 -0700 Subject: [PATCH 42/51] Route graph vis fixes: Fixes regressions from #5452 (#5507) * Fixes for route graph Signed-off-by: SteveMacenski * Fix route graph vis Signed-off-by: SteveMacenski --------- Signed-off-by: SteveMacenski --- nav2_route/include/nav2_route/utils.hpp | 26 +++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/nav2_route/include/nav2_route/utils.hpp b/nav2_route/include/nav2_route/utils.hpp index 04deda14bec..9b9e9078e7e 100644 --- a/nav2_route/include/nav2_route/utils.hpp +++ b/nav2_route/include/nav2_route/utils.hpp @@ -65,10 +65,10 @@ inline visualization_msgs::msg::MarkerArray toMsg( nodes_marker.header.stamp = now; nodes_marker.action = 0; nodes_marker.ns = "route_graph_nodes"; - nodes_marker.type = visualization_msgs::msg::Marker::POINTS; - nodes_marker.scale.x = 0.5; - nodes_marker.scale.y = 0.5; - nodes_marker.scale.z = 0.5; + nodes_marker.type = visualization_msgs::msg::Marker::SPHERE_LIST; + nodes_marker.scale.x = 0.1; + nodes_marker.scale.y = 0.1; + nodes_marker.scale.z = 0.1; nodes_marker.color.r = 1.0; nodes_marker.color.a = 1.0; nodes_marker.points.reserve(graph.size()); @@ -79,7 +79,7 @@ inline visualization_msgs::msg::MarkerArray toMsg( edges_marker.action = 0; edges_marker.ns = "route_graph_edges"; edges_marker.type = visualization_msgs::msg::Marker::LINE_LIST; - edges_marker.scale.x = 0.1; // Line width + edges_marker.scale.x = 0.05; // Line width edges_marker.color.g = 1.0; edges_marker.color.a = 0.5; // Semi-transparent green so bidirectional connections stand out constexpr size_t points_per_edge = 2; @@ -92,14 +92,28 @@ inline visualization_msgs::msg::MarkerArray toMsg( geometry_msgs::msg::Point edge_end; visualization_msgs::msg::Marker node_id_marker; + node_id_marker.header.frame_id = frame; + node_id_marker.header.stamp = now; + node_id_marker.action = 0; node_id_marker.ns = "route_graph_node_ids"; node_id_marker.type = visualization_msgs::msg::Marker::TEXT_VIEW_FACING; + node_id_marker.scale.x = 0.1; + node_id_marker.scale.y = 0.1; node_id_marker.scale.z = 0.1; + node_id_marker.color.a = 1.0; + node_id_marker.color.r = 1.0; visualization_msgs::msg::Marker edge_id_marker; + edge_id_marker.header.frame_id = frame; + edge_id_marker.header.stamp = now; + edge_id_marker.action = 0; edge_id_marker.ns = "route_graph_edge_ids"; edge_id_marker.type = visualization_msgs::msg::Marker::TEXT_VIEW_FACING; + edge_id_marker.scale.x = 0.1; + edge_id_marker.scale.y = 0.1; edge_id_marker.scale.z = 0.1; + edge_id_marker.color.a = 1.0; + edge_id_marker.color.g = 1.0; for (const auto & node : graph) { node_pos.x = node.coords.x; @@ -141,8 +155,8 @@ inline visualization_msgs::msg::MarkerArray toMsg( } } - msg.markers.push_back(nodes_marker); msg.markers.push_back(edges_marker); + msg.markers.push_back(nodes_marker); return msg; } From 41bd110f9b681f68ea6cd843aca9139a603ba9d3 Mon Sep 17 00:00:00 2001 From: Steve Macenski Date: Wed, 3 Sep 2025 11:32:51 -0700 Subject: [PATCH 43/51] Removing openMP dep on MPPI (#5506) Signed-off-by: SteveMacenski --- nav2_mppi_controller/package.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/nav2_mppi_controller/package.xml b/nav2_mppi_controller/package.xml index a27716ab997..450dd5254cc 100644 --- a/nav2_mppi_controller/package.xml +++ b/nav2_mppi_controller/package.xml @@ -14,7 +14,6 @@ angles backward_ros geometry_msgs - libomp-dev nav2_common nav2_core nav2_costmap_2d From 491433a5bf0ea2d4f22af6cd6d6d5aff6a204743 Mon Sep 17 00:00:00 2001 From: mini-1235 Date: Fri, 5 Sep 2025 02:27:58 +0800 Subject: [PATCH 44/51] Fix pose timestamp when using transformPoseInTargetFrame (#5499) * Fix pose timestamp when using transformPoseInTargetFrame Signed-off-by: mini-1235 * Fix frame Signed-off-by: mini-1235 * Update nav2_route/src/goal_intent_extractor.cpp Co-authored-by: Steve Macenski Signed-off-by: mini-1235 --------- Signed-off-by: mini-1235 Co-authored-by: Steve Macenski --- nav2_controller/src/controller_server.cpp | 1 + .../dwb_core/src/dwb_local_planner.cpp | 16 ++++++++++++---- nav2_route/src/goal_intent_extractor.cpp | 2 ++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/nav2_controller/src/controller_server.cpp b/nav2_controller/src/controller_server.cpp index 66b62e667e2..98a6eda8629 100644 --- a/nav2_controller/src/controller_server.cpp +++ b/nav2_controller/src/controller_server.cpp @@ -799,6 +799,7 @@ bool ControllerServer::isGoalReached() geometry_msgs::msg::Twist velocity = getThresholdedTwist(odom_sub_->getRawTwist()); geometry_msgs::msg::PoseStamped transformed_end_pose; + end_pose_.header.stamp = pose.header.stamp; nav2_util::transformPoseInTargetFrame( end_pose_, transformed_end_pose, *costmap_ros_->getTfBuffer(), costmap_ros_->getGlobalFrameID(), costmap_ros_->getTransformTolerance()); diff --git a/nav2_dwb_controller/dwb_core/src/dwb_local_planner.cpp b/nav2_dwb_controller/dwb_core/src/dwb_local_planner.cpp index 5c1b47322a6..b85727b95a0 100644 --- a/nav2_dwb_controller/dwb_core/src/dwb_local_planner.cpp +++ b/nav2_dwb_controller/dwb_core/src/dwb_local_planner.cpp @@ -290,6 +290,7 @@ DWBLocalPlanner::prepareGlobalPlan( } goal_pose.header.frame_id = global_plan_.header.frame_id; + goal_pose.header.stamp = pose.header.stamp; goal_pose.pose = global_plan_.poses.back().pose; nav2_util::transformPoseInTargetFrame( goal_pose, goal_pose, *tf_, @@ -535,10 +536,17 @@ DWBLocalPlanner::transformGlobalPlan( // Helper function for the transform below. Converts a pose2D from global // frame to local auto transformGlobalPoseToLocal = [&](const auto & global_plan_pose) { - geometry_msgs::msg::PoseStamped transformed_pose; - nav2_util::transformPoseInTargetFrame( - global_plan_pose, transformed_pose, *tf_, - transformed_plan.header.frame_id, transform_tolerance_); + geometry_msgs::msg::PoseStamped stamped_pose, transformed_pose; + stamped_pose.header.frame_id = global_plan_.header.frame_id; + stamped_pose.header.stamp = robot_pose.header.stamp; + stamped_pose.pose = global_plan_pose.pose; + if (!nav2_util::transformPoseInTargetFrame( + stamped_pose, transformed_pose, *tf_, + transformed_plan.header.frame_id, transform_tolerance_)) + { + throw nav2_core::ControllerTFError("Unable to transform plan pose into local frame"); + } + transformed_pose.pose.position.z = 0.0; return transformed_pose; }; diff --git a/nav2_route/src/goal_intent_extractor.cpp b/nav2_route/src/goal_intent_extractor.cpp index 14443b77641..483739aca05 100644 --- a/nav2_route/src/goal_intent_extractor.cpp +++ b/nav2_route/src/goal_intent_extractor.cpp @@ -175,6 +175,7 @@ GoalIntentExtractor::findStartandGoal(const std::shared_ptr goal) node_pose.pose.position.x = node_data.coords.x; node_pose.pose.position.y = node_data.coords.y; node_pose.header.frame_id = node_data.coords.frame_id; + node_pose.header.stamp = start_pose.header.stamp; candidate_nodes.push_back(transformPose(node_pose, costmap_frame_id)); } @@ -202,6 +203,7 @@ GoalIntentExtractor::findStartandGoal(const std::shared_ptr goal) node_pose.pose.position.x = node_data.coords.x; node_pose.pose.position.y = node_data.coords.y; node_pose.header.frame_id = node_data.coords.frame_id; + node_pose.header.stamp = goal_pose.header.stamp; candidate_nodes.push_back(transformPose(node_pose, costmap_frame_id)); } From c0da22a1c8bcb43a7762e7c32569059f3257682f Mon Sep 17 00:00:00 2001 From: Tony Najjar Date: Thu, 4 Sep 2025 21:11:58 +0200 Subject: [PATCH 45/51] Use integral path distance for MPPI Critics (#5495) * Implement integral distance for critics Signed-off-by: Tony Najjar * . Signed-off-by: Tony Najjar * linting Signed-off-by: Tony Najjar * more linting Signed-off-by: Tony Najjar * fix tests Signed-off-by: Tony Najjar * reset path length Signed-off-by: Tony Najjar * fix sign Signed-off-by: Tony Najjar * fix return value Signed-off-by: Tony Najjar * Revert "fix return value" This reverts commit 8b21e890de598ab6b35c4a0200c82ebfc4b0c9c8. Signed-off-by: Tony Najjar * remove extra variable Signed-off-by: Tony Najjar * add doxygen Signed-off-by: Tony Najjar * rename getCriticGoalPathDistance Signed-off-by: Tony Najjar * delete withinPositionGoalTolerance Signed-off-by: Tony Najjar * use only one path distance Signed-off-by: Tony Najjar * cleanup Signed-off-by: Tony Najjar * linting Signed-off-by: Tony Najjar * reorder Signed-off-by: Tony Najjar * format Signed-off-by: Tony Najjar * fix tests Signed-off-by: Tony Najjar --------- Signed-off-by: Tony Najjar --- .../critics/cost_critic.hpp | 1 - .../critics/goal_angle_critic.hpp | 1 - .../critics/goal_critic.hpp | 1 - .../critics/obstacles_critic.hpp | 1 - .../critics/path_align_critic.hpp | 1 - .../critics/path_angle_critic.hpp | 1 - .../critics/path_follow_critic.hpp | 1 - .../critics/prefer_forward_critic.hpp | 1 - .../critics/twirling_critic.hpp | 1 - .../nav2_mppi_controller/models/state.hpp | 1 + .../nav2_mppi_controller/optimizer.hpp | 1 + .../nav2_mppi_controller/tools/utils.hpp | 75 ------------- .../src/critics/cost_critic.cpp | 6 +- .../src/critics/goal_angle_critic.cpp | 12 +- .../src/critics/goal_critic.cpp | 13 +-- .../src/critics/obstacles_critic.cpp | 6 +- .../src/critics/path_align_critic.cpp | 14 +-- .../src/critics/path_angle_critic.cpp | 11 +- .../src/critics/path_follow_critic.cpp | 10 +- .../src/critics/prefer_forward_critic.cpp | 8 +- .../src/critics/twirling_critic.cpp | 15 ++- nav2_mppi_controller/src/optimizer.cpp | 1 + nav2_mppi_controller/test/critics_tests.cpp | 18 +++ .../test/optimizer_smoke_test.cpp | 4 +- nav2_mppi_controller/test/utils_test.cpp | 105 ------------------ 25 files changed, 41 insertions(+), 268 deletions(-) diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/critics/cost_critic.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/critics/cost_critic.hpp index 2646c98fd9e..c285a14b664 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/critics/cost_critic.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/critics/cost_critic.hpp @@ -144,7 +144,6 @@ class CostCritic : public CriticFunction std::string inflation_layer_name_; unsigned int power_{0}; - bool enforce_path_inversion_{false}; }; } // namespace mppi::critics diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/critics/goal_angle_critic.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/critics/goal_angle_critic.hpp index 58c108b21cd..08ec354cf3b 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/critics/goal_angle_critic.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/critics/goal_angle_critic.hpp @@ -46,7 +46,6 @@ class GoalAngleCritic : public CriticFunction float threshold_to_consider_{0}; unsigned int power_{0}; float weight_{0}; - bool enforce_path_inversion_{false}; }; } // namespace mppi::critics diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/critics/goal_critic.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/critics/goal_critic.hpp index 6b248f29945..8fb8fb688ae 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/critics/goal_critic.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/critics/goal_critic.hpp @@ -46,7 +46,6 @@ class GoalCritic : public CriticFunction unsigned int power_{0}; float weight_{0}; float threshold_to_consider_{0}; - bool enforce_path_inversion_{false}; }; } // namespace mppi::critics diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/critics/obstacles_critic.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/critics/obstacles_critic.hpp index 23fb3db2ca0..31b6a3d0df8 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/critics/obstacles_critic.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/critics/obstacles_critic.hpp @@ -97,7 +97,6 @@ class ObstaclesCritic : public CriticFunction unsigned int power_{0}; float repulsion_weight_, critical_weight_{0}; - bool enforce_path_inversion_{false}; std::string inflation_layer_name_; }; diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_align_critic.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_align_critic.hpp index 133447a8774..f7ad2fda6a9 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_align_critic.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_align_critic.hpp @@ -52,7 +52,6 @@ class PathAlignCritic : public CriticFunction bool use_path_orientations_{false}; unsigned int power_{0}; float weight_{0}; - bool enforce_path_inversion_{false}; }; } // namespace mppi::critics diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_angle_critic.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_angle_critic.hpp index 85b9d48eb83..b137270aba9 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_angle_critic.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_angle_critic.hpp @@ -81,7 +81,6 @@ class PathAngleCritic : public CriticFunction unsigned int power_{0}; float weight_{0}; - bool enforce_path_inversion_{false}; }; } // namespace mppi::critics diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_follow_critic.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_follow_critic.hpp index aacec055ccf..1ccd08c32fe 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_follow_critic.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_follow_critic.hpp @@ -53,7 +53,6 @@ class PathFollowCritic : public CriticFunction unsigned int power_{0}; float weight_{0}; - bool enforce_path_inversion_{false}; }; } // namespace mppi::critics diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/critics/prefer_forward_critic.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/critics/prefer_forward_critic.hpp index 811ba9ae9a6..e17ad235e5c 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/critics/prefer_forward_critic.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/critics/prefer_forward_critic.hpp @@ -45,7 +45,6 @@ class PreferForwardCritic : public CriticFunction unsigned int power_{0}; float weight_{0}; float threshold_to_consider_{0}; - bool enforce_path_inversion_{false}; }; } // namespace mppi::critics diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/critics/twirling_critic.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/critics/twirling_critic.hpp index bc40b194404..3e316a567e0 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/critics/twirling_critic.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/critics/twirling_critic.hpp @@ -44,7 +44,6 @@ class TwirlingCritic : public CriticFunction protected: unsigned int power_{0}; float weight_{0}; - bool enforce_path_inversion_{false}; }; } // namespace mppi::critics diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/models/state.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/models/state.hpp index a8c81c3b07d..50f9d400a91 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/models/state.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/models/state.hpp @@ -40,6 +40,7 @@ struct State geometry_msgs::msg::PoseStamped pose; geometry_msgs::msg::Twist speed; + float local_path_length; /** * @brief Reset state data diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/optimizer.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/optimizer.hpp index e70cfcab786..1914abde647 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/optimizer.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/optimizer.hpp @@ -33,6 +33,7 @@ #include "geometry_msgs/msg/pose_stamped.hpp" #include "geometry_msgs/msg/twist_stamped.hpp" #include "nav_msgs/msg/path.hpp" +#include "nav2_util/geometry_utils.hpp" #include "nav2_mppi_controller/models/optimizer_settings.hpp" #include "nav2_mppi_controller/motion_models.hpp" diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils.hpp index f518f16e169..4a3cc6b7c06 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils.hpp @@ -243,81 +243,6 @@ inline geometry_msgs::msg::Pose getLastPathPose(const models::Path & path) return pathPose; } -/** - * @brief Get the target pose to be evaluated by the critic - * @param data Data to use - * @param enforce_path_inversion True to return the cusp point (last pose of the path) - * instead of the original goal - * @return geometry_msgs::msg::Pose Target pose for the critic - */ -inline geometry_msgs::msg::Pose getCriticGoal( - const CriticData & data, - bool enforce_path_inversion) -{ - if (enforce_path_inversion) { - return getLastPathPose(data.path); - } else { - return data.goal; - } -} - -/** - * @brief Check if the robot pose is within the Goal Checker's tolerances to goal - * @param global_checker Pointer to the goal checker - * @param robot Pose of robot - * @param goal Goal pose - * @return bool If robot is within goal checker tolerances to the goal - */ -inline bool withinPositionGoalTolerance( - nav2_core::GoalChecker * goal_checker, - const geometry_msgs::msg::Pose & robot, - const geometry_msgs::msg::Pose & goal) -{ - if (goal_checker) { - geometry_msgs::msg::Pose pose_tolerance; - geometry_msgs::msg::Twist velocity_tolerance; - goal_checker->getTolerances(pose_tolerance, velocity_tolerance); - - const auto pose_tolerance_sq = pose_tolerance.position.x * pose_tolerance.position.x; - - auto dx = robot.position.x - goal.position.x; - auto dy = robot.position.y - goal.position.y; - - auto dist_sq = dx * dx + dy * dy; - - if (dist_sq < pose_tolerance_sq) { - return true; - } - } - - return false; -} - -/** - * @brief Check if the robot pose is within tolerance to the goal - * @param pose_tolerance Pose tolerance to use - * @param robot Pose of robot - * @param goal Goal pose - * @return bool If robot is within tolerance to the goal - */ -inline bool withinPositionGoalTolerance( - float pose_tolerance, - const geometry_msgs::msg::Pose & robot, - const geometry_msgs::msg::Pose & goal) -{ - const double & dist_sq = - std::pow(goal.position.x - robot.position.x, 2) + - std::pow(goal.position.y - robot.position.y, 2); - - const float pose_tolerance_sq = pose_tolerance * pose_tolerance; - - if (dist_sq < pose_tolerance_sq) { - return true; - } - - return false; -} - /** * @brief normalize * Normalizes the angle to be -M_PIF circle to +M_PIF circle diff --git a/nav2_mppi_controller/src/critics/cost_critic.cpp b/nav2_mppi_controller/src/critics/cost_critic.cpp index dedf910187e..6b15d992ca8 100644 --- a/nav2_mppi_controller/src/critics/cost_critic.cpp +++ b/nav2_mppi_controller/src/critics/cost_critic.cpp @@ -23,8 +23,6 @@ namespace mppi::critics void CostCritic::initialize() { auto getParentParam = parameters_handler_->getParamGetter(parent_name_); - getParentParam(enforce_path_inversion_, "enforce_path_inversion", false); - auto getParam = parameters_handler_->getParamGetter(name_); getParam(consider_footprint_, "consider_footprint", false); getParam(power_, "cost_power", 1); @@ -135,8 +133,6 @@ void CostCritic::score(CriticData & data) return; } - geometry_msgs::msg::Pose goal = utils::getCriticGoal(data, enforce_path_inversion_); - // Setup cost information for various parts of the critic is_tracking_unknown_ = costmap_ros_->getLayeredCostmap()->isTrackingUnknown(); auto * costmap = collision_checker_.getCostmap(); @@ -153,7 +149,7 @@ void CostCritic::score(CriticData & data) // If near the goal, don't apply the preferential term since the goal is near obstacles bool near_goal = false; - if (utils::withinPositionGoalTolerance(near_goal_distance_, data.state.pose.pose, goal)) { + if (data.state.local_path_length < near_goal_distance_) { near_goal = true; } diff --git a/nav2_mppi_controller/src/critics/goal_angle_critic.cpp b/nav2_mppi_controller/src/critics/goal_angle_critic.cpp index 8ac899140f8..8792df2aaa2 100644 --- a/nav2_mppi_controller/src/critics/goal_angle_critic.cpp +++ b/nav2_mppi_controller/src/critics/goal_angle_critic.cpp @@ -20,8 +20,6 @@ namespace mppi::critics void GoalAngleCritic::initialize() { auto getParentParam = parameters_handler_->getParamGetter(parent_name_); - getParentParam(enforce_path_inversion_, "enforce_path_inversion", false); - auto getParam = parameters_handler_->getParamGetter(name_); getParam(power_, "cost_power", 1); getParam(weight_, "cost_weight", 3.0f); @@ -36,17 +34,11 @@ void GoalAngleCritic::initialize() void GoalAngleCritic::score(CriticData & data) { - if (!enabled_) { + if (!enabled_ || data.state.local_path_length > threshold_to_consider_) { return; } - geometry_msgs::msg::Pose goal = utils::getCriticGoal(data, enforce_path_inversion_); - - if (!utils::withinPositionGoalTolerance( - threshold_to_consider_, data.state.pose.pose, goal)) - { - return; - } + geometry_msgs::msg::Pose goal = utils::getLastPathPose(data.path); double goal_yaw = tf2::getYaw(goal.orientation); diff --git a/nav2_mppi_controller/src/critics/goal_critic.cpp b/nav2_mppi_controller/src/critics/goal_critic.cpp index 927ef950de8..9889ef3f071 100644 --- a/nav2_mppi_controller/src/critics/goal_critic.cpp +++ b/nav2_mppi_controller/src/critics/goal_critic.cpp @@ -22,10 +22,7 @@ namespace mppi::critics void GoalCritic::initialize() { auto getParentParam = parameters_handler_->getParamGetter(parent_name_); - getParentParam(enforce_path_inversion_, "enforce_path_inversion", false); - auto getParam = parameters_handler_->getParamGetter(name_); - getParam(power_, "cost_power", 1); getParam(weight_, "cost_weight", 5.0f); getParam(threshold_to_consider_, "threshold_to_consider", 1.4f); @@ -37,17 +34,11 @@ void GoalCritic::initialize() void GoalCritic::score(CriticData & data) { - if (!enabled_) { + if (!enabled_ || data.state.local_path_length > threshold_to_consider_) { return; } - geometry_msgs::msg::Pose goal = utils::getCriticGoal(data, enforce_path_inversion_); - - if (!utils::withinPositionGoalTolerance( - threshold_to_consider_, data.state.pose.pose, goal)) - { - return; - } + geometry_msgs::msg::Pose goal = utils::getLastPathPose(data.path); auto goal_x = goal.position.x; auto goal_y = goal.position.y; diff --git a/nav2_mppi_controller/src/critics/obstacles_critic.cpp b/nav2_mppi_controller/src/critics/obstacles_critic.cpp index db2972dfc9e..0577db0a76f 100644 --- a/nav2_mppi_controller/src/critics/obstacles_critic.cpp +++ b/nav2_mppi_controller/src/critics/obstacles_critic.cpp @@ -24,8 +24,6 @@ namespace mppi::critics void ObstaclesCritic::initialize() { auto getParentParam = parameters_handler_->getParamGetter(parent_name_); - getParentParam(enforce_path_inversion_, "enforce_path_inversion", false); - auto getParam = parameters_handler_->getParamGetter(name_); getParam(consider_footprint_, "consider_footprint", false); getParam(power_, "cost_power", 1); @@ -130,11 +128,9 @@ void ObstaclesCritic::score(CriticData & data) possible_collision_cost_ = findCircumscribedCost(costmap_ros_); } - geometry_msgs::msg::Pose goal = utils::getCriticGoal(data, enforce_path_inversion_); - // If near the goal, don't apply the preferential term since the goal is near obstacles bool near_goal = false; - if (utils::withinPositionGoalTolerance(near_goal_distance_, data.state.pose.pose, goal)) { + if (data.state.local_path_length < near_goal_distance_) { near_goal = true; } diff --git a/nav2_mppi_controller/src/critics/path_align_critic.cpp b/nav2_mppi_controller/src/critics/path_align_critic.cpp index 3ea13c641ab..befa9e42eb9 100644 --- a/nav2_mppi_controller/src/critics/path_align_critic.cpp +++ b/nav2_mppi_controller/src/critics/path_align_critic.cpp @@ -20,12 +20,9 @@ namespace mppi::critics void PathAlignCritic::initialize() { auto getParentParam = parameters_handler_->getParamGetter(parent_name_); - getParentParam(enforce_path_inversion_, "enforce_path_inversion", false); - auto getParam = parameters_handler_->getParamGetter(name_); getParam(power_, "cost_power", 1); getParam(weight_, "cost_weight", 10.0f); - getParam(max_path_occupancy_ratio_, "max_path_occupancy_ratio", 0.07f); getParam(offset_from_furthest_, "offset_from_furthest", 20); getParam(trajectory_point_step_, "trajectory_point_step", 4); @@ -42,16 +39,7 @@ void PathAlignCritic::initialize() void PathAlignCritic::score(CriticData & data) { - if (!enabled_) { - return; - } - - geometry_msgs::msg::Pose goal = utils::getCriticGoal(data, enforce_path_inversion_); - - // Don't apply close to goal, let the goal critics take over - if (utils::withinPositionGoalTolerance( - threshold_to_consider_, data.state.pose.pose, goal)) - { + if (!enabled_ || data.state.local_path_length < threshold_to_consider_) { return; } diff --git a/nav2_mppi_controller/src/critics/path_angle_critic.cpp b/nav2_mppi_controller/src/critics/path_angle_critic.cpp index 96ceae1b93b..6f42d33b042 100644 --- a/nav2_mppi_controller/src/critics/path_angle_critic.cpp +++ b/nav2_mppi_controller/src/critics/path_angle_critic.cpp @@ -23,7 +23,6 @@ namespace mppi::critics void PathAngleCritic::initialize() { auto getParentParam = parameters_handler_->getParamGetter(parent_name_); - getParentParam(enforce_path_inversion_, "enforce_path_inversion", false); float vx_min; getParentParam(vx_min, "vx_min", -0.35); if (fabs(vx_min) < 1e-6f) { // zero @@ -62,15 +61,7 @@ void PathAngleCritic::initialize() void PathAngleCritic::score(CriticData & data) { - if (!enabled_) { - return; - } - - geometry_msgs::msg::Pose goal = utils::getCriticGoal(data, enforce_path_inversion_); - - if (utils::withinPositionGoalTolerance( - threshold_to_consider_, data.state.pose.pose, goal)) - { + if (!enabled_ || data.state.local_path_length < threshold_to_consider_) { return; } diff --git a/nav2_mppi_controller/src/critics/path_follow_critic.cpp b/nav2_mppi_controller/src/critics/path_follow_critic.cpp index 4f8c1c70eec..240c49725d6 100644 --- a/nav2_mppi_controller/src/critics/path_follow_critic.cpp +++ b/nav2_mppi_controller/src/critics/path_follow_critic.cpp @@ -22,10 +22,7 @@ namespace mppi::critics void PathFollowCritic::initialize() { auto getParentParam = parameters_handler_->getParamGetter(parent_name_); - getParentParam(enforce_path_inversion_, "enforce_path_inversion", false); - auto getParam = parameters_handler_->getParamGetter(name_); - getParam( threshold_to_consider_, "threshold_to_consider", 1.4f); @@ -40,12 +37,7 @@ void PathFollowCritic::score(CriticData & data) return; } - geometry_msgs::msg::Pose goal = utils::getCriticGoal(data, enforce_path_inversion_); - - if (data.path.x.size() < 2 || - utils::withinPositionGoalTolerance( - threshold_to_consider_, data.state.pose.pose, goal)) - { + if (data.path.x.size() < 2 || data.state.local_path_length < threshold_to_consider_) { return; } diff --git a/nav2_mppi_controller/src/critics/prefer_forward_critic.cpp b/nav2_mppi_controller/src/critics/prefer_forward_critic.cpp index 47c84321559..20c58324365 100644 --- a/nav2_mppi_controller/src/critics/prefer_forward_critic.cpp +++ b/nav2_mppi_controller/src/critics/prefer_forward_critic.cpp @@ -22,8 +22,6 @@ namespace mppi::critics void PreferForwardCritic::initialize() { auto getParentParam = parameters_handler_->getParamGetter(parent_name_); - getParentParam(enforce_path_inversion_, "enforce_path_inversion", false); - auto getParam = parameters_handler_->getParamGetter(name_); getParam(power_, "cost_power", 1); getParam(weight_, "cost_weight", 5.0f); @@ -41,11 +39,7 @@ void PreferForwardCritic::score(CriticData & data) return; } - geometry_msgs::msg::Pose goal = utils::getCriticGoal(data, enforce_path_inversion_); - - if (utils::withinPositionGoalTolerance( - threshold_to_consider_, data.state.pose.pose, goal)) - { + if (data.state.local_path_length < threshold_to_consider_) { return; } diff --git a/nav2_mppi_controller/src/critics/twirling_critic.cpp b/nav2_mppi_controller/src/critics/twirling_critic.cpp index c9cfc79233c..2c6842c3a26 100644 --- a/nav2_mppi_controller/src/critics/twirling_critic.cpp +++ b/nav2_mppi_controller/src/critics/twirling_critic.cpp @@ -22,10 +22,7 @@ namespace mppi::critics void TwirlingCritic::initialize() { auto getParentParam = parameters_handler_->getParamGetter(parent_name_); - getParentParam(enforce_path_inversion_, "enforce_path_inversion", false); - auto getParam = parameters_handler_->getParamGetter(name_); - getParam(power_, "cost_power", 1); getParam(weight_, "cost_weight", 10.0f); @@ -39,12 +36,14 @@ void TwirlingCritic::score(CriticData & data) return; } - geometry_msgs::msg::Pose goal = utils::getCriticGoal(data, enforce_path_inversion_); + if (data.goal_checker != nullptr) { + geometry_msgs::msg::Pose pose_tolerance; + geometry_msgs::msg::Twist velocity_tolerance; + data.goal_checker->getTolerances(pose_tolerance, velocity_tolerance); - if (utils::withinPositionGoalTolerance( - data.goal_checker, data.state.pose.pose, goal)) - { - return; + if (data.state.local_path_length < pose_tolerance.position.x) { + return; + } } if (power_ > 1u) { diff --git a/nav2_mppi_controller/src/optimizer.cpp b/nav2_mppi_controller/src/optimizer.cpp index 97a16249814..013b29de502 100644 --- a/nav2_mppi_controller/src/optimizer.cpp +++ b/nav2_mppi_controller/src/optimizer.cpp @@ -257,6 +257,7 @@ void Optimizer::prepare( { state_.pose = robot_pose; state_.speed = robot_speed; + state_.local_path_length = nav2_util::geometry_utils::calculate_path_length(plan); path_ = utils::toTensor(plan); costs_.setZero(); goal_ = goal; diff --git a/nav2_mppi_controller/test/critics_tests.cpp b/nav2_mppi_controller/test/critics_tests.cpp index 813cba14318..88e823019a7 100644 --- a/nav2_mppi_controller/test/critics_tests.cpp +++ b/nav2_mppi_controller/test/critics_tests.cpp @@ -251,16 +251,19 @@ TEST(CriticTests, GoalAngleCritic) goal.orientation.y = 0.0; goal.orientation.z = 1.0; goal.orientation.w = 0.0; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); critic.score(data); EXPECT_NEAR(costs.sum(), 0, 1e-6); // Let's move it even closer, just to be sure it still doesn't trigger state.pose.pose.position.x = 9.2; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); critic.score(data); EXPECT_NEAR(costs.sum(), 0, 1e-6); // provide state pose and path below `threshold_to_consider` to consider state.pose.pose.position.x = 9.7; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); critic.score(data); EXPECT_GT(costs.sum(), 0); EXPECT_NEAR(costs(0), 9.42, 0.02); // (3.14 - 0.0) * 3.0 weight @@ -305,6 +308,7 @@ TEST(CriticTests, GoalCritic) path.x(9) = 10.0; path.y(9) = 0.0; goal.position.x = 10.0; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); critic.score(data); EXPECT_NEAR(costs(2), 0.0, 1e-6); // (0 * 5.0 weight EXPECT_NEAR(costs.sum(), 0.0, 1e-6); // Should all be 0 * 1000 @@ -314,6 +318,7 @@ TEST(CriticTests, GoalCritic) path.x(9) = 0.5; path.y(9) = 0.0; goal.position.x = 0.5; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); critic.score(data); EXPECT_NEAR(costs(2), 2.5, 1e-6); // (sqrt(10.0 * 10.0) * 5.0 weight EXPECT_NEAR(costs.sum(), 2500.0, 1e-3); // should be 2.5 * 1000 @@ -360,12 +365,14 @@ TEST(CriticTests, PathAngleCritic) state.pose.pose.position.y = 0.0; path.x(9) = 0.15; goal.position.x = 0.15; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); critic.score(data); EXPECT_NEAR(costs.sum(), 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; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); 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; @@ -478,12 +485,14 @@ TEST(CriticTests, PreferForwardCritic) state.pose.pose.position.x = 1.0; path.x(9) = 10.0; goal.position.x = 10.0; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); critic.score(data); EXPECT_NEAR(costs.sum(), 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.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); state.vx.setOnes(); critic.score(data); EXPECT_NEAR(costs.sum(), 0.0f, 1e-6f); @@ -536,12 +545,14 @@ TEST(CriticTests, TwirlingCritic) state.pose.pose.position.x = 1.0; path.x(9) = 10.0; goal.position.x = 10.0; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); critic.score(data); EXPECT_NEAR(costs.sum(), 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.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); state.wz.setZero(); critic.score(data); EXPECT_NEAR(costs.sum(), 0.0, 1e-6); @@ -601,6 +612,7 @@ TEST(CriticTests, PathFollowCritic) state.pose.pose.position.x = 2.0; path.x(5) = 1.8; goal.position.x = 1.8; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); critic.score(data); EXPECT_NEAR(costs.sum(), 0.0, 1e-6); @@ -608,6 +620,7 @@ TEST(CriticTests, PathFollowCritic) // pose differential is (0, 0) and (0.15, 0) path.x(5) = 0.15; goal.position.x = 0.15; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); critic.score(data); EXPECT_NEAR(costs.sum(), 750.0, 1e-2); // 0.15 * 5 weight * 1000 } @@ -653,6 +666,7 @@ TEST(CriticTests, PathAlignCritic) state.pose.pose.position.x = 1.0; path.x(9) = 0.85; goal.position.x = 0.85; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); critic.score(data); EXPECT_NEAR(costs.sum(), 0.0, 1e-6); @@ -660,6 +674,7 @@ TEST(CriticTests, PathAlignCritic) // but data furthest point reached is 0 and offset default is 20, so returns path.x(9) = 0.15; goal.position.x = 0.15; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); critic.score(data); EXPECT_NEAR(costs.sum(), 0.0, 1e-6); @@ -668,6 +683,7 @@ TEST(CriticTests, PathAlignCritic) *data.furthest_reached_path_point = 21; path.x(9) = 0.15; goal.position.x = 0.15; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); critic.score(data); EXPECT_NEAR(costs.sum(), 0.0, 1e-6); @@ -699,6 +715,7 @@ TEST(CriticTests, PathAlignCritic) path.x(20) = 0.9; path.x(21) = 0.9; goal.position.x = 0.9; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); generated_trajectories.x.setConstant(0.66f); critic.score(data); // 0.66 * 1000 * 10 weight * 6 num pts eval / 6 normalization term @@ -719,6 +736,7 @@ TEST(CriticTests, PathAlignCritic) path.x.setConstant(1.5f); path.y.setConstant(1.5f); goal.position.x = 1.5; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); critic.score(data); EXPECT_NEAR(costs.sum(), 0.0, 1e-6); } diff --git a/nav2_mppi_controller/test/optimizer_smoke_test.cpp b/nav2_mppi_controller/test/optimizer_smoke_test.cpp index 733229b77df..c88422f3a84 100644 --- a/nav2_mppi_controller/test/optimizer_smoke_test.cpp +++ b/nav2_mppi_controller/test/optimizer_smoke_test.cpp @@ -22,6 +22,7 @@ #include #include #include "tf2_ros/buffer.hpp" +#include "nav2_util/geometry_utils.hpp" #include "nav2_mppi_controller/optimizer.hpp" #include "nav2_mppi_controller/tools/parameters_handler.hpp" @@ -79,7 +80,8 @@ TEST_P(OptimizerSuite, OptimizerTest) { auto goal = path.poses.back().pose; nav2_core::GoalChecker * dummy_goal_checker{nullptr}; - auto [cmd, trajectory] = optimizer->evalControl(pose, velocity, path, goal, dummy_goal_checker); + auto [cmd, trajectory] = optimizer->evalControl(pose, velocity, path, goal, + dummy_goal_checker); EXPECT_GT(trajectory.rows(), 0); EXPECT_GT(trajectory.cols(), 0); } diff --git a/nav2_mppi_controller/test/utils_test.cpp b/nav2_mppi_controller/test/utils_test.cpp index 6f70e19cfa5..70733e37db4 100644 --- a/nav2_mppi_controller/test/utils_test.cpp +++ b/nav2_mppi_controller/test/utils_test.cpp @@ -116,56 +116,6 @@ TEST(UtilsTests, ConversionTests) 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; - Eigen::ArrayXf 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 @@ -650,61 +600,6 @@ TEST(UtilsTests, getLastPathPoseTest) EXPECT_NEAR(last_path_pose.orientation.w, 0.0, 1e-3); } -TEST(UtilsTests, getCriticGoalTest) -{ - geometry_msgs::msg::Pose pose; - pose.position.x = 10.0; - pose.position.y = 1.0; - - nav_msgs::msg::Path path; - path.poses.resize(10); - path.poses[9].pose.position.x = 5.0; - path.poses[9].pose.position.y = 50.0; - path.poses[9].pose.orientation.x = 0.0; - path.poses[9].pose.orientation.y = 0.0; - path.poses[9].pose.orientation.z = 1.0; - path.poses[9].pose.orientation.w = 0.0; - - geometry_msgs::msg::Pose goal; - goal.position.x = 6.0; - goal.position.y = 60.0; - goal.orientation.x = 0.0; - goal.orientation.y = 0.0; - goal.orientation.z = 0.0; - goal.orientation.w = 1.0; - - // Create CriticData with state and goal initialized - models::State state; - state.pose.pose = pose; - models::Trajectories generated_trajectories; - models::Path path_t = toTensor(path); - Eigen::ArrayXf costs; - float model_dt; - CriticData data = { - state, generated_trajectories, path_t, goal, - costs, model_dt, false, nullptr, nullptr, std::nullopt, std::nullopt}; - - bool enforce_path_inversion = true; - geometry_msgs::msg::Pose target_goal = utils::getCriticGoal(data, enforce_path_inversion); - - EXPECT_EQ(target_goal.position.x, 5); - EXPECT_EQ(target_goal.position.y, 50); - EXPECT_NEAR(target_goal.orientation.x, 0.0, 1e-3); - EXPECT_NEAR(target_goal.orientation.y, 0.0, 1e-3); - EXPECT_NEAR(target_goal.orientation.z, 1.0, 1e-3); - EXPECT_NEAR(target_goal.orientation.w, 0.0, 1e-3); - - enforce_path_inversion = false; - target_goal = utils::getCriticGoal(data, enforce_path_inversion); - - EXPECT_EQ(target_goal.position.x, 6); - EXPECT_EQ(target_goal.position.y, 60); - EXPECT_NEAR(target_goal.orientation.x, 0.0, 1e-3); - EXPECT_NEAR(target_goal.orientation.y, 0.0, 1e-3); - EXPECT_NEAR(target_goal.orientation.z, 0.0, 1e-3); - EXPECT_NEAR(target_goal.orientation.w, 1.0, 1e-3); -} - int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); From f0403189ae3a7fec7382fe1bf1ae7bb081d11d1b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 18:45:35 -0700 Subject: [PATCH 46/51] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20Bump=20actions/se?= =?UTF-8?q?tup-python=20from=205=20to=206=20(#5510)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 337b0966f0d..895bc2152c3 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 - uses: pre-commit/action@v3.0.1 env: SKIP: >- From 627a8b6cb2f9140f5412272c19f562351319bb26 Mon Sep 17 00:00:00 2001 From: Tony Najjar Date: Fri, 5 Sep 2025 21:13:48 +0200 Subject: [PATCH 47/51] Add debug topic to visualize whether MPPI critic has an effect on costs (#5485) * Publish criticsStats Signed-off-by: Tony Najjar * linting Signed-off-by: Tony Najjar * change header to stamp Signed-off-by: Tony Najjar * make unique_pointer Signed-off-by: Tony Najjar * typo Signed-off-by: Tony Najjar * Add readme Signed-off-by: Tony Najjar * add to readme Signed-off-by: Tony Najjar * fixes Signed-off-by: Tony Najjar --------- Signed-off-by: Tony Najjar --- nav2_mppi_controller/CMakeLists.txt | 2 + nav2_mppi_controller/README.md | 22 +++++++++ .../nav2_mppi_controller/critic_manager.hpp | 4 ++ nav2_mppi_controller/media/critics_stats.png | Bin 0 -> 119317 bytes nav2_mppi_controller/package.xml | 1 + nav2_mppi_controller/src/critic_manager.cpp | 45 +++++++++++++++++- nav2_msgs/CMakeLists.txt | 1 + nav2_msgs/msg/CriticsStats.msg | 5 ++ .../gps_navigation/nav2_no_map_params.yaml | 1 + 9 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 nav2_mppi_controller/media/critics_stats.png create mode 100644 nav2_msgs/msg/CriticsStats.msg diff --git a/nav2_mppi_controller/CMakeLists.txt b/nav2_mppi_controller/CMakeLists.txt index 931af234887..c28c711dc50 100644 --- a/nav2_mppi_controller/CMakeLists.txt +++ b/nav2_mppi_controller/CMakeLists.txt @@ -8,6 +8,7 @@ find_package(geometry_msgs REQUIRED) find_package(nav2_common REQUIRED) find_package(nav2_core REQUIRED) find_package(nav2_costmap_2d REQUIRED) +find_package(nav2_msgs REQUIRED) find_package(nav2_util REQUIRED) find_package(nav_msgs REQUIRED) find_package(pluginlib REQUIRED) @@ -59,6 +60,7 @@ target_link_libraries(mppi_controller PUBLIC nav2_ros_common::nav2_ros_common nav2_costmap_2d::layers nav2_costmap_2d::nav2_costmap_2d_core + ${nav2_msgs_TARGETS} ${nav_msgs_TARGETS} pluginlib::pluginlib rclcpp::rclcpp diff --git a/nav2_mppi_controller/README.md b/nav2_mppi_controller/README.md index 68db5b73719..630aa19f214 100644 --- a/nav2_mppi_controller/README.md +++ b/nav2_mppi_controller/README.md @@ -59,6 +59,7 @@ This process is then repeated a number of times and returns a converged solution | retry_attempt_limit | int | Default 1. Number of attempts to find feasible trajectory on failure for soft-resets before reporting failure. | | regenerate_noises | bool | Default false. Whether to regenerate noises each iteration or use single noise distribution computed on initialization and reset. Practically, this is found to work fine since the trajectories are being sampled stochastically from a normal distribution and reduces compute jittering at run-time due to thread wake-ups to resample normal distribution. | | publish_optimal_trajectory | bool | Publishes the full optimal trajectory sequence each control iteration for downstream control systems, collision checkers, etc to have context beyond the next timestep. | + | publish_critics_stats | bool | Default false. Whether to publish statistics about each critic's performance. When enabled, publishes a `nav2_msgs::msg::CriticsStats` message containing critic names, whether they changed costs, and the sum of costs added by each critic. Useful for debugging and tuning critic behavior. | #### Trajectory Visualizer @@ -293,6 +294,7 @@ controller_server: |---------------------------|----------------------------------|-----------------------------------------------------------------------| | `trajectories` | `visualization_msgs/MarkerArray` | Randomly generated trajectories, including resulting control sequence | | `transformed_global_plan` | `nav_msgs/Path` | Part of global plan considered by local planner | +| `critics_stats` | `nav2_msgs/CriticsStats` | Statistics about each critic's performance (published when `publish_critics_stats` is enabled) | ## Notes to Users @@ -328,6 +330,26 @@ As you increase or decrease your weights on the Obstacle, you may notice the afo Once you have your obstacle avoidance behavior tuned and matched with an appropriate path following penalty, tune the Path Align critic to align with the path. If you design exact-path-alignment behavior, its possible to skip the obstacle critic step as highly tuning the system to follow the path will give it less ability to deviate to avoid obstacles (though it'll slow and stop). Tuning the critic weight for the Obstacle critic high will do the job to avoid near-collisions but the repulsion weight is largely unnecessary to you. For others wanting more dynamic behavior, it _can_ be beneficial to slowly lower the weight on the obstacle critic to give the path alignment critic some more room to work. If your path was generated with a cost-aware planner (like all provided by Nav2) and providing paths sufficiently far from obstacles for your satisfaction, the impact of a slightly reduced Obstacle critic with a Path Alignment critic will do you well. Not over-weighting the path align critic will allow the robot to deviate from the path to get around dynamic obstacles in the scene or other obstacles not previous considered during path planning. It is subjective as to the best behavior for your application, but it has been shown that MPPI can be an exact path tracker and/or avoid dynamic obstacles very fluidly and everywhere in between. The defaults provided are in the generally right regime for a balanced initial trade-off. +### Critic costs debugging + +The `publish_critics_stats` parameter enables publishing of statistics about each critic's performance, which can be visualized using tools like PlotJuggler or Foxglove to analyze and debug critic behavior. + +The published `nav2_msgs::msg::CriticsStats` message contains the following fields: + +- **stamp**: Timestamp of when the statistics were computed +- **critics**: Array of critic names that were evaluated (e.g., "ConstraintCritic", "GoalCritic", "ObstaclesCritic") +- **changed**: Boolean array indicating whether each critic modified the trajectory costs. `true` means the critic added non-zero costs, `false` means it had no effect +- **costs_sum**: Array of the total cost contribution from each critic. This represents the sum of all costs added by that specific critic across all trajectory candidates + +This data is invaluable for understanding: +- Which critics are actively influencing trajectory selection +- The relative impact of each critic on the final trajectory choice +- Whether critics are working as expected or if parameter tuning is needed (e.g. `threshold_to_consider`) + +More detailed statistics could be added in the future. + +![critics_stats](media/critics_stats.png) + ### MFMA and AVX2 Optimizations This MPPI is made possible to run on CPU-only by using a very well optimized implementation that rely on CPU vectorization through AVX2 and MFMA. All even remotely modern computers support this (2013+), but if using a very old computer you may not be able to use the plugin. Note that MPC is computationally heavy to begin with, so computers circa-2013 even if it were to have those compiler flags available probably wouldn't run it at a satisfactory rate anyway. diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/critic_manager.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/critic_manager.hpp index fed85867905..237649bedde 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/critic_manager.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/critic_manager.hpp @@ -22,6 +22,7 @@ #include "geometry_msgs/msg/twist.hpp" #include "geometry_msgs/msg/twist_stamped.hpp" +#include "nav2_msgs/msg/critics_stats.hpp" #include "nav2_costmap_2d/costmap_2d_ros.hpp" #include "rclcpp_lifecycle/lifecycle_node.hpp" @@ -96,6 +97,9 @@ class CriticManager std::unique_ptr> loader_; Critics critics_; + nav2::Publisher::SharedPtr critics_effect_pub_; + bool publish_critics_stats_; + rclcpp::Logger logger_{rclcpp::get_logger("MPPIController")}; }; diff --git a/nav2_mppi_controller/media/critics_stats.png b/nav2_mppi_controller/media/critics_stats.png new file mode 100644 index 0000000000000000000000000000000000000000..c373f8e0dcc5edfb00b450e2f6c7d24ea562eec5 GIT binary patch literal 119317 zcmdSBbyU<{+xJaLD5WS0Qi>7+A`C;9A|Q=~bi*JF2!cqLDBV3nh(R|)NQtD>fD%KO z0!ky@JbN(C^NRaUV6zQ<=1q^c}ScAEAy4h{~P+`YSMI5>os zI5>EG#CYHpODgLW@CUA=n(Q5%f-br_92_Pbxx2SDTyPg_ceTzByx;ad_F?7d;Tf2o z4e}D{;X~)$F{kXKqrTbhFQED_!77MkWOf@zOW6W{sr{L+Oll;7sqTAHZBvhLFH zkSBJ{9FTNV=qwZ@t@v_T+&3`L*kwmgGHB&Hu%%-;u{U(AGq_w%QUz!?3cn2GB5 zyKo3Rd~xz<@@W1xL|R;Jh!Jt%WAZ;wF{LTsVDfrjn~&}LD*hV9C(1-VD+ebhv=xPS zPA;5937I!{Izap$4yKdH?zA$=U*A9sK2IC+>#p6bcOAV+%9c^k;!2aWJG9(qCWZfs_;> zo1MbNqn}^}>#@I0Y)SlU;!I3j;JaQPDaj4H2Av@~SvghWENLAJi>k~My4g6OHcWwcy04cW=V;f z!32HQvuANmP8H6}oX%SHf^`MUPaW6m0*%#<2SUVyxEFd^g_+Irexwm%IE7rX|lp|B9_^2BBQ8Bdi2aq zXQz{6Bi@W|(}}r^mM%^|+`cf3R~~^n{j@H8@7|)8x@U>C(o==SZ!FX6Y!cSrpXFbd z9tyfo9xxwk%9~)zVH7{)#>FY~<9nT^WBHSM66+}-F_g27~n>*C^UI^WJ zL)B|Z9jj6BQdF>p8IC!3!B#JIJVGhd!aj=aZ!5xe={`(OPA)EA!h8a;SJ#xmt~Ba9 zT#2elGoI;6nL(qE*GzkL490AG9B-IGi=pg}ly6aXlxua@S}&51Di< z?a;?MOMdEye$#f;1MkC4EwFx1mpLUKe~Hc9Qxu}{o8ps}!Q@@v8?Ln+$!W9lGZSY> zPOYdk9f^)7=3;Twc|4bFzK!AJ<8j|y-mLT+J2cCjrK1Zk%8I?-+?+c|>ujr+(atJ#&oCsPCQGiqU3$!Yp;fsGT8g+ zs=mtPAsWxd14Hg5Go9v<31AN8>&p^|V6a;RW^w&G7qgj-Ov_+wY^k_s=#~tP9;7UX z*&Thfx4<2PQH-)EAmPq?-GHI@P{zy-v2`zLFr$hIX!!`cnd?$3l4m;A z$|kGrVv0Fv7}M-$g>S`ob>AbU2t(_tue*I6Ebk_i^%pW#%b2aW`-q4`KA}wJ5z_IK zF6x1vR=J7ekAgRvmou6JWScX+%QJS2l{VY_nsZBt$_EQ}f}}0yK&)JvAcRt z3yo@PZC2Wlht&g$wj@yc@S?eNKdqG}u0uE=-acczn{MUS2OdQR^d*CN!96aUxABq!jz$|)gXgnFf@e%tf=%}1)T zq-||d;BX1lWe49iJ9UafC0S5!|MD2wR{SWHYns^DY!HjRD3F$R5z(y(yPr&grU z>wsz7ny84R7O4oR3M4GK}} z)Gm6jpFXA0TQ+8h0*+^fh+=tI)U0ok+U!D%P+ieO<(5y0iA@_q>0HN&v<%b|j~z8N zE$XMEelSvWB^&9M3$u#zaeCdkKNLTMz&A5Him%PGUz)yszc3#$8>?HRoeS*^yYWVC zHi033_}w)}Rnvn{R&aUwZ=_*Q@1}PSmAHE@x9%i1#nQkBx;xA_NwXK0q{x4Tn;mYj z`1Io<5nuSIFpFP1rJI*hKP|=x+IqIlwsEawJ{$^)7qo(qq#C1zYXq`>M=?W6RlQqi zcXXe0m6da2c0Nt^n<3uWf-A16KbnHnY2g`{_#hZX*KKc^7H#QB;J29y`LRw>z!|wf z@(u+nfQq5-moISKkB$ENf|t2F;D&*d3$H&o@0j3jm)h=J>yDhZX-x}jct&DjR@0TQ z@izHvDLicQhl1uVUrINdrH6h@)6;&InvJ{2-4&{)TXww^!|A<^8Dd8gi8e|wAjQob zrGG@nW>Lc*l$*dz7tYw~4L`bqkiL)(M-kNyK_1$mb-U2eoKR&%o$YqX{rr^Ytjae> zYrRZOI|xf{UMh_o)mP*c*$Ov`U`EAlWAfKb6GP9ai9O z$Qc$njpS@34yYeQ!EnQ(taKT?t`r@@W6-u3nMHg|Nn%~168+M6N& z*=0UHOsHs2VoXeK4RND^R5LOPtuNvnA7d#Rw>{zf5FL1)nZm3J1#_J3bP~h$!i3rg zz#knY3`a%P!sh7))O*lY;Gb*$(6Q6V&hWgUg+LBb*u&XH^o1EX>dSE504JZ@>dvWC zmpeM-+bbS%<_~HLf3;i6*I9e^WuWl$!~KHHeqru?=VDmSicc_IGNQt|w|vZ^c}7oQ zWn8^yZEGZ+mG4YuRH3i6Rdra+IRXCb&ixtD?ZMGY9S-W_qOay)g*xvZX=HnOL4Yqt zFUMb8EEf$#ig_!!N=sY(Kwd`_9`9~J>B!XRp8d?jd`bxdp;F1SkMLY*Gb_trg>__- zhju!)6NG7~fxIG%H{yAS>gs16zfzL4D{1AhxVX68-Q8Iu+u29@&BzPq^K2d+X@NMi zcK?XI=VaNOUxQpAr(l_XS4O#{I!6jVYyX_Rx~)|mF-@YhR+`axHe>N_Z?c~+wAb^J zx`28wYbD)`Nhio5&iZO=IVYK0Yaro{dvwfjYA7FvfaSdr2~FYGGF=mD@7cI?4{dR# z@GT$R7!%*zWGKyhW_l5hsT#;@F3bKZoKWfug?lbL$jL8Qm7Xq%x4ZVK0%kkr&kExj zE~#dMV7}JEUI_j8Y};)otjh?sMZpTYCHUN0`_LAb^(F=|{K-GkU!DIIpr@}$=FxoN z^LHLy72)eIzLx|&W9NwvOnGrNOnopKqSm@?C39U_UvF!vE%-X( z{;SVYFq?vhu7s!z8-vf79!xC^*&e;iFGr6g|W5wfdxw*^Q%qXUrrdOv;dVb08Pm!*m*xp7;G-GZC|-#hXso z!f0q{K-x!hQv#gB`MqW3GLZ5ac^}SXpHp*Vel`c(K9a(9P(2&Q5|t#9uk!NY3_{6| zg%A@;ERxP8v~g#AX;eo!53y@^UXxJsTzhY!eE8#rK_pFuH2Ytr&qHD+l&J6j{dTUy166OcF&T-_V)I4-5<-Rz2p0U-9|Q%iv%XFIkfk{I0EV@KVqDK-cFc zVnu6HvVMLOh}Dxu;FAs(yryh!|E_1>g36T;PWab@DcH}-X}x^$%0T{W0RP>dQ)j?qN~iwA$7m(If=z@zHYn2}ziRH953C)CV=8y*buM}TUM+b7 z=Mw0`5bE2~$mYz9QH-9Rp8L|luEKQh8(E66{aTx$H6)T53YCq&X&`gs`>ckRx0bY)@m29xiT)aK6LyVb9UnXh&s5UxUuk-!WALnQ!_UaGd6eCrx>4sw zGZSg{vSeaUm9~p{yI)mIzRvy68A?g~E2BEMm%q&QT87zKK6#?(wCVh|D~kE!-quoP zlH-m&S@!yvv+VxX(zjvDIQk)HSDBgXj*rZBhl!h! zDZ)-II}F{aheJr{i?_-qQ&l=BSgzyy9aFA0o!Y%KYL;kZ0|H`Mxh3Q{yQrCqVRKqr zgH>T}|4ypGAtq*;6v5=lMr^xnmgwgDiXR1qDV_87Cznm^r~Tgisn$)$eZxiNWJr@* zMmz%NvDPckLoAgeLYYx|lbQjsmbR=2f1tT(tFM0$c$T@93OQEoT<7wGal!+&Fj%~j zz|}EOTi6lL#m%N&`S@#sv~<$x(+_SMR4KL%6`3%%=~VG~HwQl>yUbr;WK`<08{8a{ z@a}1u%a5LegPDbai}`vLS0uc5UrUD=EHM3T4+$t&&%!aa`_*2rI&0LmHsaB6IIN6t zYu-cHBn?;AOE$%hM{rKFTBjBnbQZ1(5gqk2;edjMz3 zL%5Y7H!7gV?(;g`%M(#7Vi?MO)S9rSr zXbCe~9zqqCZw<$?S2B96)(;kcz4zklM-?`VO=lv{`Yf7tx5|!3ef-H5)^IoGMhcBK z>iq)RIE2U z6*_L@QGSm^@j6AVB7H>9p(0>EeeNZ89g-&Jl}$O zfWaxq%g3E2qF{L7jh-p7=;(e~_hflgdS;&|flJS8d9)d2$<5YJ&21=b*3wy2bT5Fp zZ;F_VoSf%qa49^ENmf<02n};SBsg`d+DkvT7;SYqDp|XuvQk8>{A)WVNv^BbePrDG zxGq*G=(#h&s&k_AsHpX+R)(pyDqcbNur3d&Y^MD{=mI7`>C!i8iC_s`)T6PgdL+UK z_wAl%Co^X!heFvPZ|1lA0g_8+Zx(X+p$5K-+Okntmp?|n;tT1kfYFa9Iexpju{eZk zw4OhWi;G)de}&cpRw2?VYxe10hHtzz701#jja_vgKU*Fm{G&PXgy^^NT3h0${od_0 z_c~BTZny!ORFW|1o_CD){IiX3HEy)pyy3w=YHZRltXDRt3MRSQp1j9l=m?Hj|iVX_an(l{UmL zyZH^6nGcaI)?g5kpu>2(&T>Dy?2pZ786xWGutIG! zDWyj)eT;5XVky` zO({vhPOoe}1Pt;bx8brX|J;L}{Rvi~6!hVqXP#b;c{t-sFoztA zs`D|D)ne1L=&HRv|LY2C3i9oS?+-jj%1zTV`e_A?KYV!htREceAD$23#>U30{$xp4 zukK~#IwfMtV3{f}FN?W_RxN%1Y!>)d-4giaK{)Id#3|&hF2NI$z%~L94vGgz|F)TE ztvsEIM`Ob__e9iZZ}tun%$4&#x??oVpT(XeC6Qbz=F)^{sxE>VayPAU<{2`-wj*xc zAVqKBbivr}W1>#;jy&x25lx4N1b*-B+YsSV1&jZjgo@jss;9zE@yU~8d?Io&uY--% zsg*BZsq!%&^eUF=gf$!&FexJy-YF2v@w$}za&lHujg4UE;F!EJUHp_T*cy#6-wY;S z1h@#7)yW^R?7g#{i0wuS39xwX+shS0&p0@8fxRR}MovHBUl}gjZi|_?Y4je6L>_Gp zxZhiE45;g9L(D(n&&?|+3TbH>#%pv0ypQgLee0o{*F^zs-pR@N!ef57jZ2v*9U+ZvoPSx;zvVbZVZ9!o zk)QX|_=|g#KYVtEk4lZC6~R0h&uj)l@@QtxadtPzlLTi7G5t=))~YfzB8Ozm2s_#H zb=>wIW}<|+Y~Sh<>g4-oDyQUwxZ>e5TZExl z%5`h85`p*%0^>RQF{9dnMX+uHk4BMV?vG5rOagb&mB<7C{wX$r+xw-klm9T1URWb0 zo_}q;mZ;i!MSTUThk`9N2B`35-mMwjo_N`gPI+%!{{<{i4yYh7Hivs*DygCzY}#ec zD@Uv^fqNAMHe1Z%oQJ9QtOj!=xisy^7<7x zZ1Jt=4O=P>unwoRw4bg?;d{c0R|uNLVEoDA>3&*%625U zO`J0fy1&+uf7cy|@p=^n->2Ikg@%X44?nAwK|9*_s{3iRHJw($t1S6LJ3odVz|(x8 z)gG1QH3yf&9y>aJ??SSF=^VQW>AA9nr0G-~yP8dnYk+6Udy)g3_q+AB3g1M2!E`DL zWYFc0Y4d0xh0{@Jlk~`IiyxW0ieRz1`J!hyuCpO$eH5j-J@oAPyW1C#KWB6+vlM2l z^0444Et|-GuR=n>H5PIQ;iP^HP;Ect>pAn=48G-m0DPXbv=M{&K}zpXa-!fpGxWWg zsm5L;(yOpQJURxl2`rrt8-zJ?p^+|pWl(07hvf9gJ6-c3+!%WBSV@wiCwaR&I<}`H zp)fu^zPi^O>46WId5Xwp?C&dWHiYP=X_dmv{nZciaX_^E7&%#Qf{+`VE#}|w1*DaKk>JnmO*$owK(U^jV3@s~E#wsGx&*>+>kIJ&*UfV0XtJP^ zg7nWItVIQ5?S4qxWI}jLCylGQV-s=4O`%{};2b-h@;BIIQ(yEmbJ}p=^XjE8yWv|p zn_rs!6}2w@j9Qp{qr-!BZe`2$nU1ocAiq4!hv(1V8hO_quY9ArOv?a<&%6Rv*F%lh z`tjS1KhSdCp6QqX=ZNauIfhG@YCwPpxmXw-6Tl8SJbYh1y+VQzp{d2VyE(Amg0X3M z9wc#m&@t6G5k)mvcRUh6@sbZJl$%37ymTe_BKTPFWxbi*hkFa2>3p|t?QG0Bf}w!) z>>{Uj-Vc4G)L1Pnn3p+@tcfNMQQz6e6>WX?({&~mDv*V@)G_*@Zs9c$Mq6$gU(S@X z#U8Wwd)6N}7X%DzT%5ppuXL2i{Hz#^MhMt_28my*NdP^AsCKRE`UB_wFJFMH%B?1k zPF`(|db6;wA}hOHF|>G@-)3<_BAl-k!By-#N9NM1OqQo5Gubc`&F8j$}#eBWF? zLgtzi#>B=Zdgql>{*b`L;%5&t80ckyX;!8({L3u-q4<_21kcH?b31tw z!=v=Q-Sut>1%(d%Fu5e{xycb6YwK{sIxl8!yQelbqu*X~du%OQ7>?Tvj4RHTlFesj z3sspm%g11BT0#*Z`?eRjD=)9>dFYwm*GaNH*Q;{Y7Ni*dq%<(jCljHvYm5Xs0!2R;Ruot zM7G|w{;O#Ul8P*XAycGq!mb8l;>-6^`~7T_XH_tYZ#o0T_Q5O+PN^no^OV4bhN!?o zZ!c^Y_ta!P?9EoUY@Modk`{P$pl0M<_wF6vwQHR)6v$^MH|9{^x{bgLXAqE!(b0s% zHczX^ih;EO2mfe1kAqWxdqqhlfMP%}1q~dk?GnZ9+w}r=7;sd#ry5zC%(;vV?Z38g zWukUJ_)1Mpo#jvR>`rZOXxO}l{S;s#sp6i+@BzaB<|nO@CU0{w+d+x~f>UDdHz??H z-n*ifr`vje{0NF*lqefo+}z|+F$CWv^0YLt`Le*HFxogP)R4M*Zv>qXW^iXQ^QBiR zzpcN+?osr^>dGB*rL(!%B_&2aYQZrVFur8z2Qf=?8hQDMCYZOCkXM43X>d@6sjsW}?B*LPsx z1La^!ZP5qA!;Jo93iP*(CqW$^9HI~eb^r0D|FyJ&soB?ZJ8mJ59g!roI2@CltL3ez z%n*+h{?hP50VEKoPg_?EEP@OT1YwHH@z$uJ05SzJ_pLABIGCAz4QG_7blbd03kPT3 zvgd6wEJ!oti<8zry$WYa3UrbYJ6UmD!Vq z`M$>xQKVABpU!rkiWaignB}e;EgxD0*~2@hg`2LY)ry7YS^4b7^Bg^KAYz^x78d=f zVsW(aa;skI9&eMWJi@Wr;)ieF$q!e;pb$ZmbSmD{Af`DN4uJ|1m~nmI8h4F97W2;D~d{>?9gU6P4l zP&e|H;N#Qhk#%)NDJVFD$$wtt&8A1%;GDzTikLNO9ehj<3viCg06#aB*xE4OZ zpjKC8Uz>^D4zwNwZ3vkeK4im%v)E`#=P?nMiF&&SP0c+zZ~s)*-{k$z+99 zH0nl+!C@Omx`GxGf6`%3I)`Of@=I~C?kVEKMU?CIvRG?JlJq$I=}ddS*Wv5NMrPoU z<6}!}l#IYC=4isfi9tk0Ub=YE8-yMNM}&m1KIj-DsYby-Uf70CQ6A2jfR}r^%T2uhOvUQ8H(CF+ z^`7V}b{oW6oiC`!ecTCK@t71YTo0Ve775mf!R9f*J=1DKLYmJ}QhoxOQN@JUetyOH zp2YjZn8;i2P|BZrUro3zo}&{1|{pd4IW z@NF&}Kzw|A=^Y9+^Fattrhir20NzpFC@Mw>DPc))Dy|f`_0MN_utIh$wlE=INl{Pjj>h%cmK7 zgxRBeRd~$G%v_C9{=D#tZ23&^6Owyn#NW?R#*hGATp|6^^DF)I#Ok^i&qOGmIDKPu z>3-i6r7(nIgiSbt{Jw00k1Wuj*|hXrsUOIz@}Y4N{;9j=lQyYw1JLvnZOM526%_Jt zL0wu$Cq`t!L-_IcuBz!STCE)EG!&E6OyIQ$b}oN@5T`l&v8je1)DLPVBdO)U)BPMu zRh}bXRuud;hvtm$GeY7_Df?N12V?mslduj&K1nEsfu!uSKC-A$nND{Qy4NZRj!;S~ zUGj_iKhmOCrP*cB4dQW>VHl#Dy(yPUF7_p~7WRK#pXyNPLet#mAgRlvLpOB@`b-~j zU`H5dO8IxC9;g|=h8tSx2mM;?Q?;NDQd@dl++81#_v0B!!jyfmT8taeeC!peO%PC$ z;PfCnA@zeMtdpZxu0FVMB9DOC&xVSY2$>S7C(t!BcMZa2DjV8S zMQYDpTamZ&ZFE+gvRu!rDPaGphJbVjUyvo_EfT?ku+JgvOa5S8b;^>A(knm`9g$yU zY5$;i5*bef9T6l0m|Z9hE>AQ$CUikQXHGx$g{I@wRSH}kQd@iK4>~_RX*E6^hV#)p zN(0h+jY=7)H&oh66_a_m4J(vcyTzK@q+Qmu`n@M{?)Q@NJj+%H|8mxH5&A~|DbQ`$ zZ-3?^GN*N$9eIXwePB2c`%}db%0uAN#Yn566=M_yHSw*LjiF=DP=v&FQfadvAB;f+ zbFJ)^I(}9Spl%5Dxg}uot&BEJehg@m@gFKV^(r62VoUW zl4&i8Cdtdtv&Y~}ITQCC4Cc5KT0+SRtbSQ-q7XAM*h}?&*JXbMLbgC-X-z1-oArZ7rN&Y0|I%u2o8sTIzE0Df{@7nNKc%K+=5R(Ye;&$=CK=9hwGg zxR*;1?z1XFVC`koFXo3r5)PD#W(~Di6~Q`_qh$GILyhO7-?%i`*^1}%CrlkBKyjB(K!Jt z4ZrjFFDxt+0aP^?u9eNbTTUNKk{9A?&8{{j=f4cUgFm!#^F$Wi{dpASpb#bADGjnL zA>aNdXS!&a4#<6xjCxMplX)5SP_EzVgC$TO&N3%fO|vmPZZU+kTWF=eh&y*)$x$-n zmF0V`>)%#`vi-`r`6QB)q3l=oFY*C}vc9bGzu0fEIH1np*6Iv-yGNfWBo;%DSAy*)u?cM(ty#_XpKf>j zzJ|tH8{=r6-bn%zuLadWu4t5{h{?GFrOCYd)BDpXOu4 z%bKT4RqzoS2rT@vKs^j~@qC{6lPsjhJq3lA0^Meo--atmg>1L`&Yr(3z9*27_JImX zSQms~kDhlnhDf^f;hHmO80^2DCWX%i_dx^BmB0huC*#92$W@{U|8VfQj8~Y-puKF6 zFI_C--dw4D4OvQgfb>~h4BFh>-rwBUw$j!po$JenOxurDfyB-$yRgnkU!V5eInX0n zdiwFbrzQoXIA~~ME*j;58X5F@MSj=fAblRKJhOngP?k+6Tj#|C-UwQ}9(V4jNj3UO z1?0RGa~Fbj4uWF)%$Wghunfb(Dd;O#OxC|~3qD=EM&ow47ypV;%v#i?NElQNLSom5 zean;1z*)Ww$YpyGQR8aZkxF9TG>At;ucs}YDIUmctiD51*?8ocgO`VhxZ5H~_E)c2 zl`3)=$*$MQN1~O|$MPUFZ^)nQvh&jL!Y~Gg;?{{a(%Ww=GRej#Bo%d-)_(jr8x4C2 zg?IJmN&xXoNlvcXeo9TPM=}XrFb?0^m<8JwJaag*M^*}laSvODI!jDGs7uixUl6K z3(~sI(R1>xF#pvQ;Ud`l>Upcb2ETe1&tM2yHWc|ZMRN@0t3zG#VupI9jvM2k<#-JO{P5h;IC^#QFs#2CYuC;UW20pE2|Kn=f< zADTY|X4jQ0BugVK<{8^VNpdG`$-e;^v@>%*XTnz+r~fj3jCcGuJq{nLA?RHi*(zCMXoYx{}+vDI~ zJ5!lJq{vX|W=;==qJ|bD7@*{|8$kt7_k*46o}^-EwfnZAu5OhY4T%=`z^mR?vA~j} zjQh=SOMqqbUH^7_2r@TA2iJF*br5%qvIVBJnz0Z7z8wv9Ac(x#Fk%LjnPd9 z^K4*Nrjb?Qc&!DBF4L+G{!oacI@`}!_5qpQsQ&g3wIumLmiUPUoSK&Q(Se-dK0+p0 zsmAEA?IOzW*|6G+EIg_V`T`v3<8 zN{iQ_>DE%N+v?2_Usub&K>u+3^Gxb*==CowjVzx zal_bPB^-|dhqb!erSpWinhS>IeKcj2}LquSpnlg}8mcl4Bbs(}M zhN!0k36l*V@0hri4q`!c+yEMP2>yftiuIx}fBx&{5UCwyPyjLpg+8YJ3pPq)nPkM| zS$3yZ*bO8kcq_dgvFL~&=%dTd7Ako%$2;I6khW>LwJ0<-71i>xsHnB5C=e)Qk&*TR z%zT1^O^;Z+jmull$ElI9a9@OTKQOGmpcWJ7t43-7|FVffJaz8~%I?lrS{T#`Twtp_+2Wvq47;aqu!{_y?crB$?STO z_wE#HEuG81p`;!&^@{hwJ;1$Rv|L)EX#6}g*A4;QC8WfoRE94rw^V{$?=wc$`pimG z$#*p&Jt?i%O&_`me!{%MN0#otNeuLVkb1Y z=S0*Hi)n6PRzDHZq^zjt!k{6cp$A9%F;-U0fzTweeH0Xax{ZsQQ(pj%u|Y3U5EHke zQ_79&?=(kA%WQ=O_jp}p-j|Un&ngWJ%mSN)p|>n^8})^r0|U0ud{QnfYO_3ehEWl?thD{>NVD2K0IK5{XB2u>qQc5 z;&A;xPg583bu{}c9zXuUEECG4^6;i!IrhR1uwCj4wl~$oNDBxXI37J`dW}j=9O3Np)Xo}C2)e9R>x;+%G zz;HY6TOHPx{<(@&!yaruMaL>q@F9IF<3`FsnPvn^*D>NPlN5S1-BM zaEitUl4#0|+gg)0I03e2RZ%l@1p-*9(DnUE1sHmww=Ru@fbN4scdBPu*+7P3o71)- zsk+r*baQdl`VC)*zBSizD#dI2ake}t3+CP?G z(U2iH^^TnPIrWz%hIFbnb4 z-t*Qrz3tkd8B%Txt#xgo!Q?ec1hrfM>Lm{&OS(@dfmuL`szXM8Ig0t5+YeVIm;mB&hmB)CkNF2A7q|? zb3m&?0S?g4^G>k7#>c!2`fp*p- z7c;z4YQD%1UmxYTmX7R^Qy|xJx5%0omcHeLzAZKbT$lr{la|uoG`2&Di;YM`rIMNw z3_E{A2qw>c+c{3lFp}#)d}reER~AQMj*#ZHE|HZ>w_>efwDy#$1bui+y~klJv)8Iy z*Z|eG%=|}v1T;kk@!7A>-E}ftu4+S0Noy&vIZBmfu52!0QZ51h;2%ZRfN00r{$Nm} zH?U^(yp+4R>=@xetfED2%AP4@Q6-v!=) zlbTyk@@LKcQTBvm-Fv}{(#3bx!*&`jP*SFeNCBm=ir~2g-tYE8@a;=#W4^c2or+gm zV}I&|p@@E<;@I=b^FCQP3sS}Q{s;f4di;xM(BV?31BkBbTCMl--6KfA)_`iCN=^}r z5MFv)^~MKo5U7yf9!IO}bMM9;DOTB2#6}*5kYa<(a9BPT-5S-o zVbpOEi%0Gi+)76x3KF<^iL#nMouI#Jqz#etjjLte;c{C!GTK}VEY)!7p$w^htc zOpKAIBr_;Huq74Cui0>eX6qsFC%2-S&dZe%2j0#RIW34qUr72yv#Pd8x(XBpXIlR~ zMG@*yA7^QjP9WV&7isk!VW-eWgXtu-{#C4DQ}oaSu+ z-vblvo2+jesYrPKLOQxVgqo|?<;NPJLk0>A7$GbQy^Vmk=!nlR(#5ZMba(>_9kV9m zWM>z9k|kq6=8h&^c1U0cec|s-)u5+7`Kw0-Ijp;SMnq-LcMvOJ{t>h`LIQUgfLq0C zzy#)npI!SRu_j7092q3eV^b@q`?{#WKylDr0wM{Ep%vgwK;v_6M1V!O>ev@Q zfI-5`ZD-XPH1Yw@XZaF+y!*|FLCkuz*4^mg!{@z2isDiM8T#+__s5(+-lG6ntx=7O z>)JGXT%0EOoL%s4d3kxeu__cmbO7iS$pTQMyu3VNr};cUF}iIeg0PI;bH~{`=H^CP z{pnqg1snPTuF+o3 ziM2lwWh-<~uPl&WK)T!iw9T4K6d0iGQW;>ax>BlLcGom>p{vVd?iLmo0N(?8ltQi! z1#X2ypk~x6yxF&*sxwsb!WMu8pg?RwW^W_U!l`M7wm-jViDB*Td|6lYYHA8#QG6*6 z$(`~M{j^zu5PXZRm5+Bo%D@txh;1VB+;ft}wZvYk@I?}74uF1r97!hKNk`yqISlAJ zB2QxhWrY6Lodg=`#r9LCDz)9qWBWifhWU~ zHhR1~dI~hPFJ7$k@;u9~$qS8poCWS^g!a_`z5lhlI_9A+Kdu4ZxZ}7Zw21luO{| z`^>bWlW@|T*L$BuU&;A1oe*?tM1az`8iYNKlztk{=k2l8233};s?!oxvH`-rJqqj0 z(J%9K`X*$;B@TJFyNuc`+qu1uY+Vt#dZ2GuKmQzF1|)>zA0NGyU#2F+^4T{BYpSFk zfTpI$fHk2|Ff=4QywYg_dz0d0L^lI~7ytzLQrwfre$rTJq_lLybzK~=N$l)aK%0}x zj?T^wl`_ElB>{3d>r`Z*VIQfGv1bbY0otPl;fqD6SCU`o`HIF#26{0a3U|Nbc;Scq z(9lc6swdZZ9!K^L<(0!8KgI{>SGqz1sAAXGW>$b~bzcwFkpPEVhetE)ZG~BR-hpVj z682h?J+VY*C?aN`@XOxacvS&2PeAP5S8@=aKW;$MC|=`{28}_}$oX)*F6`YAJSas@ z0U(tBN`zv5ijElvYniz`V0C0LHh?bK+uQftm*GGIccOQ{ID}3)iT^8bPe3YS5VU{z z!N>QSL+a9ad8Yu(fF&fDkJxaEWygGI665`jwM zEgs-3fhJn0U->^kK-Tc5^L+@{b?U5q8X+Q6G*+In(au4`v^Ca;0lf};W0IaYVfCQ&EJj=eZ!S66E{%+@d{|oc=fx0@y`H6_lxY*dZ*c#W>^{u7qxw$vzt>RL} zb4p6AyHn-M0HV8V{b-rb8G{9Z-&S62eUh(ZHP^MaW_J$_+@FP|iUy(dohvx-7Avy; zC6EnjZm%_)O;1KOxExIkYYou(cNom9c4 zT@)z#%Blw$7r%V4w=mYe%k=8s#SM^7tTD(z5!jORxe0nyh<5;VVf})xWngcOkgAQ{ z|Dc0KGSdElQ0zZpt-1=Bq4#B=oYr~l&Vz#3vNI8&Kc%kgm<*P0vFz%4MkbgwLlpSs z6-j7A?RVid10ZYg0Bil!y>)de8({4{+Ni<79iTruuZ$ZC1^e&B|RfU!gJ4byrx^~P6Pj=QGi8C8`xfRWyzz-glbe^k&v;Di!mgv z@4FE-pGjAs3#-b%W@Tf7)?Z4jru&a(4EZc)3-~W+y zXCy7dCR|qRO$ie4HgY)hl$0N=#^%<>Kpy(^OFn@)&#ILm6bNhtA;IbRGWG$}QY&a%?#odM*%((&mgwr8|o?nxD8H%>E<5(FkolbJ#fG;PPvP z1_Xs-ih1m-OX>~gfZ%nZd>U-`9HIv1EEO!A>4~vG7na6c$qe*%bngG*k?)p zzIz<&QT2Ueh- zdNVJ!v2BPhvU$|`q%v7K`(IBjGs_mgCH!EPVy1U{ zd31mIwrlixh}QIvy)QJUE1eLRFKQ9wn+lbGY4eSY}(JZ-C&b1k-v?g;o# zAUBo!Bj-M*FFkxBmQJzgIpKLM<>a;4c{&Wb@VR%(|Pvek-y6{Ugk??2# z>K=**LLGS4o|vT5DR<5KaL&H}|7t^G(o5Fp@e2R9)DpD1Pnd0G%_bI|+J|!Tb+#Ff zq4q;RVwbkn6;clXB7WR2vY4{G(t;j}DSyLLw)1!~uW(}SsGuiO{bYoH{A7!j#pfjq zSg;Bzb6P4Kb2ITg`IFdFbA~TciF`_=|3peU^`DstM)cJXhl6Sag?m86!Ki65K}&=@ zm+R}~{9&Zdn*gwh5yz)Pnfk|FGu>BNW7Nu_H3J>vUmM38EBc7Md)D3?7f763;`(Q| z6dMwid4^-g-hrRUoM$`b-=+Xq2=xC>o-MIk57ffM+z4B`_E;?R8e8p-?iISEV@Kz7 z+`TQ4`rytyskY0GXYQMoiT}geTR>Hnwr}H<(xM_Fp@=952qW~4i~$C?Vbk4O;L@E9lDzj^(4>a$sQJ2f)O`Im z1Ny%Y`v#PHN^uPlV5r4zKURP)Bg_>Ldfc6RYD#vWnTi#t*@ad59f3u+?rBu*hTa|Q z6nHYVT|=cKtFt+|tJ)wmJbJ!hW#xI-Xh2Sl=t4{R1rhv@I!Lurq!r)gZx{6+;?Mok zE6nZd9XReta4cND#sA4L9c?)i@T>tH-)QXikFpmKjKME_yDL|x5pI0{cB|m-@KxTh z{Url^v~ZPioPfwpJ5zbqZ8s0dM}+@c`RK^Z!S!-G>v+>Q+@08cogsiSN-C`W}ieiDz-Os5O+wy;hvOtc@ z!)i7bNR=Hgyln*{NX{x6k=jgko+Nvxg{xTBl;g&!is4RX=VIUTA2kvds!9&)@RXnc z0eO+=q5C+%Zfif07Op=A`GN7(Ck~=zG8*oe%Ia0+*F`LFPaU1dJ=Kv^7g!OS!uVZXOLMs2$oo`Hxy0_5a7338sv< zCF+93n$Pmc-d@|eF7f^+a;6Vx3`+wg@@nVE>0bW?MGG+{^Vct5tiKhkLca&Q{98c} z?1&J0!JC|%2V1KVAXvNgQsoCFiX+~-E2U}F%WJ^e89peRrdRGjEgPxYwHd7zGJ7az zF!2u5m3iv#yFlZl{=N`gG79urGc)H(O2kfDybcSy6W<{td){uQEguw!5wDJWtfAw1 zOgO_DIELdE<3%_mWTLrykwq+*FQ3Bps=aRL=Lk~Lpbri+efiY`r|9U!Y({1Piwp<| z_;Grrx0PR1*%+;3^P{MLWa!b<9yY1>|LdHnE?T|lkgF@)(Nb$?glnYi7q3{J@=^VH z^=K{GF$;%N>67^X^k}Li{!T&06n@XOK0ADLZ`?WITV)*Nzh4-z(wd(}Yk@`_HUO=@ z_MfbOWKGX??C`k#id|W`izSh&dhYSy15DeAn`lb#;>@I-gFG?4f7mPhsj5lk!vppOKG$>3s0PR;_nkYdr4 zxo;KDhcE=V>kk6Ts3<~9J~x;!3=Pet)WP3CBB-ib?fAp#A??|-t3Ywh$fyQ(Vsk&6 z$?enNa=-;Tx-MD^zQ?n+ujF(3!5$`Gw2k&ERFJ7T7ya-N8UWTYtFesO>txP=h#gM({MZ2*MMj}V-k zB>c9>fqAJ6e?|u!49Z3?fHX5N-v;QuEc+DaZ&oIb-%?Px-ftl#rMAQP;`{eRfBzr6 zCfTtg@C5*WHbTMxeHTFJ&mD<%_jW+f2?LbxGx-Ac-`{q1h*379$<0cTI}K}Q1ir>_sBVfrx@R2mho>73o^}9` zrJ2tfTcRUJ3=IrC54Nf8pUn5rTp2}xSx;Dukk#j>{(($EFBj;jQupsj{pxMxo>JJK z-~ZJtbBg_@weir0)r}2`+Bznxx`z)RV)5j6@AL}$WSVLm(mi`JUIIi8-}W0LK=l%< z5Du-;Je!T-lTN!oxOl&PEHZ$lTgvG#vFL-05uOJF2u#OHK`)BjeLdsG)vF%>U7P%h zZl1jy9)3gBp%f&otQ^BMXU^oDbs#3jXAtwekK*MO?osD~D|Q#@p08F!Hd9M~7_^hZ z67e_WW2FTj?w?pVMH3K(8_g)=n`a7`3yrr5b*H%M<|MV5dJ*SZ1uiGS9~N5TvS7wW z8B|z?Wbpjv0E1kJ4j~uf^xvzOJ1={#PKZ@+Bm3SPj&f1oTV7iM;FhVBPIycZ z+y~r%4{2`hst{%f*cgaj$w}-NhBOqxe9G3=6b1}1ASu!0Aq3)3nGkrgRX?8l$rnX= z)5U%XNS{V!&rYUFmR6Q5A{=1+Wk30q3s7I;6DM->4wis3f9qBVF8*_bLrQ8e=z6W; z=iXgxn5chXVUZtB2|v{gKqss|jMdO_yDs2#0{>2MjCHVIv-%`PZV(q&ti zteSL+R#qB8#%eWM&2)2T@?He9*prnH@(G?Kxr&?joadpe2P0^Hy(Wa4 z!-4c0+IT&OtdUHg_OT=u8o$bhE46nSE z<7g9Zg1IItnwT{|2*51_I~ZQ~l^g$O1{8gS%n;pANVotFiy&|I8r{oH#C6?4j^9O?E*jz&PzY| zpBzjAtP6%HG(t}Ff~g-q_<`aVED_)r!Cq%=n&a}el^ePkV>wdk4|+$Zr5qv>`;!)D z&wM{8$UO<7M=`IF$IBzW;02d;EP^Y&7n4J7f7f=!!UDr46{U5!bK+GCjUYEW6B7`5 z?|~8!=7}_7p3UI7#VpfUe`FulO8NH5_>Yh9Zc`nl`#QaOhrZ_7TR08GI2i5k^{Wx*+1;G((nxZoO*~3Np)iWoucf4BzQ|!ce zW23;?nNo?+O4n34tTh!{fe^Z!=$n;Nw10f6?^?j@uKPdUnnT}48Rw7nN=oQy!K9$`$fIdb`#)bZL>m`hY;&$_k+HBq^If(wsPyaO4YCm~;xp9?}98KPV!LE9Fs#x6=! z0(4GrATiE1>JnGIx%!6SzpbrbavTyJxkhBVKL?&*b*IdyB}R~`p4YpPq@caeWvFi{ zwq#ej@WaAwcG)pAOVn&?-)nG?)A-KRN5%i(-@WgX+4h~*z70ZzDv3LzBuB)C>UNhVQ9iV>j5F`(YkrxSk}Pzu^vw`!l}jtEMz?M|?}phO z-!yjmaweR{rN#nuIu?risnhYNQTiJLG}-)?q~dQ7D3>*PT?BzSmH~`sf-hhhL1-bP^EOsE zmF(E({hYO1$BYm~h)>Nl`Y_sN$I{AN7_x*o)LXhzyOF+gO`|=gpuDTk0x9!pu!DMu?vaj`=0J+g*yhlMKcD{ur{QZyM)*rIUqZtq2pfr6^%H1D< zFf&~%c>%BR+yF1oTxCX>{y-yLS`I3jxNXC9^auaKDZTx^jRog({Zm`(dD3eG9{|8wjCo}~W;u8xM- zTTuffaXj~3lc5hy!g3u3@55U7^?+^4J9hCMQ3gzY_UL3=X`G_5Bcq}Fe{(X0eKCMb z|8uv_cc3mQ zFyfw=R&(-&-WJ-EC&x}CgJ%6a!HsQkV1|R;4|eQ^wMm1|%mN5U|DYx69GYz=0dsZ@ z50x&9)30PO(z}T&+M&bgF4r}`BM%hzuA0p?FnM3k9Wq# zuKk06UWal)!?*o!vghc&2l!^sYwE7zqCm5cyxQ5fiYgk2%i8BGgcl<(}MqZ!% zuaT_s7J-6RhqS*LOYnT?JT7l~oM7*8p~^*>O1dV)qs?oSf!`)2x${d#f4Q5@G)?rG z7e!Z}dK=scUJJ{38KP)-hKJ^KFjg|9q(zAJ_&3wZ^i?W*9e%9_>QpSEt`Cv8_ta;a>DyHbH&>e>? z5m%a&+as^&X)?d#BShHpJu8ZD@->tGCPe#`(`vxch!8RC0wy8mZFi&4H>9s`G^;Ax z1(YCfKGSnLt{V4lU74`ab(S?hI~uIhC@-?x)Oy_SiVHr~Mw%0Sd!G>VvLi@lFx4dm zu}I`5E%B+p_y;(fek5N z7B`{*bx)<|tjw7^;$#CotzaJZlRb&1@F(+7`sLyNbK{lkT0Ay3HTl(STZ?6M92{0t zQ|Vw=9{D8W?c4AZ($e>v9|zT=B8qnQwu9jMx*zR15RL&EuKMG7uU=7}4MOlcqrqZ{ zSZoDJp_XFfo0bj*&%_Pluv4Yt5hk@V6r z1Qw-WW~l3#S?-+QG&m@4gl3kPd-Rc;l2G}rddM$2YZs??XQNv zV6H*qH9iZN!?T5jO0XGz{z>ka%%kbXKw1Dt8cal13_)8Vx}WIjHHWA6JYuolnc)X7P{ z{#&xn@zeJ(GAXTLLKU4`K0R**>H{xX1Y`kAZu@%^cYGNk0fEQnaQxS=gg{*$EHtKT zAB2_hR!xO{SzUdQt!fU-onbmerd@6hT+#lBEFbSXkmc0r>LVN2&S3g2+H!( zcCkY^reXy6L`6jobhM}^Bh}_As*?3{prS1+5y{OFrl#f(oLThoVdUTleHEd!N}QI8 zHnsAwEZYUv;)j&)TfuZC>2fMxzPxnbDu|j{ymFEe?t`bPDs1SAGBwTRJ&iDWr*>hf7;pI!>WrBf_CHBt+&ax`B|m9Z`2R zg%}-{nm8o(Y{~MaZS~Du4Fv^Lx81c+v5}WwxI+)6*rN28!Pv{x5?5Enp@aAaR~^Y4Ob10{rFL1Z!BlGdP5EC_8FR;l_&G|Ix8dN z`>c8HQY7q#mZqklz(9DCYaILGGmG8n>3uyzk@4r-2Px)qPk>Ok zbaG17K}>VuTj^p^M#eXbK64V%paO*3R)HgYuSxS78XqAfLFoDZUD*ijXMrw2uyctG zm%J9+gz!Yosr=&Ub3WtT=z4?tiI?wM^>S$vNuxgoW#0NEQ;<~ZT?7Zz#dr@0l-4$W z(zsjK!7G$8*Vg?B$e=)`iOjjRVzjTEi{P@kc%%EvId#^_dfzvwS8~~ zt!2AvfDpZ`Kk><@?n&>teiUfM#5{*CoI2&7B_D^Pr>5p2o3aBPLqJGM(%1r7P7)=* z1LegS2`wIz0%S8+7_zZB#x~*AV-rKawJtOCt5*-{LqeJ&(l03my!Hj@^7sCt(N3Pm zH2H)%+sAjLzWVu->sB~P+-rI?@RJQ(<1GTd zL}#lYuZrd%nAR_KJKaDKy_HfKIBENu`^Hs51v&!NGiL=rPgB-(!n}NqH!YV2C3KVi zyjs4FF4!;rlr}Ttxqm-Iw^ChO(Jn1*T2)Riz6Uj4hD#LOo4}8p@P_2q9Hmi+S%5w3 z=f|3n3*2)(ivUArX68h~yen5^vvYGJN;HASEae>fvr_vBSyu@Xz9buS*Q<12BzS=2 z!T<6;QxQ+W{r2HvdT+}U=IGeZDApQr6yrZr@`H2)>4_6w@K}%{;A;eoFjxJ3^Z4vt z`GhIk$3(gPjp3_~j+FAzwp{EMadc)!$MZZP-kHf>qq9N1us&;~ zj|FamV|dyqDp3VltA$XM8L!bp6IC@9Ys--1r71@75yYm-kTv7>9QUE}Kh}2X;#aFe zU(}Ltu$e``_sF{!fE7l*SKeA!s6>(rYI+$6ADoHZE;D1Vu7E&Iu1T{9vI4kXpF!+= zCR`)Nv|4X^zy^o8&KA++!~{E=+~A^UbFPM|=@W=z85ySZU%Dx=QBW#1yq`urb;err zgg75z39=33jJg-mVNv|!qF3hN^O{NR26h@|V!{wIwb%Ak-^Y&@z&}*g-e+%j!v-!^ z!5fxu0f_Xa=29g*=6|V<>hx(H-OZ)i)?dvskm9L#c7fHQWrF@nw?PtcV02M4yYq z&mhNRCn1fM6n{B*RBfRew0Jaa@IP_R{hZ2%J9{!B>UEHW!LT%9&-_W0UPxxyoOING z64#d|n?Nt3`4k(-Z67?)dh*0?{RH~yQyZuSn6IRMTdYn^Wg)`6oLPj!T+on|U_a8G zJsbau8?5T)laiJ@9neO7sPlpkD;-Qu-K5@oRMgc)iOoI@pne5*Wrr3tr?L}a`H{!0PDS12kF+xx2-WuZaJijE5%4Cj#x}K>Vs6^Tn1QWUI%0V z1STDs4;aho)|X)5LNa>%y1kLGIrw6iL+of7;EZL3n8qzpPc%W6Lz74yU5breyjfiV z-bIlwZV7r1Tr5Z`_@qJb<%6t~LU&xc%PXKRyX4G@;QU@jDeprO5Lkvgc9DjrXnABY zRytnDdAv2+p%LZp?+-$X1Bew85_PMT&3?|(I_1j2)=kCzI>M>dm6i4|Tz_bB_Vnpe z2b0b?A##cf#oyt8G%cQrVdufpMab+BcHn*a`%5}iP<%#RVP@{dV6>~AaMuI@uPRhZQi;HVA zAb#XLB&!m_d?vV=YGfNO4e{!+{;i9Y_t$chnO2!E`w{R;oISo8$%`$Hb7;zt4~3@L z2>;f7_#l3`KVyyxy&Y3zAc+CZ6;v6pS>1juaIx>wMPd4hlfroL@GvqnZ(G_vWl=`P z&*NxNh)g*m9Dt{e_oo8W2jP&~bZ7hbu!EA;><%e#<|T-gtqAcRpzrzAPv1u0@E#j< z$a84U8-HDM^5<)4=hExT0~z^`4%v5Q~un( zG-6&(3WN*W56W8E;466E{9B8-0VdzL`5Aa5nP_*x_Owa|ng1mb9j zNnAO_i+vOVF1F-C;Z>LP97x+A8ztY zONowDCh%6ytWnxBQ+=7cC)0rH$bZz4@0 zZ{+Sw2e!k0V~OstFUoVH9Nteb>qgSJJ|_r(i{TMcI^mjAUaVVhrEkojn(dnIINmmG zi!@(D~I0~_PaF{ zhk06ys2hgX^5-d(M2gZcCOhGl2=4h53C3>7T#yl}9qByz*sME`>Gv|x2azoGiT)m_ z;EENgrb(vXA!qX62nuVLBw;}UwY#a?D8>g8AH7asLx;OtFyeuHg0z6Fg!7GF9`Y;w zR<`299GBRRdGk=cy#Ex({7U5d8{D=X8n&08j;gByWKV23Ta00d-h5PU#srxK`pk)U zd4cbF3d%3(-4+$+!!5jUj0_{Qeas+>ryEx4;wG&H#&^3AQKoSU$U>}5v`$opOcYH)S zSFhQE-qD&;fv!RI9E|{}**O&QFF_UUe+5;79rPw?O_LE00@Y9!E(R5AQ!%@GQ zNnZHuRU+hEaU2UN?_^tQB@rb?O5Z@-KlQ!1C3cqn;XXxS0f}ZuInW33Tf}$;Udf3} znO6m1OV55m4=Iy=9Q8i1sS<)h${WrmWW*iWA6YP&?sT=*G8atdIWOwQR*3X@v7AT` zO_}IBa&E?VLd!|mjL)jhgGcXQ@pe9%B>J2(*`Eqw;*4NJ_Lp$p1}~Xx5jFs$gL9(0 z1D;TxCaHR9N#O5@pmyM!H!Mn&pLt;#a-2&0ltMiX1~o_T1f`qf(=H`#4x>@K_gV+e z=N}4c9cD0mH|*`cDdgB|FJ)xORBIADA~+XQnW&F@6`OO~O?_pEX4@*Ty^{$E<|U5w zIA@w`+HL0JRQP`?Kk>{bzmYKEn=q0~bT7OE^l9z7#WXMTFcGW%jaVmRebXr+Iy^YD zVFIB7)$^b4<@>kPIrV6({bWz|JMZE#{h8UXd6**wG!vTKZ2ZyoER&I@1ir=KTKpY2lGBWP*hF{)$Rv`C=lO@AL=K(^K6T zpWVbgTfSa1C3ny;a0k>tLrpz5*oYhP%)j{j_Wnad6JAkpTilgne0C{ZR1fW%8wDnF zM!YHN0dYQ2mv~DOdBsDx8c#Id6+g_f3hDnzE5M`*#rr1BpNscnfAM~^__o8lS(Yus zRcTBq7B?{$^*wvKhA&xQysnEia_t23?@SorZ@+VPzqG=9x%iY``U3{}AU4_H+mU#L zHU6*THAa^h1h#T-j7a3zY|5{=Q7^{P>d|nI)z<|{jGjM-SU7KGDOvLE2u_m1w6=4- z8w}0QuYtedxys7&+3lpxupj5(cbl6dut2-VK5Ty{<(5kuVXEMsmL&*Tq*FZ82~wB} z2TkA^?jf<&yvkVxV22gtStJj4q7lTTN1~WP)Sp27FF+$UMQKKxpyp>*dJ{uTDAA< z`XT?$ub_jKPv7G>h(|?VYL?CfSFboR%g8xCjpO7fMv?{$=C{#ph^fkdEU{hRH#Q7P zmrQ%6et5;=bnT{CpG@@Zu5}Inh(P$+{ZaeV2MD*W_i*0zZgnE)Wfa?HM zn%uy^Zql>Y?(~_WQI24%C~PpFZSHpe^a z9$^c!{E&#OvTro%D#?jzxA2YDQ#d}TDD+Bpe4?p`d3kKnpTPjUzueGCLcGri>Ngp9 zuj!dHH`rm`rufw&>19pP*TyH)m-hFqEN1GAa&O$TsqE}?TVJ@R%TIRBU|G!c+n4+$ z*U)%h_S?5PaGlDFJ5Y@G4__fNP45f5(x3axmD}(2Sfl7z-C|8XYIdy+=f~jwpqiYz zz);@n+zjEPM0r&UJOXlDT(Sj2=&tLDOVsUyVZPrpX?n2(rgBXd`PTq{yvU1v^G40q z_O4>*CY%`ZRQ|4>-d%-`AbCAKq5*IXp+DI%*XGCGrlZ0Z*G#s{t&g{hP!{E|DzEDt zw|y+^b+8Hc1HhM5JUx>d5Oc#2-1VX}#F0oRjeb@|eR}eGXyOAXLW>7=t7RzPh}n*< zz@b!@e9I`5R#Q`^fUZjIsMt!3jgwA=>e~0FA^@6H^IBi2%HKyhZCV@Nxf6+=uCh0F z(j0iKJpF4?2r!+n*PCJV?sguFPU~~6r?elrbLye*)L5JILtO8Dm8hM zyCHEg(Nx#60zq{P<`l;J7QWX=CRk&z5vb_+U%oH^KK2QztvlBBRMoI7c?KjC z=W2}dkJ45boQ$Fg*WP(u&$7NRzgpLswzMzu7?b|t(&e7h@V&Qa7Om^CUt%sn3NibQ zNhj_bT21s(O7s*`MVTC>WMhkf{QGzAd;!XbV2D=rUNSmHSJ=o7$Xak1UxTqiZA**A z!JfZ{M*p|)W5@iD69$Rhx|Mj!@FBB@r-Xp;70*Lnrx6qFE&@7EOBMH6RmqXulvG(b z7{uggdV!ALzXd4n!onjJ|0ZQ7(_zQ&}B@r6eXj>TW5F4htxBM)E5 z@Xil!MyChg1OPED@OtSraX(~oWOmS|_Kl+_aZ6=GE6tljI`f;4r&;i=Tpa_lie`=d z`{%kITKvlIkLZ%nQ7aX$3eFdDsuKQh1h^-wcHpmTFrGJIt7O|ayf-4Rd~x$9WHa+G!S1FZT}>&qV;UxgWVkOhU4Kuv--z zOc5{af=MlN$K)TL%fGm`NdbOdoLperA>22x^BsIi;c%3KL60=~M4S(Kjad zU%u?$TtYZEe=#o_1In3kU24d}l=+v+Tb-SEOJl9k3Jy{H9|SaNp8? zck7{yMa{`+9vAyAd9nSWt}dsIdB9BGBry-I>5ijreU9?@OhbBKS>_1|oG`1`d+(Fq zJneyN;!Jnlc~n+j$Z=?Jl}}9z$k7><Xd zmnL(supj%rZgQv~k#Y}Ol6$lJf-i_HJ!81v(!#E-QN-YR74-iKmJ5vll>%?d1PH5q zF`pbftzUjhKPya&(~S%t&MPo8w2k=us@e*!~7v_EqH`0RVu>+m9d!|>_b76Dw@ zWt*fH0lMUjB}h=PzvZ)1)f`x$;h9cL_X>*_}LUHSB9%6lb#{2R|8=IQyN*nw7#6&zWFpJ>fDY}BWR#Etq zo=wnm?+Xm|8d2jLrU0?SQ7ry`cZy3zL!QsNmq!mTMZ76Ho@-30Q8YGIfWb73kJl-R zKgvlQKS;T!1bObJ)4~%E4<#9y1vqOkgyFCgxwidlPged)A4z28HLhLHZvS4(Z{~FPh-cwro=(+4dc8;ii&dh`MrHsPjvsWmfb4Fhd}JQom%ez zmjfZbv%`t~F0I(hm;rwk#`rAq2iJ0b5SzK%Nst=q{s%Z^#>GzXU@1qD%n0RN}%z5Z8# zRTie($fX@`Y()7~D?0k{+D;2_R$mml$aL%5Nj>VWlc^=Q&K_Hhl*Pt+jlXIq{>!A# zwG}l!GS<7%oi5M7z##Th+O*mB=IU;`@G^SsS006vcpQ7^ij_y(F1hZ4z;?B? zG<^%_$%rSAQ=6M%PQ|scx~lcwK*htOyDPQDWh5jgdgYr!`G>99wm8YF9+Og54<82B zc#XvgZ9wS;KL(@;6zalp!e>c%)>~YqdfLS9#=4c`w^CaD8YnPihe>7+3U~_4CnZu} zA8#y*k#82wh;Lq9$6TZp0H(trP4O+&!N8cvbSw~ZrW9U!y96x}4(>%b-TajLh2CvT z@Dkdcun7;t!DBuNL342XOXz2t%=tV8brzoqv+&0=X4Xb%0(loNm&3U0JWx0$#m8;! zcfX}5CFLM)AAGNWO1((UorW#pH>QUoa6L?nTC3g4I)OLgyaeY|&)D1ld@=$Pdpj_M zH);2zAPrJ4VXMelIFt1}ho-#-3hRfC@Tj|+4CdzMa6ag$y@QY}ef!|%GPM@=o;zxJ-R5bds6{$BO8PJ zPB%8rs}rAOYBu!-c&^UB<5TqH2fr@&AG3G$9SSfqcO@b*$2HD;QR2f6=}gFB0MJ8| zg^yj)gUbas+Y%-};O_6dsFtf`K6yRz)5RLokjbY38e@GOGI&+}C`O8jN7Mlql&%~W zu%%ydVU^aQ65$#S-3zsBZCV!oJ0ctN?kl#)}&soP{0Tix3Czs&@!bPGmKJF64-iROJqnmIE zqughuG7GAH#8!!sefFjPYE6m9tgBS@SXm^>QrMutd+a%5G%r*jr;5tX_J5g6E@x_u z$_z9YWD>ZmKynM$>CexDVOrBOJ%in_?}BmEc_fBU5;#kOVy9GBJ+1^0;8zNA@1Ih~ zMKcz}20uk8Jr3s0<~@SN_70kQob}>zc-7QMX|(8KJBE0br~sF<1!M2|%L5m)&1*eK zd=cG($^P@$N5SQRCvZr0yf~_Wr-yFcLo`_VAe4-ZVhxVvL|=M@I;I+`{8;Caq|@n~ z=r)_TG9HAOruz<#C1LNc)e)m*c~lfROhP7)@2o-r>v)t@+DTmnOjee%q)ucHZp{>4&im>mzz4_ptADNwR3BYzy9YVkgA+e=MhZ$>4N=v>ag&Wx?vn7gcuou zJ0CJ}3k%9kQa_`{KeAr-#yN?DwfrNw$EDai$USDe9Y2q<&0~AS#8u} zR0Z718!EUJnj9KizF==N^7lNhfU2HK)i|lnLxpL|oIjK6laxWEL7E!KQz@Y|a}vgM zM?*(*#)?AZ$g<>4NKmiC<*JH2P58uk*9pnP5XBSBDp;_Y<*&fVuU7v7HUrdI8x+1D z$rdAV;+PvT3|k@I{Ijkp2{S%IFnc$YWK+fmXo;D(=((whzRam zg0vSaNTv?9|F1ahB;Q|v|2VfC6^D*tT06p(&%^=}`FH|po#lV67)hMEjp+=hZ`clI zc)t<41AYei__=3!C`Nn-9<~4%G9V}$w=h6;D1`(&vq|{`RoB&_dgXTI#xL&-{yc3L zx1!h5HyXQ^qHl^i-vcQQCU}BODtyn9Bn>zs+1en?&lLPI)iq}%WhnK}?T5Ka+thw{ zh^2?an3n5_H0nN;mgq@=muh~;F0ioRJ>^G;e{~e$yoJTqJ^liq`0o=Im=zJdom>Xv zu$!*}p8`K7P;RJI7Q56mn&+>Ghu~s*z0)FP+L^M(){N%auF_mvbLU!9lkKpGh2blUIunJ7LCg4j7x`6-hVt2 zQ9zCUOqW>9%jRLt?$4+f;xG8L?aorw#TbfhuO$ECfdSb#eXQ+kqqSnhXQ%76jh(T{z?g#4+J-MBso*GdU%D59AKT=9b;Fk1cc^t4!7hmwH5*+b;z8y}a9g9PQrs4O>UN_0mpQAC}cvhYXD3L5hW*TC13ArxDMRd zy|1xbbL)t}rmflhzd zzRJ~}N8KcR^!vcr`51PH+wCh5FhVp^{%x%aQ9f`xOjb>&Q21 zN%%x3PE@#+eeUb?nre;(c=;Pj#O3ET78Wixw(@uH3X0dI)43&Lj@a=*!0Xe?t8~RtQx#|^Whow z3`;*GRJTNtrV*bsluuic^aqjuP^?VlE*%wUU-i| z(P1YkFN0<75Y6gZOfN2O_NpDL*>_4#P0a<)D4gncdYp=qrwiz~_o72c`Ykd@?*RSVqDi3wo)&NWyFS6@0LPrfT42Xcxrk|#a7X4rW~7@`7tUYf~5qxWrYKpkWPSzMI=f|OPugNYEp;d&8yB<>4`QCM7ASK1Dq{I6a-96QMl=bttaC zr-c(Njl){9Y)q-;$ll!kE;xqkDo@1tVJv@s&jidD9LXV=CZ%w)O*W(Y7pp<%GEy}d z_NJ_~pa5%^3e4Et>p8viaS4YmdU}n!%k54AH*0GHKy{LnGxUOzg*1ru9?T6v{{REg z+Ijd}Nr|(c;8ifr>fkYU1}1pp>FX?%|Jh*NutJB`osLchxY$ScjfSez{Y0#3x0$7T z$Cbx7m+2oB8o?Zq6~<^yuckmpQopG4K>^sXVEmt%A>g~2BHkWd)S~ID31-5&k&y)Le7i1{lW`$*s@&(6e>I0%gWl* zFIMfzlkZ$L$_Rcjwrkg1w?K5j4lvXA?>*3z0pqn8wmJL>LWXNlL7YB)T9E)>#`+H~ zKn6MnOQ8M$+tx8V4^UHCS&E{fHDJ>A^w4a1T>=|=kcyY@?X`o1Dv&A~xXd+PH6IHK zh$JuWv0I>%lBIhvBcCU(2@3X(&wh%jHT}=LuFp&sE6b#(DY5YK0<I1RwE^ZGW-nX*SmTA2kUk2x5^TU8o zr^?kzUH!@Z=Jf|}vVsDusn3V5d_+ozoLL^y@M|L#>&q!#|B|UhwYq8uMom2!3_3R2 zM}A1@^btK!KTq$3d95z+P|tAT`V?8R>!FR_hw(s}BkizOg)MU;dd<)f2+oI`m}ATR z`LkO^MU7c5)_uAagdf?kb|Y1UFgFH!hF=3ED40|xA}U-rb|93f-?P~+>K z1u=-YavOf#_4luUcK{+2_)^WRt+i@A4evD(KTF2Pu0{tTg{J5}uh@+nQ$T^=ng}{? z+7zQ8m$;s*<%mF(z2?#lfBV)iC)zfbrVwdnHrJkTSXEG93K|s<6G0$<^YzqpqbAQ| z%B!bM2#n$AA}m2ZZ2>Fh(!iTG!UHW_oy<9b7^zRi0pDmuW{;|osTwim^$XsARA5U` zfl&cn{uVbk5@dd}wvR!M^k10V{uUJlh>FLVw8O8=imIO6_i%BU0{>*_1VMY!7N@3Y zS6(%|IvHvVl#oMST^RY!x;{f{sq`AJ>miO*b|8$iF?0ED`xr(@tkXGAmUuT=!KA9hvrM5bG5rPRYe(Wx* zTLdURj=5<2I+qk9bCy(4iNnaAmw)sDw#n6q!@$uV_2aW|gOf>mvP7qEcQ zg!y}Q@OJv|8WvCiEmysV$OpzTP!|mho}V2z06mtPYJ7=$PO?-m#J4or2NjSTfNl?3 z4p3F>tP7SRO^s=E5Ki!qPZ7KDB7v08$JZ8xV81rB@gu*6xayqXVlztLZrVJGhJ^Hg z3Vu*6D_J}JE7hFo<#xBcSiW|J&RcvlJY1F7PWQf+mX_l1j5mlcP}}=bhu{E6Q~%@k z*tJ2DaXy(^&VGdjqb&c0P9rSVauq~TV`D1-bB`A2C)XQ*x77ex$-(YiU)MMx{j38O zOi!b$=A2)?V-E9N<0*|q1kmw$7Am0@UH3k$d!{Qq$3T}iKd%c8+Tf1pGEz}B?33FM zsXswvcetJFWDPlvOY!O?3nR})6N}b^2VQqzlH+#WPG(NCsJ2!cqV$y*?s1)`2nm=m zmzk5WCxJuK?e@sw`aiLW3y`1sdGXTVnPzV}#~;V!z^PlxqRmzz!%Geadt44PZIdq8 z0$59PAWcp-&7SNUfnpofr2yZa_zXjxo44jN<-CV-5p=T+>B`H~0@m*sDB2NBy2tQL z`U43NQtAQBbb)c$G>9TQN`rFPQ@?_OLPb1>4yNAd&P?Zhw4c&++{hPR*4FVXPx=0( zK))2mC_0tu=g*u0f!#Ho%4?S|S5{ZwGevXi1RX@h4juORyoZJmAaPh+Z11FOUWsTaKu8!^z%do7 z+57`S9y_hSoEpBoh=8*bH-R}6&BK$D_24Ze;c+@1$#wxsjK2^s@o-l!_FGpwDbq;U z#w;!+i}Qy9_Nevvgz8iTZJnC212^|{tccqIG>ssif*hb;pk3UiNCh-75F-EHsS`8; zp%EC_cWy!9_7p++nbGpJKnn&B3xLFCp5+!q#Vn^{5tp1l>r|d4PtN!L02$9LXQSKZ z%>?Pr4qLBqx22Mp;5TEy;cKuaU^pov$Llln2GT4NSmWC zF&RQ1E2TBqou}qhYK)v7E5mJ#DO&+~5DcvD-UO7!`~^*( z?B3mu#0lt}pbo+nD&eZx+X30h;NW>vvzSJQ7a-B2QAKX0JjxaD1(|C+YX!($#9w<0 zMq`x?GOWkD!TRvIYi_Bk+qrsu@VNgIP50A{ke~zs0Z{}LBo{eDktvWQS%N^3K|xT0L=lxF65lH9KDW<3 z=e*Z<+&2b4T5YYb_P5vCYtJ?3TzM?ERsqt;N+Lg(%BMDTcthnkUe4fI3$Q-(IuzoP z`~hiPyb32+JJwMSXxr`~%~b?MFgWz0#RaFBJJ1#2roRW6)@Q)tV{uy5vSbVJ^iTTU zay$!*ORk=)P&#W8&Pt9bdpZ=|12po#9U7QT-+)7fRZTfg4)n*`{4@34=}C4Eek_2X z7cNQ+lZDq-Aer0Kn1n&yGF6TIoG%Txy%YcTNsNOpBpx3Lb&B@F!sdN+`n4Uz`_`X& z6XoDrpaA@Gq?OWKA`-9=(b#huB%=-2U z3-M(*QJE!KTqGGt>nRZClMamdgNzo||Bm#J!(jgbau>1nUi?@On6C~thvc<5X{)cx zEY60Jo8a7@P_TW{DAsJ_w=awsAOa;_4u?M>+K6&;+h#gz+nZ5xCCJ}uq$19;#A5og z>lhptJ(caS=YsB#aG9i3`A@?73h$TyN#OB^m|ZU%$o>1NU*|hsi-|sE@0KdH*;}1! zkheTPt$hAIfdDl~b_0Z3^fSp0aW;rEJxkJGc4ug0s}Fb&yAUr6@?NcLDV()HY223`PRlb?(;lOz0N;sx*WFX~hxA`J4j?KWP)%&scpWICqQ(KJO|y-YqS`)h4nQ2GiBMbvInDr7(DhU4dK`iZqfP7pS=(yKgZj13;+7c8| zsp2hBioyFUAZR3ahf&8C#7Q_s>md^j#bf|}5N4}KLa1vej(ngITZ4bR#jz%;_UUSl zU$OWKh3Iu*Qhh`cA@3j3p;~|FdLP%ZHTk`90J#1Z=|&e=;f*`UkK+z<`O1+{^>>Bt z*tpA;InQ&BB&_6Vt>n4mkaBX+4k6yk#R@+W29FMeR>qMD58NXFJ%gRqawPCe^|cxz zds?wl3(y3`Cad->9Ia?yBN5{K7JW_7^If(bt~*TG1ZesNY4%|g8HM6jT8Vev!z?K1 zr%@}e$-lU&HE>1T%Gf!EoxkseTHZ&VTBXg!dqs+6F~#I%IW4$eMc;zZ2)D$;(^G=S zCWZr4m^@7#PbC6&edHJ6U)(DW0WMH`z%{6n)y&Kne&8u#t%60@=6=QMwj#Zpb1dIA zm&YDw{Ng!sN+|5{F%vz4%BL^B&Z0ygw8fwH&A|zUuyTZGQr{L=0&UF+I?F@7)nl1P zps(dB)=qOXfv*^Mj3YuvWI*)gNq10R<$-h%TTQsTYD|D$6G9J$MvOU-A#^DT{J>W2^Hm4Z>-dQyT`0& zYI>>rWLDkl*D!ZGE;>4l1?NlQRI&vI*o?G8c>@H50egD^HM)tvLRJQ8$D>DWJHOWw zCvGo4c3CWQ*xEpWIN%b}glybm{#d3-`{uN+$~-o+W-R@|`_m^CaZ-VFGm(k}$Om?B z)w#cs16CK(op8=`0Ger!-@%zOx@5_x@R5rtJVIF~Tq21K2a}Ne{jMDLqWvrHUr*%a zKm8^zLa8)bd*94p)C{u%dqK=Y}43?nh4Q0jCtQ;6mP;4*F15=H39pEYRWUv;5) z6Qgmi?t#!!F{ee*EPLPV3w^39FRc=CUX0R-V=b-fq`Fq%O;X}SGN*+V3tZO+b!KEa zQcY6AqpZNJ0a)>0-hIPHMGg7vePq7kY6E5tm$b5<^1ik#G|1(87(}0ebA}=5q{#;y zr16P+L-z3zRIty)kYgF51kEm2`kJeQ{2eN7*c#Ntka=)NwVQLR!K-KMg-4q$KIbHp zDV4?O!mgV7AM7f(q5o1&wf4>EolXRofqJ2x)vjW4^7>Y_JyubtTcA;J^^`i;6>-3V zn{3|~g7dvXUbDf-tyd~d8Q4A-tZ6+}V|5>egzQm1kuP)AfL9CxhywQmDEiInz1>=Z z!u)@dxvmKosYy+^&#TS=j@-jprQsbp*YzYX>55J;qwK*7)b9rQ3A#3PMs=POz&K5_ z+24Qk$s4l!(QaRa6UAI8XXodAPqVPtV>~Xf!>qPS+pX_5wjMMt zHxkXhlJLC;;*DfkM@*(jP}q+OWAf;zKS1+{*t3 zcXUYOz-Z;OZ-L626yL5ShlL+p$5rnqD~zI4QY15~tLf@^*Oo_*Vvtr8ELYaSQycK3 z=PX=2+~bdX*I)>fm{>J)yNBD}MUN*)bjn`o#=-XO1*xfCU&_6C6K|>DiYCX|=)ey2 zUv&>wmfeMb9VaDmk+Ji=0-#!Q+h#WZv9nP}Y~6$Oc%Z*8X}7{H%#??lbtF|l)_xOU zn0H0P+$P)RU<{$Fs|t53z|&|e#H~7qbn;E#mj?x9!##b>;Uloy0GBx#y&S9(n`m{M zq<~>RY%nHOvcyh2^rJ}^o#p5|3&lh9o7Vj}EL=YDep(0aWOH^*D7dT<93p1_N$>X0 zJ^ldoKlEbPPm6Bxl&^PhyB{<*-3|M3+TvlYufN&++KSlo1e|0nl~wUcy*R(!d-01Z zduDJQ=ma(DC>%HpS0C`dLIU3MW9=xLFUS_4R1N2t$H#i-nK|ft0pk&f$AHQI)(Sw6P$`C6+;`tXN$CTu$n~&i z^B!JGN=$}A?m)Cnz%rc_b)L@I+WNWdo&xqbF7rOsu6f&2ExPbWMu>6fo&lhAZ-2KH zpuFd&c^JgX5DrM>X0^*nbsp%p#KZ_kH2^&ociH9UhEMbGu;=7dU3ees=?+$ zf8_RdHn=i;e+}cliqg{Zzyt?g*U(TokTj>KGeEdu{91pn^`tmJxZuELfDyp)UXQ6jkSTX2cDu5AJ>zdwFu+<=ye>F0NIP@H?L4jnf8#(?)(Z_`iBjIDZ; zuRvio8f&MiFa6*YTtE&KJ7WZGZbjb-8CBK`r!z!+zCTHcb=-M@J8iSvHT5Z0`e+BJ zvUOzWb=TkE{0bfcmFsWlR+$0~ZLstdm1apIOTGok_w1(@cDmZ=)k2hym+mW#oOd-n zaKT0e*MHmenhB$^?LXkTFlC0hz@il9iTTDf)C;QD!uA4&IsXyV#^PW*4C>r%QtG}& zmc~BiAx?jhz<%=?pB#=p>-ecY>s4rnyEEhEmJ zO)w4J(}LUDw8zeIsVGM<5}yAG z)<%Y45(Zpl=%~DZ|KOA3&)~2y7tt%YcCWT}cgmgUnEfi>gQ>W<_b@$CP*CuUeN4*1 zX#K+{f!F;9n=z&pOOJ(SJj5ln9I(54gkP3cz(c<$H#aVt3&DET1|p!*nL_n-dAfhd zmw+PCmucyQc=kcQ{pIWwjQSNU%6HKlj^Vw5ga(L&;jsIF<`Tw2jq+BNV@dyRWL4&0VCyM&HrM~BP zCHqZQ6X@JK?R?AU*nW*@gqYuqrlF^-)RMA?z!oV(`rkiS?|mhXU+a9=_v{-{PjwS< z2$!zB4HlfsVR&ssQc~C2CJcYL`)Ob7nSu8D-g6e>i(kL7z8~2X6cyE8cWOz2V z+`_N6H*V+ybP25QNjGi9=X_oN50GjXJQ`cvT@GUkd{kjEquJXKkTg- z9a#p0JQ%{rYitrnmAnBBRU2TDD8v5^tzY0%*SQyxyu)_H(a;XPeUo@R8ypDqgFBgl zUYSG~E<>2#ld(iR{6wCpXs4mr0hH05&#&r*gD4(;z$N7Id_DlOAF5oxux@~U`)2~x zg@T@YV|3OA9JrQ+YeF9Ry~_Ua*HDq1nzF>v`2U}TvN5)yGPr-df7T;!hjmvbZF(WQ z(?0g5_(G8SpD&{$K=fPo^6S=tUmj`lB^)&D=j-QYpPe@+$`fi4_yR`^xXD~|reqd< zZPg_lwLkSl)0l+4Ep#zT_W^NCd1I9`jR3;MXTO?xCrK-Zb9Hru{P-%!ft+2g-j#Bh z8wUfwz#{Sq=TVp&$-rO&200dQa*ZmlU=8nlWudiheb&9Y+IjRO8ZzA|yj@=2(?&eW zl`o)mQeRJ2S>-rEVu^j_FzWfaLnDz-nY7pN!TBv^=UpNDku)QN$HOX0x6!~xdcj?W ze~eMz>VpLj!e=ji*|(ZD@#+Yj<>Qg=D^~>41kK7}l%Kiwif>)i>$@%oGK2;YUEU_n zF+6+~k?9st6+XLnD{@8@SInOJ`^7AzJ3A~$d^J5gj@rje{#7QGdWf+h?ExvC)GhrgeG9$OK z9~a(^-i^e#$8VHN(BMi7FkZAw$JAxv7AfSTA%->=k3X{6oE_H#q&^$_r zm&MENW9|=0Ig<4+UcRI6q43)MIh@2deyvwXd~g5hL9%$Y?c)1aFWNqkZ07ik{8k0j zRPg9WlF=N9t*2zsC0P*jUHPK?^x36SBpI2Oo&6UMRpm(!FI81jP~p%L)t)zhG(J8I zg_^^wl|ly3qEYA>L&0Fxr8^I0?Z=bBhNIP(1pvO0 z$6sXjgB0&}TIXA?BlstrmYX9l_tYbQI&AV?2^Rek!rvYG~Ow9ju#ZSdy_uPN; z9J$;9y>4*0UV`HQ&9y{-kNE{A=nS{*>EA);JHa~=cu<c&D+cLF~3yWNOmWG(Ns00GbsPrrb z2qU=*UJlrO?@FYF#ems?Auv>S>b%0bV%!uZ5K@P9_oq|x%!8?qbjB$nKWt6y6|sdl z)?5qE6b3LgUbD^%V7k|Ls0!k9jj91`o_mJLjiaitIoXwfX7jpo5l{(&V}EO_A6l8f z3L<9hfW&=cMA<{|F?&)NXPFrJZQ0A0pvz}o7~f<58)rW@45xOuhchvB=6*M~Vhl0B zm;osMdp;Yj-=AE78hH8K0fWCu$q%$pct9v6=oN~zN|&N3i1!IX`#>wb{T`9@ou1(% zIB%P;-%z&NS;u5zaejPHScYz4Ys^MJ(?WB+#+rRt(=mDGb=9-w^Z$j$F{14;NX-O7 zfKk15k_hKTYiwca1=oi(^+#(_Ts#~iSrQfHAolrrEH2Y6Hq&IBKC&pR`} zE56$R>B6-|?ML~I|7t3@P98H9)T>VH5yU2f-0&yIX1^=<>fmJFX|*hLkHDBmHzCvwB*jlmj5z5FARzX zQAu+Guh{d*XrVHDEK(#Tv^ybVGuPAE12#nrhDx=PIPmf4&3ymmMSVaXt$V{s{#Y?Q zqW2)s0eY@JB8;f}$9nppUGC& zPz_iV%;v&B2y&UywF$hQLkSKZ5xjRQYCQP%${7evE0^vQ%5`OQzoX|{)*lbM zjU>_pecjh?a@yl+cn}0mwY<7j7{Blgq62&))@dEl{ssKmALt3~IT9uQ)YHM*+ zQU&)-+Os$bMp4JsXR<_0YHH*ljZm+4iX(#5NRQsP>1@`ZG76B&1)jNv}z%l;%YQk1Z+KlQFCC~%uR2E;O^O%UA(~S$eneAvpJMWL|)$Z zB03uH5keCT%+?Q9aq=%IUY!#Qh8U2$rD@6WeS#J>dGXWL9%u=4sOT7qS?ZWiVEA#h zFmuSO{S+Wo{UmrL@qk?7z2H>mW~T|@*^cL*vJDXc|4R0x zyt}j)$`C1i>E$#2$O}2S7mvwTPy-oyGC5b_)foTK4x@jAd7H@~q+Q6b&`2u_ zFReQ9(e;w&M=8ly@OUN2xS;$%ZtKxz=rldeSvHM==9}fz#V6cG6BvO*`%KEe1vDLY zkZ>zCcHTbd?!Yw^q0#ZZ;9#1_^&=WgXT&_R$2iqZ3GoT6m%F6n|6#VIo&-|5Dts`< zDRiKlO=4(?4GRFe{o6lZ(V@9G%qPHq-fRMHXOERBI7$BEph+~;R!XNp(j&_llzrH6 z<`A-KW?m~`SYnnPm0_3gxJMo>iuTXU?$OmbO>Td#kH=c>I!8iMkbxnzjh@^=`m zKms~BmlFHoRqks9S1ImSuuzU(WfE?;I?wzZI-%)s*q6Qq6f-B<6$`XAr=^AZW$8Oq z`WpI^+yoZmV5a`OkYB)wet=&Pi*qdl)zgd75#{&tT*nYCI=PfMH@nX>>NS zF_@C0GKNz4&_&xT7*a^J$A+`D932Eq5|R$zv(+Bv@Q6Mh5CO=(5-nsFD4MRuIRp_L z`THldA=QtZ3xy2PD*|py6r9f5#5dIrq;)(l7KGtBjkKxg@W4mw=;zWVPk0Lm+*P&w z$ZXK@pqhr>JhvY*WmaMPNz0IecsJM2z{s3Hv)bmGCZ6ju*!0FVGc;c2F_*oxaWoA& z?A8DMH4#h~JW61noi&)9bHDxxolhtSUSb$W_mI7=vfm{;f80LMu)u=Y3(kwFXh+Tz z_FG#6Y@$a-0K9Gvyw}lecp8-rMdAO%1a0GQl zAxHgM^H%RbfueOUKI%TqB~X0O8S_VK_@C}Ol95oKO3NI+0qY!&vrf8lO1BOC3Qi>Y z+cDyfO`Pw|#Z5L=pPt7ONAK?QbDt&UQw|Upl#kNpNxXKyNAVn~OH|Z}80+)cp{F?A z)54j$Hq^!>`H#~MC?_kg+270q|GzR1&~q)>EH7n_{g&5QoA2(+n1_P)Ow_1%k^5j7SCT0KYEuL437(9t^S@A8VAvKATE678V$Ms^1fRzIN{$>!^rXf=h=YYEG2We3>{2 zb42Y?o(!JoPd5BPR1ohSN;{mPBaqX0ptZ{3Q?>&0&hHcq=g>%Ass*?MN8GId8Ev%hzQg2AFx zzrb-HWc-MEQgAGnKX%E%vx7NrXGZ1qCVSBM_x0Qpnx z&bjny&W9_HBkQmBgieK=B?Wd+d^m|J*OSov2ae4u1;Av8B`F>{r*pL#9FuiRke)tV z(2idB8!$L2`h_U>^_5iJ9!0gAlLb$b(KUbYY_ib_mpHps&9nnsN@N>H`OEnT2|vej z=Vpf=$R2dcwoQ|S@B?wH*bfewxKs7G69xzj&+m1V4NGuE;*!WpU?u(ox4reJ1cQCK zM+^{D)`65aKLW{4TETq5bkgCXskxIhPo}vsqbRs4(#HRxG`z+V+{_{VFoJ-a<%}PO z@_}MLfn`>vc8uIlBxfm7Hh|C`t8L+vRkhl~Ef`%KNaZjYn@GOO_V&_~@uRnJ+8Dp^ zLIb#|36d6^&YQk;=m`PojOFEP;9dEJHuu#vj;g(L!vjSDw7UW`nLLi5NRSe+3UoX z*X8Rn*Wm`Vy>$tFES%0<(~Ptr-V^C*4|^Gusu*2L0~eYVtq9zbnqDc42UWm7e8B){ zhItX4in#zz-Mn$5T)zNk+8@2AVmQ~AW&!+OJt^1RDqw88u0I8UAKd6|J#1CAwT&Gd z#?$0r`s`2eb^kVsNtv&E+zLM6x<+2@7NtGOdrj@Ecfp+=Jw2liYd7(B0jDieXgUAp zNDq;y>wSE?nyV)Aggr$a>b<{BxAYN9CJ}{3+6yyU%n?bSJX&uooFwX4k9U_}#_iRN zuaqjlP^)w56n8n)0>2jY=Rbmj^c!tw!=34RRdAg`QT0;fhf3d4r>U1<@t+L0Z92LT zVj9)*N^!|urz|deVTZWF(jQHr2k_ZmEY2k_L+a+=_t~F^Q4FG#XD-9&OYlAU_VG?f zmywaa{`&gj8raC{>id8f6G*tq^b5=$>U;-}A8z`5^p?x>_w}HS#F*-XK*7h;8qlfmeZxiV!Lo( zSvBUFkWsl!UaJL*glA$)|BwA0jMT2vEa>uBo<9c*mhZ2BK0X@T=UMLdMN-ncY75xy zvAiaZjy~G@`hlSXzqJnaTNdpA6W7<{(}w`U_Wr%-lZU(=eS-UevSvGr&v$M%}ri2BhjIl-X%h zlFy~UOdZVX85vhTRcX}P>Ad>QZ8U(bH8v)$p9sn=aLa$7bqXJ@nE$)|U-u}YMhI#r zs2bj!w1lyelHX*GwUO(%>3-9cK@8-uh#prX` zgM%X+#esyR{selw&&r;r`=|R8sTcc`6U3@CJ+9(7ORBhhjf^Cg>Sp>4`Y?rvzMIN| zj+bTKnN+5!2jAtMsB4Q)-d8=?em98tg}skVoO_oo5ZyVtA~vs^R68^iqL-~Ytfi(? z_eD&|P0D0GG3w_1mE98WUz~Y%uWq-mQMQMHqyOnELyu2?HpXO+EWZAk@W}y8**8n$sDP(U415o6Xg<8;~h~BGToW^Y&#<#}oi^Z8m zrf05)?1smdp&2|s(vVqmzGO%g(jt@RkvHZcF!bXu!|w@gRGc8NyF^1T))-H$W010& znq8|#IIJw3INtgB0{ukuXLqZ~WntNOlz3(K3PIe)Xrz9XOib>HSZl1bkXHP$uT7y- z`48Z4&_8#F#QzWyzoW9%vbHwD)$_|(1rvC}l;{^27z`$$Ucr3MQpi^@=T$Wm#Uk$B zY*ET_LCxvPH@cItFvlR|WbyL&uk=<6m==>E_oOBhJsOX&{<-YYYEk<9)a9j-Lh*%y z(L$s9;c;#-I6FI=hZD_UCoeA~?lQN$GQpB5nEg^$6I|ZE*nlInT ze$1VuF`mUJ!djY#pm~8>6hXk{)^*&jW=BvRn_2qVky5I3f%nqSB|__;Tegsbh0F); zzA+T)vMIcom+yx`e(tt8K6mfBh=GRiF*LARzlK}o@91(&OvH7*&&x#`y>j5;DSR5D z1lbpX>|XlT0D_vIFbAIb`4`RMWf(kMsb6sO=Gz75p50y9^EGJ)K0hb0adGBThf;m81>D(5x*;TYq<#ORh1sWAES6`144ar3e=qy_(?<2&y63xm# z-z=lj%G`IFkd>GBiY1-gU!-J@;Th0y>?tg$$92-6wyGjaG00TZ&G~Cy`~h|xSvi-l zi(7_K){TbZ-fJej*L71wCOMD6>U(U$-la>C;emnLh$k~Zn+}<2&sq8Kb8;pw({5yB zb2^&q6R*B{^(`58&y{;q&GKi5m!tJIO{<(PUv}=E!(z9{R3hwx8c5WQj6RKQYWf}| zy?PaEse{2LB=DKl!#GWETvjEq@m19(@!_-2fW;We(Rf*HUOb}=sn7BRg&6IIUkFj^bJ`a-gP z$nob>7-U|a*5E*P{r)zSUh$Rfog|QpGGDf!9ck^r08vE?AY~XKOlNFY(FXMB@x|9q zB!&FM&n1LQ*axvAwB_YRA=20xP3WB~NUJiNdhkRsx4Bj+a^BGt8(^4uG_+=c*+hK|@byqPYU4&&rQMZ6-GR=FCqWadrxvHf47#D!&J2N4;pfHIj}ZK2B+{ zJ^Co;V5W-!`{D=*iQix*0`=W~fX3ZmPzpXZVf9@5MTWXo9fDpA2(j33@zq{PR>4B1iv; z5W7~@*GrFFr$|I=@vdc@VjE1oI766^{3Y>q(~2vTUSCM*BF3tGgpKekI=&Dimex)3 zsqcK#%cev@5w6Q3k=(7eVXW=tsyBJ}cZH0PQa{Bb8~qmFHxZUMNieLOSzrA+>Y?nQ z6^{d+_1}3t>%FKuAIDvn-~WK=G%(Mc9IN$kZ@Z{y1%BMVEM*~bEYB6PV>MQT)?~m9 zXP2+3SK?b!HDO2SWPU#Kv-5=(bi5FPSm|!4!jzHY+gm=n3HOEfXG13Ys1~UN(a6ut zHZ@M1nPk|XQRhgT+4fqi{_#q~9`jEq+t$G2LZe7d$aF|jxDzVz)tb?lhU+lcLm zIX^oNt7#Fj-SM^4i2{Dzq>}sBvX>1Ini0-gLPqa@=)CRuxE1QHK=|WKrD$A$oE+o9 zy6xbeQa_D?(#~;od{pozGlhW{-KUC7h>5AxA{3c_s)o{P6~ooEJb^ABtvADGCz}ii z|8iptx*&MWCA$C>u?I@J`rO=HsN^I!hB%@cY_$f7@kisupRiOw zGx!A3Bw2CYcYU`(IkZ7}UWLzEkc#zQNPrl>Xl|;#{cJU10MChm{YZhVa@zUuPL7^H zqM&X>`%YCyVwdnU5%xUJ%y z^3OKGOzPNj^;HcfpB0bes#MZQBG!E5tDd=+L53P!@ZvS!G^KRVj?O6M*~NVD=DfYp zb!JLnm8z)qra#48R1LY2HDytY!vHQK{yCx!9eMl45J(ZaB9MfbsX~7UrYK(w+ z1Yx3qdi2KI!-JgzJvyj%cV)$gOY}iF8|lJO+%V(qind+X5#_nMcbZH*VwTMBzv`6v zy3Emb&B<}#pyI{=q8Jl#&5dO(cQ2jMw-W5fD$%}ZIF zhCb$ZUFK&6eXxC{Vdjzy)u|*QIqOk)axtEV zi^s0#zGyzl0x3mPFKzviMwm60NXG94&y5zpKyIE540BEGWjfI%G{Y6g%|XQDkNYVe zYl;%X@+K_DW_IGXDuE(-QxFXtYo+>7?4n!V^j z74PhJQ+9Lb(I74d-pfPf4Q-%dVmop2(u*Srn?vCe!@O`r<*>PINZvgztDquxBLP8- z(_+;dQUXBsZ3s?xNIQx3WM zUU1OaBh96V9{>32k-wx5lHqQC>lb;*Ag_Y_v$@C`r(ZFb;d8rB!sjxuYIWKaM)(}_ zi?NIOG$?uh0KR?S`Ik);XoieNjxz|mX0LI7yX^bCS7A=3gCQTgSta0l-E26qfgW{$ z?F}m?|L~aB=ChH19K-!D6nrcqB*i68Kf;_gB)NlMCgX%2?&#%{RL2?$KkL4PMk3sO zUcPhI`<&9NBoG&2hr}w&Tnq{eW3^79le+zkjg*%)pmn`uJ=ld!1`;~sVM5=XD~LbR z(h>lBqri+F-_9lb(7Q_;@mtU|ZM~6-yJv*@JYA&;Ssz{eL7KSHCt!0l{FuL1AyQum z<0gDN?fNU?UDq?zuB%>SIT91ZXa);;63h)DBli_`$k5fjQV#~1bCG3fS(hUN3re4T zL-oTNJK+s06F+Y|TXa_I9E}3f4|PI6!U(|p<`rsn%xxu4lAiO&vTYAz=#ia*6!))S z`R(QP2Sh!dHVoy&9VAV8x-NMr^B+o7M5}d3Lv76UN})E@#78=q;1`v18$l<^7*h32IZXLfJieX6EqO;620&V?B#nK}v&bMCDHxaiM-9iYR{~eCMa?aku8tpELZep9>}q^3`OIm@s`ounG(kCAN&h24pi1~(uG%>rOjOOY z0STjPbD4Gur9rAou@uM%JNnZnwN@4= zUxWlF^LzHkKH(_jJ;o?_L-5jo5=6H0jgA3LsV-bC{)iAAPtF`BZ!Dj!z!BPwCv>jl zY$Xf_yY_B&`9W7=p!wEa_t)}S0A)UsDCdZIjS7{LyN?W>)i52cv;e(#*f# ziqt7P_4Mwq^8vV}?Kh0i=Ou)4!~$ zRbYG^I$C{w(MegFEb94A z#c02e(|+G!!B#&TNr7C~L>O5k7-ePSk7*+~F*jwir~49;aE}{G`|mgA(??w`Yg*z* zH8on_JZ)N7h!^5Mf7#-DMrp)I36+sC?RF5-)Sq2I@RY&xFmko9(-x^>x#_BSvW7{= zTQ%8-E^Con*Ey+-sc$_kZ3=ku@>kxAiV$r~_|v#FXBtqrlo&kdUO02EeMafU`Gu81 zX`u|MV8zlK4ea$21lBZjtZW#+ZbPAj+5@DInz*DeqyW@4fjBE2#QHqg+ zQR z^x*qrU#)IqD*((s8S;}Vr#uNNoRKWHW;43ni}C!iWo zw9K97N~4!ZV-UM?&eFzJ_9KlyoBjoCX@!`R+Ku6E0p1J6GmddWC*p}oNLrCGz1}PL zY(75?PwPj#@?}Z*wU$%q_x2ZRLs^gq7zE&+;3P=zOc&1> z&o4kY6x#S~95*DG5Rfhd=a+QVFVRTx>=#YNvOA?`epWNwrVu38i6WK0!4;8~TpVcV zIl_Iu8C6qod(I_x?aIpBt50nbXLK;BG65I39p!{0?XMg$Qh3fY8fx}D=AH9>hef1u z|7RkoVRDh{Ick^a7c1XXUY~Sm-X*l84}Ys>*_kYKZuV5&HymKZV5AYEsxjJ|REEs< zvkT&gfT(j~_uNTfBTYs1KXaiL;7Kw4@&pH_#Qn$3s;GmJ8tUdgy@s(vhJsE)kF!xD%Ds9NR z(e#INPQvE<_yR{eYP1nDR3%k2tMZ0gK5MQDFs-pXFVl{mo+V{);5y~`AN}c7mpm*f zkJ}2w{dh;+JYzv5(_vG1>AvfQsL>l^yz$}F*-E!nDR2R5`F{^kclzB`A-r8v4A%>* zhnn%p2mt{>(S@b50#>Gb2it@1g!{d)ANk^U`6@8=9^7BrIuoa+&ir$1vLHO_7wqvO zG6r+*)`pi(?Ip(4kJj!)c2!Q^wdrxFON8HooSLqM?Dd=)E5D$2wI=UE{^CqiOvEFZ zCV1`E4G$!C;?;8QO0ZJ@z)-6^WaDKSn8ZwBq1p46hLUw7$8nG%zl@O9qm0t0`5i+Yfe{R;fSP7^q{)DBJ7CH zK#k|rOW~yXR^zT38SouroW;(_Wt^M7Apb^<(SbqC!T;F7u~(tqPMr?wxmLTO-=}WB ztvT_-wuR%pzii7}<;cN50$1|C1ulo#CSN{^XL}C&d0@24wg5Fn#7oVN{3;&PZ+IK4 zT$&;tt^NL@4i-7IZ0ZK{AU_@aM2k2^vsM|s6IXvH&d=?f-tofB^koyb4i?Hgt))Pflzf-RLXbpSUZ&`0_Nu02=d4Yo-qN3Kqhw1N`0ck!v;e z@<&pa=T&FR*Ut=d*Dcv*K3DnVIn_VRXT|eL(xN%w-KauqSl5H;;qrD(ib2xsSKqQ8{vC^}^ zvU;%G50P3@0mzEm5l*nwk~8VgZaxl!pLAv4x;IUsS(kVH#nvAn73`1k4iMmLBQCL1 z7#M!`6~o;gyi4p9d^%YMev=wgLmE4lIc2E)1N?n<7l{wPn*j?B3aE#*Hq>K}LV4~C zxXF1`JKJsW9%eCZcQi>z)%NJE@DpKnjqxP%CnfR9j!FkTyIH}0l{b5N+$Vx5;z|&o zv#jL{-rE??J6~DfUrC_pbUIT>H$*aUH%WRCp;_c3d!zMQdn83Sb*l|M8W8ZHP>7UU(FmdC zkc4Ht4h;%g+8}P8)dpsDtoQVN0at3AIk+DwoJNnSZ{Xo8<7p$pn6%Zgg8l>MZNt4J zEXFno1lD8MtakA&87?|nGiYH;M$UckaadZf`S@s-^<(nJ+(@9nhu165;nz^Zs~OeG zj-0l-SA;STD;Vz&8xXxt%*abHOp(z@R-{Xzj-AXAJ;YO<{L_cN4%Sigv&-&jzi~-V ziYXK?UHgcr7D3(|30r>uWy7G}=YXuulBrl07oi@vhx+WU`$pgQ3GGD{O01b#vDS+q zozoOWAhWs{ZfjHQ=8+yj#I!jPXd?`1!p;odTfq8A3$3M@b@k~SvLthn`WGJ&Q8f}t z3k_Bc6Ade$yU2nx-gD?{di?5=Q?`nB8>@88qiOe;8aoc1c7Vm57hPN`eT8RBPqyjt zUPF(TC%KtxTpmm|OHfB@E%MEy8DzuuLudteyR*p!jft#r_5P$?E8Z}LPvD`sz4ueR zzkFy*g8l{onoHiHQTH}5O{8p!Zhi>%a63o5GDemRt4uqNheq+H!`vJmW6gSeg_0X) z!Gm)lnXEizs;2EG88hEYJw@XnrZoBFY}K*jiP{vSDpHafEZPXx?FtGLVgiVce|n`} zs{i?w_UmE==vkJ{$`1^^+pcCFnNpLp)4<*{+-^>vnK&JJT(q$%R0nb1ARVq;xyomP zlv@*G#qs~cHyf<_@Sm4cw>l$X{Y*_%vXGqF@y1^WhXAy;*+ z8$vC=Zi;2O)e*u^OS?6>KhMxe1c~9bt)Sm^^6LVe5B#isbBA`LhXOu z_s^#z)JANbpdi07Q=P>z1GNhE`&7S!1`BV_eGd;`@NvW=J&Knq+BPYkQ8{>sF z7<};zNS=K#I2&KEJiheMn_{%z6gnJ9M4p6h4><$E=3qyV*9U^pW#U*Uf|%$(R* zlo>p}hP*;T4f$Gb_7c^sA0f<4Azq_gjOUh%d47z-P532GSTk(R;FlYmBmE=liZc7W`c* z%h5V|Z7a`SC9oFbj9SAJuuAp!GgAwa2<(F68vX0{agjuNTqIB-DF#9sA-I0~gkD5m1lc)+ zG?uiWg9pa5n%^hO9W7;-Ahdo-GAQ`RN@TFc?7lhs(A^UrM53HxjKDHrGAh5i7-c1V znnma#nP%;^yXWYvp>R^^7@?S9Ot37;yH zI;LpL4S8R&kklV`;lD#90w>s0k?k?-6p1$(l?VUmyoUPd|0~v2lx-r!l%#XjZK^G> z`&aeduMb@1w2*s)E*~E@eH>eRvv{D&Wg;SgIX}M`xn^2@frw;!gd!KI@pcq8QOOZ0 z)yZ2N!$ol6ug+kCOt|*^cP3m|Y1399|6e~G$HP%yz`;klX^ja}0y{l+AHxAKhVkVR zKWLgsIdC4e7EzSiu^j%v`o;4g#`KrA;uPb$iXVR$sqsl|M5rvPu?a4<@Yf4aqgN8v zyv8b0gjx{ncS1feQ^X%3z#X1%fJN}{irtc0qM_yYFOwf3ZU)$*{sb$JEBy;t**T1B zX%61xDlGoOa`DsJ@cKjX81DzpQ>DEEhW4_}K^&I!*V;H#c+FmDBfh*pVm5H?09Mev z{|5jh2VWXf4)`dM%KB79nW!_v)VR8iRnGlW^iQu;+*OnBKU_84WBpJpMgs^Z4qJYe zH5iO535d$J`CBz^Ad0r1a~hHBO68FjCRMFQxes%5I_{2}d{PKgg$rW5HpZc|rX68u zUor!EF-KdFqt}%7^4SyCj2c)A1+`C|#s39~ogiu&88u#6;ON~mFDWAvwdZJU9)1R; zF~{Rz7s!PdDJ}q%*bRSR|{J?ulL~(%y;v1 zSy}s00Ri#a*s^5rg2nx;YA>Gv9fa$Oa#v%;J(;g6iVHuQ8n z+6)|nXO9bGU5!l_5`h%*&o=ooLWpiC>RT3Qxti}3KMk_OcglXI^^rk3W`0_sNZanx zSI=Z`LWvPq1#7z4<*#ZTobe+y!fD+cg%`gPU$M_t5@L!L&d=RTc)u>2CnD9=)D%hv zT{I7i`+!tunS6jdp1{N@0lK@UD{u0(!E*DK3>Cra9W(PO0yVcjr$Jvm9>U;nv|b*{ zo^n5d!*P|A`LF)=@MLgJM1-oAo*mzOW|K>gXN?w}O~SYI;C1}0*viK~?4a9X4+osi zPLZlta+)0nsjS^bQD(D(NZV&79cTVXw1IKY=j$TeN$QucQw2gJZudEYNA6{Px~dY- z!K_Jwx>W21lEv=mA%nWy{Hv0OSt_BK5U%ppCs(uF9 zP39!rhLz@#zJrH)#MwBCCwN=uk2e}$`amncRgD|1am$P(aTGDP(0`xiRys$=yO=a) z_TlCJ*$+#eN036x@;aGxRy(l0N8E6&KX_V49mLBsxybX!Zrc{cSm6MJ$)#^XN6E=I z$=(93a=XY**ww~TTH3FZS>^->be#OI-|-<3O=x5O(|oH3#V|*KjyHFGMmP0J@0wf% zo~TyqrEe5$T`tb&^f-Mk>`}cdI^7oMoxw>t2j|}CaVyyT zJGUka;-lz(m(jmls}!th0@dl?&`2O(!LeV-58WdL4(@J0#8GsaBz^@q{Zf2m{1r|1 zCx0?+E~SUguhE3GG4$t>CEBXHP7$)z;$=4ID+qnSuxAw!cr{;WEs4w06YBbAd%d_k zR`S?0qcb$W6O-k0vutPn+s*0b98tIiUAl5K4h?tWx3rBon8m?--!;VS>7lb5*~-^~ z{Z(T}(j2cCHGLM^NmuRGXn;^Y_b1W7X#^Td>R5^ltpd`@y0eUE;>LJRRS#=CgX9pV z>M5ICI-mA_qD#q;}s6u0tA5P#MLF6Krv#I5f?*e~5*rXm_j<|mM zGWYFx`al$l9eQt1(gv8GUt_oC`nCSTo4H%@ zkWbKqsup{9sw~0BJN*g{DG49$MF9&)e#d3a9`EqIL{qWjC{P`b5)w&zvhn}qvO5y`qg<3Iql*&PWyO=TOMjE(gL6WsLCe9>`BlJVj7}xI@*}sWcRXX zo`&M4Zc_W&&(7=&rbe|*bD<5qcWrXkk_SD$^(3+4NIyuG!h-H{VDJeW6}4JSRNg%> zy32Crj_CvQldFFF3GwkNF~DyFre3kx!AFbI&5ehn%9yD-*Q`FoO$h zNhdJ#(ZCCHxLc|ZqnEuG2Slo?U2;^q?C#tNdBGayasv?ymw~9}I8RBv5Ii@}F%|;r zuLa}fL_>}QL;5Vg8~cB(xQo>0o6}GM{^-Egk@RqUC+QeE!=#JFg&cCZfI;#d2WfL( z+}4>9QGHw^ZoW@y7Ft*Z{f^;w^&M72`-{1dI%=d0ssJx=i^~*@o zeDa5zS1Ju}CnOkA)3irLMTsZMIRl6j!p%J8O;xaa!Tcr~%*MzzaPQ0zYIma!GJ z&!$ed1WFN}m{a5+?Cr_Fx3@Ka+wnFL%cLDw4k!lVm{pzhZ6I;0{Nz)~&p-QrSbGboD%)-ETWOI-kdPJ- z0g+gAw*t~#N-VlTq!FY;Iu@yPOG}GLNw+iz(hbt_UJG@fz2E&j`#tA7-x!Q%JYzVN z`@Zfg<~8T<|DXG+gY$N*wY}MSANP`|F~)5qGud!vm3rcsT)?ITg+jp#fCiW9g&Nb7 zXF*_E0K2}oK|ul@r(%xbmKUe=N1KygYr~1yLK{xp#nqJ!2S;ziLiU%N@{5Y%1uW>;j!)OS-`Aa zopVwr+;`^YD=PAyEjIvXis}0Cw8-7uA(xW$bTo>WjEvge_U|(Sw!iJ30|ccnh41Qk z0b+ENX$Fpq?_WBX`@H=8%E2fn_qrWN9f0cE*9KuYSr&oF#eTmZd7bD1xhOWs9_MSJB zsSJ7>_qYx3Y~bBU!Ad<4S-UAfNOU%xl&;_)4| zp6@w7N-lOTzaCxQBj8Fs{U)%EgUU`Pdq~gU=A%+hELYI!9?OvnRpKTptLV70cUOcI zl+4aK&UAH_k_ma5uMQgX@UYuV*Or?OD222>O@-9iEsRc7uD{&g0>Eijj!BffspsK% z&B=ZUz%JLvgn-rV!o`I%AyY113M85YbZ*WqSg+>cDF=2PXVcDHz;Zj0U4ZW;|JAT^ zQi+~Z#CHIgN#u|7ZP%--RvLYc!N`mRfM0G!diqMDNvadKH3z4)JFsaa3nv}Kx6Rb1 z9l;Pl3Vy>*(1|f)k$rLb!yLCi+^?3_HalFUlPX<}T8xW=d+0~r_ps||H%7I5V`C$l zUI^fJl&a+!QnTubJR&%4VWeKiJ4qecb!SDThS8Oaxo6^8=`gc*iy55Pj1#4|ELmfr zdEC(D@aM5cO^@^0wmugO3%%pIIB=u;b-MUainqSif3-uwh;@zwB^L46NOcPI)l`F! z-Vx-sQ)^o!pbaX|G1~z5YbAwW0}Q`W94okp;-`$%zT9jBEG`qFh1%!=pm4Vv-8Mka z6pOK9{5)uTcnaWsJ3H~gq-t%=VyXXdTj(8df*T$^q!x62d9bppt~bu8)RTzIeF=MGo+VOM z+m6c8ig&_KJNr4h9MEv*)1QV%^Mzotg2CI_u?=YjGC36`xRbWw+J$zb`wwM#D^boQ zP6)Ghm5xk!t-K1jBN09oG)L?zxP^}zktY_X+5>J4Y;n~D4fe`7NN;nYK9qAARv@3* zDR1uoMyl<;iFl}VX832L0B(345-|m!A?TQ5afB!O;Wl~yg#w?S@tv7;CydN`E$_^4 z`Q5(Db8jpwe;hmt*xRF{q6QX{-_vtQy#HoV(okFsYdD-(kEWBeJ=pLJbN zIXy)gKPf2DpZ#{cSl(wnU6z#v_%FcvRW5ea;u-q@l;yj8{Phfjz-O3B8G~7Fr4uJT z0q2s_6lO@c6hPOElyH!AP2dUr(yeSj=7``#C=p&~TP&&{h%x_R0d&UM_nlKil6a-i zFqlkTnu?vDM>WLpqDPZ~B&++>21uhRdH?gYK8u;g&BEN=!jjqkSua#F zlNNo%+@d1Tqb9HE3RsfK06u;VG$MH`pTdhNP!5jfe`Ss_h5nT}{tqY7Qog3`PWt(| z&)l3yi$5kG*d)ixO=Ql3pT%hwv8q{g%bWD5nRZfL{NL!AH{^9$Yh_5S+pDaC`HHmN9~W64mBB*)+Ml}CBJjI_6W@;&`YK9g z(pm${_Kzu?-5M=heaBUfuLL8ukC5EeAt50${EoTnXWKRuf>RygKi zw`RR;ff}bQKXHVl&UyP1aMfYtBTNq-lxZ0P{&~xB=Imm-NxM8-?Y@1`qu5^9i%yL3 z0yL9!SY(}bEXVnQ>Ob%fbcKTbcdF>gpb6`0!1b=#4PW>>C;PVt&KoWcaSLGtckUGG zU2uOL+aqMp3+qb2LAu%dFT3Ri*7>ddI6zx6eRD{4eD65QROzbzOlI~=yY_$y@jxPr z3P@B%3r;+#Z8h5~Oe}6u_*KXN;UeBLKdV(IJHh5MJ0X|f8>!IVDxuoX!_k z40YLw3C4eF#xUvm)=ETH{7JoAE zl@2$gP_bWDRW=0jx$ZnS(VgaX*VJ#i1k@d9LPg#5Zj}vHUHvIYpozC(@4aw=yuiCz z#up(am#PaYbB$-s+K0PBc}Bje{jxC%-V0q#bYNwn5azsXRxA) zFa&?~=_8f~1&G7Ai6uOPdj?ovVH$iw=-*I|ifTPE*-|hsIENRAW+pfdY8oqh2M=xF z8_^f6i0Rj9&$U5qPM?ltMv^yjjR|nl4=t1HuIL8qe^EQ{gO_rlE9|i2mK38ju>kR` z*ijbV^S3+@GNVSDEW5`tIiGffAJno29WArdlh(?X>OO*#Lo``v%Y4oujCY6(d14v~gOEGeZR=RsHS{izr}i~ESk6O;El-QAT_MT=4^Nz6>1t~BB%Bb zf0P8#>VG=wz+H*@>nlNE{+)~)2K%pS;Mdlb%}eU6Cu(Jy6%A&c+q0qpUHzqOARm4d z@=kU$pLlS3XY5AXoyC}ji}P=viNB_JDBuw%#i-SSOrG&W=!$T1@N2HS@1Dn{Ahb-Q zjL5zTFCg#*^F}cAMpX9GPTwYf&-2{P) z=I_oZ4o;8TIWo&IMWnk7UPS()m%%KTq7f>UPD>$w`$-d<*|+M(I3>4;@WTYc5z9>z z8|%yM5XPoU^OamyfDG|SVFj7jd!h}4W4>kJ}^I!ZfOxAxBxlA+RUT}3Qh_= z;(-5V1!0`{{Z1JIFqm)t4zr*0w-XKppo!JK&M3a}6 z5~+W*CJ!PW=z!Gro#!?42;6lyquxn*04Hm8`B zcB40&^-l_5Y3=y_U)Ckc^wL38Z1y8;L{~^*@=bkrjg{PPk^_@mA$hgi{cSUTj`s!V> zQrF&}$rZ@r+=)c`i%iBf9sF0xoWw;uLNZW&r-)4^10k2nx4=uV1A;0Wxbb$ae@q&) zjfW7*@~0(*`@fa`VrO6P701gIz$ur0n*BL;o?l>X#UD-$NCH|GW?Hq9_q zT~L!SLv^drbD`8s=Y>Q*lpv`VBpnt12>g?6Gs%x0T^eh$*QAkKPAo>c*7LwI?Lm-I z`Y)RVIHvsN;H6G z@NBa5di)4@HOVqtCJv6nyM2?qNu6{c-v{yyD}_KpBT<42baS%N z^$g$mXud?pIKou{HUmN$tl~6EHQ{gK<0S`L)&zf*Dk?NH zO(rI`f1t&t|AH2ec2OBhFmV>(2|*Z?zJR)L|4r7tP$Fgt+1qO8=2x9R)^8siV6P zylOE-0(5}zEf#%N@dl^;DEJA>?&&bv?K?l;oUIBSWeEZOormDgIDlmT)!%uy{2w#q zUl{G5ltxOpEpNr1>QgUi!XK~@*5i?H@nhRMKG~IznSj;+GWzGz)9DO?w5lV~{{lIR!!^pI zrsJjb*DEMFz$*nrw(r8OIK-Wa@uGAyM5s2^?W6ASZJ-ZSfXbA(@-pPaaDX{V>M4rf zAS-tmYi@+^Is{64pfKfX;nGiz3^WaLqQsRYUV{S7%OczrFldEA{~Ecj5WI5c0i=Nc zpONj6+hCTax&|dQ+p!hfFI;G+dFkiV=muGA)z*(63k#<=PUug+CL9e!y0fbgbC z@Z%BPRS7b@7i65x{#Tz3C1ozKt3t(5#0=V>$Oy!W!8Mk|7cRr3AqQ@s0;J|-?NUpr z{b%#i0B;R~=B-hA5OFO!P0>;$9t>OjWC$NKQeTk;FHsDJILk&8;FEXbRmq{m;kEA+ zK>+|)Xo7(cE{-zGA3uO>f_Mo0;L_k3^DU?*nzp7FI1Kz;QvDd}Q%2v!d=_umvI zb>jh6{?kzG(dC))CR6O1w-z#1EA^Z#ZgwqWL#`oRpHvglfLD8*Wz;E!VC4hDmNRy3 zmYheEG|WF$uzsD7%ZfHkgWESL0HxdJ688|E7?05_NYiS#-Q+67Sx{y>4yABKlBdDh z?}-3;ra>;l+3>}z!x}lM_@<-fjo7|-sgxs3E}ekcicv`b65F$U;_18dw|yRaZk0`x zUzM*Nzl|;NidWMTGTp)1A2+wbEh2K9oh{+epK$wO5~su51^;4^e^Xrl^6AI?NgfDg za}5l>Ja;_kCLxR>Vm%)CCT*b+$ovT1LyVoJ8IPDGl&M|PFGtetr*-=cbemiRDys>m z6$vu-EZfoBW0}*pDOfpQ?I^Gb{VaV6L>Snx&!6LP0;Pa}(c&xjP7aY@U%7Me z&sSzb^C_f`o3XtQ4yI=^X)B=#(zu|4gcTC5j!o!Y*ehkofJa?w&S|sHS1I6?q4-02?RQIe9JRSYiRQ9jlI;{e`#A0RJ( zg%y1HM4NXFYe#=Q@Y3a~v$0zmG1fd=&bsrq^l}K6C=EHYxY!`hc<+OVShpm9zMwTp zVGbo{k_O$vO#hh@Zpa0xsOMb|CVOohs@z%E{VWV+l5DX-9Pr>iQ^VzEVv)L~Rk#$_ zk-tPcDhjf^&hA>_-Qw|=mx4~?sh`r*Voh~YTz#%<-fQ^xSHdJt1?E~Zyd2u>6HN3A zwr3;c>J-7$7_+prjYPD$a925#BuN3r5;k(t7y2u%rX~8D^-J{ezAC%QMcQb$yV)W4eSZS_ zi}$dLN%?Ese@jr%a4|x`LPPp$Z8d~G^l|?K6-DYBR7F5c+Nd`S5^yw9CT}ppo^`m1 z;vlVO@>Fr6Zc*jIG~0fFwToFU_{uNDX)qZ?58t}?68m(q!tUX+ak#b}mKd@#+Db^? z`lTU}kDdWoJE9=}=_8ZEz#u(p|K7kLtU#ATT7$;h;|10}fNlq>4mh#FE9vPGrP34g zOccZc7Ve_>7Dj=>;6enDQm^>v$glWGstl)JUaRj)rHC}kszX}v*ZdZKO2>n1AUp%R z^fy2(VEZ*k3b{Ljb)Kk}lMS`cslU7U7I8Ke%KFcQ^jOGWqVMEP9Z4esnqorg>cE7a-&CY3)jQP08qxO}K#Feen^agT)Ob5wE>z<9sN2lweRqlrFO*g3G$ZyS$m71WBH zJ||wg(l{HMZ%JJ5y{4UODBKLNT@4j=D2gj_AE%Y8;EHVz=lsNDwOZgD`Rd-0jn!A! z2EaKW9iSuNtj&P4EtLn=dqW=khZlQIGa&rNV!F8>?UbdA)=8N8f@2m%g&jY^ zq0#_eyY0sMo822RJ_WhS9rH%h*v3~w%`}zl<;1NKxsxX`z)%jfDOMBF^yJgS(IW@D&$~K4rO$OjQp)w~Q!l!6hN@Gs zgN`-dWeW;#L|iX+j5sY(8zv=-1a_%AJcc~oE^|v&UnzBQt^o0+-EhNc~bgU1Za_ z19K4~T`c#kYrV|c*s(9$DT1$j#9H>MJxskyZH8m`gRNI<%O(qQy|6(JFK8+7A6IUq zk-{zV13luhpv z1>IKwJuilGu_q_YZ1p-!2B=*g;SR9%hw$G2Y);IjJM8*g;e)ZxvyT21AfdqBjRwLW ztfc2HUcSsU6v=F)cg%@}XxxuA%B;oRCqCS&N>^YPq?EiqGkD%Q4uS>qtl->bpo5sv z01U#hfWdoq0}$1KpOs1jy~~cw&iz8%vd)IRnx!)J%F%1RclXtC?9RsoRGWSlOWB8( z;)qnL>{->;Kx_I4HI>_K!pD%M$ID51ctx?}+vqretB}uNYSa92ItO#*3U{tBN%$S- z3)BH#k*#vh{~);6tOj8ul<*0G(C?!~0E89gU&^5#X@-GYq~#4GeBvbZ zNfRz86PClx4rZJyLIZr5rv({f0f@>1XxQ`eY9F8?Z*ezEVQ0PJN%YA^XfCPY9>K)5xcoT_$ zPv!=h<}zi7#$t&OskB5lsUG!GNiwECS(5pUMC!38GPl!?m$9XmjZsjLfwFeNP_v6q zQ2(TE@(|xfGW<#RwpqN@fI2XpF%TK2dc{9vUGn~tgbi?TFtzLCr2FX+yCXRO+-Cp` zjpB-f)BgvoJo@=SGhTeX|RA0SE8WKUx`>+^D5q%V=Zvo&m7g~R@=)Vi_d+nMj!l*-9LpiIqmb(B@HM!m>hvhe6> zcZo}9)p95pURdJ$gFQB9u#LrVpT|6CeHU3=e@F$y+#O~HF-vf&ygSEBNNgF{EzgC4 zdRRKr&6}Ixx8qyIia_vhzOyx(=tC(COx(d^Mb|N{dmP5T8GaTe zfi5aoY90rwKohkl7m^T}Ze7j2uayJiFN0TdKZ0!($LQfq1pX^gWu)k9)Op6!zU3A8 zec*F~da}CgCp<8ubQ9m8Bj&{hlgS3WHzF+>nkhZ_L(`Dk`JffLpG>?E zX=ZRjaeSZzt%>qV)^?q3=lD^;$_efbqhJS0rJkMWP*AlUC{F}O4zyyF=nNRi=4*b7 z==?#Nf2gm&)7Eet>H_c-5Z@xBAS-_{{YG$mgy~&3)KU-Q?dL8=kk9fqZ=002CW5ak zK{+t3HiK_zAOL?GacyhxAK|H5aeECr!tSUtP^tq$PPZ`+zRb3#zUgwejr_Edm~QRTN?p$Rj6s5 zq57hVW#;(LmgBa=hj4xJy^jwBX-JXRQA30A;Ah76OBOX;cY^Qf&j7iZ40N^Ks2z7+ z1YZY{;G_hzrZpDgnGK^drJ9cvq4Lf6-Y!481O57$F!+8LIMBL@xO6LjN5tGs!v|a- zVS)Y4)fNpn9vDL}%Fq;&*o)v+4nCBB8WB4X41(D1vILg*0DcC8^kSuJ{6XcZt1pqY zTf>*oaYPftDaR3nj#}G1(hxmuf230ov4bm%@`&t#Y0fQ^9`Sp>cJ4s7o~8;G?a0}o zXVto~3$GnX_Xuo$pRUS)Jp8MR+h6eD`wsA=>mxxL{~sR!_?XRtyFgEvI5YSt1<@ov zaEj|j#}@k+^2~hAXEOTNIZy_V2s;h?&)*$Tzshfsvr&_{4Mh<}3cgz$E_Ok!)@Zxj zlhEB)E&s4`44f#K$)M)Sg9?nN$+<~Am?U_dS39GvO>Tzbr$}77Cvr}|ljubMeXoR* z8govQXRJdXqMdr>nZ9DpR>;m-BA9#2;+9E6LVEJML5f-B+lmr|RA6}^$CH0{Ctzmf zR)*70lg~aifTMx{djxJbl48?FbGn<1%P4ZUwxZUGb++gLR6jXGW_P1x7v zp!~1`x#R12S(f&1;^qG~hFuH;Px-Vr^=uE@jy;aL1Bi>W$^qMobbTiNl>azy6LG-u;cl_?s?*`Af?ueV-CDn>l)Z zJK?Pa(EeM=sm6RUX@N#6tl(ts3Ph+zC6*EqBdY8?UMTzg?_uxcDTp>$v&@;mk?X9; z@N+m|+V=MihxdOy9wd1%%%Gq&okqd!POi4TJjsX)Z9rM8a+cRfg$KL10Q;RlLHae% zNsz)N`ZZDD`Fnr~aD-;n1j`x*nvNiVzSa3>H+$FIAH*S;2R~NTxoJYc?HKZhJ)h=w zp9}jpEv61c2}09_y9iPgAR{-356f&wGJBg0Nzy#q!_T>NI6U=u(PA*Y(KNpX+<=tT zflm(5tB`I*U8#afB|Kcbh4SBF;EQ9>e4>a|ySuzb@fSlC6a2U_T2 z*0XxWWWIUpDA&AAH0Gd#>j{HDt*-j--3EW2D_>J_Ks5FR8}r`tA=&mHe!alriNAy5 zzgZmiw+9YHKqpG)KCQHxg!uaP>x^Y+YhSXtsqLeO60Kor4L^szF_p{1z+`XN7k#kl zO0L3wcYpCEt2(2D5uw=g+`aCw(RxSwvWRx{>~)1KbO!c^vuwE|;9< zC-!iz7nD4@IIOl=5F8mR*uQPU50@i+u$B^El&Rhh^Uoq&e& zwkREUsN8DIp>lE)=lc5Ui9hn6HTL!U+5!U)aAj1Nq{6=b7J_bbPr9Jj-#X%bR z`Z7tAZ$jWfk8+_@RPQ1q2}4@fes}=`zMk#*j>uiYkP+db$jG>e2!)m-glYo#$Z>c5 zvk`YXK*<4Y(bd~4=(6YQP|-50q}=u$H!c<7a2Txd@g*51*sB=ql>51$1w^3dk$e6#KXZ6mgLUV#hWw7wSD57 za?=*tuC_?55aRu~7U|m2a}O>^2`d9S5_0QOjqGPB!K&rIO~^Q2{KnsSUEss5Bs2(+ zicHyax_~sd8DC|T2!_w{c?A(3qyj#nD&;`G7R>y$7L@Lpo@C^gkR?vfJkSn#3I%tp z$h2=kLxyc~_{K^#_c*v62M0gb3VE@4bS-qe_{Uq^1(6&uy9!c_QM;~+d!G$P@sMNO zJGO5!+{*oh(kpV*)-V3Q-o&2{Km-6X<>C}P*(lepwlp)dbXNiqGkpBbaHN44CYo(G z>E5*~8L@CS+H999ur2^4Db4zm+w8J7+<39cVJy+v@ht8?4D8m&Uj*fGM;lP{Kl0~q zY?p>8D^W3EeF@O{y`6WL1)Ia+`lBpb2ma&SYq;L_DV!|NY~f-?lwc>m=vX6aW^0w3H=in@lUhc>;*2IKA>NC-f%nIM}Ee8GT6P7Db%Uro-` zfR%ogXs5SY<5MnB^aEI|8RY&?gH+w#(L`GZ% z7%>urY9-$=XO5YwzS+CQKD_e!H!;P&O8T`wl<*pq+V^FLKe-k);=fW-vbc@$z&m!$ zpArpB>THu9dvjhwvP-J0=f<|u36y9cY;&0u>YLq6M@w{!Re;j;!9ZcTQ%TE=AhiOv zL-i;BP+sUcilM^p#M9S9&jE4~z_w|DaDzqk2B zXD-!pfVw|h7RcG3N#PUH`Dkm`1Jlw>G;0rEwyM$r7Q>)jVD$_ybtUK}+%)7OpI18! z6adAV%5E2@M;`uSIf#_XZ%Xc~Q+1H5!(A)jA2NUZm}dl8JA7%Yp1*h_@d<(a`0Q~8C0bq< zDPhA&A8|+$g0Bz5AR*FCDhV2p-qpNTnLD~|)y@14!0A$g&8v*NMq5J)uZB*{^QK(As*gaAQwpK zbpz(eA6?45hI5hb7XxZNKWEu6VByrEZxa{^FSKR3?=)+wtRFR9S%WzRKYZftR}{u7 zEoP4nJ(Uy`ViLHs0l;dwU=!g`*RTP8U3)m$?AXx{AU-)+N}5$yd$hHFR#sEvw?Xt3 z^gSq06WHyfPt$K$`!pS$ugeX*Ta&o$_D=SJ3e9qB3L$5oyzz?@Ga)0$&YL>AbpB^Q zzg*~`hCNq6nucS1jN8xGo@={Q)5aG#sO~5G;&;4Nd#_~how((Aoc>ij$X=d zS*zJw)*_%$ey>Jtx#7)DTHLgU9^vKJ(r_DCf@Bp_H|0z>5OR#Dxnkc{g45|7E_Qtn z*f`Aqi3T9kh$LKPbg1A_tlJ}F^9)}oF#S->kSe}fXa~2hR=G54!(~0SS303Q_{(}+ za``>KZw6w3({PO0_}@8n12D9EaEKkud7)=3o|nb9i8v9$a%!<(4R-MJnkRe zfPFPqyHlR|;f&k1Hq&TkrqKbniKi#kXg$z50N9+Jj_xJsZPLNSY?Xhlzsb#6UN}6A zXC}0em9_M0!Y_c@*#&uB5sDc9FgS;JUwMc+&?sd;m0^6Ac4>Qltm57%s|#7*ybzb5 z!zTa&h-;!hJNHpC1mEv{oD4W4nT3hQZ(Msk->i(e6>@K`+b6$J{RP)i9$?^FzkFd$;^LA?>)%*AwK88D1}@(-o@crt zt-*~b{exu?!URM`Zbq%r)og#Uj0*S$6 ze4+d&qo1!OVgKjmRa{1ks!wH#pWV9RI(XW4wq^j#J`k$g?Vg*Ha@Mk`a( zFYwVgJ#BP9`7!n(!V$c9gWHiIdm$_$*U|qy~Ciit*vbnh_!&`t9P~Dd8vDxL77cDD^zwNd_Ed%Oajhlhtrpy^t==P zd~`_>sy!c!{PT0+Wc(9EC4m5dQj;MMDb01?VOhPW5Y6!|>-Dwy_8&d!9qBud07-@Z zVaoPq^0aqO39AJ4R|O=k3?x0EiLsHmCWMYjpw#dgU3OXPN9})LhqhH%bo#03o^2j z;Kl?VyYGt`Qg3DWfY9uz(O0<8tOM^cJwA6*3vn;(rU{6sM{$G-P=7<3F@JF_2m2vN z(1x35AM{8a)I4!Gr3{dgw&51t9f7V$lc~W8w9PKHj;eLf?7{BZZOxE?PoF-q);R3L z(vq|qltKiQNq;}N%PzqP6Qji`sf{BqXwayC<#~3fmWS{|AV0Cf<|z+In}}yy!adBH zZaRR@QanI?jo6!w`?!e8Qw{sirTs6mB~K^NefTTw%mz^F7S|Mem6hPnB&WP(6cBUm z#P~qlh!S5YFqk}z$=R{|HZ&^{(r<;RihzjNb>v)qzAkJ$rieJQs`dgk(OM|)RR+i< ztVWgOLZ5!d??oMvHIBQ;hIcgZEl2sMPC?zVII_P`6&$}w&@74vFvGu^Md9W~Re?0J zX4FD~mvipn7AnK-WhG0+(b!(?5>{gwqM-X$8>zq%~kf3D8vko5Z<5N`BK6CI5yAV*i2K z%?{U~yl;kNQbvYsMw(_AGG;5&C-2E&FNv?60s~;>07`JXST7%IvL@q$Ja`o`Blvs* zgeoBW1UT3Y?gbX?mCgGSXJFuh-|{i=ax4Xt85v%@(CPxVCcr8I0RfnTM*>su<97k% zr>NVP@o}6zz%qOC0GNUw?6hFF$n;=LiGVu+%5i@_!nGm@RiRE?ycOvGn{!)lU6+f` zbs&s_AEyD_I6I`bwbHw7)*gU`_tkP^n8VXRu`8vs7X>s_2oXRu3jKV6G-@YaEbGry z0DCdrUJhRl0N?>N0A!KCq^P>n<0c``Jt6e!6xoy}Eu@|wwB*u&)~w0kd)M6-&Yrzo zrXGsnA1VvV@3`6o)o`o}GcOq;23IhWA8vWEoAmxvLB-?qI&HQAaIQzcr}KNyK@(Mak#X^3R-Npl=GcC9+4QonSj&f=l^V^VG`+|} z4Mp*DwB#pr(#DUoir<5rM33x?yWPi`J21QgDM6wX-0iAK=6!K!Z}aaBKd z?*UVU54MLx8E^D1e))h4waB^gce@uOf^R^cO!m)SAApi}SOPf~gEu1_j(VR!H-&Xe zNs}S4GY(i}0_U}rSFk92HVSLED3m%5kjx@E?Qp*; zA2C$=CA~V}rhOLu7$RZVT|$=y>CwRwI1sv*$vC zSAex8_=(W-@69$-8Jeq7H+uR{))K5S1^1;BF%tO${MuhPA><7|vFzk!74-V)BX(6W z*CW&}pyfY7t^bmfi4|h~bf!=)379c!UA4>mhu>qEq}Yq>1Xe??TG3=Cnwe!}hB3s^ zk=ln_u@dcP8)Jb*#>hw$qMEjy#H&J3q+S+?LUG)#m-^{dq9t zAl%Wc3_KKQL<2>b*$%96%|(nxE(uuJD)=4lIPZa+#JSBLY7liGXgorNbmym@<-BB^_aA|CH!%M0yEOn%I@(pgMWmlb;(?Ic3B z*y*Zb0G!K>{7Sy;C>V^DDZ#dW-ZXcep`4S&HmQsvG8KDRNrN+}RH_h}Cz#url5=w; zw89k3OU}1R!)n;u>u=sZ3t8P3e?&cpKKP&^(7U2p7FBF*U!dem^-aoXG<^vbb~K;+ z0wOBxp6ca3sC*Bly9FPVo_}L`0iiHkC85l|_kF#7UtQ}|yse1b2T~cD6cEB90J~_m zJNl?(B|wu&mn|iWt?|MJ*|H*XHd2<*7kk@mliQmxZ{)noCtzUZkNH-a-+-KdaWglM4r3BE zd37G-*MJilLWNVNppZbv^(&s^w0@y}pFOW5eq)$}M-S1z{2rljG{dbM9m>?ydN-L1 z)3pb%H3#vNU+|UB8P??{8wG8A^=o^cH$KkDDaqN7<3#_~iNe zqknr7)O=#Bdp;881c^wA3M!cC*=S5{Omm?(MW<#ftuj{SRUm%GhzhsfAjAm7Q%@GLSCXprRjVx(^I?WA(Wa((Z@v9N`4uG&OawF<0dTQ z_9?sA_IPNF0^6%N)aWzC{pF33snVcPic8B|=v5+8Z!*=8;|H%lF<)JT7)WJU|(O#f-iohRn1w#cVc zGOj+0=_3@OR}X(C@D6Uy>fIE^~AMr0LT2Yt7{-;lV6f(3Dz+|+T0 zRA09qUI5%&8y6pIJQ#*A*8Q zTCUbBcjfQDK^&R3bJyib@qW~BeT3ih4@-V%dRYkAd9b@#+qTN;A1V4Urjg&vzGKhR zL-bp{?HO|?7H2jBzd4grC8c{f(%MsBHDB123n1U5Z#7ki=00O;oHU26*?O-3Y|_R)j=*gWi3tmfHq+~XKI!IVA)w!E zXP+D<2W z2mY`C%#MMZ0B&8lnOfTcf5ITegh)$oE3W01OtbM+_|;JxwWDbkeOcN48N1u95f*z7ZRPC3Zf?A68FB54N!b~g`3^+mg{NNB4JxRI(N9tzQX@`(Awnz+Yp;cM;Gr_>0? zF&v)i4zgbK=da&BQ@*eeP)*xUN>J#$^WKHZ{La$-5~bJeQg!S>OncPEh8a&qqh=Wx z!#(xA_{*j+iez7e4pMXy$p!Q*P_I`#Uc+uvC2;wY3w34cI^SYpJiF2!J}Q4M9OJfR z+gx-JRjl+ikx_a9eP*Mx6(`Qof9l@bL0a-G8JwgSax$>S6Poo;3}TE}QBUN3lsXPD zUcX|2ujW&b+xW|km%5z>biR98&K3p?eI}GAB!lE=*P=>I_q=>* zvg@{2-FMZ5z?jdso_ZNP|Hz;YVXr7l9jTRIa7Iu-C*Bv0y_5zUiwQT7*S;3}AYDWq zO^F|ze{=ZG>(&J1WLDwDSKl2cMS}57g6zcBl4|NGy0<9Zd3n*Srl(g3lx!vvMr?a zJ8VmS4=*C8#)*1u0sDy<@H|pWf=-0)jw)g<140PvZEi#Fj@<{U5E&AI}~B(#n8Ml*D@EK z!4VE)J4p@}@vx|=Z)g@89VEq_gf4lPZIZ?!bXSu~#DrZ)_24|6cZhP?<=z~tP37c9g~nF)lC8qgyP%V#8+;qgwj_Ms`)8ud>BNI2 zFyyMOGWiFRXNV#SUzX&1&S#Sg0~oSa6xBNJ)QG&*v^O#27DODuK*egNt_+s-k$sSl zqM(dKXh>+=b|Z2@Q3>LvhZ{9_tOZ$c2yI^q&>}U>p*6l#A|SW4;t#NJDW}BQTDi~+ z?`6{cv+xYvJ<@jyt0IxRcud(wy8GLP=<1PtC^1nSM$goF&Jnu2E7F(b=OTG&9}Tk8 z-)HfO$Y$pJ$DmQX2Sdia5m9M&Y);}{L?2F;Ont)rdufT^)~tYJE%~WLfKf}5FsfXw zD^(Qi;I@$~3f7tW7G`zcRlV0*Fsf)0@Pe`*n>YjZE|$%aWFcpb|Fe*4rd~{*)u-U` zCWfsHYtOF4W9;o%iZd&<@rOs_S=ooFW%e(_x|39Em6VT)@NMaRyVNM(2 zvx8iTEq8u&p(j#vt+2{~Ro4#g8}py2G&0p9pG4e}7`b5{{K=pjp_ho#p8sWQV-4mF zp0N!{&$$}erbIQ#3E8$DpHKP)EncU~RdUBV<&F2Zw;zfL^apb}&GIneB34ht;W{`RyFC`aXtuL1kum2mx1O9@R zvccz%8yIUpt4(8SxqFUAb!qvR_(`9a`bM-Ziy7qoiCOV{cPbfv21)RIW3Vrxr+(E; zN{T%4ou>NndVS`jy|wkXO>9xOyylGcl^^XP1h_P_+(jzCClYQ}mdD5sg1?OX^z~mu zYIEaLVWQ|+9ph6$?Dw;70+5U(MeecR*{pKOYn~KQGgP zniD6GOuPa=Yaa?_;c-=fF|D3)c3zJ?<|wo_m(6a0z2awoBDWdNAh&G?vl7Md zV($=u7p(6E7v%xg@C(*JeFTAqX>&oXKw+(brLI?VRb*{k$eVq>F|wa6ta8Ycmo{bz6QSg)ZA*{ZlV&_Gk-hb8pXlAZU1O%sp1)QOSG-KYAC z&R;=6;7O#4toz!tV@Ma`d-ux|T?_w>@Zsl=EzdJr^0`bbKdOD$u6Ztep9p6h^+gGq za1)xW>)}^Mdt9dfc^4&rk#p z%{~H`#J>#9-qcEY!+5Jqfk`tACClumh};%Ga8z9k_`ydULeyhXCZWf?d9$OjwG(S! z^x<7F8MhWnS5<60^1rm6^kh`XIAqLECf2S>5R4A>3tJDb9Dt+D=CckoSEY$ zAQus{m?M^K#!r>x*c}c7RLgS@Mc#fnNezl658A&cHkc?|zexc0S}kPgsp^rmhed*{ zs&da4lUh5=`rx~R?$LA?%>b2l6pZ2{ZP^7Kx43Q52=i2p@Qg$iA>J_R4@BkO>?mkX zZ*M@HEC=Flns8t~9r_bGc3{h>hDeC-C|!_}1~p*_gligz;JV|!Uej5BGIh18q&(u| zQfu3CTyyMkIg+N<-B2Ib`G$qVt0394gJ=ZfBCwopi#aFu^hODUrWi(?FgbB7W%6k1 zd+e;@s}+-G@7#^!ZJ(Qw;gPFG3$}gQ3#rwTW5ZDBWO}JXztKqliD{)Z+id^OC$jP{ zcbXbw#SCU21d+*^1z$c-9i&}*7y*AJ{QvPQ*&E^x)Mr{-zUliA@0t+5_(V^ce96!8 zwc}GkvOT6}ZJK-3J_(n-!;xDFm(PF>-aQzfV9Yg&i(9pngV;!Q*9LYxP|LKpCeH{X zx2aqrdRi+hWON_`CFdz^eT(H5pJ}=pV&e-V%qQLu(%KNqH5hfLI_a1>$s{mALVQ0+ zBXkQzJhhc#4aSpDv!hX^gQ6VWJS@H@Rp^y(VU7qKdef**xNi}t^dx9$)m(7Nu*@7v z_k7+>{z84~n<&0)43PjEX^yivBt+xtj{A+9!obiRX_4kR!Tn3s4x`7O7t7vWJNgOU zgktB|wQb@2C%Haq-rnYTV1&i4=O;J2Do;qi7boQ&K9lr7RjDG`vprD*W-r&Er8#dV}9WaCBOM+1e=QJc6;ziI`Ac>72mK(#yax1gRQkZKSdr5^MGj6!KCY zem@;fwQ4KP-CgX|r;V5sw%PEWZ<|PyhYeD>i-^+Dh>_A}Bk^?A$O1kPE&+X2VIiOW zwxb6JVkB3vml$;47bDb^v)&W>Kdik4P}SYnHjGM0gGh^%fOL0>q@>af(kb1gAT8a8 zl7>TfgMf5*r*wD6yYc?t&-2bZ?>FD~%zVz7GsuYN$KGr2wbpfAYc0@36vAq|giQkT zT$=GI$7j2CXhJs5Q11}z>3!`qsJ|GcjsI4y@B#*kKk!>#@{@sqceJLSTELR;^we|4 zgI2XwhwGJ{9fIN?2HwEVTViu?)Vxx_b)UoMDVNV-Ae5r)ll^;|L@3G2%~x@hP1x}R zN};oD!R~NG1=bGujavhELcBEBls0}fHJ(86udz*-;y(}>!#zt6Qo!mzLhzH`!#5IlDlus9mx9md3>IGN*T8$?0tUEz-kp zpF`lF)6sUyAD@6dhIukh3Mv^YRQ)o+@61Dr#77zd^RfT=fRT=h)S$G8BT?&4L-tt1 z%f+d9m3O1sc+;v4c9nVeCn&k5UD;Jm435!EB(pC)^;D9_Li0imq;tM(MXXh$$INp_ z+dDgK;Uchx+1F)>zJK7gLN)d$?+ATC{MiGkaqy)LyudMviwM;lZgVEQ34!HrqEh7B z_41P*%GTBeE32s`Vn%&7zOy{OMr(VX@^d%kTE8qR*t;uUD3yP2%eYn^5oJcD9Bt*y zz3&y&o1K)rmUaz&V=z+xk;n(cyEcx<(odd#g&}#z{^TcwR1%*D%oKq>L!VMX0)Ox3 z_$=T+Dx6J7_+fe~SJ{iB>s&yhu#j$G)0}_VBl)&y9URlklTfXlJi4w-u^>r25b(vF zb4oe*dUi2%e-344&EjF5S7ObRC*YHJ9}rUxeS|2V44aSl&0L`pqoU^g_H8mGo`&n- z^)*s+^W9ERZKA%@kz`i_XyMbIjp~r#^+osW7Tx6ndOhCqUPF+CM&WJn6O*+nDd`;8QGsi z<_jwM&`pvkrbk#JH5(e#63g|tsZe*(UMF zfBq{yYyHbN9(c;l8s>{75IDp=9#Bn<@09jDEAM zrFh2DRA^wD<`M_o_;y(pS%%qx?HSq1Q z0XD`&F|QO-TukY_>{1ua7+3ny=Vg*MQTFi4{UA4^2v$TYLxLJf6Yb@WSDyXB#j2`q zJ+%g|vQ`pv0B4_dncd{j%L7Sg5&NrWq6s#7C#pSG*~ldne=fKw#^=FX2~MRc%tU(| zCskT`5|20NT}Qn9o%l~kBdeV`69!n;j|kv-go6zw!0fJVj9W7vqnV`DriR~4>+InXh|z<_CIv|8y(*f>WV`p1IA)ivea;*uC~|L8J!Rv=HFKxf}&)7)@ZYNLIIUj z;Z)`qb|!u82nWY--G**j9n+xpgR6vXRlz1eD}4WMf4fvBx2!o$gDiuvI48w332 zuiV7!s^)taT>TgA4UA@WH)9v3J?d#?ZH-Kb`8Nn0Eaqy4>sR#B^fGxI-D?&GYh7gyldPpU-rOd}!imG2|0J z8FY{KkpN&HS5)d3cqcPR_~a4(7s7ZagAPU?y&f>b9s!~Bu1)g`tnzzw%i{2$P!Lzc zDPfl)dN#;jk-bzIg;}L0#VP>%a5ZLl)W)4y*UBuTh+1X_HTvgP1S2?ZkU)!tQ~FrE zF1EfG1#LchXvJZsyqYc#cFVu5T~nh8pS@`$qYy_Mr&Fvcu+)5Wdya1IRq!J}KnjY} z7xcrhr|J=Ks?(sqwbIj@;Qgf>C=93>F01oox~I8YE`^Ugl4(5O5C;bK!SnBImW&dA z5w$k#{#`&X$-(4<96;E=f#YlW9Nn-P!F^@Qhw&Ii;$+H;x7jMfmqWC)q4xk47fivQ zaZw63*j8lq-y{i3;=)qlJv|J}yql#3=+P82$eA8QIH25Zp{9f-$6*W5hdtY2s*OMt znIoQSWi(rhYBn2N#N2|x`qTLzF2Q~Cjqx+`vffc`oKVu<gww9e||!tMi={_-FN-QV6itXMt6i6QZnKMB+axz-}LM%CFi95GXt3f zcO64^Gpj=;;Q}#&)x&oyQW!Ic^Z}e2JV47$Ouft`EnYO9ph^%9NWTFz!BWn;+>0k{(4h=VAL^x2j}6$g!b|D zZK@eOFIJ_6@Lw7&Gt25-kQ>;uOcXN~a8w_VB;IW((Pn!xSO-n$c@2a>$4npXpZ@}OGU$LyT zeaxTPq4YB>OFy!OOx6D$?F>zJeI`86Dq6MKv#rSk0Sbj?!3!xV$yi!yneRl###YVD z)LotA2dw^-X+c<%yys+L5n{nwg#i)X8Z1)K3*?~4ne8a>a$=NQXyjYltOKc5bg)Ky zYQ|2a6QjRW6qFVVIVkg5AwDJGer#QgG}W-ZJbZ8+42YDI3n4t_F7Lv*nloV*7LbeE zA}4->B};NPNLMDhdpcH=R{yR}TbU2p22u89HdPVE@B6CMi(T@?h9C9@nupFB5C^IROqJU{i6>3xB>f|H%pV;^FWlH-_^B?Vbz| zW9_1$Z`!r`bP3(=f{$9=i!|RAJFtmjP$zQOP6N{io64V{`8q@IV0dg!UUWBCLjz!o zo7AY3U?R;U3o^bD2Ftw^J`4MkGH%0Q0%oJTD?A1LV^pXjROpu)4Gs;6O3Xy`Ju&ZplhDA*{nw6F{GQ=A z3iSGAeJdD5vejLp8=jH3EYF%TnnYUkaZ8U&eMu)sZE~4exypL9Wy*Y)%B-I~J~ZKAo7 z8ePrY$RM+PLw9{jWGA-BU<&nBSM)V^g3a~>q(wYT#uOMbp#p+-TUIBKGHi(rFd}QQ zSS@5^*ugvfeyyfGNG^Y@8Qv>cjiRK20;+->gdX@qum(;xdnb zF53U_5R8tF1_vjiHs9RcxM7Ubad1>+X1*MtvJFZwAR;>gkP%sQwkZ*C3~^_0X=`cS zrv=i2lwua|?cY{#IT%IIlc|#imoLFaqEihOVkpEjSfA37O}c`}=5>yQ&3Iprlb@HV1Hi*QHj0<}o>!cn zw{4*{2f%k-q7A7@K7zX^PM}qRMAvV8$}CaIUC4+{-cOs#Yto-0_g`5p4}p~9+B7k2vb62PjL|5yjAI!mX>zh+ZOP+?h7T-A|lEUFWs9OroiYEf1M9C3w_1yd3nfu zN(50@C+Vk>f&o|<%fY*Qw!5RC*oq)vVPd`Xk} z>?-u?otsi3p)XfKLA=mkS;*}y?WI=#=!&`=9^+F~a?v!a5yeW0(CKQnpIIUbI&pa! zA(wKJPyN0oK^Vs=3*yptekyT1oCg6f2cWu9<;gMCu>Mv9z1OBoprj9658wr$E=Mtq zMb*xa?)y!Ky6b&aEkhkXAn{*c*8uN&oh~(Kv%{Ug`r!ZY4PqmhR`>^$HzG2^=XW3eCQ&QIFXYmtOcaFBex!_W zCbBVWpbc}HLSGteOt&cMTuk0yez;0r(EW_)DiV~ljQZm+vb0+pDLvw-9s>9p3|LZb z3|JdDQ?M?PP<+fqlO>d1@5A{rc_r40)a&(7Uj*&v@FHuS_b*dDsvhcx&eMMEg-N;U z3?$XJ&QQ&bc;M9C8<>_oDs^np&mMDcPnPr+T@$8-g6O0I^KN$n5-CS$!K)bgemoQa zXCLI9Pwq}{_1K1|6oITq=X;r+JodFxKJimYSTwFtc;cVg2<(pnB9J@wtC>O*=3e)X zfvA9GS$?g@PE_ku)^JCtBK58_AA%M!{ ztMnW`)Eo-*)md4V2EX-6r)chNoGz4N{mUFJ4Gqat^TovI=;w&aPrS7%hf0j{B=-3!6Loluq1rPj`t(H>UicT z*jLzXJ!xLv-qL9~W*(D>9pcIZ>`X(A{wZHi6MwR6F8RjDc>pT%vUWJ~lK`invCMC* zD&aVzi-=a_F}&ZCF7Z!f8-Yp<>70M#QewZbJ`>cbX=D(?EJY;j9t0%XF%4>^B;cq!7y*2y785( zvSlWYy8nAA_02-xO)dXTckdjNG)}&B&i6ePB2o@qQN+SD`k#nMG8oAYt%}2;+1*Z< zLfOPv)0xzH5RV%E^A4Y$_~`*CVnY9)j#H;dyuhwchJaT3=poe4X}litI{#)%?@lfb zqK%5JJ)M_b2nW%u5x$SMYP`*<`XPKQRAEhT;zljfd%Y| zeO_9^j_>XxqbuWB>YE1UuYRsv9;^#6LwK&g`4V%%Hwz5O#2F3@z4+rz^2k`y*D=+& zqbg{GrR8B)>E>IB@oVa6EXL=Z867Ys1>UMi>7=UAbX{GJ&FwJW;S`h|C%5PZRo`RY zraxg3k~7H_dvqY5-N$zuFBYPpSb4hH1tZvj!r_(XK(&Vx4C0|AXL!*}jF0)mRhbx3 zv(MkrbMhn*JRJZBIkW6vx=u7*)>Xs1R6=B=l`bV+Xb>7Q29X|l&r9dAdb<8%Rk~Q= zK;3E^s2CxS*5S2OX(~U?R^^`|Y#+Xca`CdMJucbNo6D{{sv${Jx|zWIRicTd8bMlF zI7N(r=nA0+UxMmRAWF&U20UHYSwdn`jg{Ipm4@yglSb;hL&#h?@%Y`P=^Q1gCV^@k zV-g;YEXwTnF84ul)TBo)8(#kO?W*7U3k8mYQ?GVhI1A6{$_>JnuW##jdYAl!?c6$P zLLZis)ibh;yT(S2-fFFUs)HR3hWANm*!Bj&Os&ZQSQPJX4>)J{FKcvluEm{@(cO2F z>)!da=rXa%5H%Edw1yNUv)^-LBU_3r_K@0a1XkI6ns3}l`buJtDGPz`;sOFuk;k=m zc?H(mt4TsJ?kzh-zP)HVYnfwVjahb8Yl8Y6#;>cYth(pc*$^f@;-6w4KSE%VsM~`% zivFX!L?IF_9yM|40+FLOim!4sWmLsdvQHNcy#Y0js|U`y;$?TN$>+Lv7oq1!dnM6F z4H;_xBu>vsq*}%EP34QvejI2Z((2>YT`hepwGez}9eV&!;nR zwvU8e?QO|5G9kZy#cH5MNDv)Sm<3*0sfoVDgqn-={46dPBSYvk^zNF&I02caOaCBH z%Z9GD2SXwqtw(`x=N@F1?>7c+r56i2GDFMLkR6>j80hz1n2R3{dhVM6O22f>-ZXBG zF)l4E?Eby^TWw+O&W}@~L6+);!3SrHg}lHdejPZKfjw{y>dE=@vF6jZAL#yA zaj8Ix2>XmoMvMA})A*$do$I;XUAbWE;sIOPIupCj!G#E4wDb^+TA703+9O|u57%UN zp89a>FX9j=uTUnb@`i`pd=d$2^l&xk1!NakBR?fJtDcfiUslw!9r-?VvFG(lG zc(ebF^edb+*)?=lga%`UPhZeqM!SlPzkfpyC;W=76XRp*flJ@7EqB;(!*o;~5sU>j zgnXmvXFD8%Gj&$E^adYX&d@hj1l0R%SAU|ma+F!!X0zJzU7Q&*Y7qU|GyYH# zSTG*WA5+E=txoKb+iU>0+97?p%i4_;v@wLyw+nMoaDy-#_Q0a!4IKTDg25|78TdI41Im?89E_yh#hH9`Ciae(>HitO``s6<-S;9@p&MuRYhm zl4onW=t&KGN{3)vB#BM(-#r?#Ipr22sy7Vq+YZPgPYsOjK@J z|61-16Uc@T9{90!tn|>v<^z(0ETYWJB10117d&2#lX1-D--mN)jg&=>@jY2j!@?Y! zV@1QsyizL5^Qn*DvVYPNG`C210IzAt&~0r~R?HLmeCqfQE0i*;@m_fhEu2P?`J+9FrER=W{q7^Ck;_Nr{ zb*8CLW;^(cehZ45tg}2bMWzC-ECN*v(}DMPm|{SiJO%e6GFeJSafUs zjrZYx@IK2Cv08Um8NNDqTM0_vnaQpso*8t@#>5DN1v7pL%#DNNJR@UJOgxp}<#;WI zR(NeK=8`Ph*Y|87xt>_S9aJd)P_P?Fa-X8RUA#Imx29}9pA_#7mmFdsT*9chcnqgNp_rnUm6g4{&h9QQ_%KFNh z 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 BN0R^m literal 0 HcmV?d00001 diff --git a/nav2_mppi_controller/package.xml b/nav2_mppi_controller/package.xml index 450dd5254cc..67067ca4ced 100644 --- a/nav2_mppi_controller/package.xml +++ b/nav2_mppi_controller/package.xml @@ -17,6 +17,7 @@ nav2_common nav2_core nav2_costmap_2d + nav2_msgs nav2_util nav_msgs pluginlib diff --git a/nav2_mppi_controller/src/critic_manager.cpp b/nav2_mppi_controller/src/critic_manager.cpp index d6acd320b05..305140e2e13 100644 --- a/nav2_mppi_controller/src/critic_manager.cpp +++ b/nav2_mppi_controller/src/critic_manager.cpp @@ -37,6 +37,7 @@ void CriticManager::getParams() auto node = parent_.lock(); auto getParam = parameters_handler_->getParamGetter(name_); getParam(critic_names_, "critics", std::vector{}, ParameterType::Static); + getParam(publish_critics_stats_, "publish_critics_stats", false, ParameterType::Static); } void CriticManager::loadCritics() @@ -46,6 +47,13 @@ void CriticManager::loadCritics() "nav2_mppi_controller", "mppi::critics::CriticFunction"); } + auto node = parent_.lock(); + if (publish_critics_stats_) { + critics_effect_pub_ = node->create_publisher( + "~/critics_stats"); + critics_effect_pub_->on_activate(); + } + critics_.clear(); for (auto name : critic_names_) { std::string fullname = getFullName(name); @@ -67,11 +75,44 @@ std::string CriticManager::getFullName(const std::string & name) void CriticManager::evalTrajectoriesScores( CriticData & data) const { - for (const auto & critic : critics_) { + std::unique_ptr stats_msg; + if (publish_critics_stats_) { + stats_msg = std::make_unique(); + stats_msg->critics.reserve(critics_.size()); + stats_msg->changed.reserve(critics_.size()); + stats_msg->costs_sum.reserve(critics_.size()); + } + + for (size_t i = 0; i < critics_.size(); ++i) { if (data.fail_flag) { break; } - critic->score(data); + + // Store costs before critic evaluation + Eigen::ArrayXf costs_before; + if (publish_critics_stats_) { + costs_before = data.costs; + } + + critics_[i]->score(data); + + // Calculate statistics if publishing is enabled + if (publish_critics_stats_) { + stats_msg->critics.push_back(critic_names_[i]); + + // Calculate sum of costs added by this individual critic + Eigen::ArrayXf cost_diff = data.costs - costs_before; + float costs_sum = cost_diff.sum(); + stats_msg->costs_sum.push_back(costs_sum); + stats_msg->changed.push_back(costs_sum != 0.0f); + } + } + + // Publish statistics if enabled + if (critics_effect_pub_) { + auto node = parent_.lock(); + stats_msg->stamp = node->get_clock()->now(); + critics_effect_pub_->publish(std::move(stats_msg)); } } diff --git a/nav2_msgs/CMakeLists.txt b/nav2_msgs/CMakeLists.txt index 93f7998c56a..14d1327508b 100644 --- a/nav2_msgs/CMakeLists.txt +++ b/nav2_msgs/CMakeLists.txt @@ -30,6 +30,7 @@ rosidl_generate_interfaces(${PROJECT_NAME} "msg/VoxelGrid.msg" "msg/BehaviorTreeStatusChange.msg" "msg/BehaviorTreeLog.msg" + "msg/CriticsStats.msg" "msg/Particle.msg" "msg/ParticleCloud.msg" "msg/WaypointStatus.msg" diff --git a/nav2_msgs/msg/CriticsStats.msg b/nav2_msgs/msg/CriticsStats.msg new file mode 100644 index 00000000000..f02406228df --- /dev/null +++ b/nav2_msgs/msg/CriticsStats.msg @@ -0,0 +1,5 @@ +# Critics statistics message +builtin_interfaces/Time stamp +string[] critics +bool[] changed +float32[] costs_sum diff --git a/nav2_system_tests/src/gps_navigation/nav2_no_map_params.yaml b/nav2_system_tests/src/gps_navigation/nav2_no_map_params.yaml index b81cef7ce56..01fe9656d5b 100644 --- a/nav2_system_tests/src/gps_navigation/nav2_no_map_params.yaml +++ b/nav2_system_tests/src/gps_navigation/nav2_no_map_params.yaml @@ -76,6 +76,7 @@ controller_server: temperature: 0.3 gamma: 0.015 motion_model: "DiffDrive" + publish_critics_stats: true visualize: true regenerate_noises: true TrajectoryVisualizer: From ab4aaf863aea6a7d1e67e0c2ad536ec1f0c8ccf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Penha=20Lopes?= <123633705+JPLDevMaster@users.noreply.github.com> Date: Sun, 7 Sep 2025 21:57:05 +0100 Subject: [PATCH 48/51] fix(nav2_theta_star_planner): Correct typo in CMakeLists ament_export_dependencies (#5514) Signed-off-by: JPLDevMaster --- nav2_theta_star_planner/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nav2_theta_star_planner/CMakeLists.txt b/nav2_theta_star_planner/CMakeLists.txt index 0a478f2fb9b..09813efc4d7 100644 --- a/nav2_theta_star_planner/CMakeLists.txt +++ b/nav2_theta_star_planner/CMakeLists.txt @@ -87,7 +87,8 @@ ament_export_libraries(${library_name}) ament_export_dependencies( geometry_msgs nav2_core - nav2_costmap_2dnav2_util + nav2_costmap_2d + nav2_util nav_msgs rclcpp rclcpp_lifecycle From 8aea1a895f23ca505da3629819b6d16f4d126a0f Mon Sep 17 00:00:00 2001 From: Guillaume Doisy Date: Wed, 10 Sep 2025 03:21:22 +0100 Subject: [PATCH 49/51] default empty constructor (#5518) Signed-off-by: Guillaume Doisy Co-authored-by: Guillaume Doisy --- .../include/nav2_behavior_tree/bt_action_server.hpp | 4 ++-- .../include/nav2_behavior_tree/bt_action_server_impl.hpp | 4 ++-- nav2_core/include/nav2_core/behavior_tree_navigator.hpp | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/bt_action_server.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/bt_action_server.hpp index 8e345abe0d7..924d1ebc28a 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/bt_action_server.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/bt_action_server.hpp @@ -53,11 +53,11 @@ class BtActionServer const std::string & action_name, const std::vector & plugin_lib_names, const std::string & default_bt_xml_filename, - const std::vector & search_directories, OnGoalReceivedCallback on_goal_received_callback, OnLoopCallback on_loop_callback, OnPreemptCallback on_preempt_callback, - OnCompletionCallback on_completion_callback); + OnCompletionCallback on_completion_callback, + const std::vector & search_directories = std::vector{}); /** * @brief A destructor for nav2_behavior_tree::BtActionServer class diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/bt_action_server_impl.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/bt_action_server_impl.hpp index b8fb983f91c..10a8289d5f4 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/bt_action_server_impl.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/bt_action_server_impl.hpp @@ -39,11 +39,11 @@ BtActionServer::BtActionServer( const std::string & action_name, const std::vector & plugin_lib_names, const std::string & default_bt_xml_filename, - const std::vector & search_directories, OnGoalReceivedCallback on_goal_received_callback, OnLoopCallback on_loop_callback, OnPreemptCallback on_preempt_callback, - OnCompletionCallback on_completion_callback) + OnCompletionCallback on_completion_callback, + const std::vector & search_directories) : action_name_(action_name), default_bt_xml_filename_(default_bt_xml_filename), search_directories_(search_directories), diff --git a/nav2_core/include/nav2_core/behavior_tree_navigator.hpp b/nav2_core/include/nav2_core/behavior_tree_navigator.hpp index 9e984bc6550..fdabbf28133 100644 --- a/nav2_core/include/nav2_core/behavior_tree_navigator.hpp +++ b/nav2_core/include/nav2_core/behavior_tree_navigator.hpp @@ -214,13 +214,13 @@ class BehaviorTreeNavigator : public NavigatorBase getName(), plugin_lib_names, default_bt_xml_filename, - search_directories, std::bind(&BehaviorTreeNavigator::onGoalReceived, this, std::placeholders::_1), std::bind(&BehaviorTreeNavigator::onLoop, this), std::bind(&BehaviorTreeNavigator::onPreempt, this, std::placeholders::_1), std::bind( &BehaviorTreeNavigator::onCompletion, this, - std::placeholders::_1, std::placeholders::_2)); + std::placeholders::_1, std::placeholders::_2), + search_directories); bool ok = true; if (!bt_action_server_->on_configure()) { From 97de6f21c067f6d514c3940b6f5d44afb2ceaf34 Mon Sep 17 00:00:00 2001 From: Sushant Chavan Date: Thu, 11 Sep 2025 18:20:02 +0200 Subject: [PATCH 50/51] Feature/vector object server (#5479) * Add Vector Object server Signed-off-by: Alexey Merzlyakov Signed-off-by: Alberto Tudela * Meet review comments Signed-off-by: Alexey Merzlyakov * Simplify shapes param configuring Signed-off-by: Alexey Merzlyakov Signed-off-by: Alberto Tudela * Rename getROSParameter() to getParameter() Signed-off-by: Alexey Merzlyakov Signed-off-by: Alberto Tudela * Return back getMaskData() to nav2_costmap_2d Signed-off-by: Alexey Merzlyakov Signed-off-by: Alberto Tudela * Add composition node support Signed-off-by: Alexey Merzlyakov * Remove redundant methods Signed-off-by: Alexey Merzlyakov Signed-off-by: Alberto Tudela * Update nav2_map_server/src/vo_server/vector_object_server.cpp Co-authored-by: Steve Macenski Signed-off-by: Alexey Merzlyakov Signed-off-by: Alberto Tudela * Avoid shapes clearing Signed-off-by: Alexey Merzlyakov Signed-off-by: Alberto Tudela * Optimize switchMapUpdate() method Signed-off-by: Alexey Merzlyakov Signed-off-by: Alberto Tudela * Switch to vector of shapes Signed-off-by: Alexey Merzlyakov Signed-off-by: Alberto Tudela * Minor fixes Signed-off-by: Alexey Merzlyakov Signed-off-by: Alberto Tudela * Meet review comments Signed-off-by: Alexey Merzlyakov Signed-off-by: Alberto Tudela * Move isPointInside algorithm to nav2_util Signed-off-by: Alexey Merzlyakov Signed-off-by: Alberto Tudela * Testcases covering new functionality Signed-off-by: Alexey Merzlyakov Signed-off-by: Alberto Tudela * Fix linting issues Signed-off-by: Alexey Merzlyakov Signed-off-by: Alberto Tudela * Adjust for Vector Objects demonstration Signed-off-by: Alexey Merzlyakov Signed-off-by: Alberto Tudela * Code clean-up * Corrected headers * Functions ordering * Comment fixes Signed-off-by: Alexey Merzlyakov Signed-off-by: Alberto Tudela * Additional code facelift * Correct licensing years * Fix Vector Object server dependencies * Funcion rename for better readability * Improve/fix comments Signed-off-by: Alexey Merzlyakov * Minor fixing after rebase Signed-off-by: Alberto Tudela * Rename vector object server Signed-off-by: Alberto Tudela * Minor changes Signed-off-by: Alberto Tudela * Update tests Signed-off-by: Alberto Tudela * Merge branch 'main' into feature/vector_object_server Signed-off-by: Sushant Chavan * Fix merge issues and pre-commit checks Signed-off-by: Sushant Chavan * Change tf2_ros headers from `.h` to `.hpp` Signed-off-by: Sushant Chavan * Fix race condition in pub-sub of VO map Signed-off-by: Sushant Chavan * Cleanup Signed-off-by: Sushant Chavan * Remove use of ament_target_dependencies Signed-off-by: Sushant Chavan * Fix review comments Signed-off-by: Sushant Chavan * Fix linter errors Signed-off-by: Sushant Chavan * Fix exception handling Signed-off-by: Sushant Chavan * Update nav2_map_server/include/nav2_map_server/vector_object_utils.hpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Steve Macenski * Update nav2_util/include/nav2_util/raytrace_line_2d.hpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Steve Macenski * Add error logs Signed-off-by: Sushant Chavan * Fix cpplint Signed-off-by: Sushant Chavan --------- Signed-off-by: Alexey Merzlyakov Signed-off-by: Alberto Tudela Signed-off-by: Sushant Chavan Signed-off-by: Steve Macenski Co-authored-by: Alexey Merzlyakov Co-authored-by: Alexey Merzlyakov <60094858+AlexeyMerzlyakov@users.noreply.github.com> Co-authored-by: Steve Macenski Co-authored-by: Alberto Tudela Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../nav2_collision_monitor/polygon.hpp | 7 - nav2_collision_monitor/src/polygon.cpp | 35 +- .../include/nav2_costmap_2d/costmap_2d.hpp | 98 -- .../costmap_filters/costmap_filter.hpp | 14 - .../plugins/costmap_filters/binary_filter.cpp | 5 +- .../costmap_filters/costmap_filter.cpp | 23 - .../costmap_filters/keepout_filter.cpp | 10 +- .../plugins/costmap_filters/speed_filter.cpp | 5 +- nav2_costmap_2d/plugins/obstacle_layer.cpp | 5 +- nav2_costmap_2d/src/costmap_2d.cpp | 8 +- .../test/regression/CMakeLists.txt | 6 - .../test/regression/costmap_bresenham_2d.cpp | 159 -- .../test/unit/costmap_filter_test.cpp | 20 +- nav2_map_server/CMakeLists.txt | 56 +- .../nav2_map_server/vector_object_server.hpp | 228 +++ .../nav2_map_server/vector_object_shapes.hpp | 413 +++++ .../nav2_map_server/vector_object_utils.hpp | 142 ++ .../launch/vector_object_server.launch.py | 169 +++ nav2_map_server/package.xml | 3 + .../params/vector_object_server_params.yaml | 30 + nav2_map_server/src/vo_server/main.cpp | 28 + .../src/vo_server/vector_object_server.cpp | 558 +++++++ .../src/vo_server/vector_object_shapes.cpp | 559 +++++++ nav2_map_server/test/unit/CMakeLists.txt | 22 + .../test/unit/test_vector_object_server.cpp | 1349 +++++++++++++++++ .../test/unit/test_vector_object_shapes.cpp | 805 ++++++++++ nav2_msgs/CMakeLists.txt | 8 +- nav2_msgs/msg/CircleObject.msg | 6 + nav2_msgs/msg/PolygonObject.msg | 5 + nav2_msgs/package.xml | 1 + nav2_msgs/srv/AddShapes.srv | 6 + nav2_msgs/srv/GetShapes.srv | 5 + nav2_msgs/srv/RemoveShapes.srv | 6 + .../include/nav2_util/geometry_utils.hpp | 43 + .../include/nav2_util/occ_grid_utils.hpp | 100 ++ .../include/nav2_util/raytrace_line_2d.hpp | 146 ++ nav2_util/test/CMakeLists.txt | 2 + nav2_util/test/regression/CMakeLists.txt | 3 + .../test/regression/map_bresenham_2d.cpp | 170 +++ 39 files changed, 4890 insertions(+), 368 deletions(-) delete mode 100644 nav2_costmap_2d/test/regression/costmap_bresenham_2d.cpp create mode 100644 nav2_map_server/include/nav2_map_server/vector_object_server.hpp create mode 100644 nav2_map_server/include/nav2_map_server/vector_object_shapes.hpp create mode 100644 nav2_map_server/include/nav2_map_server/vector_object_utils.hpp create mode 100644 nav2_map_server/launch/vector_object_server.launch.py create mode 100644 nav2_map_server/params/vector_object_server_params.yaml create mode 100644 nav2_map_server/src/vo_server/main.cpp create mode 100644 nav2_map_server/src/vo_server/vector_object_server.cpp create mode 100644 nav2_map_server/src/vo_server/vector_object_shapes.cpp create mode 100644 nav2_map_server/test/unit/test_vector_object_server.cpp create mode 100644 nav2_map_server/test/unit/test_vector_object_shapes.cpp create mode 100644 nav2_msgs/msg/CircleObject.msg create mode 100644 nav2_msgs/msg/PolygonObject.msg create mode 100644 nav2_msgs/srv/AddShapes.srv create mode 100644 nav2_msgs/srv/GetShapes.srv create mode 100644 nav2_msgs/srv/RemoveShapes.srv create mode 100644 nav2_util/include/nav2_util/occ_grid_utils.hpp create mode 100644 nav2_util/include/nav2_util/raytrace_line_2d.hpp create mode 100644 nav2_util/test/regression/CMakeLists.txt create mode 100644 nav2_util/test/regression/map_bresenham_2d.cpp diff --git a/nav2_collision_monitor/include/nav2_collision_monitor/polygon.hpp b/nav2_collision_monitor/include/nav2_collision_monitor/polygon.hpp index 3191de1727e..02fc680afe5 100644 --- a/nav2_collision_monitor/include/nav2_collision_monitor/polygon.hpp +++ b/nav2_collision_monitor/include/nav2_collision_monitor/polygon.hpp @@ -239,13 +239,6 @@ class Polygon rcl_interfaces::msg::SetParametersResult dynamicParametersCallback( std::vector parameters); - /** - * @brief Checks if point is inside polygon - * @param point Given point to check - * @return True if given point is inside polygon, otherwise false - */ - bool isPointInside(const Point & point) const; - /** * @brief Extracts Polygon points from a string with of the form [[x1,y1],[x2,y2],[x3,y3]...] * @param poly_string Input String containing the verteceis of the polygon diff --git a/nav2_collision_monitor/src/polygon.cpp b/nav2_collision_monitor/src/polygon.cpp index 5e13f5d5ef8..b37f677fb57 100644 --- a/nav2_collision_monitor/src/polygon.cpp +++ b/nav2_collision_monitor/src/polygon.cpp @@ -22,6 +22,7 @@ #include "tf2/transform_datatypes.hpp" #include "nav2_ros_common/node_utils.hpp" +#include "nav2_util/geometry_utils.hpp" #include "nav2_util/robot_utils.hpp" #include "nav2_util/array_parser.hpp" @@ -229,7 +230,7 @@ int Polygon::getPointsInside(const std::vector & points) const { int num = 0; for (const Point & point : points) { - if (isPointInside(point)) { + if (nav2_util::geometry_utils::isPointInsidePolygon(point.x, point.y, poly_)) { num++; } } @@ -602,38 +603,6 @@ void Polygon::polygonCallback(geometry_msgs::msg::PolygonStamped::ConstSharedPtr updatePolygon(msg); } -inline bool Polygon::isPointInside(const Point & point) const -{ - // Adaptation of Shimrat, Moshe. "Algorithm 112: position of point relative to polygon." - // Communications of the ACM 5.8 (1962): 434. - // Implementation of ray crossings algorithm for point in polygon task solving. - // Y coordinate is fixed. Moving the ray on X+ axis starting from given point. - // Odd number of intersections with polygon boundaries means the point is inside polygon. - const int poly_size = poly_.size(); - int i, j; // Polygon vertex iterators - bool res = false; // Final result, initialized with already inverted value - - // Starting from the edge where the last point of polygon is connected to the first - i = poly_size - 1; - for (j = 0; j < poly_size; j++) { - // Checking the edge only if given point is between edge boundaries by Y coordinates. - // One of the condition should contain equality in order to exclude the edges - // parallel to X+ ray. - if ((point.y <= poly_[i].y) == (point.y > poly_[j].y)) { - // Calculating the intersection coordinate of X+ ray - const double x_inter = poly_[i].x + - (point.y - poly_[i].y) * (poly_[j].x - poly_[i].x) / - (poly_[j].y - poly_[i].y); - // If intersection with checked edge is greater than point.x coordinate, inverting the result - if (x_inter > point.x) { - res = !res; - } - } - i = j; - } - return res; -} - bool Polygon::getPolygonFromString( std::string & poly_string, std::vector & polygon) diff --git a/nav2_costmap_2d/include/nav2_costmap_2d/costmap_2d.hpp b/nav2_costmap_2d/include/nav2_costmap_2d/costmap_2d.hpp index fbacf15aa24..fe3764c443a 100644 --- a/nav2_costmap_2d/include/nav2_costmap_2d/costmap_2d.hpp +++ b/nav2_costmap_2d/include/nav2_costmap_2d/costmap_2d.hpp @@ -461,105 +461,7 @@ class Costmap2D */ virtual void initMaps(unsigned int size_x, unsigned int size_y); - /** - * @brief Raytrace a line and apply some action at each step - * @param at The action to take... a functor - * @param x0 The starting x coordinate - * @param y0 The starting y coordinate - * @param x1 The ending x coordinate - * @param y1 The ending y coordinate - * @param max_length The maximum desired length of the segment... - * allows you to not go all the way to the endpoint - * @param min_length The minimum desired length of the segment - */ - template - inline void raytraceLine( - ActionType at, unsigned int x0, unsigned int y0, unsigned int x1, - unsigned int y1, - unsigned int max_length = UINT_MAX, unsigned int min_length = 0) - { - int dx_full = x1 - x0; - int dy_full = y1 - y0; - - // we need to chose how much to scale our dominant dimension, - // based on the maximum length of the line - double dist = std::hypot(dx_full, dy_full); - if (dist < min_length) { - return; - } - - unsigned int min_x0, min_y0; - if (dist > 0.0) { - // Adjust starting point and offset to start from min_length distance - min_x0 = (unsigned int)(x0 + dx_full / dist * min_length); - min_y0 = (unsigned int)(y0 + dy_full / dist * min_length); - } else { - // dist can be 0 if [x0, y0]==[x1, y1]. - // In this case only this cell should be processed. - min_x0 = x0; - min_y0 = y0; - } - unsigned int offset = min_y0 * size_x_ + min_x0; - - int dx = x1 - min_x0; - int dy = y1 - min_y0; - - unsigned int abs_dx = abs(dx); - unsigned int abs_dy = abs(dy); - - int offset_dx = sign(dx); - int offset_dy = sign(dy) * size_x_; - - double scale = (dist == 0.0) ? 1.0 : std::min(1.0, max_length / dist); - // if x is dominant - if (abs_dx >= abs_dy) { - int error_y = abs_dx / 2; - - bresenham2D( - at, abs_dx, abs_dy, error_y, offset_dx, offset_dy, offset, (unsigned int)(scale * abs_dx)); - return; - } - - // otherwise y is dominant - int error_x = abs_dy / 2; - - bresenham2D( - at, abs_dy, abs_dx, error_x, offset_dy, offset_dx, offset, (unsigned int)(scale * abs_dy)); - } - private: - /** - * @brief A 2D implementation of Bresenham's raytracing algorithm... - * applies an action at each step - */ - template - inline void bresenham2D( - ActionType at, unsigned int abs_da, unsigned int abs_db, int error_b, - int offset_a, - int offset_b, unsigned int offset, - unsigned int max_length) - { - unsigned int end = std::min(max_length, abs_da); - for (unsigned int i = 0; i < end; ++i) { - at(offset); - offset += offset_a; - error_b += abs_db; - if ((unsigned int)error_b >= abs_da) { - offset += offset_b; - error_b -= abs_da; - } - } - at(offset); - } - - /** - * @brief get the sign of an int - */ - inline int sign(int x) - { - return x > 0 ? 1.0 : -1.0; - } - mutex_t * access_; protected: diff --git a/nav2_costmap_2d/include/nav2_costmap_2d/costmap_filters/costmap_filter.hpp b/nav2_costmap_2d/include/nav2_costmap_2d/costmap_filters/costmap_filter.hpp index 25238cf8a35..da4720ff798 100644 --- a/nav2_costmap_2d/include/nav2_costmap_2d/costmap_filters/costmap_filter.hpp +++ b/nav2_costmap_2d/include/nav2_costmap_2d/costmap_filters/costmap_filter.hpp @@ -185,20 +185,6 @@ class CostmapFilter : public Layer const std::string mask_frame, geometry_msgs::msg::Pose & mask_pose) const; - /** - * @brief: Convert from world coordinates to mask coordinates. - Similar to Costmap2D::worldToMap() method but works directly with OccupancyGrid-s. - * @param filter_mask Filter mask on which to convert - * @param wx The x world coordinate - * @param wy The y world coordinate - * @param mx Will be set to the associated mask x coordinate - * @param my Will be set to the associated mask y coordinate - * @return True if the conversion was successful (legal bounds) false otherwise - */ - bool worldToMask( - nav_msgs::msg::OccupancyGrid::ConstSharedPtr filter_mask, - double wx, double wy, unsigned int & mx, unsigned int & my) const; - /** * @brief Get the data of a cell in the filter mask * @param filter_mask Filter mask to get the data from diff --git a/nav2_costmap_2d/plugins/costmap_filters/binary_filter.cpp b/nav2_costmap_2d/plugins/costmap_filters/binary_filter.cpp index 097537d40c6..c805a537396 100644 --- a/nav2_costmap_2d/plugins/costmap_filters/binary_filter.cpp +++ b/nav2_costmap_2d/plugins/costmap_filters/binary_filter.cpp @@ -44,6 +44,7 @@ #include "nav2_costmap_2d/costmap_filters/filter_values.hpp" #include "nav2_util/occ_grid_values.hpp" +#include "nav2_util/occ_grid_utils.hpp" namespace nav2_costmap_2d { @@ -190,8 +191,8 @@ void BinaryFilter::process( // Converting mask_pose robot position to filter_mask_ indexes (mask_robot_i, mask_robot_j) unsigned int mask_robot_i, mask_robot_j; - if (!worldToMask(filter_mask_, mask_pose.position.x, mask_pose.position.y, mask_robot_i, - mask_robot_j)) + if (!nav2_util::worldToMap(filter_mask_, mask_pose.position.x, mask_pose.position.y, + mask_robot_i, mask_robot_j)) { // Robot went out of mask range. Set "false" state by-default RCLCPP_WARN( diff --git a/nav2_costmap_2d/plugins/costmap_filters/costmap_filter.cpp b/nav2_costmap_2d/plugins/costmap_filters/costmap_filter.cpp index 06d7ed2d025..58b89740b8b 100644 --- a/nav2_costmap_2d/plugins/costmap_filters/costmap_filter.cpp +++ b/nav2_costmap_2d/plugins/costmap_filters/costmap_filter.cpp @@ -193,29 +193,6 @@ bool CostmapFilter::transformPose( return true; } -bool CostmapFilter::worldToMask( - nav_msgs::msg::OccupancyGrid::ConstSharedPtr filter_mask, - double wx, double wy, unsigned int & mx, unsigned int & my) const -{ - const double origin_x = filter_mask->info.origin.position.x; - const double origin_y = filter_mask->info.origin.position.y; - const double resolution = filter_mask->info.resolution; - const unsigned int size_x = filter_mask->info.width; - const unsigned int size_y = filter_mask->info.height; - - if (wx < origin_x || wy < origin_y) { - return false; - } - - mx = static_cast((wx - origin_x) / resolution); - my = static_cast((wy - origin_y) / resolution); - if (mx >= size_x || my >= size_y) { - return false; - } - - return true; -} - unsigned char CostmapFilter::getMaskCost( nav_msgs::msg::OccupancyGrid::ConstSharedPtr filter_mask, const unsigned int mx, const unsigned int & my) const diff --git a/nav2_costmap_2d/plugins/costmap_filters/keepout_filter.cpp b/nav2_costmap_2d/plugins/costmap_filters/keepout_filter.cpp index 48cd6c74e42..c432a917e59 100644 --- a/nav2_costmap_2d/plugins/costmap_filters/keepout_filter.cpp +++ b/nav2_costmap_2d/plugins/costmap_filters/keepout_filter.cpp @@ -35,15 +35,17 @@ * Author: Alexey Merzlyakov *********************************************************************/ +#include "nav2_costmap_2d/costmap_filters/keepout_filter.hpp" + #include #include #include #include "tf2/convert.hpp" #include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" -#include "nav2_costmap_2d/costmap_filters/keepout_filter.hpp" #include "nav2_costmap_2d/costmap_filters/filter_values.hpp" #include "nav2_util/geometry_utils.hpp" +#include "nav2_util/occ_grid_utils.hpp" namespace nav2_costmap_2d { @@ -200,8 +202,8 @@ void KeepoutFilter::updateBounds( geometry_msgs::msg::Pose mask_pose; if (transformPose(global_frame_, pose, filter_mask_->header.frame_id, mask_pose)) { unsigned int mask_robot_i, mask_robot_j; - if (worldToMask(filter_mask_, mask_pose.position.x, mask_pose.position.y, mask_robot_i, - mask_robot_j)) + if (nav2_util::worldToMap(filter_mask_, mask_pose.position.x, mask_pose.position.y, + mask_robot_i, mask_robot_j)) { auto data = getMaskCost(filter_mask_, mask_robot_i, mask_robot_j); is_pose_lethal_ = (data == INSCRIBED_INFLATED_OBSTACLE || data == LETHAL_OBSTACLE); @@ -366,7 +368,7 @@ void KeepoutFilter::process( msk_wy = gl_wy; } // Get mask coordinates corresponding to (i, j) point at filter_mask_ - if (worldToMask(filter_mask_, msk_wx, msk_wy, mx, my)) { + if (nav2_util::worldToMap(filter_mask_, msk_wx, msk_wy, mx, my)) { data = getMaskCost(filter_mask_, mx, my); // Update if mask_ data is valid and greater than existing master_grid's one if (data == NO_INFORMATION) { diff --git a/nav2_costmap_2d/plugins/costmap_filters/speed_filter.cpp b/nav2_costmap_2d/plugins/costmap_filters/speed_filter.cpp index a66886bc6d0..c1e822cb03a 100644 --- a/nav2_costmap_2d/plugins/costmap_filters/speed_filter.cpp +++ b/nav2_costmap_2d/plugins/costmap_filters/speed_filter.cpp @@ -43,6 +43,7 @@ #include #include "nav2_costmap_2d/costmap_filters/filter_values.hpp" +#include "nav2_util/occ_grid_utils.hpp" namespace nav2_costmap_2d { @@ -198,8 +199,8 @@ void SpeedFilter::process( // Converting mask_pose robot position to filter_mask_ indexes (mask_robot_i, mask_robot_j) unsigned int mask_robot_i, mask_robot_j; - if (!worldToMask(filter_mask_, mask_pose.position.x, mask_pose.position.y, mask_robot_i, - mask_robot_j)) + if (!nav2_util::worldToMap(filter_mask_, mask_pose.position.x, mask_pose.position.y, + mask_robot_i, mask_robot_j)) { return; } diff --git a/nav2_costmap_2d/plugins/obstacle_layer.cpp b/nav2_costmap_2d/plugins/obstacle_layer.cpp index b897ac515be..c09d60da61b 100644 --- a/nav2_costmap_2d/plugins/obstacle_layer.cpp +++ b/nav2_costmap_2d/plugins/obstacle_layer.cpp @@ -45,7 +45,9 @@ #include "pluginlib/class_list_macros.hpp" #include "sensor_msgs/point_cloud2_iterator.hpp" +#include "nav2_util/raytrace_line_2d.hpp" #include "nav2_costmap_2d/costmap_math.hpp" +#include "nav2_ros_common/node_utils.hpp" #include "rclcpp/version.h" PLUGINLIB_EXPORT_CLASS(nav2_costmap_2d::ObstacleLayer, nav2_costmap_2d::Layer) @@ -768,7 +770,8 @@ ObstacleLayer::raytraceFreespace( unsigned int cell_raytrace_min_range = cellDistance(clearing_observation.raytrace_min_range_); MarkCell marker(costmap_, FREE_SPACE); // and finally... we can execute our trace to clear obstacles along that line - raytraceLine(marker, x0, y0, x1, y1, cell_raytrace_max_range, cell_raytrace_min_range); + nav2_util::raytraceLine( + marker, x0, y0, x1, y1, size_x_, cell_raytrace_max_range, cell_raytrace_min_range); updateRaytraceBounds( ox, oy, wx, wy, clearing_observation.raytrace_max_range_, diff --git a/nav2_costmap_2d/src/costmap_2d.cpp b/nav2_costmap_2d/src/costmap_2d.cpp index f3903308cff..28798b0f8a3 100644 --- a/nav2_costmap_2d/src/costmap_2d.cpp +++ b/nav2_costmap_2d/src/costmap_2d.cpp @@ -43,6 +43,7 @@ #include #include "nav2_costmap_2d/cost_values.hpp" #include "nav2_util/occ_grid_values.hpp" +#include "nav2_util/raytrace_line_2d.hpp" namespace nav2_costmap_2d { @@ -462,14 +463,15 @@ void Costmap2D::polygonOutlineCells( { PolygonOutlineCells cell_gatherer(*this, costmap_, polygon_cells); for (unsigned int i = 0; i < polygon.size() - 1; ++i) { - raytraceLine(cell_gatherer, polygon[i].x, polygon[i].y, polygon[i + 1].x, polygon[i + 1].y); + nav2_util::raytraceLine( + cell_gatherer, polygon[i].x, polygon[i].y, polygon[i + 1].x, polygon[i + 1].y, size_x_); } if (!polygon.empty()) { unsigned int last_index = polygon.size() - 1; // we also need to close the polygon by going from the last point to the first - raytraceLine( + nav2_util::raytraceLine( cell_gatherer, polygon[last_index].x, polygon[last_index].y, polygon[0].x, - polygon[0].y); + polygon[0].y, size_x_); } } diff --git a/nav2_costmap_2d/test/regression/CMakeLists.txt b/nav2_costmap_2d/test/regression/CMakeLists.txt index d468dd37987..435d9072d03 100644 --- a/nav2_costmap_2d/test/regression/CMakeLists.txt +++ b/nav2_costmap_2d/test/regression/CMakeLists.txt @@ -1,9 +1,3 @@ -# Bresenham2D corner cases test -ament_add_gtest(costmap_bresenham_2d costmap_bresenham_2d.cpp) -target_link_libraries(costmap_bresenham_2d - nav2_costmap_2d_core -) - # OrderLayer for checking Costmap2D plugins API calling order add_library(order_layer SHARED order_layer.cpp) target_link_libraries(order_layer PUBLIC diff --git a/nav2_costmap_2d/test/regression/costmap_bresenham_2d.cpp b/nav2_costmap_2d/test/regression/costmap_bresenham_2d.cpp deleted file mode 100644 index c4afee34e00..00000000000 --- a/nav2_costmap_2d/test/regression/costmap_bresenham_2d.cpp +++ /dev/null @@ -1,159 +0,0 @@ -/********************************************************************* -* Software License Agreement (BSD License) -* -* Copyright (c) 2022 Samsung Research Russia -* All rights reserved. -* -* Redistribution and use in source and binary forms, with or without -* modification, are permitted provided that the following conditions -* are met: -* -* * Redistributions of source code must retain the above copyright -* notice, this list of conditions and the following disclaimer. -* * Redistributions in binary form must reproduce the above -* copyright notice, this list of conditions and the following -* disclaimer in the documentation and/or other materials provided -* with the distribution. -* * Neither the name of Willow Garage, Inc. nor the names of its -* contributors may be used to endorse or promote products derived -* from this software without specific prior written permission. -* -* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -* POSSIBILITY OF SUCH DAMAGE. -* -* Author: Alexey Merzlyakov -*********************************************************************/ -#include -#include - -class CostmapAction -{ -public: - explicit CostmapAction( - unsigned char * costmap, unsigned int size, unsigned char mark_val = 128) - : costmap_(costmap), size_(size), mark_val_(mark_val) - { - } - - inline void operator()(unsigned int off) - { - ASSERT_TRUE(off < size_); - costmap_[off] = mark_val_; - } - - inline unsigned int get(unsigned int off) - { - return costmap_[off]; - } - -private: - unsigned char * costmap_; - unsigned int size_; - unsigned char mark_val_; -}; - -class CostmapTest : public nav2_costmap_2d::Costmap2D -{ -public: - CostmapTest( - unsigned int size_x, unsigned int size_y, double resolution, - double origin_x, double origin_y, unsigned char default_val = 0) - : nav2_costmap_2d::Costmap2D(size_x, size_y, resolution, origin_x, origin_y, default_val) - { - } - - unsigned char * getCostmap() - { - return costmap_; - } - - unsigned int getSize() - { - return size_x_ * size_y_; - } - - void raytraceLine( - CostmapAction ca, unsigned int x0, unsigned int y0, unsigned int x1, - unsigned int y1, - unsigned int max_length = UINT_MAX, unsigned int min_length = 0) - { - nav2_costmap_2d::Costmap2D::raytraceLine(ca, x0, y0, x1, y1, max_length, min_length); - } -}; - -TEST(costmap_2d, bresenham2DBoundariesCheck) -{ - const unsigned int sz_x = 60; - const unsigned int sz_y = 60; - const unsigned int max_length = 60; - const unsigned int min_length = 6; - CostmapTest ct(sz_x, sz_y, 0.1, 0.0, 0.0); - CostmapAction ca(ct.getCostmap(), ct.getSize()); - - // Initial point - some asymmetrically standing point in order to cover most corner cases - const unsigned int x0 = 2; - const unsigned int y0 = 4; - // (x1, y1) point will move - unsigned int x1, y1; - - // Running on (x, 0) edge - y1 = 0; - for (x1 = 0; x1 < sz_x; x1++) { - ct.raytraceLine(ca, x0, y0, x1, y1, max_length, min_length); - } - - // Running on (x, sz_y) edge - y1 = sz_y - 1; - for (x1 = 0; x1 < sz_x; x1++) { - ct.raytraceLine(ca, x0, y0, x1, y1, max_length, min_length); - } - - // Running on (0, y) edge - x1 = 0; - for (y1 = 0; y1 < sz_y; y1++) { - ct.raytraceLine(ca, x0, y0, x1, y1, max_length, min_length); - } - - // Running on (sz_x, y) edge - x1 = sz_x - 1; - for (y1 = 0; y1 < sz_y; y1++) { - ct.raytraceLine(ca, x0, y0, x1, y1, max_length, min_length); - } -} - -TEST(costmap_2d, bresenham2DSamePoint) -{ - const unsigned int sz_x = 60; - const unsigned int sz_y = 60; - const unsigned int max_length = 60; - const unsigned int min_length = 0; - CostmapTest ct(sz_x, sz_y, 0.1, 0.0, 0.0); - CostmapAction ca(ct.getCostmap(), ct.getSize()); - - // Initial point - const double x0 = 2; - const double y0 = 4; - - unsigned int offset = y0 * sz_x + x0; - unsigned char val_before = ca.get(offset); - // Same point to check - ct.raytraceLine(ca, x0, y0, x0, y0, max_length, min_length); - unsigned char val_after = ca.get(offset); - ASSERT_FALSE(val_before == val_after); -} - -int main(int argc, char ** argv) -{ - testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/nav2_costmap_2d/test/unit/costmap_filter_test.cpp b/nav2_costmap_2d/test/unit/costmap_filter_test.cpp index 320eeaa1653..8cf4583d15d 100644 --- a/nav2_costmap_2d/test/unit/costmap_filter_test.cpp +++ b/nav2_costmap_2d/test/unit/costmap_filter_test.cpp @@ -19,6 +19,7 @@ #include "rclcpp/rclcpp.hpp" #include "nav2_util/occ_grid_values.hpp" +#include "nav2_util/occ_grid_utils.hpp" #include "nav_msgs/msg/occupancy_grid.hpp" #include "geometry_msgs/msg/pose.hpp" #include "nav2_costmap_2d/costmap_2d.hpp" @@ -30,13 +31,6 @@ class CostmapFilterWrapper : public nav2_costmap_2d::CostmapFilter public: CostmapFilterWrapper() {} - bool worldToMask( - nav_msgs::msg::OccupancyGrid::ConstSharedPtr filter_mask, - double wx, double wy, unsigned int & mx, unsigned int & my) const - { - return nav2_costmap_2d::CostmapFilter::worldToMask(filter_mask, wx, wy, mx, my); - } - unsigned char getMaskCost( nav_msgs::msg::OccupancyGrid::ConstSharedPtr filter_mask, const unsigned int mx, const unsigned int & my) const @@ -52,7 +46,7 @@ class CostmapFilterWrapper : public nav2_costmap_2d::CostmapFilter void resetFilter() {} }; -TEST(CostmapFilter, testWorldToMask) +TEST(CostmapFilter, testWorldToMap) { // Create occupancy grid for test as follows: // @@ -82,19 +76,19 @@ TEST(CostmapFilter, testWorldToMask) CostmapFilterWrapper cf; unsigned int mx, my; // Point inside mask - ASSERT_TRUE(cf.worldToMask(mask, 4.0, 5.0, mx, my)); + ASSERT_TRUE(nav2_util::worldToMap(mask, 4.0, 5.0, mx, my)); ASSERT_EQ(mx, 1u); ASSERT_EQ(my, 2u); // Corner cases - ASSERT_TRUE(cf.worldToMask(mask, 3.0, 3.0, mx, my)); + ASSERT_TRUE(nav2_util::worldToMap(mask, 3.0, 3.0, mx, my)); ASSERT_EQ(mx, 0u); ASSERT_EQ(my, 0u); - ASSERT_TRUE(cf.worldToMask(mask, 5.9, 5.9, mx, my)); + ASSERT_TRUE(nav2_util::worldToMap(mask, 5.9, 5.9, mx, my)); ASSERT_EQ(mx, 2u); ASSERT_EQ(my, 2u); // Point outside mask - ASSERT_FALSE(cf.worldToMask(mask, 2.9, 2.9, mx, my)); - ASSERT_FALSE(cf.worldToMask(mask, 6.0, 6.0, mx, my)); + ASSERT_FALSE(nav2_util::worldToMap(mask, 2.9, 2.9, mx, my)); + ASSERT_FALSE(nav2_util::worldToMap(mask, 6.0, 6.0, mx, my)); } TEST(CostmapFilter, testGetMaskCost) diff --git a/nav2_map_server/CMakeLists.txt b/nav2_map_server/CMakeLists.txt index a003a8fb087..6459b42ed2c 100644 --- a/nav2_map_server/CMakeLists.txt +++ b/nav2_map_server/CMakeLists.txt @@ -5,21 +5,26 @@ list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/cmake_modules) find_package(ament_cmake REQUIRED) find_package(backward_ros REQUIRED) +find_package(geometry_msgs REQUIRED) find_package(GRAPHICSMAGICKCPP REQUIRED) find_package(lifecycle_msgs REQUIRED) find_package(nav_msgs REQUIRED) find_package(nav2_common REQUIRED) find_package(nav2_msgs REQUIRED) find_package(nav2_util REQUIRED) +find_package(PkgConfig REQUIRED) find_package(rclcpp REQUIRED) find_package(rclcpp_components REQUIRED) find_package(rclcpp_lifecycle REQUIRED) find_package(std_msgs REQUIRED) find_package(tf2 REQUIRED) +find_package(tf2_ros REQUIRED) find_package(nav2_ros_common REQUIRED) find_package(yaml_cpp_vendor REQUIRED) find_package(yaml-cpp REQUIRED) +pkg_search_module(UUID REQUIRED uuid) + nav2_package() set(map_server_executable map_server) @@ -28,12 +33,16 @@ set(library_name ${map_server_executable}_core) set(map_io_library_name map_io) +set(vo_library_name vector_object_core) + set(map_saver_cli_executable map_saver_cli) set(map_saver_server_executable map_saver_server) set(costmap_filter_info_server_executable costmap_filter_info_server) +set(vo_server_executable vector_object_server) + add_library(${map_io_library_name} SHARED src/map_mode.cpp src/map_io.cpp) @@ -78,6 +87,31 @@ target_link_libraries(${library_name} PRIVATE yaml-cpp::yaml-cpp ) +add_library(${vo_library_name} SHARED + src/vo_server/vector_object_shapes.cpp + src/vo_server/vector_object_server.cpp) +target_include_directories(${vo_library_name} + PUBLIC + "$" + "$" + "$") +target_link_libraries(${vo_library_name} PUBLIC + ${geometry_msgs_TARGETS} + ${nav_msgs_TARGETS} + ${nav2_msgs_TARGETS} + nav2_util::nav2_util_core + rclcpp::rclcpp + rclcpp_lifecycle::rclcpp_lifecycle + rclcpp_components::component + tf2_ros::tf2_ros + ${UUID_LIBRARIES} +) +target_link_libraries(${library_name} PRIVATE + ${lifecycle_msgs_TARGETS} + rclcpp_components::component + yaml-cpp::yaml-cpp +) + add_executable(${map_server_executable} src/map_server/main.cpp) target_include_directories(${map_server_executable} @@ -142,12 +176,28 @@ target_link_libraries(${costmap_filter_info_server_executable} PRIVATE rclcpp_lifecycle::rclcpp_lifecycle ) +add_executable(${vo_server_executable} + src/vo_server/main.cpp) +target_include_directories(${vo_server_executable} + PRIVATE + "$" + "$") +target_link_libraries(${vo_server_executable} PRIVATE + ${vo_library_name} + ${nav_msgs_TARGETS} + ${nav2_msgs_TARGETS} + nav2_util::nav2_util_core + rclcpp::rclcpp + rclcpp_lifecycle::rclcpp_lifecycle +) + rclcpp_components_register_nodes(${library_name} "nav2_map_server::CostmapFilterInfoServer") rclcpp_components_register_nodes(${library_name} "nav2_map_server::MapSaver") rclcpp_components_register_nodes(${library_name} "nav2_map_server::MapServer") +rclcpp_components_register_nodes(${vo_library_name} "nav2_map_server::VectorObjectServer") install(TARGETS - ${library_name} ${map_io_library_name} + ${library_name} ${map_io_library_name} ${vo_library_name} EXPORT ${library_name} ARCHIVE DESTINATION lib LIBRARY DESTINATION lib @@ -155,13 +205,14 @@ install(TARGETS install(TARGETS ${map_server_executable} ${map_saver_cli_executable} ${map_saver_server_executable} - ${costmap_filter_info_server_executable} + ${costmap_filter_info_server_executable} ${vo_server_executable} RUNTIME DESTINATION lib/${PROJECT_NAME}) install(DIRECTORY include/ DESTINATION include/${PROJECT_NAME}) install(DIRECTORY launch DESTINATION share/${PROJECT_NAME}) +install(DIRECTORY params DESTINATION share/${PROJECT_NAME}) if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED) @@ -178,6 +229,7 @@ ament_export_include_directories("include/${PROJECT_NAME}") ament_export_libraries( ${library_name} ${map_io_library_name} + ${vo_library_name} ) ament_export_dependencies(nav_msgs nav2_msgs nav2_util rclcpp rclcpp_lifecycle nav2_ros_common) ament_export_targets(${library_name}) diff --git a/nav2_map_server/include/nav2_map_server/vector_object_server.hpp b/nav2_map_server/include/nav2_map_server/vector_object_server.hpp new file mode 100644 index 00000000000..ed4186722bc --- /dev/null +++ b/nav2_map_server/include/nav2_map_server/vector_object_server.hpp @@ -0,0 +1,228 @@ +// Copyright (c) 2023 Samsung R&D Institute Russia +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NAV2_MAP_SERVER__VECTOR_OBJECT_SERVER_HPP_ +#define NAV2_MAP_SERVER__VECTOR_OBJECT_SERVER_HPP_ + +#include +#include +#include + +#include "rclcpp/rclcpp.hpp" +#include "nav_msgs/msg/occupancy_grid.hpp" + +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/transform_listener.hpp" + +#include "nav2_msgs/srv/add_shapes.hpp" +#include "nav2_msgs/srv/remove_shapes.hpp" +#include "nav2_msgs/srv/get_shapes.hpp" +#include "nav2_ros_common/lifecycle_node.hpp" + +#include "nav2_map_server/vector_object_utils.hpp" +#include "nav2_map_server/vector_object_shapes.hpp" + +namespace nav2_map_server +{ + +/// @brief Vector Object server node +class VectorObjectServer : public nav2::LifecycleNode +{ +public: + /** + * @brief Constructor for the VectorObjectServer + * @param options Additional options to control creation of the node. + */ + explicit VectorObjectServer(const rclcpp::NodeOptions & options = rclcpp::NodeOptions()); + +protected: + /** + * @brief: Initializes TF buffer/listener, obtains ROS-parameters, creates incoming services, + * and output map publisher + * @param state Lifecycle Node's state + * @return Success or Failure + */ + nav2::CallbackReturn on_configure(const rclcpp_lifecycle::State & state) override; + /** + * @brief: Activates output map publisher and creates bond connection + * @param state Lifecycle Node's state + * @return Success or Failure + */ + nav2::CallbackReturn on_activate(const rclcpp_lifecycle::State & state) override; + /** + * @brief: Deactivates map publisher and timer (if any), destroys bond connection + * @param state Lifecycle Node's state + * @return Success or Failure + */ + nav2::CallbackReturn on_deactivate(const rclcpp_lifecycle::State & state) override; + /** + * @brief: Resets all services, publishers, map and TF-s + * @param state Lifecycle Node's state + * @return Success or Failure + */ + nav2::CallbackReturn on_cleanup(const rclcpp_lifecycle::State & state) override; + /** + * @brief Called in shutdown state + * @param state Lifecycle Node's state + * @return Success or Failure + */ + nav2::CallbackReturn on_shutdown(const rclcpp_lifecycle::State & state) override; + + /** + * @brief Supporting routine obtaining all ROS-parameters + * @return True if all parameters were obtained or false in the failure case + */ + bool obtainParams(); + + /** + * @brief Finds the shape with given UUID + * @param uuid Given UUID to search with + * @return Iterator to the shape, if found. Otherwise past-the-end iterator. + */ + std::vector>::iterator findShape(const unsigned char * uuid); + + /** + * @brief Transform all vector shapes from their local frame to output map frame + * @return Whether all vector objects were transformed successfully + */ + bool transformVectorObjects(); + + /** + * @brief Obtains map boundaries to place all vector objects inside + * @param min_x output min X-boundary of required map + * @param min_y output min Y-boundary of required map + * @param max_x output max X-boundary of required map + * @param max_y output max Y-boundary of required map + * @throw std::exception if can not obtain one of the map boundaries + */ + void getMapBoundaries(double & min_x, double & min_y, double & max_x, double & max_y) const; + + /** + * @brief Creates or updates existing map with required sizes and fills it with default value + * @param min_x min X-boundary of new map + * @param min_y min Y-boundary of new map + * @param max_x max X-boundary of new map + * @param max_y max Y-boundary of new map + * @throw std::exception if map has negative X or Y size + */ + void updateMap( + const double & min_x, const double & min_y, const double & max_x, const double & max_y); + + /** + * @brief Processes all vector objects on raster output map + */ + void putVectorObjectsOnMap(); + + /** + * @brief Publishes output map + */ + void publishMap(); + + /** + * @brief Calculates new map sizes, updates map, processes all vector objects on it + * and publishes output map one time + */ + void processMap(); + + /** + * @brief If map to be update dynamically, creates map processing timer, + * otherwise process map once + */ + void switchMapUpdate(); + + /** + * @brief Callback for AddShapes service call. + * Reads all input vector objects from service request, + * creates them or updates their shape in case of existing objects + * and switches map processing/publishing routine + * @param request_header Service request header + * @param request Service request + * @param response Service response + */ + void addShapesCallback( + const std::shared_ptr request_header, + const std::shared_ptr request, + std::shared_ptr response); + + /** + * @brief Callback for GetShapes service call. + * Gets all shapes and returns them to the service response + * @param request_header Service request header + * @param request Service request + * @param response Service response + */ + void getShapesCallback( + const std::shared_ptr request_header, + const std::shared_ptr request, + std::shared_ptr response); + + /** + * @brief Callback for RemoveShapes service call. + * Try to remove requested vector objects and switches map processing/publishing routine + * @param request_header Service request header + * @param request Service request + * @param response Service response + */ + void removeShapesCallback( + const std::shared_ptr request_header, + const std::shared_ptr request, + std::shared_ptr response); + +protected: + /// @brief TF buffer + std::shared_ptr tf_buffer_; + /// @brief TF listener + std::shared_ptr tf_listener_; + + /// @brief All shapes vector + std::vector> shapes_; + + /// @brief Output map resolution + double resolution_; + /// @brief Default value the output map to be filled with + int8_t default_value_; + /// @brief @Overlay Type of overlay of vector objects on the map + OverlayType overlay_type_; + + /// @brief Output map with vector objects on it + nav_msgs::msg::OccupancyGrid::SharedPtr map_; + /// @brief Whether to process and publish map + double process_map_; + + /// @brief Frame of output map + std::string global_frame_id_; + /// @brief Transform tolerance + double transform_tolerance_; + + /// @brief Frequency to dynamically update/publish the map (if necessary) + double update_frequency_; + /// @brief Map update timer + rclcpp::TimerBase::SharedPtr map_timer_; + + /// @brief AddShapes service + nav2::ServiceServer::SharedPtr add_shapes_service_; + /// @brief GetShapes service + nav2::ServiceServer::SharedPtr get_shapes_service_; + /// @brief RemoveShapes service + nav2::ServiceServer::SharedPtr remove_shapes_service_; + + /// @beirf Topic name where the output map to be published to + std::string map_topic_; + /// @brief Output map publisher + nav2::Publisher::SharedPtr map_pub_; +}; + +} // namespace nav2_map_server + +#endif // NAV2_MAP_SERVER__VECTOR_OBJECT_SERVER_HPP_ diff --git a/nav2_map_server/include/nav2_map_server/vector_object_shapes.hpp b/nav2_map_server/include/nav2_map_server/vector_object_shapes.hpp new file mode 100644 index 00000000000..dd5cae95b62 --- /dev/null +++ b/nav2_map_server/include/nav2_map_server/vector_object_shapes.hpp @@ -0,0 +1,413 @@ +// Copyright (c) 2023 Samsung R&D Institute Russia +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NAV2_MAP_SERVER__VECTOR_OBJECT_SHAPES_HPP_ +#define NAV2_MAP_SERVER__VECTOR_OBJECT_SHAPES_HPP_ + +#include +#include + +#include "rclcpp/rclcpp.hpp" +#include "geometry_msgs/msg/polygon.hpp" +#include "geometry_msgs/msg/point32.hpp" +#include "nav_msgs/msg/occupancy_grid.hpp" + +#include "tf2_ros/buffer.hpp" + +#include "nav2_msgs/msg/polygon_object.hpp" +#include "nav2_msgs/msg/circle_object.hpp" +#include "nav2_ros_common/lifecycle_node.hpp" + +#include "nav2_map_server/vector_object_utils.hpp" + +namespace nav2_map_server +{ + +/// @brief Possible vector object shape types +enum ShapeType +{ + UNKNOWN = 0, + POLYGON = 1, + CIRCLE = 2 +}; + +/// @brief Basic class, other vector objects to be inherited from +class Shape +{ +public: + /** + * @brief Shape basic class constructor + * @param node Vector Object server node pointer + */ + explicit Shape(const nav2::LifecycleNode::WeakPtr & node); + + /** + * @brief Shape destructor + */ + virtual ~Shape(); + + /** + * @brief Returns type of the shape + * @return Type of the shape + */ + ShapeType getType(); + + /** + * @brief Supporting routine obtaining shape UUID from ROS-parameters + * for the given shape object + * @return True if UUID was obtained or false in failure case + */ + bool obtainShapeUUID(const std::string & shape_name, unsigned char * out_uuid); + + /** + * @brief Gets the value of the shape. + * Empty virtual method intended to be used in child implementations + * @return OccupancyGrid value of the shape + */ + virtual int8_t getValue() const = 0; + + /** + * @brief Gets frame ID of the shape. + * Empty virtual method intended to be used in child implementations + * @return Frame ID where the shape is + */ + virtual std::string getFrameID() const = 0; + + /** + * @brief Gets UUID of the shape. + * Empty virtual method intended to be used in child implementations + * @return Shape UUID string + */ + virtual std::string getUUID() const = 0; + + /** + * @brief Checks whether the shape is equal to a given UUID. + * Empty virtual method intended to be used in child implementations + * @param uuid Given UUID to check with + * @return True if the shape has the same as given UUID, otherwise false + */ + virtual bool isUUID(const unsigned char * uuid) const = 0; + + /** + * @brief Whether the shape to be filled or only its borders to be put on map. + * Empty virtual method intended to be used in child implementations + * @return True if shape to be filled + */ + virtual bool isFill() const = 0; + + /** + * @brief Supporting routine obtaining ROS-parameters for the given vector object. + * Empty virtual method intended to be used in child implementations + * @param shape_name Name of the shape + * @return True if all parameters were obtained or false in failure case + */ + virtual bool obtainParams(const std::string & shape_name) = 0; + + /** + * @brief Transforms shape coordinates to a new frame. + * Empty virtual method intended to be used in child implementations + * @param to_frame Frame ID to transform to + * @param tf_buffer TF buffer to use for the transformation + * @param transform_tolerance Transform tolerance + * @return Whether it was transformed successfully + */ + virtual bool toFrame( + const std::string & to_frame, + const std::shared_ptr tf_buffer, + const double transform_tolerance) = 0; + + /** + * @brief Gets shape box-boundaries. + * Empty virtual method intended to be used in child implementations + * @param min_x output min X-boundary of shape + * @param min_y output min Y-boundary of shape + * @param max_x output max X-boundary of shape + * @param max_y output max Y-boundary of shape + */ + virtual void getBoundaries(double & min_x, double & min_y, double & max_x, double & max_y) = 0; + + /** + * @brief Is the point inside the shape. + * Empty virtual method intended to be used in child implementations + * @param px X-coordinate of the given point to check + * @param py Y-coordinate of the given point to check + * @return True if given point inside the shape + */ + virtual bool isPointInside(const double px, const double py) const = 0; + + /** + * @brief Puts shape borders on map. + * Empty virtual method intended to be used in child implementations + * @param map Output map pointer + * @param overlay_type Overlay type + */ + virtual void putBorders( + nav_msgs::msg::OccupancyGrid::SharedPtr map, const OverlayType overlay_type) = 0; + +protected: + /// @brief Type of shape + ShapeType type_; + + /// @brief VectorObjectServer node + nav2::LifecycleNode::WeakPtr node_; +}; + +/// @brief Polygon shape class +class Polygon : public Shape +{ +public: + /* + * @brief Polygon class constructor + * @param node Vector Object server node pointer + * @note setParams()/obtainParams() should be called after to configure the shape + */ + explicit Polygon(const nav2::LifecycleNode::WeakPtr & node); + + /** + * @brief Gets the value of the shape. + * @return OccupancyGrid value of the shape + */ + int8_t getValue() const; + + /** + * @brief Gets frame ID of the shape. + * @return Frame ID where the shape is + */ + std::string getFrameID() const; + + /** + * @brief Gets UUID of the shape. + * @return Shape UUID string + */ + std::string getUUID() const; + + /** + * @brief Checks whether the shape is equal to a given UUID. + * @param uuid Given UUID to check with + * @return True if the shape has the same as given UUID, otherwise false + */ + bool isUUID(const unsigned char * uuid) const; + + /** + * @brief Whether the shape to be filled or only its borders to be put on map. + * @return True if shape to be filled + */ + bool isFill() const; + + /** + * @brief Supporting routine obtaining ROS-parameters for the given vector object. + * @param shape_name Name of the shape + * @return True if all parameters were obtained or false in failure case + */ + bool obtainParams(const std::string & shape_name); + + /** + * @brief Gets Polygon parameters + * @return Polygon parameters + */ + nav2_msgs::msg::PolygonObject::SharedPtr getParams() const; + + /** + * @brief Tries to update Polygon parameters + * @return False in case of inconsistent shape + */ + bool setParams(const nav2_msgs::msg::PolygonObject::SharedPtr params); + + /** + * @brief Transforms shape coordinates to a new frame. + * @param to_frame Frame ID to transform to + * @param tf_buffer TF buffer to use for the transformation + * @param transform_tolerance Transform tolerance + * @return Whether it was transformed successfully + */ + bool toFrame( + const std::string & to_frame, + const std::shared_ptr tf_buffer, + const double transform_tolerance); + + /** + * @brief Gets shape box-boundaries + * @param min_x output min X-boundary of shape + * @param min_y output min Y-boundary of shape + * @param max_x output max X-boundary of shape + * @param max_y output max Y-boundary of shape + */ + void getBoundaries(double & min_x, double & min_y, double & max_x, double & max_y); + + /** + * @brief Is the point inside the shape. + * @param px X-coordinate of the given point to check + * @param py Y-coordinate of the given point to check + * @return True if given point inside the shape + */ + bool isPointInside(const double px, const double py) const; + + /** + * @brief Puts shape borders on map. + * @param map Output map pointer + * @param overlay_type Overlay type + */ + void putBorders(nav_msgs::msg::OccupancyGrid::SharedPtr map, const OverlayType overlay_type); + +protected: + /** + * @brief Checks that shape is consistent for further operation + * @return False in case of inconsistent shape + */ + bool checkConsistency(); + + /// @brief Input polygon parameters (could be in any frame) + nav2_msgs::msg::PolygonObject::SharedPtr params_; + /// @brief Polygon in the map's frame + geometry_msgs::msg::Polygon::SharedPtr polygon_; +}; + +/// @brief Circle shape class +class Circle : public Shape +{ +public: + /* + * @brief Circle class constructor + * @param node Vector Object server node pointer + * @note setParams()/obtainParams() should be called after to configure the shape + */ + explicit Circle(const nav2::LifecycleNode::WeakPtr & node); + + /** + * @brief Gets the value of the shape. + * @return OccupancyGrid value of the shape + */ + int8_t getValue() const; + + /** + * @brief Gets frame ID of the shape. + * @return Frame ID where the shape is + */ + std::string getFrameID() const; + + /** + * @brief Gets UUID of the shape. + * @return Shape UUID string + */ + std::string getUUID() const; + + /** + * @brief Checks whether the shape is equal to a given UUID. + * @param uuid Given UUID to check with + * @return True if the shape has the same as given UUID, otherwise false + */ + bool isUUID(const unsigned char * uuid) const; + + /** + * @brief Whether the shape to be filled or only its borders to be put on map. + * @return True if shape to be filled + */ + bool isFill() const; + + /** + * @brief Supporting routine obtaining ROS-parameters for the given vector object. + * @param shape_name Name of the shape + * @return True if all parameters were obtained or false in failure case + */ + bool obtainParams(const std::string & shape_name); + + /** + * @brief Gets Circle parameters + * @return Circle parameters + */ + nav2_msgs::msg::CircleObject::SharedPtr getParams() const; + + /** + * @brief Tries to update Circle parameters + * @return False in case of inconsistent shape + */ + bool setParams(const nav2_msgs::msg::CircleObject::SharedPtr params); + + /** + * @brief Transforms shape coordinates to a new frame. + * @param to_frame Frame ID to transform to + * @param tf_buffer TF buffer to use for the transformation + * @param transform_tolerance Transform tolerance + * @return Whether it was transformed successfully + */ + bool toFrame( + const std::string & to_frame, + const std::shared_ptr tf_buffer, + const double transform_tolerance); + + /** + * @brief Gets shape box-boundaries + * @param min_x output min X-boundary of shape + * @param min_y output min Y-boundary of shape + * @param max_x output max X-boundary of shape + * @param max_y output max Y-boundary of shape + */ + void getBoundaries(double & min_x, double & min_y, double & max_x, double & max_y); + + /** + * @brief Is the point inside the shape. + * @param px X-coordinate of the given point to check + * @param py Y-coordinate of the given point to check + * @return True if given point inside the shape + */ + bool isPointInside(const double px, const double py) const; + + /** + * @brief Puts shape borders on map. + * @param map Output map pointer + * @param overlay_type Overlay type + */ + void putBorders(nav_msgs::msg::OccupancyGrid::SharedPtr map, const OverlayType overlay_type); + +protected: + /** + * @brief Checks that shape is consistent for further operation + * @return False in case of inconsistent shape + */ + bool checkConsistency(); + + /** + * @brief Converts circle center to map coordinates + * considering FP-accuracy losing on small values when using conventional + * nav2_util::worldToMap() approach + * @param map Map pointer + * @param mcx Output X-coordinate of associated circle center on map + * @param mcy Output Y-coordinate of associated circle center on map + * @return True if the conversion was successful + */ + bool centerToMap( + nav_msgs::msg::OccupancyGrid::ConstSharedPtr map, + unsigned int & mcx, unsigned int & mcy); + + /** + * @brief Put Circle's point on map + * @param mx X-coordinate of given point + * @param my Y-coordinate of given point + * @param map Output map pointer + * @param overlay_type Overlay type + */ + void putPoint( + unsigned int mx, unsigned int my, + nav_msgs::msg::OccupancyGrid::SharedPtr map, + const OverlayType overlay_type); + + /// @brief Input circle parameters (could be in any frame) + nav2_msgs::msg::CircleObject::SharedPtr params_; + /// @brief Circle center in the map's frame + geometry_msgs::msg::Point32::SharedPtr center_; +}; + +} // namespace nav2_map_server + +#endif // NAV2_MAP_SERVER__VECTOR_OBJECT_SHAPES_HPP_ diff --git a/nav2_map_server/include/nav2_map_server/vector_object_utils.hpp b/nav2_map_server/include/nav2_map_server/vector_object_utils.hpp new file mode 100644 index 00000000000..f478f71b3ea --- /dev/null +++ b/nav2_map_server/include/nav2_map_server/vector_object_utils.hpp @@ -0,0 +1,142 @@ +// Copyright (c) 2023 Samsung R&D Institute Russia +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NAV2_MAP_SERVER__VECTOR_OBJECT_UTILS_HPP_ +#define NAV2_MAP_SERVER__VECTOR_OBJECT_UTILS_HPP_ + +#include +#include +#include + +#include "rclcpp/rclcpp.hpp" +#include "nav_msgs/msg/occupancy_grid.hpp" + +#include "nav2_ros_common/lifecycle_node.hpp" +#include "nav2_ros_common/node_utils.hpp" +#include "nav2_util/occ_grid_values.hpp" + +namespace nav2_map_server +{ + +// ---------- Working with UUID-s ---------- + +/** + * @brief Converts UUID from input array to unparsed string + * @param uuid Input UUID in array format + * @return Unparsed UUID string + */ +inline std::string unparseUUID(const unsigned char * uuid) +{ + char uuid_str[37]; + uuid_unparse(uuid, uuid_str); + return std::string(uuid_str); +} + +// ---------- Working with shapes' overlays ---------- + +/// @brief Type of overlay between different vector objects and map +enum class OverlayType : uint8_t +{ + OVERLAY_SEQ = 0, // Vector objects are superimposed in the order in which they have arrived + OVERLAY_MAX = 1, // Maximum value from vector objects and map is being chosen + OVERLAY_MIN = 2 // Minimum value from vector objects and map is being chosen +}; + +/** + * @brief Updates map value with shape's one according to the given overlay type + * @param map_val Map value. To be updated with new value if overlay is involved + * @param shape_val Vector object value to be overlaid on map + * @param overlay_type Type of overlay + * @throw std::exception in case of unknown overlay type + */ +inline void processVal( + int8_t & map_val, const int8_t shape_val, + const OverlayType overlay_type) +{ + switch (overlay_type) { + case OverlayType::OVERLAY_SEQ: + map_val = shape_val; + return; + case OverlayType::OVERLAY_MAX: + if (shape_val > map_val) { + map_val = shape_val; + } + return; + case OverlayType::OVERLAY_MIN: + if ((map_val == nav2_util::OCC_GRID_UNKNOWN || shape_val < map_val) && + shape_val != nav2_util::OCC_GRID_UNKNOWN) + { + map_val = shape_val; + } + return; + default: + throw std::runtime_error{"Unknown overlay type"}; + } +} + +/** + * @brief Updates the cell on the map with given shape value according to the given overlay type + * @param map Output map to be updated with + * @param offset Offset to the cell to be updated + * @param shape_val Vector object value to be updated map with + * @param overlay_type Type of overlay + */ +inline void processCell( + nav_msgs::msg::OccupancyGrid::SharedPtr map, + const unsigned int offset, + const int8_t shape_val, + const OverlayType overlay_type) +{ + int8_t map_val = map->data[offset]; + processVal(map_val, shape_val, overlay_type); + map->data[offset] = map_val; +} + +/// @brief Functor class used in raytraceLine algorithm +class MapAction +{ +public: + /** + * @brief MapAction constructor + * @param map Pointer to output map + * @param value Value to put on map + * @param overlay_type Overlay type + */ + MapAction( + nav_msgs::msg::OccupancyGrid::SharedPtr map, + int8_t value, OverlayType overlay_type) + : map_(map), value_(value), overlay_type_(overlay_type) + {} + + /** + * @brief Map' cell updating operator + * @param offset Offset on the map where the cell to be changed + */ + inline void operator()(unsigned int offset) + { + processCell(map_, offset, value_, overlay_type_); + } + +protected: + /// @brief Output map pointer + nav_msgs::msg::OccupancyGrid::SharedPtr map_; + /// @brief Value to put on map + int8_t value_; + /// @brief Overlay type + OverlayType overlay_type_; +}; + +} // namespace nav2_map_server + +#endif // NAV2_MAP_SERVER__VECTOR_OBJECT_UTILS_HPP_ diff --git a/nav2_map_server/launch/vector_object_server.launch.py b/nav2_map_server/launch/vector_object_server.launch.py new file mode 100644 index 00000000000..f3c2a20ff1b --- /dev/null +++ b/nav2_map_server/launch/vector_object_server.launch.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2023 Samsung R&D Institute Russia +# +# 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. + +import os + +from ament_index_python.packages import get_package_share_directory +from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument, GroupAction +from launch.conditions import IfCondition +from launch.substitutions import LaunchConfiguration, NotEqualsSubstitution, PythonExpression +from launch_ros.actions import LoadComposableNodes, Node, PushROSNamespace, SetParameter +from launch_ros.descriptions import ComposableNode, ParameterFile +from nav2_common.launch import RewrittenYaml + + +def generate_launch_description() -> LaunchDescription: + # Environment + package_dir = get_package_share_directory('nav2_map_server') + + # Constant parameters + lifecycle_nodes = ['vector_object_server', 'costmap_filter_info_server'] + remappings = [('/tf', 'tf'), + ('/tf_static', 'tf_static')] + + # Launch arguments + # 1. Create the launch configuration variables + namespace = LaunchConfiguration('namespace') + use_sim_time = LaunchConfiguration('use_sim_time') + params_file = LaunchConfiguration('params_file') + autostart = LaunchConfiguration('autostart') + use_composition = LaunchConfiguration('use_composition') + container_name = LaunchConfiguration('container_name') + + # 2. Declare the launch arguments + declare_namespace_cmd = DeclareLaunchArgument( + 'namespace', + default_value='', + description='Top-level namespace') + + declare_use_sim_time_cmd = DeclareLaunchArgument( + 'use_sim_time', + default_value='True', + description='Use simulation (Gazebo) clock if true') + + declare_params_file_cmd = DeclareLaunchArgument( + 'params_file', + default_value=os.path.join(package_dir, 'params', 'vector_object_server_params.yaml'), + description='Full path to the ROS2 parameters file to use for all launched nodes') + + declare_autostart_cmd = DeclareLaunchArgument( + 'autostart', default_value='True', + description='Automatically startup Vector Object server') + + declare_use_composition_cmd = DeclareLaunchArgument( + 'use_composition', default_value='True', + description='Use composed bringup if True') + + declare_container_name_cmd = DeclareLaunchArgument( + 'container_name', default_value='nav2_container', + description='the name of container that nodes will load in if use composition') + + configured_params = ParameterFile( + RewrittenYaml( + source_file=params_file, + root_key=namespace, + param_rewrites={}, + convert_types=True, + ), + allow_substs=True, + ) + + # Declare node launching commands + load_nodes = GroupAction( + condition=IfCondition(PythonExpression(['not ', use_composition])), + actions=[ + SetParameter('use_sim_time', use_sim_time), + PushROSNamespace( + condition=IfCondition(NotEqualsSubstitution(LaunchConfiguration('namespace'), '')), + namespace=namespace), + Node( + package='nav2_map_server', + executable='vector_object_server', + name='vector_object_server', + output='screen', + emulate_tty=True, # https://github.com/ros2/launch/issues/188 + parameters=[configured_params], + remappings=remappings), + Node( + package='nav2_map_server', + executable='costmap_filter_info_server', + name='costmap_filter_info_server', + output='screen', + emulate_tty=True, # https://github.com/ros2/launch/issues/188 + parameters=[configured_params], + remappings=remappings), + Node( + package='nav2_lifecycle_manager', + executable='lifecycle_manager', + name='lifecycle_manager_vo_server', + output='screen', + emulate_tty=True, # https://github.com/ros2/launch/issues/188 + parameters=[{'autostart': autostart}, + {'node_names': lifecycle_nodes}], + remappings=remappings) + ] + ) + + load_composable_nodes = GroupAction( + condition=IfCondition(use_composition), + actions=[ + SetParameter('use_sim_time', use_sim_time), + PushROSNamespace( + condition=IfCondition(NotEqualsSubstitution(LaunchConfiguration('namespace'), '')), + namespace=namespace), + LoadComposableNodes( + target_container=container_name, + composable_node_descriptions=[ + ComposableNode( + package='nav2_map_server', + plugin='nav2_map_server::VectorObjectServer', + name='vector_object_server', + parameters=[configured_params], + remappings=remappings), + ComposableNode( + package='nav2_map_server', + plugin='nav2_map_server::CostmapFilterInfoServer', + name='costmap_filter_info_server', + parameters=[configured_params], + remappings=remappings), + ComposableNode( + package='nav2_lifecycle_manager', + plugin='nav2_lifecycle_manager::LifecycleManager', + name='lifecycle_manager_vo_server', + parameters=[{'autostart': autostart}, + {'node_names': lifecycle_nodes}], + remappings=remappings) + ], + ) + ] + ) + + ld = LaunchDescription() + + # Launch arguments + ld.add_action(declare_namespace_cmd) + ld.add_action(declare_use_sim_time_cmd) + ld.add_action(declare_params_file_cmd) + ld.add_action(declare_autostart_cmd) + ld.add_action(declare_use_composition_cmd) + ld.add_action(declare_container_name_cmd) + + # Node launching commands + ld.add_action(load_nodes) + ld.add_action(load_composable_nodes) + + return ld diff --git a/nav2_map_server/package.xml b/nav2_map_server/package.xml index ffb3d3d1fce..44045314fa3 100644 --- a/nav2_map_server/package.xml +++ b/nav2_map_server/package.xml @@ -22,12 +22,15 @@ yaml_cpp_vendor launch_ros launch_testing + geometry_msgs tf2 + tf2_ros nav2_msgs nav2_util graphicsmagick lifecycle_msgs nav2_ros_common + uuid ament_lint_common ament_lint_auto diff --git a/nav2_map_server/params/vector_object_server_params.yaml b/nav2_map_server/params/vector_object_server_params.yaml new file mode 100644 index 00000000000..6a6a5fc2b68 --- /dev/null +++ b/nav2_map_server/params/vector_object_server_params.yaml @@ -0,0 +1,30 @@ +vector_object_server: + ros__parameters: + map_topic: "vo_map" + global_frame_id: "map" + resolution: 0.05 + default_value: -1 + overlay_type: 0 + update_frequency: 1.0 + transform_tolerance: 0.1 + shapes: ["Poly", "Circle"] + Poly: + type: "polygon" + frame_id: "map" + closed: True + value: 100 + points: [0.3, 0.5, -0.4, 1.2, -0.4, -0.2] + Circle: + type: "circle" + frame_id: "map" + fill: True + value: 100 + center: [1.5, 0.5] + radius: 0.7 +costmap_filter_info_server: + ros__parameters: + type: 0 + filter_info_topic: "vo_costmap_filter_info" + mask_topic: "vo_map" + base: 0.0 + multiplier: 1.0 diff --git a/nav2_map_server/src/vo_server/main.cpp b/nav2_map_server/src/vo_server/main.cpp new file mode 100644 index 00000000000..d2352b7aad3 --- /dev/null +++ b/nav2_map_server/src/vo_server/main.cpp @@ -0,0 +1,28 @@ +// Copyright (c) 2023 Samsung R&D Institute Russia +// +// 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 + +#include "nav2_map_server/vector_object_server.hpp" +#include "rclcpp/rclcpp.hpp" + +int main(int argc, char * argv[]) +{ + rclcpp::init(argc, argv); + auto node = std::make_shared(); + rclcpp::spin(node->get_node_base_interface()); + rclcpp::shutdown(); + + return 0; +} diff --git a/nav2_map_server/src/vo_server/vector_object_server.cpp b/nav2_map_server/src/vo_server/vector_object_server.cpp new file mode 100644 index 00000000000..89d7c3c825f --- /dev/null +++ b/nav2_map_server/src/vo_server/vector_object_server.cpp @@ -0,0 +1,558 @@ +// Copyright (c) 2023 Samsung R&D Institute Russia +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "nav2_map_server/vector_object_server.hpp" + +#include +#include +#include +#include +#include +#include + +#include "rclcpp/create_timer.hpp" + +#include "tf2_ros/create_timer_ros.hpp" + +#include "nav2_util/occ_grid_utils.hpp" +#include "nav2_util/occ_grid_values.hpp" + +using namespace std::placeholders; + +namespace nav2_map_server +{ + +VectorObjectServer::VectorObjectServer(const rclcpp::NodeOptions & options) +: nav2::LifecycleNode("vector_object_server", "", options), process_map_(false) +{} + +nav2::CallbackReturn +VectorObjectServer::on_configure(const rclcpp_lifecycle::State & /*state*/) +{ + RCLCPP_INFO(get_logger(), "Configuring"); + + // Transform buffer and listener initialization + tf_buffer_ = std::make_shared(this->get_clock()); + auto timer_interface = std::make_shared( + this->get_node_base_interface(), + this->get_node_timers_interface()); + tf_buffer_->setCreateTimerInterface(timer_interface); + tf_listener_ = std::make_shared(*tf_buffer_); + + // Obtaining ROS parameters + if (!obtainParams()) { + return nav2::CallbackReturn::FAILURE; + } + + map_pub_ = create_publisher( + map_topic_, + nav2::qos::LatchedPublisherQoS()); + + add_shapes_service_ = create_service( + "~/add_shapes", + std::bind(&VectorObjectServer::addShapesCallback, this, _1, _2, _3)); + + get_shapes_service_ = create_service( + "~/get_shapes", + std::bind(&VectorObjectServer::getShapesCallback, this, _1, _2, _3)); + + remove_shapes_service_ = create_service( + "~/remove_shapes", + std::bind(&VectorObjectServer::removeShapesCallback, this, _1, _2, _3)); + + return nav2::CallbackReturn::SUCCESS; +} + +nav2::CallbackReturn +VectorObjectServer::on_activate(const rclcpp_lifecycle::State & /*state*/) +{ + RCLCPP_INFO(get_logger(), "Activating"); + + map_pub_->on_activate(); + + // Trigger map to be published + process_map_ = true; + switchMapUpdate(); + + // Creating bond connection + createBond(); + + return nav2::CallbackReturn::SUCCESS; +} + +nav2::CallbackReturn +VectorObjectServer::on_deactivate(const rclcpp_lifecycle::State & /*state*/) +{ + RCLCPP_INFO(get_logger(), "Deactivating"); + + if (map_timer_) { + map_timer_->cancel(); + map_timer_.reset(); + } + process_map_ = false; + + map_pub_->on_deactivate(); + + // Destroying bond connection + destroyBond(); + + return nav2::CallbackReturn::SUCCESS; +} + +nav2::CallbackReturn +VectorObjectServer::on_cleanup(const rclcpp_lifecycle::State & /*state*/) +{ + RCLCPP_INFO(get_logger(), "Cleaning up"); + + add_shapes_service_.reset(); + get_shapes_service_.reset(); + remove_shapes_service_.reset(); + + map_pub_.reset(); + map_.reset(); + + shapes_.clear(); + + tf_listener_.reset(); + tf_buffer_.reset(); + + return nav2::CallbackReturn::SUCCESS; +} + +nav2::CallbackReturn +VectorObjectServer::on_shutdown(const rclcpp_lifecycle::State & /*state*/) +{ + RCLCPP_INFO(get_logger(), "Shutting down"); + return nav2::CallbackReturn::SUCCESS; +} + +bool VectorObjectServer::obtainParams() +{ + auto node = shared_from_this(); + + // Main ROS-parameters + map_topic_ = nav2::declare_or_get_parameter(node, "map_topic", std::string{"vo_map"}); + global_frame_id_ = nav2::declare_or_get_parameter(node, "global_frame_id", std::string{"map"}); + resolution_ = nav2::declare_or_get_parameter(node, "resolution", 0.05); + default_value_ = nav2::declare_or_get_parameter(node, "default_value", + static_cast(nav2_util::OCC_GRID_UNKNOWN)); + overlay_type_ = static_cast(nav2::declare_or_get_parameter(node, "overlay_type", + static_cast(OverlayType::OVERLAY_SEQ))); + update_frequency_ = nav2::declare_or_get_parameter(node, "update_frequency", 1.0); + transform_tolerance_ = nav2::declare_or_get_parameter(node, "transform_tolerance", 0.1); + + // Shapes + auto shape_names = nav2::declare_or_get_parameter(node, "shapes", std::vector()); + for (std::string shape_name : shape_names) { + std::string shape_type; + try { + shape_type = nav2::declare_or_get_parameter(node, shape_name + ".type", + rclcpp::PARAMETER_STRING); + } catch (const std::exception & ex) { + RCLCPP_ERROR( + get_logger(), "Error while getting shape %s type: %s", shape_name.c_str(), ex.what()); + return false; + } + + if (shape_type == "polygon") { + auto polygon = std::make_shared(node); + if (!polygon->obtainParams(shape_name)) { + return false; + } + shapes_.push_back(polygon); + } else if (shape_type == "circle") { + auto circle = std::make_shared(node); + if (!circle->obtainParams(shape_name)) { + return false; + } + shapes_.push_back(circle); + } else { + RCLCPP_ERROR(get_logger(), + "Please specify the correct type for shape %s. Supported types are 'polygon' and 'circle'", + shape_name.c_str()); + return false; + } + } + + return true; +} + +std::vector>::iterator +VectorObjectServer::findShape(const unsigned char * uuid) +{ + for (auto it = shapes_.begin(); it != shapes_.end(); it++) { + if ((*it)->isUUID(uuid)) { + return it; + } + } + return shapes_.end(); +} + +bool VectorObjectServer::transformVectorObjects() +{ + for (auto shape : shapes_) { + if (shape->getFrameID() != global_frame_id_ && !shape->getFrameID().empty()) { + // Shape to be updated dynamically + if (!shape->toFrame(global_frame_id_, tf_buffer_, transform_tolerance_)) { + RCLCPP_ERROR( + get_logger(), "Can not transform vector object from %s to %s frame", + shape->getFrameID().c_str(), global_frame_id_.c_str()); + return false; + } + } + } + + return true; +} + +void VectorObjectServer::getMapBoundaries( + double & min_x, double & min_y, double & max_x, double & max_y) const +{ + min_x = std::numeric_limits::max(); + min_y = std::numeric_limits::max(); + max_x = std::numeric_limits::lowest(); + max_y = std::numeric_limits::lowest(); + + double min_p_x, min_p_y, max_p_x, max_p_y; + for (auto shape : shapes_) { + shape->getBoundaries(min_p_x, min_p_y, max_p_x, max_p_y); + min_x = std::min(min_x, min_p_x); + min_y = std::min(min_y, min_p_y); + max_x = std::max(max_x, max_p_x); + max_y = std::max(max_y, max_p_y); + } + + if ( + min_x == std::numeric_limits::max() || + min_y == std::numeric_limits::max() || + max_x == std::numeric_limits::lowest() || + max_y == std::numeric_limits::lowest()) + { + throw std::runtime_error("Can not obtain map boundaries"); + } +} + +void VectorObjectServer::updateMap( + const double & min_x, const double & min_y, const double & max_x, const double & max_y) +{ + // Calculate size of update map + int size_x = static_cast((max_x - min_x) / resolution_) + 1; + int size_y = static_cast((max_y - min_y) / resolution_) + 1; + + if (size_x < 0) { + throw std::runtime_error("Incorrect map x-size"); + } + + if (size_y < 0) { + throw std::runtime_error("Incorrect map y-size"); + } + + if (!map_) { + map_ = std::make_shared(); + } + + if ( + map_->info.width != static_cast(size_x) || + map_->info.height != static_cast(size_y)) + { + // Map size was changed + map_->data = std::vector(size_x * size_y, default_value_); + map_->info.width = size_x; + map_->info.height = size_y; + } else if (size_x > 0 && size_y > 0) { + // Map size was not changed + memset(map_->data.data(), default_value_, size_x * size_y * sizeof(int8_t)); + } + + map_->header.frame_id = global_frame_id_; + map_->info.resolution = resolution_; + map_->info.origin.position.x = min_x; + map_->info.origin.position.y = min_y; +} + +void VectorObjectServer::putVectorObjectsOnMap() +{ + // Filling the shapes + for (auto shape : shapes_) { + if (shape->isFill()) { + // Put filled shape on map + double wx1 = std::numeric_limits::max(); + double wy1 = std::numeric_limits::max(); + double wx2 = std::numeric_limits::lowest(); + double wy2 = std::numeric_limits::lowest(); + unsigned int mx1 = 0; + unsigned int my1 = 0; + unsigned int mx2 = 0; + unsigned int my2 = 0; + + shape->getBoundaries(wx1, wy1, wx2, wy2); + if ( + !nav2_util::worldToMap(map_, wx1, wy1, mx1, my1) || + !nav2_util::worldToMap(map_, wx2, wy2, mx2, my2)) + { + RCLCPP_ERROR( + get_logger(), + "Error to get shape boundaries on map (UUID: %s)", shape->getUUID().c_str()); + return; + } + + unsigned int it; + for (unsigned int my = my1; my <= my2; my++) { + for (unsigned int mx = mx1; mx <= mx2; mx++) { + it = my * map_->info.width + mx; + double wx, wy; + nav2_util::mapToWorld(map_, mx, my, wx, wy); + if (shape->isPointInside(wx, wy)) { + processVal(map_->data[it], shape->getValue(), overlay_type_); + } + } + } + } else { + // Put shape borders on map + shape->putBorders(map_, overlay_type_); + } + } +} + +void VectorObjectServer::publishMap() +{ + if (map_) { + auto map = std::make_unique(*map_); + map_pub_->publish(std::move(map)); + } +} + +void VectorObjectServer::processMap() +{ + if (!process_map_) { + return; + } + + try { + if (shapes_.size() > 0) { + if (!transformVectorObjects()) { + return; + } + double min_x, min_y, max_x, max_y; + + getMapBoundaries(min_x, min_y, max_x, max_y); + updateMap(min_x, min_y, max_x, max_y); + putVectorObjectsOnMap(); + } else { + updateMap(0.0, 0.0, 0.0, 0.0); + } + } catch (const std::exception & ex) { + RCLCPP_ERROR(get_logger(), "Can not update map: %s", ex.what()); + return; + } + + publishMap(); +} + +void VectorObjectServer::switchMapUpdate() +{ + for (auto shape : shapes_) { + if (shape->getFrameID() != global_frame_id_ && !shape->getFrameID().empty()) { + if (!map_timer_) { + map_timer_ = this->create_timer( + std::chrono::duration(1.0 / update_frequency_), + std::bind(&VectorObjectServer::processMap, this)); + } + RCLCPP_INFO(get_logger(), "Publishing map dynamically at %f Hz rate", update_frequency_); + return; + } + } + + if (map_timer_) { + map_timer_->cancel(); + map_timer_.reset(); + } + RCLCPP_INFO(get_logger(), "Publishing map once"); + processMap(); +} + +void VectorObjectServer::addShapesCallback( + const std::shared_ptr/*request_header*/, + const std::shared_ptr request, + std::shared_ptr response) +{ + // Initialize result with true. If one of the required vector object was not added properly, + // set it to false. + response->success = true; + + auto node = shared_from_this(); + + // Process polygons + for (auto req_poly : request->polygons) { + nav2_msgs::msg::PolygonObject::SharedPtr new_params = + std::make_shared(req_poly); + + auto it = findShape(new_params->uuid.uuid.data()); + if (it != shapes_.end()) { + // Vector Object with given UUID was found: updating it + // Check that found shape has correct type + if ((*it)->getType() != POLYGON) { + RCLCPP_ERROR( + get_logger(), + "Shape (UUID: %s) is not a polygon type for a polygon update. Not adding shape.", + (*it)->getUUID().c_str()); + response->success = false; + // Do not add this shape + continue; + } + + std::shared_ptr polygon = std::static_pointer_cast(*it); + + // Preserving old parameters for the case, if new ones to be incorrect + nav2_msgs::msg::PolygonObject::SharedPtr old_params = polygon->getParams(); + if (!polygon->setParams(new_params)) { + RCLCPP_ERROR( + get_logger(), + "Failed to update existing polygon object (UUID: %s) with new params. " + "Reverting to old polygon params.", + (*it)->getUUID().c_str()); + // Restore old parameters + polygon->setParams(old_params); + // ... and set the failure to return + response->success = false; + } + } else { + // Vector Object with given UUID was not found: creating a new one + std::shared_ptr polygon = std::make_shared(node); + if (polygon->setParams(new_params)) { + shapes_.push_back(polygon); + } else { + RCLCPP_ERROR( + get_logger(), "Failed to create a new polygon object using the provided params."); + response->success = false; + } + } + } + + // Process circles + for (auto req_crcl : request->circles) { + nav2_msgs::msg::CircleObject::SharedPtr new_params = + std::make_shared(req_crcl); + + auto it = findShape(new_params->uuid.uuid.data()); + if (it != shapes_.end()) { + // Vector object with given UUID was found: updating it + // Check that found shape has correct type + if ((*it)->getType() != CIRCLE) { + RCLCPP_ERROR( + get_logger(), + "Shape (UUID: %s) is not a circle type for a circle update. Not adding shape.", + (*it)->getUUID().c_str()); + response->success = false; + // Do not add this shape + continue; + } + + std::shared_ptr circle = std::static_pointer_cast(*it); + + // Preserving old parameters for the case, if new ones to be incorrect + nav2_msgs::msg::CircleObject::SharedPtr old_params = circle->getParams(); + if (!circle->setParams(new_params)) { + RCLCPP_ERROR( + get_logger(), + "Failed to update existing circle object (UUID: %s) with new params. " + "Reverting to old circle params.", + (*it)->getUUID().c_str()); + // Restore old parameters + circle->setParams(old_params); + // ... and set the failure to return + response->success = false; + } + } else { + // Vector Object with given UUID was not found: creating a new one + std::shared_ptr circle = std::make_shared(node); + if (circle->setParams(new_params)) { + shapes_.push_back(circle); + } else { + RCLCPP_ERROR( + get_logger(), "Failed to create a new circle object using the provided params."); + response->success = false; + } + } + } + + switchMapUpdate(); +} + +void VectorObjectServer::getShapesCallback( + const std::shared_ptr/*request_header*/, + const std::shared_ptr/*request*/, + std::shared_ptr response) +{ + std::shared_ptr polygon; + std::shared_ptr circle; + + for (auto shape : shapes_) { + switch (shape->getType()) { + case POLYGON: + polygon = std::static_pointer_cast(shape); + response->polygons.push_back(*(polygon->getParams())); + break; + case CIRCLE: + circle = std::static_pointer_cast(shape); + response->circles.push_back(*(circle->getParams())); + break; + default: + RCLCPP_WARN(get_logger(), "Unknown shape type (UUID: %s)", shape->getUUID().c_str()); + } + } +} + +void VectorObjectServer::removeShapesCallback( + const std::shared_ptr/*request_header*/, + const std::shared_ptr request, + std::shared_ptr response) +{ + // Initialize result with true. If one of the required vector object was not found, + // set it to false. + response->success = true; + + if (request->all_objects) { + // Clear all objects + shapes_.clear(); + } else { + // Find objects to remove + for (auto req_uuid : request->uuids) { + auto it = findShape(req_uuid.uuid.data()); + if (it != shapes_.end()) { + // Shape with given UUID was found: remove it + (*it).reset(); + shapes_.erase(it); + } else { + // Required vector object was not found + RCLCPP_ERROR( + get_logger(), + "Can not find shape to remove with UUID: %s", + unparseUUID(req_uuid.uuid.data()).c_str()); + response->success = false; + } + } + } + + switchMapUpdate(); +} + +} // namespace nav2_map_server + +#include "rclcpp_components/register_node_macro.hpp" + +// Register the component with class_loader. +// This acts as a sort of entry point, allowing the component to be discoverable when its library +// is being loaded into a running process. +RCLCPP_COMPONENTS_REGISTER_NODE(nav2_map_server::VectorObjectServer) diff --git a/nav2_map_server/src/vo_server/vector_object_shapes.cpp b/nav2_map_server/src/vo_server/vector_object_shapes.cpp new file mode 100644 index 00000000000..72466486c01 --- /dev/null +++ b/nav2_map_server/src/vo_server/vector_object_shapes.cpp @@ -0,0 +1,559 @@ +// Copyright (c) 2023 Samsung R&D Institute Russia +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "nav2_map_server/vector_object_shapes.hpp" + +#include +#include +#include +#include +#include +#include + +#include "geometry_msgs/msg/pose_stamped.hpp" + +#include "nav2_util/occ_grid_utils.hpp" +#include "nav2_util/occ_grid_values.hpp" +#include "nav2_util/geometry_utils.hpp" +#include "nav2_util/raytrace_line_2d.hpp" +#include "nav2_util/robot_utils.hpp" + +namespace nav2_map_server +{ + +// ---------- Shape ---------- + +Shape::Shape(const nav2::LifecycleNode::WeakPtr & node) +: type_(UNKNOWN), node_(node) +{} + +Shape::~Shape() +{} + +ShapeType Shape::getType() +{ + return type_; +} + +bool Shape::obtainShapeUUID(const std::string & shape_name, unsigned char * out_uuid) +{ + auto node = node_.lock(); + if (!node) { + throw std::runtime_error{"Failed to lock node"}; + } + + try { + // Try to get shape UUID from ROS-parameters + std::string uuid_str = nav2::declare_or_get_parameter( + node, shape_name + ".uuid", rclcpp::PARAMETER_STRING); + if (uuid_parse(uuid_str.c_str(), out_uuid) != 0) { + RCLCPP_ERROR( + node->get_logger(), + "[%s] Can not parse UUID string for shape: %s", + shape_name.c_str(), uuid_str.c_str()); + return false; + } + } catch (const std::exception &) { + // If no UUID was specified, generate a new one + uuid_generate(out_uuid); + + char uuid_str[37]; + uuid_unparse(out_uuid, uuid_str); + RCLCPP_INFO( + node->get_logger(), + "[%s] No UUID is specified for shape. Generating a new one: %s", + shape_name.c_str(), uuid_str); + } + + return true; +} + +// ---------- Polygon ---------- + +Polygon::Polygon( + const nav2::LifecycleNode::WeakPtr & node) +: Shape::Shape(node) +{ + type_ = POLYGON; +} + +int8_t Polygon::getValue() const +{ + return params_->value; +} + +std::string Polygon::getFrameID() const +{ + return params_->header.frame_id; +} + +std::string Polygon::getUUID() const +{ + return unparseUUID(params_->uuid.uuid.data()); +} + +bool Polygon::isUUID(const unsigned char * uuid) const +{ + return uuid_compare(params_->uuid.uuid.data(), uuid) == 0; +} + +bool Polygon::isFill() const +{ + return params_->closed; +} + +bool Polygon::obtainParams(const std::string & shape_name) +{ + auto node = node_.lock(); + if (!node) { + throw std::runtime_error{"Failed to lock node"}; + } + + if (!params_) { + params_ = std::make_shared(); + } + if (!polygon_) { + polygon_ = std::make_shared(); + } + + params_->header.frame_id = nav2::declare_or_get_parameter( + node, shape_name + ".frame_id", std::string{"map"}); + params_->value = nav2::declare_or_get_parameter( + node, shape_name + ".value", static_cast(nav2_util::OCC_GRID_OCCUPIED)); + params_->closed = nav2::declare_or_get_parameter( + node, shape_name + ".closed", true); + + std::vector poly_row; + try { + poly_row = nav2::declare_or_get_parameter>( + node, shape_name + ".points", rclcpp::PARAMETER_DOUBLE_ARRAY); + } catch (const std::exception & ex) { + RCLCPP_ERROR( + node->get_logger(), + "[%s] Error while getting polygon parameters: %s", + shape_name.c_str(), ex.what()); + return false; + } + // Check for points format correctness + if (poly_row.size() < 6 || poly_row.size() % 2 != 0) { + RCLCPP_ERROR( + node->get_logger(), + "[%s] Polygon has incorrect points description", + shape_name.c_str()); + return false; + } + + // Obtain polygon vertices + geometry_msgs::msg::Point32 point; + bool first = true; + for (double val : poly_row) { + if (first) { + point.x = val; + } else { + point.y = val; + params_->points.push_back(point); + } + first = !first; + } + + // Filling the polygon_ with obtained points in map's frame + polygon_->points = params_->points; + + // Getting shape UUID + return obtainShapeUUID(shape_name, params_->uuid.uuid.data()); +} + +nav2_msgs::msg::PolygonObject::SharedPtr Polygon::getParams() const +{ + return params_; +} + +bool Polygon::setParams(const nav2_msgs::msg::PolygonObject::SharedPtr params) +{ + params_ = params; + + if (!polygon_) { + polygon_ = std::make_shared(); + } + polygon_->points = params_->points; + + // If no UUID was specified, generate a new one + if (uuid_is_null(params_->uuid.uuid.data())) { + uuid_generate(params_->uuid.uuid.data()); + } + + return checkConsistency(); +} + +bool Polygon::toFrame( + const std::string & to_frame, + const std::shared_ptr tf_buffer, + const double transform_tolerance) +{ + geometry_msgs::msg::PoseStamped from_pose, to_pose; + from_pose.header = params_->header; + for (unsigned int i = 0; i < params_->points.size(); i++) { + from_pose.pose.position.x = params_->points[i].x; + from_pose.pose.position.y = params_->points[i].y; + from_pose.pose.position.z = params_->points[i].z; + if ( + nav2_util::transformPoseInTargetFrame( + from_pose, to_pose, *tf_buffer, to_frame, transform_tolerance)) + { + polygon_->points[i].x = to_pose.pose.position.x; + polygon_->points[i].y = to_pose.pose.position.y; + polygon_->points[i].z = to_pose.pose.position.z; + } else { + return false; + } + } + + return true; +} + +void Polygon::getBoundaries(double & min_x, double & min_y, double & max_x, double & max_y) +{ + min_x = std::numeric_limits::max(); + min_y = std::numeric_limits::max(); + max_x = std::numeric_limits::lowest(); + max_y = std::numeric_limits::lowest(); + + for (auto point : polygon_->points) { + min_x = std::min(min_x, static_cast(point.x)); + min_y = std::min(min_y, static_cast(point.y)); + max_x = std::max(max_x, static_cast(point.x)); + max_y = std::max(max_y, static_cast(point.y)); + } +} + +bool Polygon::isPointInside(const double px, const double py) const +{ + return nav2_util::geometry_utils::isPointInsidePolygon(px, py, polygon_->points); +} + +void Polygon::putBorders( + nav_msgs::msg::OccupancyGrid::SharedPtr map, const OverlayType overlay_type) +{ + unsigned int mx0, my0, mx1, my1; + + auto node = node_.lock(); + if (!node) { + throw std::runtime_error{"Failed to lock node"}; + } + + if (!nav2_util::worldToMap(map, polygon_->points[0].x, polygon_->points[0].y, mx1, my1)) { + RCLCPP_ERROR( + node->get_logger(), + "[UUID: %s] Can not convert (%f, %f) point to map", + getUUID().c_str(), polygon_->points[0].x, polygon_->points[0].y); + return; + } + + MapAction ma(map, params_->value, overlay_type); + for (unsigned int i = 1; i < polygon_->points.size(); i++) { + mx0 = mx1; + my0 = my1; + if (!nav2_util::worldToMap(map, polygon_->points[i].x, polygon_->points[i].y, mx1, my1)) { + RCLCPP_ERROR( + node->get_logger(), + "[UUID: %s] Can not convert (%f, %f) point to map", + getUUID().c_str(), polygon_->points[i].x, polygon_->points[i].y); + return; + } + nav2_util::raytraceLine(ma, mx0, my0, mx1, my1, map->info.width); + } +} + +bool Polygon::checkConsistency() +{ + if (params_->points.size() < 3) { + auto node = node_.lock(); + if (!node) { + throw std::runtime_error{"Failed to lock node"}; + } + + RCLCPP_ERROR( + node->get_logger(), + "[UUID: %s] Polygon has incorrect number of vertices: %li", + getUUID().c_str(), params_->points.size()); + return false; + } + + return true; +} + +// ---------- Circle ---------- + +Circle::Circle( + const nav2::LifecycleNode::WeakPtr & node) +: Shape::Shape(node) +{ + type_ = CIRCLE; +} + +int8_t Circle::getValue() const +{ + return params_->value; +} + +std::string Circle::getFrameID() const +{ + return params_->header.frame_id; +} + +std::string Circle::getUUID() const +{ + return unparseUUID(params_->uuid.uuid.data()); +} + +bool Circle::isUUID(const unsigned char * uuid) const +{ + return uuid_compare(params_->uuid.uuid.data(), uuid) == 0; +} + +bool Circle::isFill() const +{ + return params_->fill; +} + +bool Circle::obtainParams(const std::string & shape_name) +{ + auto node = node_.lock(); + if (!node) { + throw std::runtime_error{"Failed to lock node"}; + } + + if (!params_) { + params_ = std::make_shared(); + } + if (!center_) { + center_ = std::make_shared(); + } + + params_->header.frame_id = nav2::declare_or_get_parameter( + node, shape_name + ".frame_id", std::string{"map"}); + params_->value = nav2::declare_or_get_parameter( + node, shape_name + ".value", static_cast(nav2_util::OCC_GRID_OCCUPIED)); + params_->fill = nav2::declare_or_get_parameter( + node, shape_name + ".fill", true); + + std::vector center_row; + try { + center_row = nav2::declare_or_get_parameter>( + node, shape_name + ".center", rclcpp::PARAMETER_DOUBLE_ARRAY); + params_->radius = nav2::declare_or_get_parameter( + node, shape_name + ".radius", rclcpp::PARAMETER_DOUBLE); + if (params_->radius < 0) { + RCLCPP_ERROR( + node->get_logger(), + "[%s] Circle has incorrect radius less than zero", + shape_name.c_str()); + return false; + } + } catch (const std::exception & ex) { + RCLCPP_ERROR( + node->get_logger(), + "[%s] Error while getting circle parameters: %s", + shape_name.c_str(), ex.what()); + return false; + } + // Check for points format correctness + if (center_row.size() != 2) { + RCLCPP_ERROR( + node->get_logger(), + "[%s] Circle has incorrect center description", + shape_name.c_str()); + return false; + } + + // Obtain circle center + params_->center.x = center_row[0]; + params_->center.y = center_row[1]; + // Setting the center_ with obtained circle center in map's frame + *center_ = params_->center; + + // Getting shape UUID + return obtainShapeUUID(shape_name, params_->uuid.uuid.data()); +} + +nav2_msgs::msg::CircleObject::SharedPtr Circle::getParams() const +{ + return params_; +} + +bool Circle::setParams(const nav2_msgs::msg::CircleObject::SharedPtr params) +{ + params_ = params; + + if (!center_) { + center_ = std::make_shared(); + } + *center_ = params_->center; + + // If no UUID was specified, generate a new one + if (uuid_is_null(params_->uuid.uuid.data())) { + uuid_generate(params_->uuid.uuid.data()); + } + + return checkConsistency(); +} + +bool Circle::toFrame( + const std::string & to_frame, + const std::shared_ptr tf_buffer, + const double transform_tolerance) +{ + geometry_msgs::msg::PoseStamped from_pose, to_pose; + from_pose.header = params_->header; + from_pose.pose.position.x = params_->center.x; + from_pose.pose.position.y = params_->center.y; + from_pose.pose.position.z = params_->center.z; + if ( + nav2_util::transformPoseInTargetFrame( + from_pose, to_pose, *tf_buffer, to_frame, transform_tolerance)) + { + center_->x = to_pose.pose.position.x; + center_->y = to_pose.pose.position.y; + center_->z = to_pose.pose.position.z; + } else { + return false; + } + + return true; +} + +void Circle::getBoundaries(double & min_x, double & min_y, double & max_x, double & max_y) +{ + min_x = center_->x - params_->radius; + min_y = center_->y - params_->radius; + max_x = center_->x + params_->radius; + max_y = center_->y + params_->radius; +} + +bool Circle::isPointInside(const double px, const double py) const +{ + return ( (px - center_->x) * (px - center_->x) + (py - center_->y) * (py - center_->y) ) <= + params_->radius * params_->radius; +} + +void Circle::putBorders( + nav_msgs::msg::OccupancyGrid::SharedPtr map, const OverlayType overlay_type) +{ + unsigned int mcx, mcy; + if (!centerToMap(map, mcx, mcy)) { + return; + } + + // Implementation of the circle generation algorithm, based on the following work: + // Berthold K.P. Horn "Circle generators for display devices" + // Computer Graphics and Image Processing 5.2 (1976): 280-288. + + // Inputs initialization + const int r = static_cast(std::round(params_->radius / map->info.resolution)); + int x = r; + int y = 1; + + // Error initialization + int s = -r; + + // Calculation algorithm + while (x > y) { // Calculating only first circle octant + // Put 8 points in each octant reflecting symmetrically + putPoint(mcx + x, mcy + y, map, overlay_type); + putPoint(mcx + y, mcy + x, map, overlay_type); + putPoint(mcx - x + 1, mcy + y, map, overlay_type); + putPoint(mcx + y, mcy - x + 1, map, overlay_type); + putPoint(mcx - x + 1, mcy - y + 1, map, overlay_type); + putPoint(mcx - y + 1, mcy - x + 1, map, overlay_type); + putPoint(mcx + x, mcy - y + 1, map, overlay_type); + putPoint(mcx - y + 1, mcy + x, map, overlay_type); + + s = s + 2 * y + 1; + y++; + if (s > 0) { + s = s - 2 * x + 2; + x--; + } + } + + // Corner case for x == y: do not put end points twice + if (x == y) { + putPoint(mcx + x, mcy + y, map, overlay_type); + putPoint(mcx - x + 1, mcy + y, map, overlay_type); + putPoint(mcx - x + 1, mcy - y + 1, map, overlay_type); + putPoint(mcx + x, mcy - y + 1, map, overlay_type); + } +} + +bool Circle::checkConsistency() +{ + if (params_->radius < 0.0) { + auto node = node_.lock(); + if (!node) { + throw std::runtime_error{"Failed to lock node"}; + } + + RCLCPP_ERROR( + node->get_logger(), + "[UUID: %s] Circle has incorrect radius less than zero", + getUUID().c_str()); + return false; + } + return true; +} + +bool Circle::centerToMap( + nav_msgs::msg::OccupancyGrid::ConstSharedPtr map, + unsigned int & mcx, unsigned int & mcy) +{ + auto node = node_.lock(); + if (!node) { + throw std::runtime_error{"Failed to lock node"}; + } + + // Get center of circle in map coordinates + if (center_->x < map->info.origin.position.x || center_->y < map->info.origin.position.y) { + RCLCPP_ERROR( + node->get_logger(), + "[UUID: %s] Can not convert (%f, %f) circle center to map", + getUUID().c_str(), center_->x, center_->y); + return false; + } + // We need the circle center to be always shifted one cell less its logical center + // and to avoid any FP-accuracy losing on small values, so we are using another + // than nav2_util::worldToMap() approach + mcx = static_cast( + std::round((center_->x - map->info.origin.position.x) / map->info.resolution)) - 1; + mcy = static_cast( + std::round((center_->y - map->info.origin.position.y) / map->info.resolution)) - 1; + if (mcx >= map->info.width || mcy >= map->info.height) { + RCLCPP_ERROR( + node->get_logger(), + "[UUID: %s] Can not convert (%f, %f) point to map", + getUUID().c_str(), center_->x, center_->y); + return false; + } + + return true; +} + +inline void Circle::putPoint( + unsigned int mx, unsigned int my, + nav_msgs::msg::OccupancyGrid::SharedPtr map, + const OverlayType overlay_type) +{ + processCell(map, my * map->info.width + mx, params_->value, overlay_type); +} + +} // namespace nav2_map_server diff --git a/nav2_map_server/test/unit/CMakeLists.txt b/nav2_map_server/test/unit/CMakeLists.txt index 4140b14d6f0..06a4b2228a0 100644 --- a/nav2_map_server/test/unit/CMakeLists.txt +++ b/nav2_map_server/test/unit/CMakeLists.txt @@ -18,3 +18,25 @@ target_link_libraries(test_costmap_filter_info_server ${library_name} ${map_io_library_name} ) + +# test_vector_object_shapes unit test +ament_add_gtest(test_vector_object_shapes + test_vector_object_shapes.cpp +) + +target_link_libraries(test_vector_object_shapes ${vo_server_dependencies}) + +target_link_libraries(test_vector_object_shapes + ${vo_library_name} +) + +# test_vector_object_server unit test +ament_add_gtest(test_vector_object_server + test_vector_object_server.cpp +) + +target_link_libraries(test_vector_object_server ${vo_server_dependencies}) + +target_link_libraries(test_vector_object_server + ${vo_library_name} +) diff --git a/nav2_map_server/test/unit/test_vector_object_server.cpp b/nav2_map_server/test/unit/test_vector_object_server.cpp new file mode 100644 index 00000000000..88bfd697d0a --- /dev/null +++ b/nav2_map_server/test/unit/test_vector_object_server.cpp @@ -0,0 +1,1349 @@ +// Copyright (c) 2023 Samsung R&D Institute Russia +// +// 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 + +#include +#include +#include +#include +#include + +#include "rclcpp/rclcpp.hpp" +#include "nav_msgs/msg/occupancy_grid.hpp" +#include "unique_identifier_msgs/msg/uuid.hpp" + +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/transform_listener.hpp" +#include "tf2_ros/transform_broadcaster.hpp" + +#include "nav2_msgs/srv/add_shapes.hpp" +#include "nav2_msgs/srv/get_shapes.hpp" +#include "nav2_msgs/srv/remove_shapes.hpp" +#include "nav2_msgs/msg/polygon_object.hpp" +#include "nav2_msgs/msg/circle_object.hpp" +#include "nav2_ros_common/lifecycle_node.hpp" +#include "nav2_util/occ_grid_utils.hpp" + +#include "nav2_map_server/vector_object_server.hpp" + +using namespace std::chrono_literals; + +static constexpr double EPSILON = std::numeric_limits::epsilon(); + +static const char POLYGON_NAME[]{"Polygon"}; +static const char CIRCLE_NAME[]{"Circle"}; +static const char GLOBAL_FRAME_ID[]{"map"}; +static const char SHAPE_FRAME_ID[]{"shape"}; +static const double UPDATE_FREQUENCY{10.0}; +static const int8_t POLYGON_VAL{nav2_util::OCC_GRID_OCCUPIED}; +static const int8_t CIRCLE_VAL{nav2_util::OCC_GRID_OCCUPIED / 2}; + +class VOServerWrapper : public nav2_map_server::VectorObjectServer +{ +public: + void start() + { + ASSERT_EQ(on_configure(get_current_state()), nav2::CallbackReturn::SUCCESS); + ASSERT_EQ(on_activate(get_current_state()), nav2::CallbackReturn::SUCCESS); + } + + void configureFail() + { + ASSERT_EQ(on_configure(get_current_state()), nav2::CallbackReturn::FAILURE); + } + + void stop() + { + ASSERT_EQ(on_deactivate(get_current_state()), nav2::CallbackReturn::SUCCESS); + ASSERT_EQ(on_cleanup(get_current_state()), nav2::CallbackReturn::SUCCESS); + ASSERT_EQ(on_shutdown(get_current_state()), nav2::CallbackReturn::SUCCESS); + } + + void cleanup() + { + ASSERT_EQ(on_cleanup(get_current_state()), nav2::CallbackReturn::SUCCESS); + ASSERT_EQ(on_shutdown(get_current_state()), nav2::CallbackReturn::SUCCESS); + } + + void setProcessMap(const bool process_map) + { + process_map_ = process_map; + } + + void getMapBoundaries(double & min_x, double & min_y, double & max_x, double & max_y) const + { + VectorObjectServer::getMapBoundaries(min_x, min_y, max_x, max_y); + } + + void updateMap( + const double & min_x, const double & min_y, const double & max_x, const double & max_y) + { + VectorObjectServer::updateMap(min_x, min_y, max_x, max_y); + } + + void putVectorObjectsOnMap() + { + VectorObjectServer::putVectorObjectsOnMap(); + } +}; // VOServerWrapper + +class Tester : public ::testing::Test +{ +public: + Tester(); + ~Tester(); + + void setVOServerParams(); + void setShapesParams(const std::string & poly_uuid, const std::string & circle_uuid); + void setPolygonParams(const std::string & uuid); + void setCircleParams(const std::string & uuid); + + nav2_msgs::msg::PolygonObject::SharedPtr makePolygonObject( + const std::vector & uuid); + nav2_msgs::msg::CircleObject::SharedPtr makeCircleObject( + const std::vector & uuid); + + void sendTransform(const double frame_shift); + + template + typename T::Response::SharedPtr sendRequest( + typename nav2::ServiceClient::SharedPtr client, + typename T::Request::SharedPtr request, + const std::chrono::nanoseconds & timeout); + + void mapCallback(nav_msgs::msg::OccupancyGrid::SharedPtr map); + bool waitMap(const std::chrono::nanoseconds & timeout); + void verifyMap(bool is_circle, double poly_x_end = 1.0, double circle_cx = 3.0); + + void comparePolygonObjects( + nav2_msgs::msg::PolygonObject::SharedPtr p1, + nav2_msgs::msg::PolygonObject::SharedPtr p2); + void compareCircleObjects( + nav2_msgs::msg::CircleObject::SharedPtr c1, + nav2_msgs::msg::CircleObject::SharedPtr c2); + +protected: + std::shared_ptr tf_buffer_; + std::shared_ptr tf_listener_; + + // Service clients for calling AddShapes.srv, GetShapes.srv, RemoveShapes.srv + nav2::ServiceClient::SharedPtr add_shapes_client_; + nav2::ServiceClient::SharedPtr get_shapes_client_; + nav2::ServiceClient::SharedPtr remove_shapes_client_; + + // Output map subscriber + rclcpp::Subscription::SharedPtr vo_map_sub_; + // Output map published by VectorObjectServer + nav_msgs::msg::OccupancyGrid::SharedPtr map_; + + // Vector Object server node + std::shared_ptr vo_server_; +}; // Tester + +Tester::Tester() +: map_(nullptr) +{ + vo_server_ = std::make_shared(); + + add_shapes_client_ = vo_server_->create_client( + std::string(vo_server_->get_name()) + "/add_shapes"); + + get_shapes_client_ = vo_server_->create_client( + std::string(vo_server_->get_name()) + "/get_shapes"); + + remove_shapes_client_ = vo_server_->create_client( + std::string(vo_server_->get_name()) + "/remove_shapes"); + + vo_map_sub_ = vo_server_->create_subscription( + "vo_map", std::bind(&Tester::mapCallback, this, std::placeholders::_1), + nav2::qos::LatchedSubscriptionQoS()); + + // Transform buffer and listener initialization + tf_buffer_ = std::make_shared(vo_server_->get_clock()); + tf_buffer_->setUsingDedicatedThread(true); // One-thread broadcasting-listening model + tf_listener_ = std::make_shared(*tf_buffer_); +} + +Tester::~Tester() +{ + vo_map_sub_.reset(); + + add_shapes_client_.reset(); + get_shapes_client_.reset(); + remove_shapes_client_.reset(); + + vo_server_.reset(); + + tf_listener_.reset(); + tf_buffer_.reset(); +} + +void Tester::setVOServerParams() +{ + vo_server_->declare_parameter( + "map_topic", rclcpp::ParameterValue("vo_map")); + vo_server_->set_parameter( + rclcpp::Parameter("map_topic", "vo_map")); + + vo_server_->declare_parameter( + "global_frame_id", rclcpp::ParameterValue("map")); + vo_server_->set_parameter( + rclcpp::Parameter("global_frame_id", "map")); + + vo_server_->declare_parameter( + "resolution", rclcpp::ParameterValue(0.1)); + vo_server_->set_parameter( + rclcpp::Parameter("resolution", 0.1)); + + vo_server_->declare_parameter( + "default_value", rclcpp::ParameterValue(nav2_util::OCC_GRID_UNKNOWN)); + vo_server_->set_parameter( + rclcpp::Parameter("default_value", nav2_util::OCC_GRID_UNKNOWN)); + + vo_server_->declare_parameter( + "overlay_type", + rclcpp::ParameterValue(static_cast(nav2_map_server::OverlayType::OVERLAY_SEQ))); + vo_server_->set_parameter( + rclcpp::Parameter( + "overlay_type", + static_cast(nav2_map_server::OverlayType::OVERLAY_SEQ))); + + vo_server_->declare_parameter( + "update_frequency", rclcpp::ParameterValue(UPDATE_FREQUENCY)); + vo_server_->set_parameter( + rclcpp::Parameter("update_frequency", UPDATE_FREQUENCY)); + + vo_server_->declare_parameter( + "transform_tolerance", rclcpp::ParameterValue(0.1)); + vo_server_->set_parameter( + rclcpp::Parameter("transform_tolerance", 0.1)); +} + +void Tester::setShapesParams(const std::string & poly_uuid, const std::string & circle_uuid) +{ + vo_server_->declare_parameter( + "shapes", rclcpp::ParameterValue(std::vector{POLYGON_NAME, CIRCLE_NAME})); + vo_server_->set_parameter( + rclcpp::Parameter("shapes", std::vector{POLYGON_NAME, CIRCLE_NAME})); + + setPolygonParams(poly_uuid); + setCircleParams(circle_uuid); +} + +void Tester::setPolygonParams(const std::string & uuid) +{ + const std::string polygon_name(POLYGON_NAME); + + vo_server_->declare_parameter( + polygon_name + ".type", rclcpp::ParameterValue("polygon")); + vo_server_->set_parameter( + rclcpp::Parameter(polygon_name + ".type", "polygon")); + + vo_server_->declare_parameter( + polygon_name + ".frame_id", rclcpp::ParameterValue(GLOBAL_FRAME_ID)); + vo_server_->set_parameter( + rclcpp::Parameter(polygon_name + ".frame_id", GLOBAL_FRAME_ID)); + + vo_server_->declare_parameter( + polygon_name + ".closed", rclcpp::ParameterValue(true)); + vo_server_->set_parameter( + rclcpp::Parameter(polygon_name + ".closed", true)); + + vo_server_->declare_parameter( + polygon_name + ".value", rclcpp::ParameterValue(POLYGON_VAL)); + vo_server_->set_parameter( + rclcpp::Parameter(polygon_name + ".value", POLYGON_VAL)); + + std::vector points{1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0}; + vo_server_->declare_parameter( + polygon_name + ".points", rclcpp::ParameterValue(points)); + vo_server_->set_parameter( + rclcpp::Parameter(polygon_name + ".points", points)); + + if (!uuid.empty()) { + vo_server_->declare_parameter( + polygon_name + ".uuid", rclcpp::ParameterValue(uuid)); + vo_server_->set_parameter( + rclcpp::Parameter(polygon_name + ".uuid", uuid)); + } +} + +void Tester::setCircleParams(const std::string & uuid) +{ + const std::string circle_name(CIRCLE_NAME); + + vo_server_->declare_parameter( + circle_name + ".type", rclcpp::ParameterValue("circle")); + vo_server_->set_parameter( + rclcpp::Parameter(circle_name + ".type", "circle")); + + vo_server_->declare_parameter( + circle_name + ".frame_id", rclcpp::ParameterValue(GLOBAL_FRAME_ID)); + vo_server_->set_parameter( + rclcpp::Parameter(circle_name + ".frame_id", GLOBAL_FRAME_ID)); + + vo_server_->declare_parameter( + circle_name + ".fill", rclcpp::ParameterValue(true)); + vo_server_->set_parameter( + rclcpp::Parameter(circle_name + ".fill", true)); + + vo_server_->declare_parameter( + circle_name + ".value", rclcpp::ParameterValue(CIRCLE_VAL)); + vo_server_->set_parameter( + rclcpp::Parameter(circle_name + ".value", CIRCLE_VAL)); + + vo_server_->declare_parameter( + circle_name + ".center", rclcpp::ParameterValue(std::vector{3.0, 0.0})); + vo_server_->set_parameter( + rclcpp::Parameter(circle_name + ".center", std::vector{3.0, 0.0})); + + vo_server_->declare_parameter( + circle_name + ".radius", rclcpp::ParameterValue(1.0)); + vo_server_->set_parameter( + rclcpp::Parameter(circle_name + ".radius", 1.0)); + + if (!uuid.empty()) { + vo_server_->declare_parameter( + circle_name + ".uuid", rclcpp::ParameterValue(uuid)); + vo_server_->set_parameter( + rclcpp::Parameter(circle_name + ".uuid", uuid)); + } +} + +void Tester::sendTransform(const double frame_shift) +{ + std::shared_ptr tf_broadcaster = + std::make_shared(vo_server_); + + geometry_msgs::msg::TransformStamped transform; + + // global_frame -> shape_frame transform + transform.header.frame_id = GLOBAL_FRAME_ID; + transform.child_frame_id = SHAPE_FRAME_ID; + + transform.header.stamp = vo_server_->now(); + transform.transform.translation.x = frame_shift; + transform.transform.translation.y = 0.0; + transform.transform.translation.z = 0.0; + transform.transform.rotation.x = 0.0; + transform.transform.rotation.y = 0.0; + transform.transform.rotation.z = 0.0; + transform.transform.rotation.w = 1.0; + + tf_broadcaster->sendTransform(transform); +} + +nav2_msgs::msg::PolygonObject::SharedPtr Tester::makePolygonObject( + const std::vector & uuid) +{ + nav2_msgs::msg::PolygonObject::SharedPtr po = std::make_shared(); + po->header.frame_id = GLOBAL_FRAME_ID; + if (uuid.size() == 16) { + // uuid could be optionally specified + std::array uuid_array; + std::copy(uuid.begin(), uuid.end(), uuid_array.begin()); + po->uuid.uuid = uuid_array; + } + geometry_msgs::msg::Point32 p; + p.x = 1.0; + p.y = 1.0; + po->points.push_back(p); + p.x = -1.0; + po->points.push_back(p); + p.y = -1.0; + po->points.push_back(p); + p.x = 1.0; + po->points.push_back(p); + po->closed = true; + po->value = POLYGON_VAL; + + return po; +} + +nav2_msgs::msg::CircleObject::SharedPtr Tester::makeCircleObject( + const std::vector & uuid) +{ + nav2_msgs::msg::CircleObject::SharedPtr co = std::make_shared(); + co->header.frame_id = GLOBAL_FRAME_ID; + if (uuid.size() == 16) { + // uuid could be optionally specified + std::array uuid_array; + std::copy(uuid.begin(), uuid.end(), uuid_array.begin()); + co->uuid.uuid = uuid_array; + } + co->center.x = 3.0; + co->center.y = 0.0; + co->radius = 1.0; + co->fill = true; + co->value = CIRCLE_VAL; + + return co; +} + +template +typename T::Response::SharedPtr Tester::sendRequest( + typename nav2::ServiceClient::SharedPtr client, + typename T::Request::SharedPtr request, + const std::chrono::nanoseconds & timeout) +{ + auto result_future = client->async_call(request); + + rclcpp::Time start_time = vo_server_->now(); + while (rclcpp::ok() && vo_server_->now() - start_time <= rclcpp::Duration(timeout)) { + std::future_status status = result_future.wait_for(10ms); + if (status == std::future_status::ready) { + return result_future.get(); + } + rclcpp::spin_some(vo_server_->get_node_base_interface()); + std::this_thread::sleep_for(10ms); + } + return nullptr; +} + +void Tester::mapCallback(nav_msgs::msg::OccupancyGrid::SharedPtr map) +{ + map_ = map; +} + +bool Tester::waitMap(const std::chrono::nanoseconds & timeout) +{ + rclcpp::Time start_time = vo_server_->now(); + while (rclcpp::ok() && vo_server_->now() - start_time <= rclcpp::Duration(timeout)) { + if (map_) { + return true; + } + rclcpp::spin_some(vo_server_->get_node_base_interface()); + std::this_thread::sleep_for(10ms); + } + return false; +} + +void Tester::verifyMap(bool is_circle, double poly_x_end, double circle_cx) +{ + // Wait for map to be received + const std::chrono::nanoseconds timeout = + std::chrono::duration_cast(1s * 1.5 / UPDATE_FREQUENCY); + ASSERT_TRUE(waitMap(timeout)); + + // Map should contain polygon and circle and will look like: + // + // polygon {x1, y1} (and map's origin): should be always {-1.0, -1.0} + // V + // xxxxxx....xxx. + // xxxxxx...xxxxx < circle is optionally placed on map + // xxxxxx...xxxxx + // xxxxxx....xxx. + // ^ + // polygon {x2, y2}. x2 = poly_x_end; y2 - is always == 1.0 + + // Polygon {x2, y2} in map coordinates + const unsigned int m_poly_x2 = 9 + poly_x_end * 10; + const unsigned int m_poly_y2 = 19; + + // Center of the circle in map coordinates (expressed in floating-point for best precision) + const double m_circle_cx = 9 + circle_cx * 10 + 0.5; // cell's origin + 0.5 center of cell + const double m_circle_cy = 9 + 0.5; // cell's origin + 0.5 center of cell + + // Radius of the circle in map coordinates + const double m_circle_rad = 10.0; + + for (unsigned int my = 0; my < map_->info.height; my++) { + for (unsigned int mx = 0; mx < map_->info.width; mx++) { + if (mx <= m_poly_x2 && my <= m_poly_y2) { + // Point belongs to the polygon + ASSERT_EQ(map_->data[my * map_->info.width + mx], POLYGON_VAL); + } else if (is_circle && std::hypot(m_circle_cx - mx, m_circle_cy - my) <= m_circle_rad) { + // Point belongs to the circle + ASSERT_EQ(map_->data[my * map_->info.width + mx], CIRCLE_VAL); + } else { + // Point does not belong to any shape + ASSERT_EQ(map_->data[my * map_->info.width + mx], nav2_util::OCC_GRID_UNKNOWN); + } + } + } +} + +void Tester::comparePolygonObjects( + nav2_msgs::msg::PolygonObject::SharedPtr p1, + nav2_msgs::msg::PolygonObject::SharedPtr p2) +{ + ASSERT_EQ(p1->header.frame_id, p2->header.frame_id); + ASSERT_EQ(p1->uuid.uuid, p2->uuid.uuid); + ASSERT_EQ(p1->points.size(), p2->points.size()); + for (unsigned int i = 0; i < p1->points.size(); i++) { + ASSERT_NEAR(p1->points[i].x, p2->points[i].x, EPSILON); + ASSERT_NEAR(p1->points[i].y, p2->points[i].y, EPSILON); + } + ASSERT_EQ(p1->closed, p2->closed); + ASSERT_EQ(p1->value, p2->value); +} + +void Tester::compareCircleObjects( + nav2_msgs::msg::CircleObject::SharedPtr c1, + nav2_msgs::msg::CircleObject::SharedPtr c2) +{ + ASSERT_EQ(c1->header.frame_id, c2->header.frame_id); + ASSERT_EQ(c1->uuid.uuid, c2->uuid.uuid); + ASSERT_NEAR(c1->center.x, c2->center.x, EPSILON); + ASSERT_NEAR(c1->center.y, c2->center.y, EPSILON); + ASSERT_NEAR(c1->radius, c2->radius, EPSILON); + ASSERT_EQ(c1->fill, c2->fill); + ASSERT_EQ(c1->value, c2->value); +} + +// ---------- ROS-parameters tests ---------- +TEST_F(Tester, testObtainParams) +{ + setVOServerParams(); + setShapesParams( + "01010101-0101-0101-0101-010101010101", + "01010101-0101-0101-0101-010101010102"); + vo_server_->start(); + + verifyMap(true); + + vo_server_->stop(); +} + +TEST_F(Tester, testObtainNoShapes) +{ + setVOServerParams(); + // Set shapes array, but does not set shapes themselves + vo_server_->declare_parameter( + "shapes", rclcpp::ParameterValue(std::vector{POLYGON_NAME, CIRCLE_NAME})); + vo_server_->set_parameter( + rclcpp::Parameter("shapes", std::vector{POLYGON_NAME, CIRCLE_NAME})); + + // Configured that way, vo_server_ should give the failure on configuration stage + vo_server_->configureFail(); + + // Cleaning-up vo_server_ w/o deactivation + vo_server_->cleanup(); +} + +TEST_F(Tester, testIncorrectShapeType) +{ + setVOServerParams(); + setShapesParams("", ""); + // Setting incorrect type of circle + vo_server_->set_parameter( + rclcpp::Parameter(std::string(CIRCLE_NAME) + ".type", "incorrect_type")); + + // Configured that way, vo_server_ should give the failure on configuration stage + vo_server_->configureFail(); + + // Cleaning-up vo_server_ w/o deactivation + vo_server_->cleanup(); +} + +TEST_F(Tester, testIncorrectPolygonParams) +{ + setVOServerParams(); + // Setting incorrect UUID. Other incorrect params to be checked inside Vector Object Shapes test + setShapesParams( + "incorrect_polygon_uuid", + "01010101-0101-0101-0101-010101010102"); + + // Set that way, vo_server_ should give the failure on configuration stage + vo_server_->configureFail(); + + // Cleaning-up vo_server_ w/o deactivation + vo_server_->cleanup(); +} + +TEST_F(Tester, testIncorrectCircleParams) +{ + setVOServerParams(); + // Setting incorrect UUID. Other incorrect params to be checked inside Vector Object Shapes test + setShapesParams( + "01010101-0101-0101-0101-010101010101", + "incorrect_circle_uuid"); + + // Set that way, vo_server_ should give the failure on configuration stage + vo_server_->configureFail(); + + // Cleaning-up vo_server_ w/o deactivation + vo_server_->cleanup(); +} + +// ---------- Service call tests ---------- +TEST_F(Tester, testAddShapes) +{ + setVOServerParams(); + vo_server_->start(); + + // Add polygon and circle on map + auto add_shapes_msg = std::make_shared(); + // Prepare polygon object to add + auto po_msg = makePolygonObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}); + // Prepare circle object to add + auto co_msg = makeCircleObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2}); + + add_shapes_msg->polygons.push_back(*po_msg); + add_shapes_msg->circles.push_back(*co_msg); + map_.reset(); // Resetting the map to ensure that updated one is received later + auto add_shapes_result = + sendRequest(add_shapes_client_, add_shapes_msg, 2s); + ASSERT_NE(add_shapes_result, nullptr); + ASSERT_TRUE(add_shapes_result->success); + + // Check the map has been updated correctly + verifyMap(true); + + // Check that getShapes.srv will return correct shapes + auto get_shapes_msg = std::make_shared(); + auto get_shapes_result = + sendRequest(get_shapes_client_, get_shapes_msg, 2s); + ASSERT_NE(get_shapes_result, nullptr); + // Verify obtained polygon + ASSERT_EQ(get_shapes_result->polygons.size(), 1u); + auto p_check = std::make_shared(get_shapes_result->polygons[0]); + comparePolygonObjects(p_check, po_msg); + // Verify obtained circle + ASSERT_EQ(get_shapes_result->circles.size(), 1u); + auto c_check = std::make_shared(get_shapes_result->circles[0]); + compareCircleObjects(c_check, co_msg); + + // Now move X-coordinate of polygon's border and + // add a new circle fully placed inside first one + // through AddShapes.srv: + // Update polygon x2-coordinate to 1.5 + po_msg->points[0].x = 1.5; + po_msg->points[3].x = 1.5; + // Prepare second circle object (fully placed inside first circle) + auto co2_msg = makeCircleObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3}); + co2_msg->radius = 0.5; + + add_shapes_msg->polygons.clear(); + add_shapes_msg->circles.clear(); + add_shapes_msg->polygons.push_back(*po_msg); + add_shapes_msg->circles.push_back(*co_msg); + add_shapes_msg->circles.push_back(*co2_msg); + map_.reset(); // Resetting the map to ensure that updated one is received later + add_shapes_result = + sendRequest(add_shapes_client_, add_shapes_msg, 2s); + ASSERT_NE(add_shapes_result, nullptr); + ASSERT_TRUE(add_shapes_result->success); + + // Check the map has been updated correctly + verifyMap(true, 1.5); + + // Check that getShapes.srv will return correct shapes + get_shapes_result = + sendRequest(get_shapes_client_, get_shapes_msg, 2s); + ASSERT_NE(get_shapes_result, nullptr); + // Verify obtained polygon + ASSERT_EQ(get_shapes_result->polygons.size(), 1u); + p_check = std::make_shared(get_shapes_result->polygons[0]); + comparePolygonObjects(p_check, po_msg); + // Verify obtained circles + ASSERT_EQ(get_shapes_result->circles.size(), 2u); + c_check = std::make_shared(get_shapes_result->circles[0]); + compareCircleObjects(c_check, co_msg); + c_check = std::make_shared(get_shapes_result->circles[1]); + compareCircleObjects(c_check, co2_msg); + + vo_server_->stop(); +} + +TEST_F(Tester, testRemoveShapes) +{ + setVOServerParams(); + vo_server_->start(); + + // Add polygon and circle on map + auto add_shapes_msg = std::make_shared(); + // Prepare polygon object to add + auto po_msg = makePolygonObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}); + // Prepare circle object to add + auto co_msg = makeCircleObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2}); + + add_shapes_msg->polygons.push_back(*po_msg); + add_shapes_msg->circles.push_back(*co_msg); + map_.reset(); // Resetting the map to ensure that updated one is received later + auto add_shapes_result = + sendRequest(add_shapes_client_, add_shapes_msg, 2s); + ASSERT_NE(add_shapes_result, nullptr); + ASSERT_TRUE(add_shapes_result->success); + + // Check that getShapes.srv will return correct shapes + auto get_shapes_msg = std::make_shared(); + auto get_shapes_result = + sendRequest(get_shapes_client_, get_shapes_msg, 2s); + ASSERT_NE(get_shapes_result, nullptr); + // Verify obtained polygon + ASSERT_EQ(get_shapes_result->polygons.size(), 1u); + auto p_check = std::make_shared(get_shapes_result->polygons[0]); + comparePolygonObjects(p_check, po_msg); + // Verify obtained circle + ASSERT_EQ(get_shapes_result->circles.size(), 1u); + auto c_check = std::make_shared(get_shapes_result->circles[0]); + compareCircleObjects(c_check, co_msg); + + // Now remove circle from map + auto remove_shapes_msg = std::make_shared(); + remove_shapes_msg->all_objects = false; + unique_identifier_msgs::msg::UUID uuid; + uuid.uuid = std::array{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2}; + remove_shapes_msg->uuids.push_back(uuid); + map_.reset(); // Resetting the map to ensure that updated one is received later + auto remove_shapes_result = + sendRequest(remove_shapes_client_, remove_shapes_msg, 2s); + ASSERT_NE(remove_shapes_result, nullptr); + ASSERT_TRUE(remove_shapes_result->success); + + // Check that there is only polygon remained on map + verifyMap(false); + + // Check that getShapes.srv will return only polygon + get_shapes_result = + sendRequest(get_shapes_client_, get_shapes_msg, 2s); + ASSERT_NE(get_shapes_result, nullptr); + // Verify obtained polygon + ASSERT_EQ(get_shapes_result->polygons.size(), 1u); + p_check = std::make_shared(get_shapes_result->polygons[0]); + comparePolygonObjects(p_check, po_msg); + // Verify obtained circle + ASSERT_EQ(get_shapes_result->circles.size(), 0u); + + // Then call RemoveShapes.srv with enabled all_objects parameter + remove_shapes_msg->all_objects = true; + remove_shapes_msg->uuids.clear(); + map_.reset(); // Resetting the map to ensure that updated one is received later + remove_shapes_result = + sendRequest(remove_shapes_client_, remove_shapes_msg, 2s); + ASSERT_NE(remove_shapes_result, nullptr); + ASSERT_TRUE(remove_shapes_result->success); + + // Check that map is empty + ASSERT_EQ(map_->info.width, 1); + ASSERT_EQ(map_->info.height, 1); + ASSERT_EQ(map_->data[0], nav2_util::OCC_GRID_UNKNOWN); + + // getShapes.srv should return empty vectors + get_shapes_result = + sendRequest(get_shapes_client_, get_shapes_msg, 2s); + ASSERT_NE(get_shapes_result, nullptr); + ASSERT_EQ(get_shapes_result->polygons.size(), 0u); + ASSERT_EQ(get_shapes_result->circles.size(), 0u); + + vo_server_->stop(); +} + +TEST_F(Tester, testAddIncorrectShapes) +{ + setVOServerParams(); + vo_server_->start(); + + // Add polygon and circle on map + auto add_shapes_msg = std::make_shared(); + // Prepare polygon object to add + auto po_msg = makePolygonObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}); + // Prepare circle object to add + auto co_msg = makeCircleObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2}); + + add_shapes_msg->polygons.push_back(*po_msg); + add_shapes_msg->circles.push_back(*co_msg); + map_.reset(); // Resetting the map to ensure that updated one is received later + auto add_shapes_result = + sendRequest(add_shapes_client_, add_shapes_msg, 2s); + ASSERT_NE(add_shapes_result, nullptr); + ASSERT_TRUE(add_shapes_result->success); + + // Check that getShapes.srv will return correct shapes + auto get_shapes_msg = std::make_shared(); + auto get_shapes_result = + sendRequest(get_shapes_client_, get_shapes_msg, 2s); + ASSERT_NE(get_shapes_result, nullptr); + // Verify obtained polygon + ASSERT_EQ(get_shapes_result->polygons.size(), 1u); + auto p_check = std::make_shared(get_shapes_result->polygons[0]); + comparePolygonObjects(p_check, po_msg); + // Verify obtained circle + ASSERT_EQ(get_shapes_result->circles.size(), 1u); + auto c_check = std::make_shared(get_shapes_result->circles[0]); + compareCircleObjects(c_check, co_msg); + + // Then try to update polygon with circle's uuid + po_msg->uuid.uuid[15] = 2; + add_shapes_msg->polygons.clear(); + add_shapes_msg->circles.clear(); + add_shapes_msg->polygons.push_back(*po_msg); + add_shapes_result = + sendRequest(add_shapes_client_, add_shapes_msg, 2s); + ASSERT_NE(add_shapes_result, nullptr); + ASSERT_FALSE(add_shapes_result->success); + po_msg->uuid.uuid[15] = 1; // Restoring back for further usage + + // Then try to update circle with polygon's uuid + co_msg->uuid.uuid[15] = 1; + add_shapes_msg->polygons.clear(); + add_shapes_msg->circles.clear(); + add_shapes_msg->circles.push_back(*co_msg); + add_shapes_result = + sendRequest(add_shapes_client_, add_shapes_msg, 2s); + ASSERT_NE(add_shapes_result, nullptr); + ASSERT_FALSE(add_shapes_result->success); + co_msg->uuid.uuid[15] = 2; // Restoring back for further usage + + // Try to update polygon with incorrect number of points + po_msg->points.resize(2); + add_shapes_msg->polygons.clear(); + add_shapes_msg->circles.clear(); + add_shapes_msg->polygons.push_back(*po_msg); + add_shapes_result = + sendRequest(add_shapes_client_, add_shapes_msg, 2s); + ASSERT_NE(add_shapes_result, nullptr); + ASSERT_FALSE(add_shapes_result->success); + + // Try to add new incorrect polygon + po_msg->uuid.uuid[15] = 100; + add_shapes_msg->polygons.clear(); + add_shapes_msg->circles.clear(); + add_shapes_msg->polygons.push_back(*po_msg); + add_shapes_result = + sendRequest(add_shapes_client_, add_shapes_msg, 2s); + ASSERT_NE(add_shapes_result, nullptr); + ASSERT_FALSE(add_shapes_result->success); + // Restoring back for further usage + po_msg = makePolygonObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}); + + // Try to update circle with incorrect radius + co_msg->radius = -1.0; + add_shapes_msg->polygons.clear(); + add_shapes_msg->circles.clear(); + add_shapes_msg->circles.push_back(*co_msg); + add_shapes_result = + sendRequest(add_shapes_client_, add_shapes_msg, 2s); + ASSERT_NE(add_shapes_result, nullptr); + ASSERT_FALSE(add_shapes_result->success); + + // Try to add new incorrect circle + co_msg->uuid.uuid[15] = 100; + add_shapes_msg->polygons.clear(); + add_shapes_msg->circles.clear(); + add_shapes_msg->circles.push_back(*co_msg); + add_shapes_result = + sendRequest(add_shapes_client_, add_shapes_msg, 2s); + ASSERT_NE(add_shapes_result, nullptr); + ASSERT_FALSE(add_shapes_result->success); + co_msg->radius = 1.0; // Restoring back for further usage + co_msg->uuid.uuid[15] = 2; // Restoring back for further usage + + // Verify that map did not corrupted after all false-manipulations + verifyMap(true); + + // Check that getShapes.srv will return correct shapes after all false-manipulations + get_shapes_msg = std::make_shared(); + get_shapes_result = + sendRequest(get_shapes_client_, get_shapes_msg, 2s); + ASSERT_NE(get_shapes_result, nullptr); + // Verify obtained polygon + ASSERT_EQ(get_shapes_result->polygons.size(), 1u); + p_check = std::make_shared(get_shapes_result->polygons[0]); + comparePolygonObjects(p_check, po_msg); + // Verify obtained circle + ASSERT_EQ(get_shapes_result->circles.size(), 1u); + c_check = std::make_shared(get_shapes_result->circles[0]); + compareCircleObjects(c_check, co_msg); + + vo_server_->stop(); +} + +TEST_F(Tester, testRemoveIncorrectShapes) +{ + setVOServerParams(); + vo_server_->start(); + + // Add polygon and circle on map + auto add_shapes_msg = std::make_shared(); + // Prepare polygon object to add + auto po_msg = makePolygonObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}); + // Prepare circle object to add + auto co_msg = makeCircleObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2}); + + add_shapes_msg->polygons.push_back(*po_msg); + add_shapes_msg->circles.push_back(*co_msg); + auto add_shapes_result = + sendRequest(add_shapes_client_, add_shapes_msg, 2s); + ASSERT_NE(add_shapes_result, nullptr); + ASSERT_TRUE(add_shapes_result->success); + + // Check that getShapes.srv will return correct shapes + auto get_shapes_msg = std::make_shared(); + auto get_shapes_result = + sendRequest(get_shapes_client_, get_shapes_msg, 2s); + ASSERT_NE(get_shapes_result, nullptr); + // Verify obtained polygon + ASSERT_EQ(get_shapes_result->polygons.size(), 1u); + auto p_check = std::make_shared(get_shapes_result->polygons[0]); + comparePolygonObjects(p_check, po_msg); + // Verify obtained circle + ASSERT_EQ(get_shapes_result->circles.size(), 1u); + auto c_check = std::make_shared(get_shapes_result->circles[0]); + compareCircleObjects(c_check, co_msg); + + // Then try to remove two shapes: non-existing shape and circle + auto remove_shapes_msg = std::make_shared(); + remove_shapes_msg->all_objects = false; + unique_identifier_msgs::msg::UUID uuid; + uuid.uuid = std::array{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 100}; + remove_shapes_msg->uuids.push_back(uuid); + uuid.uuid = std::array{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2}; + remove_shapes_msg->uuids.push_back(uuid); + map_.reset(); // Resetting the map to ensure that updated one is received later + auto remove_shapes_result = + sendRequest(remove_shapes_client_, remove_shapes_msg, 2s); + ASSERT_NE(remove_shapes_result, nullptr); + ASSERT_FALSE(remove_shapes_result->success); + + // Check that despite on the error, the circle was removed from map + verifyMap(false); + + // Check that getShapes.srv will return only polygon + get_shapes_result = + sendRequest(get_shapes_client_, get_shapes_msg, 2s); + ASSERT_NE(get_shapes_result, nullptr); + // Verify obtained polygon + ASSERT_EQ(get_shapes_result->polygons.size(), 1u); + p_check = std::make_shared(get_shapes_result->polygons[0]); + comparePolygonObjects(p_check, po_msg); + // Verify obtained circle + ASSERT_EQ(get_shapes_result->circles.size(), 0u); + + vo_server_->stop(); +} + +// ---------- Overlay tests ---------- +TEST_F(Tester, testOverlayMax) +{ + setVOServerParams(); + vo_server_->set_parameter( + rclcpp::Parameter( + "overlay_type", + static_cast(nav2_map_server::OverlayType::OVERLAY_MAX))); + vo_server_->start(); + + // Add polygon and circle on map + auto add_shapes_msg = std::make_shared(); + // Prepare polygon object to add + auto po_msg = makePolygonObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}); + // Prepare circle object to add + auto co_msg = makeCircleObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2}); + // Make circle to be overlapped with polygon + co_msg->center.x = 1.0; + + add_shapes_msg->polygons.push_back(*po_msg); + add_shapes_msg->circles.push_back(*co_msg); + map_.reset(); // Resetting the map to ensure that updated one is received later + auto add_shapes_result = + sendRequest(add_shapes_client_, add_shapes_msg, 2s); + ASSERT_NE(add_shapes_result, nullptr); + ASSERT_TRUE(add_shapes_result->success); + + // Wait for the map + waitMap(500ms); + + // Verify that overlap on map generated correctly + double my = 9; + double mx = 1; // inside polygon + ASSERT_EQ(map_->data[my * map_->info.width + mx], POLYGON_VAL); + mx = 14; // in the overlapping area + ASSERT_EQ(map_->data[my * map_->info.width + mx], POLYGON_VAL); + mx = 20; // inside circle + ASSERT_EQ(map_->data[my * map_->info.width + mx], CIRCLE_VAL); + my = 18; // outside any shape + mx = 28; // outside any shape + ASSERT_EQ(map_->data[my * map_->info.width + mx], nav2_util::OCC_GRID_UNKNOWN); + + vo_server_->stop(); +} + +TEST_F(Tester, testOverlayMin) +{ + setVOServerParams(); + vo_server_->set_parameter( + rclcpp::Parameter( + "overlay_type", + static_cast(nav2_map_server::OverlayType::OVERLAY_MIN))); + vo_server_->start(); + + // Add polygon and circle on map + auto add_shapes_msg = std::make_shared(); + // Prepare polygon object to add + auto po_msg = makePolygonObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}); + // Prepare circle object to add + auto co_msg = makeCircleObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2}); + // Make circle to be overlapped with polygon + co_msg->center.x = 1.0; + + add_shapes_msg->polygons.push_back(*po_msg); + add_shapes_msg->circles.push_back(*co_msg); + map_.reset(); // Resetting the map to ensure that updated one is received later + auto add_shapes_result = + sendRequest(add_shapes_client_, add_shapes_msg, 2s); + ASSERT_NE(add_shapes_result, nullptr); + ASSERT_TRUE(add_shapes_result->success); + + // Wait for the map + waitMap(500ms); + + // Verify that overlap on map generated correctly + double my = 9; + double mx = 1; // inside polygon + ASSERT_EQ(map_->data[my * map_->info.width + mx], POLYGON_VAL); + mx = 14; // in the overlapping area + ASSERT_EQ(map_->data[my * map_->info.width + mx], CIRCLE_VAL); + mx = 20; // inside circle + ASSERT_EQ(map_->data[my * map_->info.width + mx], CIRCLE_VAL); + my = 18; // outside any shape + mx = 28; // outside any shape + ASSERT_EQ(map_->data[my * map_->info.width + mx], nav2_util::OCC_GRID_UNKNOWN); + + vo_server_->stop(); +} + +TEST_F(Tester, testOverlaySeq) +{ + setVOServerParams(); + vo_server_->set_parameter( + rclcpp::Parameter( + "overlay_type", + static_cast(nav2_map_server::OverlayType::OVERLAY_SEQ))); + vo_server_->start(); + + // Sequentially add polygon and then circle overlapped with it on map + auto add_shapes_msg = std::make_shared(); + + // Prepare first polygon object to add + auto po_msg = makePolygonObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}); + add_shapes_msg->polygons.push_back(*po_msg); + auto add_shapes_result = + sendRequest(add_shapes_client_, add_shapes_msg, 2s); + ASSERT_NE(add_shapes_result, nullptr); + ASSERT_TRUE(add_shapes_result->success); + + // Then prepare circle object to add over the polygon + auto co_msg = makeCircleObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2}); + // Make circle to be overlapped with polygon + co_msg->center.x = 1.0; + // Also check that putBorders part works correctly + co_msg->fill = false; + add_shapes_msg->polygons.clear(); + add_shapes_msg->circles.push_back(*co_msg); + map_.reset(); // Resetting the map to ensure that updated one is received later + add_shapes_result = + sendRequest(add_shapes_client_, add_shapes_msg, 2s); + ASSERT_NE(add_shapes_result, nullptr); + ASSERT_TRUE(add_shapes_result->success); + + // Wait for the map + waitMap(500ms); + + // Verify that overlap on map generated correctly + double my = 9; + double mx = 1; // inside polygon + ASSERT_EQ(map_->data[my * map_->info.width + mx], POLYGON_VAL); + mx = 10; // on the circle border laying over the polygon + ASSERT_EQ(map_->data[my * map_->info.width + mx], CIRCLE_VAL); + mx = 14; // inside circle and polygon + ASSERT_EQ(map_->data[my * map_->info.width + mx], POLYGON_VAL); + mx = 24; // inside circle only + ASSERT_EQ(map_->data[my * map_->info.width + mx], nav2_util::OCC_GRID_UNKNOWN); + mx = 29; // on the circle border laying outside the polygon + ASSERT_EQ(map_->data[my * map_->info.width + mx], CIRCLE_VAL); + my = 18; // outside any shape + mx = 28; // outside any shape + ASSERT_EQ(map_->data[my * map_->info.width + mx], nav2_util::OCC_GRID_UNKNOWN); + + vo_server_->stop(); +} + +TEST_F(Tester, testOverlayUnknown) +{ + setVOServerParams(); + vo_server_->set_parameter( + rclcpp::Parameter("overlay_type", static_cast(1000))); + vo_server_->start(); + + // Try to add polygon on map + auto add_shapes_msg = std::make_shared(); + auto po_msg = makePolygonObject(std::vector()); + add_shapes_msg->polygons.push_back(*po_msg); + map_.reset(); // Resetting the map to ensure that updated one is received later + auto add_shapes_result = + sendRequest(add_shapes_client_, add_shapes_msg, 2s); + ASSERT_NE(add_shapes_result, nullptr); + ASSERT_TRUE(add_shapes_result->success); + + // Wait for the map + waitMap(500ms); + + // Check that polygon was not added on map: map is empty + ASSERT_EQ(map_->info.width, 1); + ASSERT_EQ(map_->info.height, 1); + ASSERT_EQ(map_->data[0], nav2_util::OCC_GRID_UNKNOWN); + + vo_server_->stop(); +} + +// ---------- Transform/dynamic tests ---------- +TEST_F(Tester, testShapesMove) +{ + setVOServerParams(); + vo_server_->start(); + + // Wait for the initial map to be received to not get it in later updates + const std::chrono::nanoseconds timeout = + std::chrono::duration_cast(1s * 1.5 / UPDATE_FREQUENCY); + ASSERT_TRUE(waitMap(timeout)); + + // No shift between polygon and map + sendTransform(0.0); + + // Add polygon and circle on map + auto add_shapes_msg = std::make_shared(); + // Prepare polygon object to add + auto po_msg = makePolygonObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}); + // Polygon and circle to be in another moving frame + po_msg->header.frame_id = SHAPE_FRAME_ID; + // Prepare circle object to add + auto co_msg = makeCircleObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2}); + + add_shapes_msg->polygons.push_back(*po_msg); + add_shapes_msg->circles.push_back(*co_msg); + map_.reset(); // Resetting the map to ensure that updated one is received later + auto add_shapes_result = + sendRequest(add_shapes_client_, add_shapes_msg, 2s); + ASSERT_NE(add_shapes_result, nullptr); + ASSERT_TRUE(add_shapes_result->success); + + // Check that polygon and circle are in correct positions on map + verifyMap(true); + + // Move frame with polygon + sendTransform(0.5); + + // Map is being published dynamically. Wait for the map to be published one more time + map_.reset(); + // Check that polygon and circle are in correct positions on map + verifyMap(true, 1.0, 2.5); // Polygon is moved 0.5m towards to the circle + // Check that map's origin was updated in accordance to polygon movement + ASSERT_NEAR(map_->info.origin.position.x, -0.5, EPSILON); // -1.0 + 0.5 + ASSERT_NEAR(map_->info.origin.position.y, -1.0, EPSILON); + + // Then try to publish polygon in incorrect frame + add_shapes_msg->polygons[0].header.frame_id = "incorrect_frame"; + map_.reset(); // Resetting the map to ensure that updated one is received later + add_shapes_result = + sendRequest(add_shapes_client_, add_shapes_msg, 2s); + ASSERT_NE(add_shapes_result, nullptr); + ASSERT_TRUE(add_shapes_result->success); + + // Having incorrect transform, map should not be published + ASSERT_FALSE(waitMap(timeout)); + + vo_server_->stop(); +} + +TEST_F(Tester, testSwitchDynamicStatic) +{ + setVOServerParams(); + vo_server_->start(); + + // Wait for the initial map to be received to not get it in later updates + const std::chrono::nanoseconds timeout = + std::chrono::duration_cast(1s * 1.5 / UPDATE_FREQUENCY); + ASSERT_TRUE(waitMap(timeout)); + + // 0.5m shift between polygon and map + sendTransform(0.5); + + // Add polygon and circle on map + auto add_shapes_msg = std::make_shared(); + // Prepare polygon object to add + auto po_msg = makePolygonObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}); + // Polygon to be in different frame + po_msg->header.frame_id = SHAPE_FRAME_ID; + // Prepare circle object to add + auto co_msg = makeCircleObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2}); + + add_shapes_msg->polygons.push_back(*po_msg); + add_shapes_msg->circles.push_back(*co_msg); + map_.reset(); // Resetting the map to ensure that updated one is received later + auto add_shapes_result = + sendRequest(add_shapes_client_, add_shapes_msg, 2s); + ASSERT_NE(add_shapes_result, nullptr); + ASSERT_TRUE(add_shapes_result->success); + + // Check that polygon and circle are in correct positions on map + verifyMap(true, 1.0, 2.5); // Polygon is moved 0.5m towards to the circle + // Check that map's origin was updated in accordance to polygon movement + ASSERT_NEAR(map_->info.origin.position.x, -0.5, EPSILON); // -1.0 + 0.5 + ASSERT_NEAR(map_->info.origin.position.y, -1.0, EPSILON); + + // Then return to the static model and ensure the everything works correctly + add_shapes_msg->polygons[0].header.frame_id = GLOBAL_FRAME_ID; + map_.reset(); // Resetting the map to ensure that updated one is received later + add_shapes_result = + sendRequest(add_shapes_client_, add_shapes_msg, 2s); + ASSERT_NE(add_shapes_result, nullptr); + ASSERT_TRUE(add_shapes_result->success); + + verifyMap(true); + + vo_server_->stop(); +} + +// ---------- Corner cases ---------- +TEST_F(Tester, switchProcessMap) +{ + setVOServerParams(); + vo_server_->start(); + + // Wait for the initial map to be received to not get it in later updates + const std::chrono::nanoseconds timeout = + std::chrono::duration_cast(1s * 1.5 / UPDATE_FREQUENCY); + ASSERT_TRUE(waitMap(timeout)); + + // Check that received default map is empty + ASSERT_EQ(map_->header.frame_id, "map"); + ASSERT_NEAR(map_->info.resolution, 0.1, EPSILON); + ASSERT_EQ(map_->info.width, 1); + ASSERT_EQ(map_->info.height, 1); + + // Disable map processing and trying to obtain the map + vo_server_->setProcessMap(false); + + // Switching map update by calling remove service + auto remove_shapes_msg = std::make_shared(); + remove_shapes_msg->all_objects = true; + map_.reset(); // Resetting the map to ensure that updated one is received later + auto remove_shapes_result = + sendRequest(remove_shapes_client_, remove_shapes_msg, 2s); + ASSERT_NE(remove_shapes_result, nullptr); + ASSERT_TRUE(remove_shapes_result->success); + + // Having process_map_ disabled, map should not be published + ASSERT_FALSE(waitMap(timeout)); + + // Then enable map processing and trying to obtain the map + vo_server_->setProcessMap(true); + + // Switching map update by calling remove service + map_.reset(); // Resetting the map to ensure that updated one is received later + remove_shapes_result = + sendRequest(remove_shapes_client_, remove_shapes_msg, 2s); + ASSERT_NE(remove_shapes_result, nullptr); + ASSERT_TRUE(remove_shapes_result->success); + + // Ensure that map is being updated + ASSERT_TRUE(waitMap(timeout)); + + // ... and it is empty + ASSERT_EQ(map_->header.frame_id, "map"); + ASSERT_NEAR(map_->info.resolution, 0.1, EPSILON); + ASSERT_EQ(map_->info.width, 1); + ASSERT_EQ(map_->info.height, 1); + + vo_server_->stop(); +} + +TEST_F(Tester, testIncorrectMapBoundaries) { + setVOServerParams(); + vo_server_->start(); + + // Set min_x > max_x + EXPECT_THROW(vo_server_->updateMap(1.0, 0.1, 0.1, 1.0), std::runtime_error); + + // Set min_y > max_y + EXPECT_THROW(vo_server_->updateMap(0.1, 1.0, 1.0, 0.1), std::runtime_error); +} + +TEST_F(Tester, testIncorrectMapBoundariesWhenNoShapes) { + setVOServerParams(); + vo_server_->start(); + double min_x, min_y, max_x, max_y; + EXPECT_THROW(vo_server_->getMapBoundaries(min_x, min_y, max_x, max_y), std::runtime_error); +} + +TEST_F(Tester, testShapeOutsideMap) { + setVOServerParams(); + vo_server_->start(); + + // Add polygon and circle on map + auto add_shapes_msg = std::make_shared(); + // Prepare polygon object to add + auto po_msg = makePolygonObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}); + // Prepare circle object to add + auto co_msg = makeCircleObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2}); + + add_shapes_msg->polygons.push_back(*po_msg); + add_shapes_msg->circles.push_back(*co_msg); + map_.reset(); // Resetting the map to ensure that updated one is received later + auto add_shapes_result = + sendRequest(add_shapes_client_, add_shapes_msg, 2s); + ASSERT_NE(add_shapes_result, nullptr); + ASSERT_TRUE(add_shapes_result->success); + + // Check the map has been updated correctly + verifyMap(true); + + // Modify the map to have a shape outside the map + vo_server_->updateMap(2.0, 2.0, 4.0, 4.0); + + // Try to put the shapes back in the map + vo_server_->putVectorObjectsOnMap(); + + // Verify that map did not corrupted after all false-manipulations + verifyMap(true); +} + +int main(int argc, char ** argv) +{ + // Initialize the system + testing::InitGoogleTest(&argc, argv); + rclcpp::init(argc, argv); + + // Actual testing + bool test_result = RUN_ALL_TESTS(); + + // Shutdown + rclcpp::shutdown(); + + return test_result; +} diff --git a/nav2_map_server/test/unit/test_vector_object_shapes.cpp b/nav2_map_server/test/unit/test_vector_object_shapes.cpp new file mode 100644 index 00000000000..43a6f43a58d --- /dev/null +++ b/nav2_map_server/test/unit/test_vector_object_shapes.cpp @@ -0,0 +1,805 @@ +// Copyright (c) 2023 Samsung R&D Institute Russia +// +// 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 + +#include +#include +#include +#include +#include +#include + +#include "rclcpp/rclcpp.hpp" +#include "geometry_msgs/msg/point32.hpp" +#include "nav_msgs/msg/occupancy_grid.hpp" + +#include "tf2_ros/buffer.hpp" +#include "tf2_ros/transform_listener.hpp" +#include "tf2_ros/transform_broadcaster.hpp" + +#include "nav2_msgs/msg/polygon_object.hpp" +#include "nav2_msgs/msg/circle_object.hpp" +#include "nav2_ros_common/lifecycle_node.hpp" +#include "nav2_util/occ_grid_utils.hpp" + +#include "nav2_map_server/vector_object_shapes.hpp" +#include "nav2_map_server/vector_object_utils.hpp" + +static constexpr double EPSILON = std::numeric_limits::epsilon(); + +static const char POLYGON_NAME[]{"Polygon"}; +static const char CIRCLE_NAME[]{"Circle"}; +static const char GLOBAL_FRAME_ID[]{"map"}; +static const char SHAPE_FRAME_ID[]{"shape"}; +static double FRAME_SHIFT = 0.1; +static std::vector POINTS{1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0}; +static std::vector CENTER{0.0, 0.0}; + +class PolygonWrapper : public nav2_map_server::Polygon +{ +public: + explicit PolygonWrapper(const nav2::LifecycleNode::WeakPtr & node) + : Polygon(node) + {} + + geometry_msgs::msg::Polygon::SharedPtr getPoly() + { + return polygon_; + } +}; // PolygonWrapper + +class CircleWrapper : public nav2_map_server::Circle +{ +public: + explicit CircleWrapper(const nav2::LifecycleNode::WeakPtr & node) + : Circle(node) + {} + + geometry_msgs::msg::Point32::SharedPtr getCenter() + { + return center_; + } +}; // CircleWrapper + +class Tester : public ::testing::Test +{ +public: + Tester(); + ~Tester(); + +protected: + void setPolygonParams(const std::string & uuid); + void setCircleParams(const std::string & uuid); + nav2_msgs::msg::PolygonObject::SharedPtr makePolygonObject( + const std::vector & uuid); + nav2_msgs::msg::CircleObject::SharedPtr makeCircleObject( + const std::vector & uuid); + + void sendTransform(); + + nav_msgs::msg::OccupancyGrid::SharedPtr makeMap(); + void verifyPolygonBorders(nav_msgs::msg::OccupancyGrid::SharedPtr map); + void verifyCircleBorders(nav_msgs::msg::OccupancyGrid::SharedPtr map); + void verifyMapEmpty(nav_msgs::msg::OccupancyGrid::SharedPtr map); + + void comparePolygonObjects( + nav2_msgs::msg::PolygonObject::SharedPtr p1, + nav2_msgs::msg::PolygonObject::SharedPtr p2); + void compareCircleObjects( + nav2_msgs::msg::CircleObject::SharedPtr c1, + nav2_msgs::msg::CircleObject::SharedPtr c2); + + std::shared_ptr tf_buffer_; + std::shared_ptr tf_listener_; + + std::shared_ptr polygon_; + std::shared_ptr circle_; + + nav2::LifecycleNode::SharedPtr node_; +}; // Tester + +Tester::Tester() +{ + node_ = std::make_shared("test_node"); + + // Create shapes + polygon_ = std::make_shared(node_); + circle_ = std::make_shared(node_); + + // Transform buffer and listener initialization + tf_buffer_ = std::make_shared(node_->get_clock()); + tf_buffer_->setUsingDedicatedThread(true); // One-thread broadcasting-listening model + tf_listener_ = std::make_shared(*tf_buffer_); +} + +Tester::~Tester() +{ + polygon_.reset(); + circle_.reset(); + + node_.reset(); + + tf_listener_.reset(); + tf_buffer_.reset(); +} + +void Tester::setPolygonParams(const std::string & uuid) +{ + const std::string polygon_name(POLYGON_NAME); + + node_->declare_parameter( + polygon_name + ".type", rclcpp::ParameterValue("polygon")); + node_->set_parameter( + rclcpp::Parameter(polygon_name + ".type", "polygon")); + + node_->declare_parameter( + polygon_name + ".frame_id", rclcpp::ParameterValue(GLOBAL_FRAME_ID)); + node_->set_parameter( + rclcpp::Parameter(polygon_name + ".frame_id", GLOBAL_FRAME_ID)); + + node_->declare_parameter( + polygon_name + ".closed", rclcpp::ParameterValue(true)); + node_->set_parameter( + rclcpp::Parameter(polygon_name + ".closed", true)); + + node_->declare_parameter( + polygon_name + ".value", rclcpp::ParameterValue(nav2_util::OCC_GRID_OCCUPIED)); + node_->set_parameter( + rclcpp::Parameter(polygon_name + ".value", nav2_util::OCC_GRID_OCCUPIED)); + + node_->declare_parameter( + polygon_name + ".points", rclcpp::ParameterValue(POINTS)); + node_->set_parameter( + rclcpp::Parameter(polygon_name + ".points", POINTS)); + + if (!uuid.empty()) { + node_->declare_parameter( + polygon_name + ".uuid", rclcpp::ParameterValue(uuid)); + node_->set_parameter( + rclcpp::Parameter(polygon_name + ".uuid", uuid)); + } +} + +void Tester::setCircleParams(const std::string & uuid) +{ + const std::string circle_name(CIRCLE_NAME); + + node_->declare_parameter( + circle_name + ".type", rclcpp::ParameterValue("circle")); + node_->set_parameter( + rclcpp::Parameter(circle_name + ".type", "circle")); + + node_->declare_parameter( + circle_name + ".frame_id", rclcpp::ParameterValue(GLOBAL_FRAME_ID)); + node_->set_parameter( + rclcpp::Parameter(circle_name + ".frame_id", GLOBAL_FRAME_ID)); + + node_->declare_parameter( + circle_name + ".fill", rclcpp::ParameterValue(true)); + node_->set_parameter( + rclcpp::Parameter(circle_name + ".fill", true)); + + node_->declare_parameter( + circle_name + ".value", rclcpp::ParameterValue(nav2_util::OCC_GRID_OCCUPIED)); + node_->set_parameter( + rclcpp::Parameter(circle_name + ".value", nav2_util::OCC_GRID_OCCUPIED)); + + node_->declare_parameter( + circle_name + ".center", rclcpp::ParameterValue(CENTER)); + node_->set_parameter( + rclcpp::Parameter(circle_name + ".center", CENTER)); + + node_->declare_parameter( + circle_name + ".radius", rclcpp::ParameterValue(1.0)); + node_->set_parameter( + rclcpp::Parameter(circle_name + ".radius", 1.0)); + + if (!uuid.empty()) { + node_->declare_parameter( + circle_name + ".uuid", rclcpp::ParameterValue(uuid)); + node_->set_parameter( + rclcpp::Parameter(circle_name + ".uuid", uuid)); + } +} + +nav2_msgs::msg::PolygonObject::SharedPtr Tester::makePolygonObject( + const std::vector & uuid) +{ + nav2_msgs::msg::PolygonObject::SharedPtr po = std::make_shared(); + po->header.frame_id = GLOBAL_FRAME_ID; + if (uuid.size() == 16) { + // uuid could be optionally specified + std::array uuid_array; + std::copy(uuid.begin(), uuid.end(), uuid_array.begin()); + po->uuid.uuid = uuid_array; + } + geometry_msgs::msg::Point32 p; + p.x = 1.0; + p.y = 1.0; + po->points.push_back(p); + p.x = -1.0; + po->points.push_back(p); + p.y = -1.0; + po->points.push_back(p); + p.x = 1.0; + po->points.push_back(p); + po->closed = true; + po->value = nav2_util::OCC_GRID_OCCUPIED; + + return po; +} + +nav2_msgs::msg::CircleObject::SharedPtr Tester::makeCircleObject( + const std::vector & uuid) +{ + nav2_msgs::msg::CircleObject::SharedPtr co = std::make_shared(); + co->header.frame_id = GLOBAL_FRAME_ID; + if (uuid.size() == 16) { + // uuid could be optionally specified + std::array uuid_array; + std::copy(uuid.begin(), uuid.end(), uuid_array.begin()); + co->uuid.uuid = uuid_array; + } + co->center.x = 0.0; + co->center.y = 0.0; + co->radius = 1.0; + co->fill = true; + co->value = nav2_util::OCC_GRID_OCCUPIED; + + return co; +} + +void Tester::sendTransform() +{ + std::shared_ptr tf_broadcaster = + std::make_shared(node_); + + geometry_msgs::msg::TransformStamped transform; + + // global_frame -> shape_frame transform + transform.header.frame_id = GLOBAL_FRAME_ID; + transform.child_frame_id = SHAPE_FRAME_ID; + + transform.header.stamp = node_->now(); + transform.transform.translation.x = FRAME_SHIFT; + transform.transform.translation.y = FRAME_SHIFT; + transform.transform.translation.z = 0.0; + transform.transform.rotation.x = 0.0; + transform.transform.rotation.y = 0.0; + transform.transform.rotation.z = 0.0; + transform.transform.rotation.w = 1.0; + + tf_broadcaster->sendTransform(transform); +} + +nav_msgs::msg::OccupancyGrid::SharedPtr Tester::makeMap() +{ + nav_msgs::msg::OccupancyGrid::SharedPtr map = std::make_shared(); + map->header.frame_id = GLOBAL_FRAME_ID; + map->info.resolution = 0.1; + map->info.width = 40; + map->info.height = 40; + map->info.origin.position.x = -2.0; + map->info.origin.position.y = -2.0; + map->data = std::vector(400 * 400, nav2_util::OCC_GRID_FREE); + + return map; +} + +void Tester::verifyPolygonBorders(nav_msgs::msg::OccupancyGrid::SharedPtr map) +{ + // Map is expected to be as follows: + // 0 0 0 0 0 0 0 0 + // 0 0 x x x x 0 0 + // 0 0 x 0 0 0 0 0 + // 0 0 x 0 0 0 0 0 + // 0 0 x x x x 0 0 + // 0 0 0 0 0 0 0 0 + const unsigned int map_center_x = 19; + const unsigned int map_center_y = 19; + + for (unsigned int my = 0; my < map->info.height; my++) { + for (unsigned int mx = 0; mx < map->info.width; mx++) { + if (my == map_center_y - 10 && mx >= map_center_x - 10 && mx <= map_center_x + 10) { + // 1st border + ASSERT_EQ(map->data[my * map->info.width + mx], nav2_util::OCC_GRID_OCCUPIED); + } else if (mx == map_center_x - 10 && my >= map_center_y - 10 && my <= map_center_y + 10) { + // 2nd border + ASSERT_EQ(map->data[my * map->info.width + mx], nav2_util::OCC_GRID_OCCUPIED); + } else if (my == map_center_y + 10 && mx >= map_center_x - 10 && mx <= map_center_x + 10) { + // 3rd border + ASSERT_EQ(map->data[my * map->info.width + mx], nav2_util::OCC_GRID_OCCUPIED); + } else { + // Does not belong to any border + ASSERT_EQ(map->data[my * map->info.width + mx], nav2_util::OCC_GRID_FREE); + } + } + } +} + +void Tester::verifyCircleBorders(nav_msgs::msg::OccupancyGrid::SharedPtr map) +{ + // Circle center in cell coordinates + const double circle_center_x = 19.5; // 19 cell's origin + 0.5 center of cell + const double circle_center_y = 19.5; // 19 cell's origin + 0.5 center of cell + + double radius; + for (unsigned int my = 0; my < map->info.height; my++) { + for (unsigned int mx = 0; mx < map->info.width; mx++) { + if (map->data[my * map->info.width + mx] == nav2_util::OCC_GRID_OCCUPIED) { + radius = std::hypot(circle_center_x - mx, circle_center_y - my); + ASSERT_NEAR(radius, 10.0, 1.0); // Border drift no more than once cell + } + } + } +} + +void Tester::verifyMapEmpty(nav_msgs::msg::OccupancyGrid::SharedPtr map) +{ + for (unsigned int my = 0; my < map->info.height; my++) { + for (unsigned int mx = 0; mx < map->info.width; mx++) { + ASSERT_EQ(map->data[my * map->info.width + mx], nav2_util::OCC_GRID_FREE); + } + } +} + +void Tester::comparePolygonObjects( + nav2_msgs::msg::PolygonObject::SharedPtr p1, + nav2_msgs::msg::PolygonObject::SharedPtr p2) +{ + ASSERT_EQ(p1->header.frame_id, p2->header.frame_id); + ASSERT_EQ(p1->uuid.uuid, p2->uuid.uuid); + ASSERT_EQ(p1->points.size(), p2->points.size()); + for (unsigned int i = 0; i < p1->points.size(); i++) { + ASSERT_NEAR(p1->points[i].x, p2->points[i].x, EPSILON); + ASSERT_NEAR(p1->points[i].y, p2->points[i].y, EPSILON); + } + ASSERT_EQ(p1->closed, p2->closed); + ASSERT_EQ(p1->value, p2->value); +} + +void Tester::compareCircleObjects( + nav2_msgs::msg::CircleObject::SharedPtr c1, + nav2_msgs::msg::CircleObject::SharedPtr c2) +{ + ASSERT_EQ(c1->header.frame_id, c2->header.frame_id); + ASSERT_EQ(c1->uuid.uuid, c2->uuid.uuid); + ASSERT_NEAR(c1->center.x, c2->center.x, EPSILON); + ASSERT_NEAR(c1->center.y, c2->center.y, EPSILON); + ASSERT_NEAR(c1->radius, c2->radius, EPSILON); + ASSERT_EQ(c1->fill, c2->fill); + ASSERT_EQ(c1->value, c2->value); +} + +//---------- Polygon testcases ---------- + +TEST_F(Tester, testPolygonObtainParams) +{ + setPolygonParams("01010101-0101-0101-0101-010101010101"); + ASSERT_TRUE(polygon_->obtainParams(POLYGON_NAME)); + + ASSERT_EQ(polygon_->getType(), nav2_map_server::POLYGON); + ASSERT_EQ(polygon_->getValue(), nav2_util::OCC_GRID_OCCUPIED); + ASSERT_EQ(polygon_->getFrameID(), GLOBAL_FRAME_ID); + ASSERT_EQ(polygon_->isFill(), true); + ASSERT_EQ(polygon_->getUUID(), "01010101-0101-0101-0101-010101010101"); +} + +TEST_F(Tester, testPolygonObtainIncorrectParams) +{ + // Setting polygon parameters w/o points + const std::string polygon_name(POLYGON_NAME); + + node_->declare_parameter( + polygon_name + ".type", rclcpp::ParameterValue("polygon")); + node_->set_parameter( + rclcpp::Parameter(polygon_name + ".type", "polygon")); + + node_->declare_parameter( + polygon_name + ".frame_id", rclcpp::ParameterValue(GLOBAL_FRAME_ID)); + node_->set_parameter( + rclcpp::Parameter(polygon_name + ".frame_id", GLOBAL_FRAME_ID)); + + node_->declare_parameter( + polygon_name + ".closed", rclcpp::ParameterValue(true)); + node_->set_parameter( + rclcpp::Parameter(polygon_name + ".closed", true)); + + node_->declare_parameter( + polygon_name + ".value", rclcpp::ParameterValue(nav2_util::OCC_GRID_OCCUPIED)); + node_->set_parameter( + rclcpp::Parameter(polygon_name + ".value", nav2_util::OCC_GRID_OCCUPIED)); + + node_->declare_parameter( + polygon_name + ".points", rclcpp::PARAMETER_DOUBLE_ARRAY); + + // Check that points is mandatory parameter for the polygon + ASSERT_FALSE(polygon_->obtainParams(polygon_name)); + + // Setting incorrect number of points + node_->set_parameter( + rclcpp::Parameter(polygon_name + ".points", std::vector{1.0, 2.0, 3.0})); + ASSERT_FALSE(polygon_->obtainParams(polygon_name)); + node_->set_parameter( + rclcpp::Parameter(polygon_name + ".points", POINTS)); + + // Setting incorrect UUID + node_->declare_parameter( + polygon_name + ".uuid", rclcpp::ParameterValue("incorrect-uuid")); + node_->set_parameter( + rclcpp::Parameter(polygon_name + ".uuid", "incorrect-uuid")); + ASSERT_FALSE(polygon_->obtainParams(polygon_name)); +} + +TEST_F(Tester, testPolygonSetParams) +{ + nav2_msgs::msg::PolygonObject::SharedPtr po = makePolygonObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2}); + + // Check that parameters were set correctly + ASSERT_TRUE(polygon_->setParams(po)); + ASSERT_EQ(polygon_->getType(), nav2_map_server::POLYGON); + ASSERT_EQ(polygon_->getValue(), nav2_util::OCC_GRID_OCCUPIED); + ASSERT_EQ(polygon_->getFrameID(), GLOBAL_FRAME_ID); + ASSERT_EQ(polygon_->isFill(), true); + ASSERT_EQ(polygon_->getUUID(), "01010101-0101-0101-0101-020202020202"); + + // Verify that getting the parameters also works correctly + nav2_msgs::msg::PolygonObject::SharedPtr params = polygon_->getParams(); + comparePolygonObjects(params, po); +} + +TEST_F(Tester, testPolygonSetIncorrectParams) +{ + nav2_msgs::msg::PolygonObject::SharedPtr po = makePolygonObject( + std::vector()); + // Setting incorrect number of vertices + po->points.resize(2); + + // And check that it is failed to set these parameters + ASSERT_FALSE(polygon_->setParams(po)); +} + +TEST_F(Tester, testPolygonBoundaries) +{ + setPolygonParams(""); + ASSERT_TRUE(polygon_->obtainParams(POLYGON_NAME)); + + double min_x, min_y, max_x, max_y; + polygon_->getBoundaries(min_x, min_y, max_x, max_y); + + ASSERT_NEAR(min_x, -1.0, EPSILON); + ASSERT_NEAR(min_y, -1.0, EPSILON); + ASSERT_NEAR(max_x, 1.0, EPSILON); + ASSERT_NEAR(max_y, 1.0, EPSILON); +} + +TEST_F(Tester, testPolygonPoints) +{ + setPolygonParams(""); + ASSERT_TRUE(polygon_->obtainParams(POLYGON_NAME)); + + ASSERT_TRUE(polygon_->isPointInside(0.0, 0.0)); + ASSERT_FALSE(polygon_->isPointInside(-2.0, -2.0)); + ASSERT_FALSE(polygon_->isPointInside(2.0, 2.0)); +} + +TEST_F(Tester, testPolygonBorders) +{ + setPolygonParams(""); + node_->set_parameter( + rclcpp::Parameter(std::string(POLYGON_NAME) + ".closed", false)); + ASSERT_TRUE(polygon_->obtainParams(POLYGON_NAME)); + + nav_msgs::msg::OccupancyGrid::SharedPtr map = makeMap(); + + polygon_->putBorders(map, nav2_map_server::OverlayType::OVERLAY_SEQ); + + verifyPolygonBorders(map); +} + +TEST_F(Tester, testPolygonBordersOutOfBoundaries) +{ + setPolygonParams(""); + node_->set_parameter( + rclcpp::Parameter(std::string(POLYGON_NAME) + ".closed", false)); + // Set polygon to be out of map + node_->set_parameter( + rclcpp::Parameter( + std::string(POLYGON_NAME) + ".points", + std::vector{5.0, 5.0, 6.0, 5.0, 5.0, 6.0})); + ASSERT_TRUE(polygon_->obtainParams(POLYGON_NAME)); + + nav_msgs::msg::OccupancyGrid::SharedPtr map = makeMap(); + + polygon_->putBorders(map, nav2_map_server::OverlayType::OVERLAY_SEQ); + + verifyMapEmpty(map); +} + +TEST_F(Tester, testPolygonBordersOnePointInsideBoundaries) +{ + setPolygonParams(""); + node_->set_parameter( + rclcpp::Parameter(std::string(POLYGON_NAME) + ".closed", false)); + // Now, set the first point inside the map and the others out of the map + node_->set_parameter( + rclcpp::Parameter( + std::string(POLYGON_NAME) + ".points", + std::vector{0.5, 0.5, 6.0, 5.0, 5.0, 6.0})); + ASSERT_TRUE(polygon_->obtainParams(POLYGON_NAME)); + + nav_msgs::msg::OccupancyGrid::SharedPtr map = makeMap(); + + polygon_->putBorders(map, nav2_map_server::OverlayType::OVERLAY_SEQ); + + verifyMapEmpty(map); +} + +TEST_F(Tester, testPolygonDifferentFrame) +{ + setPolygonParams(""); + // Change shape's frame ID to differ from global one + node_->set_parameter( + rclcpp::Parameter(std::string(POLYGON_NAME) + ".frame_id", SHAPE_FRAME_ID)); + ASSERT_TRUE(polygon_->obtainParams(POLYGON_NAME)); + + sendTransform(); + + // Check that shape coordinates are not transformed + geometry_msgs::msg::Polygon::SharedPtr poly = polygon_->getPoly(); + ASSERT_NEAR(poly->points[0].x, 1.0, EPSILON); + ASSERT_NEAR(poly->points[0].y, 1.0, EPSILON); + ASSERT_NEAR(poly->points[1].x, -1.0, EPSILON); + ASSERT_NEAR(poly->points[1].y, 1.0, EPSILON); + ASSERT_NEAR(poly->points[2].x, -1.0, EPSILON); + ASSERT_NEAR(poly->points[2].y, -1.0, EPSILON); + ASSERT_NEAR(poly->points[3].x, 1.0, EPSILON); + ASSERT_NEAR(poly->points[3].y, -1.0, EPSILON); + + // Transform shape coordinates to global frame + ASSERT_TRUE(polygon_->toFrame(GLOBAL_FRAME_ID, tf_buffer_, 1.0)); + + // Verify that shape coordinates were transformed to global frame successfully + poly = polygon_->getPoly(); + ASSERT_NEAR(poly->points[0].x, 1.0 + FRAME_SHIFT, EPSILON); + ASSERT_NEAR(poly->points[0].y, 1.0 + FRAME_SHIFT, EPSILON); + ASSERT_NEAR(poly->points[1].x, -1.0 + FRAME_SHIFT, EPSILON); + ASSERT_NEAR(poly->points[1].y, 1.0 + FRAME_SHIFT, EPSILON); + ASSERT_NEAR(poly->points[2].x, -1.0 + FRAME_SHIFT, EPSILON); + ASSERT_NEAR(poly->points[2].y, -1.0 + FRAME_SHIFT, EPSILON); + ASSERT_NEAR(poly->points[3].x, 1.0 + FRAME_SHIFT, EPSILON); + ASSERT_NEAR(poly->points[3].y, -1.0 + FRAME_SHIFT, EPSILON); + + // Try to transform to incorrect frame + ASSERT_FALSE(polygon_->toFrame("incorrect_frame", tf_buffer_, 0.1)); +} + +//---------- Circles testcases ---------- + +TEST_F(Tester, testCircleObtainParams) +{ + setCircleParams("01010101-0101-0101-0101-010101010101"); + ASSERT_TRUE(circle_->obtainParams(CIRCLE_NAME)); + + ASSERT_EQ(circle_->getType(), nav2_map_server::CIRCLE); + ASSERT_EQ(circle_->getValue(), nav2_util::OCC_GRID_OCCUPIED); + ASSERT_EQ(circle_->getFrameID(), GLOBAL_FRAME_ID); + ASSERT_EQ(circle_->isFill(), true); + ASSERT_EQ(circle_->getUUID(), "01010101-0101-0101-0101-010101010101"); +} + +TEST_F(Tester, testCircleObtainIncorrectParams) +{ + const std::string circle_name(CIRCLE_NAME); + + // Setting circle parameters w/o center and radius + node_->declare_parameter( + circle_name + ".type", rclcpp::ParameterValue("circle")); + node_->set_parameter( + rclcpp::Parameter(circle_name + ".type", "circle")); + + node_->declare_parameter( + circle_name + ".frame_id", rclcpp::ParameterValue(GLOBAL_FRAME_ID)); + node_->set_parameter( + rclcpp::Parameter(circle_name + ".frame_id", GLOBAL_FRAME_ID)); + + node_->declare_parameter( + circle_name + ".fill", rclcpp::ParameterValue(true)); + node_->set_parameter( + rclcpp::Parameter(circle_name + ".fill", true)); + + node_->declare_parameter( + circle_name + ".value", rclcpp::ParameterValue(nav2_util::OCC_GRID_OCCUPIED)); + node_->set_parameter( + rclcpp::Parameter(circle_name + ".value", nav2_util::OCC_GRID_OCCUPIED)); + + node_->declare_parameter( + circle_name + ".center", rclcpp::PARAMETER_DOUBLE_ARRAY); + node_->declare_parameter( + circle_name + ".radius", rclcpp::PARAMETER_DOUBLE); + + // Check that center and radius are mandatory parameter for the circle + ASSERT_FALSE(circle_->obtainParams(circle_name)); + node_->set_parameter( + rclcpp::Parameter(circle_name + ".center", CENTER)); + + // Setting incorrect radius + node_->set_parameter( + rclcpp::Parameter(circle_name + ".radius", -1.0)); + ASSERT_FALSE(circle_->obtainParams(circle_name)); + node_->set_parameter( + rclcpp::Parameter(circle_name + ".radius", 1.0)); + + // Setting incorrect center format + node_->set_parameter( + rclcpp::Parameter(circle_name + ".center", std::vector{0.0})); + ASSERT_FALSE(circle_->obtainParams(circle_name)); + node_->set_parameter( + rclcpp::Parameter(circle_name + ".center", CENTER)); + + // Setting incorrect UUID + node_->declare_parameter( + circle_name + ".uuid", rclcpp::ParameterValue("incorrect-uuid")); + node_->set_parameter( + rclcpp::Parameter(circle_name + ".uuid", "incorrect-uuid")); + ASSERT_FALSE(circle_->obtainParams(circle_name)); +} + +TEST_F(Tester, testCircleSetParams) +{ + nav2_msgs::msg::CircleObject::SharedPtr co = makeCircleObject( + std::vector{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2}); + + // Check that parameters were set correctly + ASSERT_TRUE(circle_->setParams(co)); + ASSERT_EQ(circle_->getType(), nav2_map_server::CIRCLE); + ASSERT_EQ(circle_->getValue(), nav2_util::OCC_GRID_OCCUPIED); + ASSERT_EQ(circle_->getFrameID(), GLOBAL_FRAME_ID); + ASSERT_EQ(circle_->isFill(), true); + ASSERT_EQ(circle_->getUUID(), "01010101-0101-0101-0101-020202020202"); + + // Verify that getting the parameters also works correctly + nav2_msgs::msg::CircleObject::SharedPtr params = circle_->getParams(); + compareCircleObjects(params, co); +} + +TEST_F(Tester, testCircleSetIncorrectParams) +{ + nav2_msgs::msg::CircleObject::SharedPtr co = makeCircleObject( + std::vector()); + // Setting incorrect radius + co->radius = -1.0; + + // And check that it is failed to set these parameters + ASSERT_FALSE(circle_->setParams(co)); +} + +TEST_F(Tester, testCircleBoundaries) +{ + setCircleParams(""); + ASSERT_TRUE(circle_->obtainParams(CIRCLE_NAME)); + + double min_x, min_y, max_x, max_y; + circle_->getBoundaries(min_x, min_y, max_x, max_y); + + ASSERT_NEAR(min_x, -1.0, EPSILON); + ASSERT_NEAR(min_y, -1.0, EPSILON); + ASSERT_NEAR(max_x, 1.0, EPSILON); + ASSERT_NEAR(max_y, 1.0, EPSILON); +} + +TEST_F(Tester, testCirclePoints) +{ + setCircleParams(""); + ASSERT_TRUE(circle_->obtainParams(CIRCLE_NAME)); + + ASSERT_TRUE(circle_->isPointInside(0.0, 0.0)); + ASSERT_FALSE(circle_->isPointInside(-1.0, -1.0)); + ASSERT_FALSE(circle_->isPointInside(1.0, 1.0)); +} + +TEST_F(Tester, testCircleBorders) +{ + setCircleParams(""); + node_->set_parameter( + rclcpp::Parameter(std::string(CIRCLE_NAME) + ".fill", false)); + ASSERT_TRUE(circle_->obtainParams(CIRCLE_NAME)); + + nav_msgs::msg::OccupancyGrid::SharedPtr map = makeMap(); + + circle_->putBorders(map, nav2_map_server::OverlayType::OVERLAY_SEQ); + + verifyCircleBorders(map); +} + +TEST_F(Tester, testCircleBordersOutOfBoundaries) +{ + setCircleParams(""); + node_->set_parameter( + rclcpp::Parameter(std::string(CIRCLE_NAME) + ".fill", false)); + // Set circle to be out of map + node_->set_parameter( + rclcpp::Parameter( + std::string(CIRCLE_NAME) + ".center", + std::vector{5.0, 5.0})); + ASSERT_TRUE(circle_->obtainParams(CIRCLE_NAME)); + + nav_msgs::msg::OccupancyGrid::SharedPtr map = makeMap(); + + circle_->putBorders(map, nav2_map_server::OverlayType::OVERLAY_SEQ); + + verifyMapEmpty(map); +} + +TEST_F(Tester, testCircleBordersOutsideMap) +{ + setCircleParams(""); + node_->set_parameter( + rclcpp::Parameter(std::string(CIRCLE_NAME) + ".fill", false)); + // Set circle to be out of map + node_->set_parameter( + rclcpp::Parameter( + std::string(CIRCLE_NAME) + ".center", + std::vector{-3.0, -3.0})); + ASSERT_TRUE(circle_->obtainParams(CIRCLE_NAME)); + + nav_msgs::msg::OccupancyGrid::SharedPtr map = makeMap(); + + circle_->putBorders(map, nav2_map_server::OverlayType::OVERLAY_SEQ); + + verifyMapEmpty(map); +} + +TEST_F(Tester, testCircleDifferentFrame) +{ + setCircleParams(""); + // Change shape's frame ID to differ from global one + node_->set_parameter( + rclcpp::Parameter(std::string(CIRCLE_NAME) + ".frame_id", SHAPE_FRAME_ID)); + ASSERT_TRUE(circle_->obtainParams(CIRCLE_NAME)); + + sendTransform(); + + // Check that shape coordinates are not transformed + geometry_msgs::msg::Point32::SharedPtr center = circle_->getCenter(); + ASSERT_NEAR(center->x, 0.0, EPSILON); + ASSERT_NEAR(center->y, 0.0, EPSILON); + // Transform shape coordinates to global frame + ASSERT_TRUE(circle_->toFrame(GLOBAL_FRAME_ID, tf_buffer_, 1.0)); + + // Verify that shape coordinates were transformed to global frame successfully + center = circle_->getCenter(); + ASSERT_NEAR(center->x, FRAME_SHIFT, EPSILON); + ASSERT_NEAR(center->y, FRAME_SHIFT, EPSILON); + + // Try to transform to incorrect frame + ASSERT_FALSE(circle_->toFrame("incorrect_frame", tf_buffer_, 0.1)); +} + +int main(int argc, char ** argv) +{ + // Initialize the system + testing::InitGoogleTest(&argc, argv); + rclcpp::init(argc, argv); + + // Actual testing + bool test_result = RUN_ALL_TESTS(); + + // Shutdown + rclcpp::shutdown(); + + return test_result; +} diff --git a/nav2_msgs/CMakeLists.txt b/nav2_msgs/CMakeLists.txt index 14d1327508b..41311173486 100644 --- a/nav2_msgs/CMakeLists.txt +++ b/nav2_msgs/CMakeLists.txt @@ -11,6 +11,7 @@ find_package(rosidl_default_generators REQUIRED) find_package(std_msgs REQUIRED) find_package(geographic_msgs) find_package(action_msgs REQUIRED) +find_package(unique_identifier_msgs REQUIRED) nav2_package() @@ -41,6 +42,8 @@ rosidl_generate_interfaces(${PROJECT_NAME} "msg/Trajectory.msg" "msg/TrajectoryPoint.msg" "srv/GetCosts.srv" + "msg/PolygonObject.msg" + "msg/CircleObject.msg" "srv/GetCostmap.srv" "srv/IsPathValid.srv" "srv/ClearCostmapExceptRegion.srv" @@ -52,6 +55,9 @@ rosidl_generate_interfaces(${PROJECT_NAME} "srv/SaveMap.srv" "srv/SetInitialPose.srv" "srv/ReloadDockDatabase.srv" + "srv/AddShapes.srv" + "srv/RemoveShapes.srv" + "srv/GetShapes.srv" "srv/SetRouteGraph.srv" "srv/DynamicEdges.srv" "action/AssistedTeleop.action" @@ -72,7 +78,7 @@ rosidl_generate_interfaces(${PROJECT_NAME} "action/UndockRobot.action" "action/ComputeRoute.action" "action/ComputeAndTrackRoute.action" - DEPENDENCIES builtin_interfaces geometry_msgs std_msgs action_msgs nav_msgs geographic_msgs + DEPENDENCIES builtin_interfaces geometry_msgs std_msgs action_msgs nav_msgs geographic_msgs unique_identifier_msgs ) ament_export_dependencies(rosidl_default_runtime) diff --git a/nav2_msgs/msg/CircleObject.msg b/nav2_msgs/msg/CircleObject.msg new file mode 100644 index 00000000000..2aa64adab62 --- /dev/null +++ b/nav2_msgs/msg/CircleObject.msg @@ -0,0 +1,6 @@ +std_msgs/Header header +unique_identifier_msgs/UUID uuid +geometry_msgs/Point32 center +float32 radius +bool fill +int8 value diff --git a/nav2_msgs/msg/PolygonObject.msg b/nav2_msgs/msg/PolygonObject.msg new file mode 100644 index 00000000000..b5c9721c629 --- /dev/null +++ b/nav2_msgs/msg/PolygonObject.msg @@ -0,0 +1,5 @@ +std_msgs/Header header +unique_identifier_msgs/UUID uuid +geometry_msgs/Point32[] points +bool closed +int8 value diff --git a/nav2_msgs/package.xml b/nav2_msgs/package.xml index 61e84addc37..33de880662e 100644 --- a/nav2_msgs/package.xml +++ b/nav2_msgs/package.xml @@ -21,6 +21,7 @@ action_msgs nav_msgs geographic_msgs + unique_identifier_msgs rosidl_interface_packages diff --git a/nav2_msgs/srv/AddShapes.srv b/nav2_msgs/srv/AddShapes.srv new file mode 100644 index 00000000000..1b3b5753874 --- /dev/null +++ b/nav2_msgs/srv/AddShapes.srv @@ -0,0 +1,6 @@ +# Add/update vector objects on map + +CircleObject[] circles +PolygonObject[] polygons +--- +bool success diff --git a/nav2_msgs/srv/GetShapes.srv b/nav2_msgs/srv/GetShapes.srv new file mode 100644 index 00000000000..79b7d6c431c --- /dev/null +++ b/nav2_msgs/srv/GetShapes.srv @@ -0,0 +1,5 @@ +# Get vector objects which are now being applied on map + +--- +CircleObject[] circles +PolygonObject[] polygons diff --git a/nav2_msgs/srv/RemoveShapes.srv b/nav2_msgs/srv/RemoveShapes.srv new file mode 100644 index 00000000000..a001f9360ef --- /dev/null +++ b/nav2_msgs/srv/RemoveShapes.srv @@ -0,0 +1,6 @@ +# Remove vector objects from map + +bool all_objects +unique_identifier_msgs/UUID[] uuids +--- +bool success diff --git a/nav2_util/include/nav2_util/geometry_utils.hpp b/nav2_util/include/nav2_util/geometry_utils.hpp index 3a56cb5feea..d67f17e3c7c 100644 --- a/nav2_util/include/nav2_util/geometry_utils.hpp +++ b/nav2_util/include/nav2_util/geometry_utils.hpp @@ -188,6 +188,49 @@ inline int find_next_matching_goal_in_waypoint_statuses( return itr - waypoint_statuses.begin(); } +/** + * @brief Checks if point is inside the polygon + * @param px X-coordinate of the given point to check + * @param py Y-coordinate of the given point to check + * @param polygon Polygon to check if the point is inside + * @return True if given point is inside polygon, otherwise false + */ +template +inline bool isPointInsidePolygon( + const double px, const double py, const std::vector & polygon) +{ + // Adaptation of Shimrat, Moshe. "Algorithm 112: position of point relative to polygon." + // Communications of the ACM 5.8 (1962): 434. + // Implementation of ray crossings algorithm for point in polygon task solving. + // Y coordinate is fixed. Moving the ray on X+ axis starting from given point. + // Odd number of intersections with polygon boundaries means the point is inside polygon. + const int points_num = polygon.size(); + int i, j; // Polygon vertex iterators + bool res = false; // Final result, initialized with already inverted value + + // Starting from the edge where the last point of polygon is connected to the first + i = points_num - 1; + for (j = 0; j < points_num; j++) { + // Checking the edge only if given point is between edge boundaries by Y coordinates. + // One of the condition should contain equality in order to exclude the edges + // parallel to X+ ray. + if ((py <= polygon[i].y) == (py > polygon[j].y)) { + // Calculating the intersection coordinate of X+ ray + const double x_inter = polygon[i].x + + (py - polygon[i].y) * + (polygon[j].x - polygon[i].x) / + (polygon[j].y - polygon[i].y); + // If intersection with checked edge is greater than point x coordinate, + // inverting the result + if (x_inter > px) { + res = !res; + } + } + i = j; + } + return res; +} + } // namespace geometry_utils } // namespace nav2_util diff --git a/nav2_util/include/nav2_util/occ_grid_utils.hpp b/nav2_util/include/nav2_util/occ_grid_utils.hpp new file mode 100644 index 00000000000..26bb8740f2b --- /dev/null +++ b/nav2_util/include/nav2_util/occ_grid_utils.hpp @@ -0,0 +1,100 @@ +// Copyright (c) 2008, 2013, Willow Garage, Inc. +// Copyright (c) 2023 Samsung R&D Institute Russia +// All rights reserved. +// +// Software License Agreement (BSD License 2.0) +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of the nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +// COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// Author: Eitan Marder-Eppstein +// David V. Lu!! +// Alexey Merzlyakov + +#ifndef NAV2_UTIL__OCC_GRID_UTILS_HPP_ +#define NAV2_UTIL__OCC_GRID_UTILS_HPP_ + +#include "nav_msgs/msg/occupancy_grid.hpp" + +namespace nav2_util +{ + +/** + * @brief: Convert from world coordinates to map coordinates. + Similar to Costmap2D::worldToMap() method but works directly with OccupancyGrid-s. + * @param map OccupancyGrid map on which to convert + * @param wx The x world coordinate + * @param wy The y world coordinate + * @param mx Will be set to the associated map x coordinate + * @param my Will be set to the associated map y coordinate + * @return True if the conversion was successful (legal bounds) false otherwise + */ +inline bool worldToMap( + nav_msgs::msg::OccupancyGrid::ConstSharedPtr map, + const double wx, const double wy, unsigned int & mx, unsigned int & my) +{ + const double origin_x = map->info.origin.position.x; + const double origin_y = map->info.origin.position.y; + const double resolution = map->info.resolution; + const unsigned int size_x = map->info.width; + const unsigned int size_y = map->info.height; + + if (wx < origin_x || wy < origin_y) { + return false; + } + + mx = static_cast((wx - origin_x) / resolution); + my = static_cast((wy - origin_y) / resolution); + if (mx >= size_x || my >= size_y) { + return false; + } + + return true; +} + +/** + * @brief Convert from map coordinates to world coordinates + * @param mx The x map coordinate + * @param my The y map coordinate + * @param wx Will be set to the associated world x coordinate + * @param wy Will be set to the associated world y coordinate + */ +inline void mapToWorld( + nav_msgs::msg::OccupancyGrid::ConstSharedPtr map, + const unsigned int mx, const unsigned int my, double & wx, double & wy) +{ + const double origin_x = map->info.origin.position.x; + const double origin_y = map->info.origin.position.y; + const double resolution = map->info.resolution; + + wx = origin_x + (mx + 0.5) * resolution; + wy = origin_y + (my + 0.5) * resolution; +} + +} // namespace nav2_util + +#endif // NAV2_UTIL__OCC_GRID_UTILS_HPP_ diff --git a/nav2_util/include/nav2_util/raytrace_line_2d.hpp b/nav2_util/include/nav2_util/raytrace_line_2d.hpp new file mode 100644 index 00000000000..b44d1e79a8d --- /dev/null +++ b/nav2_util/include/nav2_util/raytrace_line_2d.hpp @@ -0,0 +1,146 @@ +// Copyright (c) 2008, 2013, Willow Garage, Inc. +// All rights reserved. +// +// Software License Agreement (BSD License 2.0) +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Willow Garage, Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +// COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// Author: Eitan Marder-Eppstein +// David V. Lu!! + +#ifndef NAV2_UTIL__RAYTRACE_LINE_2D_HPP_ +#define NAV2_UTIL__RAYTRACE_LINE_2D_HPP_ + +#include +#include +#include + +namespace nav2_util +{ + +/** + * @brief get the sign of an int + */ +inline int sign(int x) +{ + return x > 0 ? 1 : -1; +} + +/** + * @brief A 2D implementation of Bresenham's raytracing algorithm... + * applies an action at each step + */ +template +inline void bresenham2D( + ActionType at, unsigned int abs_da, unsigned int abs_db, int error_b, + int offset_a, int offset_b, unsigned int offset, + unsigned int max_length) +{ + unsigned int end = std::min(max_length, abs_da); + for (unsigned int i = 0; i < end; ++i) { + at(offset); + offset += offset_a; + error_b += abs_db; + if ((unsigned int)error_b >= abs_da) { + offset += offset_b; + error_b -= abs_da; + } + } + at(offset); +} + +/** + * @brief Raytrace a line and apply some action at each step + * @param at The action to take... a functor + * @param x0 The starting x coordinate + * @param y0 The starting y coordinate + * @param x1 The ending x coordinate + * @param y1 The ending y coordinate + * @param step_x OX-step on map + * @param max_length The maximum desired length of the segment... + * allows you to not go all the way to the endpoint + * @param min_length The minimum desired length of the segment + */ +template +inline void raytraceLine( + ActionType at, unsigned int x0, unsigned int y0, unsigned int x1, + unsigned int y1, unsigned int step_x, + unsigned int max_length = UINT_MAX, unsigned int min_length = 0) +{ + int dx_full = x1 - x0; + int dy_full = y1 - y0; + + // we need to chose how much to scale our dominant dimension, + // based on the maximum length of the line + double dist = std::hypot(dx_full, dy_full); + if (dist < min_length) { + return; + } + + unsigned int min_x0, min_y0; + if (dist > 0.0) { + // Adjust starting point and offset to start from min_length distance + min_x0 = (unsigned int)(x0 + dx_full / dist * min_length); + min_y0 = (unsigned int)(y0 + dy_full / dist * min_length); + } else { + // dist can be 0 if [x0, y0]==[x1, y1]. + // In this case only this cell should be processed. + min_x0 = x0; + min_y0 = y0; + } + unsigned int offset = min_y0 * step_x + min_x0; + + int dx = x1 - min_x0; + int dy = y1 - min_y0; + + unsigned int abs_dx = abs(dx); + unsigned int abs_dy = abs(dy); + + int offset_dx = sign(dx); + int offset_dy = sign(dy) * step_x; + + double scale = (dist == 0.0) ? 1.0 : std::min(1.0, max_length / dist); + // if x is dominant + if (abs_dx >= abs_dy) { + int error_y = abs_dx / 2; + + bresenham2D( + at, abs_dx, abs_dy, error_y, offset_dx, offset_dy, offset, (unsigned int)(scale * abs_dx)); + return; + } + + // otherwise y is dominant + int error_x = abs_dy / 2; + + bresenham2D( + at, abs_dy, abs_dx, error_x, offset_dy, offset_dx, offset, (unsigned int)(scale * abs_dy)); +} + +} // namespace nav2_util + +#endif // NAV2_UTIL__RAYTRACE_LINE_2D_HPP_ diff --git a/nav2_util/test/CMakeLists.txt b/nav2_util/test/CMakeLists.txt index 09d7d4a38c0..c8447eaab6e 100644 --- a/nav2_util/test/CMakeLists.txt +++ b/nav2_util/test/CMakeLists.txt @@ -1,3 +1,5 @@ +add_subdirectory(regression) + ament_add_gtest(test_execution_timer test_execution_timer.cpp) target_link_libraries(test_execution_timer ${library_name}) diff --git a/nav2_util/test/regression/CMakeLists.txt b/nav2_util/test/regression/CMakeLists.txt new file mode 100644 index 00000000000..f216fbe7751 --- /dev/null +++ b/nav2_util/test/regression/CMakeLists.txt @@ -0,0 +1,3 @@ +# Bresenham2D corner cases test +ament_add_gtest(map_bresenham_2d map_bresenham_2d.cpp) +target_link_libraries(map_bresenham_2d ${library_name}) diff --git a/nav2_util/test/regression/map_bresenham_2d.cpp b/nav2_util/test/regression/map_bresenham_2d.cpp new file mode 100644 index 00000000000..220962948e5 --- /dev/null +++ b/nav2_util/test/regression/map_bresenham_2d.cpp @@ -0,0 +1,170 @@ +// Copyright (c) 2022 Samsung R&D Institute Russia +// All rights reserved. +// +// Software License Agreement (BSD License 2.0) +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of the nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +// COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// Author: Alexey Merzlyakov + +#include + +#include + +#include "nav2_util/raytrace_line_2d.hpp" + +// MapAction - is a functor class used to cover raytraceLine algorithm. +// It contains char map inside, which is an abstract one and not related +// to any concrete representation (like Costmap2D or OccupancyGrid). +class MapAction +{ +public: + explicit MapAction( + char * map, unsigned int size, char mark_val = 100) + : map_(map), size_(size), mark_val_(mark_val) + { + } + + inline void operator()(unsigned int off) + { + ASSERT_TRUE(off < size_); + map_[off] = mark_val_; + } + + inline unsigned int get(unsigned int off) + { + return map_[off]; + } + +private: + char * map_; + unsigned int size_; + char mark_val_; +}; + +class MapTest +{ +public: + MapTest( + unsigned int size_x, unsigned int size_y, + char default_val = 0) + : size_x_(size_x), size_y_(size_y) + { + map_ = new char[size_x * size_y]; + memset(map_, default_val, size_x * size_y); + } + + char * getMap() + { + return map_; + } + + unsigned int getSize() + { + return size_x_ * size_y_; + } + + void raytraceLine( + MapAction ma, unsigned int x0, unsigned int y0, unsigned int x1, + unsigned int y1, + unsigned int max_length = UINT_MAX, unsigned int min_length = 0) + { + nav2_util::raytraceLine(ma, x0, y0, x1, y1, size_x_, max_length, min_length); + } + +private: + char * map_; + unsigned int size_x_, size_y_; +}; + +TEST(map_2d, bresenham2DBoundariesCheck) +{ + const unsigned int sz_x = 60; + const unsigned int sz_y = 60; + const unsigned int max_length = 60; + const unsigned int min_length = 6; + MapTest mt(sz_x, sz_y); + MapAction ma(mt.getMap(), mt.getSize()); + + // Initial point - some asymmetrically standing point in order to cover most corner cases + const unsigned int x0 = 2; + const unsigned int y0 = 4; + // (x1, y1) point will move + unsigned int x1, y1; + + // Running on (x, 0) edge + y1 = 0; + for (x1 = 0; x1 < sz_x; x1++) { + mt.raytraceLine(ma, x0, y0, x1, y1, max_length, min_length); + } + + // Running on (x, sz_y) edge + y1 = sz_y - 1; + for (x1 = 0; x1 < sz_x; x1++) { + mt.raytraceLine(ma, x0, y0, x1, y1, max_length, min_length); + } + + // Running on (0, y) edge + x1 = 0; + for (y1 = 0; y1 < sz_y; y1++) { + mt.raytraceLine(ma, x0, y0, x1, y1, max_length, min_length); + } + + // Running on (sz_x, y) edge + x1 = sz_x - 1; + for (y1 = 0; y1 < sz_y; y1++) { + mt.raytraceLine(ma, x0, y0, x1, y1, max_length, min_length); + } +} + +TEST(map_2d, bresenham2DSamePoint) +{ + const unsigned int sz_x = 60; + const unsigned int sz_y = 60; + const unsigned int max_length = 60; + const unsigned int min_length = 0; + MapTest mt(sz_x, sz_y, 0.1); + MapAction ma(mt.getMap(), mt.getSize()); + + // Initial point + const double x0 = 2; + const double y0 = 4; + + unsigned int offset = y0 * sz_x + x0; + char val_before = ma.get(offset); + // Same point to check + mt.raytraceLine(ma, x0, y0, x0, y0, max_length, min_length); + char val_after = ma.get(offset); + ASSERT_FALSE(val_before == val_after); +} + +int main(int argc, char ** argv) +{ + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From f5b12f3f8dc5e0a4db039627f0c70b238611b630 Mon Sep 17 00:00:00 2001 From: Jakubach Date: Sun, 4 Jan 2026 14:02:29 +0100 Subject: [PATCH 51/51] Nav2 update --- nav2_behavior_tree/CMakeLists.txt | 3 + .../remove_in_collision_goals_action.hpp | 4 + .../action/remove_passed_goals_action.hpp | 5 + .../condition/is_path_valid_condition.hpp | 2 +- .../remove_in_collision_goals_action.cpp | 52 +- .../action/remove_passed_goals_action.cpp | 63 +- nav2_bringup/launch/cbf_supervisor_launch.py | 27 + nav2_bringup/launch/tb4_simulation_launch.py | 2 +- nav2_bringup/params/nav2_params.yaml | 865 +++- ...hrough_poses_w_replanning_and_recovery.xml | 54 - ...h_poses_w_replanning_and_recovery_copy.xml | 157 + ...ning_and_recovery_copy_test_replanning.xml | 160 + ...s_w_replanning_and_recovery_if_invalid.xml | 181 + ...ng_and_recovery_if_invalid_copy copy 2.xml | 134 + ...ning_and_recovery_if_invalid_copy copy.xml | 88 + ...eplanning_and_recovery_if_invalid_copy.xml | 157 + .../footprint_collision_checker copy.hpp | 94 + .../nav2_costmap_2d/obstacle_layer.hpp | 1 + .../include/nav2_costmap_2d/static_layer.hpp | 1 + nav2_costmap_2d/plugins/obstacle_layer.cpp | 11 +- nav2_costmap_2d/plugins/static_layer.cpp | 13 +- .../src/footprint_collision_checker copy.cpp | 268 ++ .../footprint_collision_checker_test copy.cpp | 793 ++++ .../opennav_docking/src/docking_server.cpp | 1 + nav2_mppi_controller/CMakeLists copy.txt | 199 + nav2_mppi_controller/CMakeLists.txt | 3 + nav2_mppi_controller/critics.xml | 6 + .../collision_checker.hpp | 148 + .../nav2_mppi_controller/controller.hpp | 15 +- .../critic_data copy 2.hpp | 58 + .../nav2_mppi_controller/critic_data.hpp | 6 +- .../nav2_mppi_controller/critic_function.hpp | 5 +- .../nav2_mppi_controller/critic_manager.hpp | 1 + .../critics/cost_critic copy.hpp | 109 + .../critics/cost_critic.hpp | 34 +- .../critics/obstacles_critic copy 3.hpp | 108 + .../critics/path_align_critic.hpp | 6 + .../critics/path_angle_critic.hpp | 5 + .../critics/path_follow_critic.hpp | 6 + .../critics/waypoint_critic.hpp | 112 + .../fixing_trajectory_validator.hpp | 71 + .../nav2_mppi_controller/optimizer copy 2.hpp | 295 ++ .../nav2_mppi_controller/optimizer.hpp | 37 +- .../tools/path_handler copy.hpp | 159 + .../tools/path_handler.hpp | 10 +- .../tools/path_handler_mod.hpp | 159 + .../tools/path_handler_mod_final.hpp | 159 + .../tools/path_handler_original.hpp | 157 + .../tools/trajectory_visualizer copy 2.hpp | 118 + .../tools/trajectory_visualizer.hpp | 88 +- .../tools/utils copy 2.hpp | 689 +++ .../nav2_mppi_controller/tools/utils copy.hpp | 824 ++++ .../tools/utils copy_mod.hpp | 757 ++++ .../tools/utils copy_original.hpp | 705 +++ .../nav2_mppi_controller/tools/utils.hpp | 132 +- .../tools/utils_mod_final.hpp | 757 ++++ .../src/collision_checker.cpp | 394 ++ .../src/controller copy 2.cpp | 157 + nav2_mppi_controller/src/controller.cpp | 66 +- nav2_mppi_controller/src/critic_manager.cpp | 38 +- .../src/critics/cost_critic copy 2.cpp | 284 ++ .../src/critics/cost_critic copy 3.cpp | 297 ++ .../src/critics/cost_critic copy.cpp | 198 + .../src/critics/cost_critic.cpp | 60 +- .../src/critics/cost_critic_clear.cpp | 281 ++ .../src/critics/obstacles_critic copy 3.cpp | 249 ++ .../src/critics/obstacles_critic copy.cpp | 373 ++ .../src/critics/obstacles_critic.cpp | 7 + .../src/critics/path_align_critic.cpp | 28 + .../src/critics/path_angle_critic.cpp | 25 + .../src/critics/path_follow_critic.cpp | 25 + .../src/critics/waypoint_critic.cpp | 207 + nav2_mppi_controller/src/optimizer copy 2.cpp | 586 +++ nav2_mppi_controller/src/optimizer.cpp | 1 + .../src/path_handler copy 2.cpp | 247 ++ .../src/path_handler copy.cpp | 247 ++ nav2_mppi_controller/src/path_handler.cpp | 60 +- nav2_mppi_controller/src/path_handler_mod.cpp | 247 ++ .../src/path_handler_original.cpp | 205 + .../fixing_trajectory_validator.cpp | 407 ++ .../src/trajectory_visualizer copy 2.cpp | 156 + .../src/trajectory_visualizer.cpp | 364 +- .../test/critic_manager_test copy 2.cpp | 160 + .../test/critic_manager_test.cpp | 5 +- .../test/critics_tests copy 2.cpp | 794 ++++ nav2_mppi_controller/test/critics_tests.cpp | 38 +- .../trajectory_visualizer_tests copy 2.cpp | 229 + .../test/trajectory_visualizer_tests.cpp | 32 +- .../test/utils_test copy 2.cpp | 614 +++ nav2_mppi_controller/test/utils_test.cpp | 17 +- .../trajectory_validators.xml | 7 + .../nav2_simple_commander/robot_navigator.py | 4 +- .../lattice_primitives/config.json | 4 +- .../generate_motion_primitives.py | 6 +- .../lattice_primitives/lattice_generator.py | 6 +- .../lattice_primitives/output.json | 3864 +++++++++++++++++ .../0.5m_turning_radius/diff/output1.json | 3864 +++++++++++++++++ .../lattice_primitives/trajectory.py | 2 +- .../trajectory_generator.py | 2 +- .../lattice_primitives/visualizations/0.0.png | Bin 0 -> 12662 bytes .../visualizations/26.56505117707799.png | Bin 0 -> 15826 bytes .../visualizations/45.0.png | Bin 0 -> 11922 bytes .../visualizations/63.43494882292201.png | Bin 0 -> 16013 bytes .../visualizations/90.0.png | Bin 0 -> 12672 bytes .../visualizations/all_trajectories.png | Bin 0 -> 42112 bytes .../include/nav2_util/controller_utils.hpp | 15 + nav2_util/src/controller_utils.cpp | 49 +- 107 files changed, 23563 insertions(+), 432 deletions(-) create mode 100644 nav2_bringup/launch/cbf_supervisor_launch.py delete mode 100644 nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery.xml create mode 100644 nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_copy.xml create mode 100644 nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_copy_test_replanning.xml create mode 100644 nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_if_invalid.xml create mode 100644 nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_if_invalid_copy copy 2.xml create mode 100644 nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_if_invalid_copy copy.xml create mode 100644 nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_if_invalid_copy.xml create mode 100644 nav2_costmap_2d/include/nav2_costmap_2d/footprint_collision_checker copy.hpp create mode 100644 nav2_costmap_2d/src/footprint_collision_checker copy.cpp create mode 100644 nav2_costmap_2d/test/unit/footprint_collision_checker_test copy.cpp create mode 100644 nav2_mppi_controller/CMakeLists copy.txt create mode 100644 nav2_mppi_controller/include/nav2_mppi_controller/collision_checker.hpp create mode 100644 nav2_mppi_controller/include/nav2_mppi_controller/critic_data copy 2.hpp create mode 100644 nav2_mppi_controller/include/nav2_mppi_controller/critics/cost_critic copy.hpp create mode 100644 nav2_mppi_controller/include/nav2_mppi_controller/critics/obstacles_critic copy 3.hpp create mode 100644 nav2_mppi_controller/include/nav2_mppi_controller/critics/waypoint_critic.hpp create mode 100644 nav2_mppi_controller/include/nav2_mppi_controller/fixing_trajectory_validator.hpp create mode 100644 nav2_mppi_controller/include/nav2_mppi_controller/optimizer copy 2.hpp create mode 100644 nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler copy.hpp create mode 100644 nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler_mod.hpp create mode 100644 nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler_mod_final.hpp create mode 100644 nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler_original.hpp create mode 100644 nav2_mppi_controller/include/nav2_mppi_controller/tools/trajectory_visualizer copy 2.hpp create mode 100644 nav2_mppi_controller/include/nav2_mppi_controller/tools/utils copy 2.hpp create mode 100644 nav2_mppi_controller/include/nav2_mppi_controller/tools/utils copy.hpp create mode 100644 nav2_mppi_controller/include/nav2_mppi_controller/tools/utils copy_mod.hpp create mode 100644 nav2_mppi_controller/include/nav2_mppi_controller/tools/utils copy_original.hpp create mode 100644 nav2_mppi_controller/include/nav2_mppi_controller/tools/utils_mod_final.hpp create mode 100644 nav2_mppi_controller/src/collision_checker.cpp create mode 100644 nav2_mppi_controller/src/controller copy 2.cpp create mode 100644 nav2_mppi_controller/src/critics/cost_critic copy 2.cpp create mode 100644 nav2_mppi_controller/src/critics/cost_critic copy 3.cpp create mode 100644 nav2_mppi_controller/src/critics/cost_critic copy.cpp create mode 100644 nav2_mppi_controller/src/critics/cost_critic_clear.cpp create mode 100644 nav2_mppi_controller/src/critics/obstacles_critic copy 3.cpp create mode 100644 nav2_mppi_controller/src/critics/obstacles_critic copy.cpp create mode 100644 nav2_mppi_controller/src/critics/waypoint_critic.cpp create mode 100644 nav2_mppi_controller/src/optimizer copy 2.cpp create mode 100644 nav2_mppi_controller/src/path_handler copy 2.cpp create mode 100644 nav2_mppi_controller/src/path_handler copy.cpp create mode 100644 nav2_mppi_controller/src/path_handler_mod.cpp create mode 100644 nav2_mppi_controller/src/path_handler_original.cpp create mode 100644 nav2_mppi_controller/src/trajectory_validators/fixing_trajectory_validator.cpp create mode 100644 nav2_mppi_controller/src/trajectory_visualizer copy 2.cpp create mode 100644 nav2_mppi_controller/test/critic_manager_test copy 2.cpp create mode 100644 nav2_mppi_controller/test/critics_tests copy 2.cpp create mode 100644 nav2_mppi_controller/test/trajectory_visualizer_tests copy 2.cpp create mode 100644 nav2_mppi_controller/test/utils_test copy 2.cpp create mode 100644 nav2_smac_planner/lattice_primitives/output.json create mode 100644 nav2_smac_planner/lattice_primitives/sample_primitives/5cm_resolution/0.5m_turning_radius/diff/output1.json create mode 100644 nav2_smac_planner/lattice_primitives/visualizations/0.0.png create mode 100644 nav2_smac_planner/lattice_primitives/visualizations/26.56505117707799.png create mode 100644 nav2_smac_planner/lattice_primitives/visualizations/45.0.png create mode 100644 nav2_smac_planner/lattice_primitives/visualizations/63.43494882292201.png create mode 100644 nav2_smac_planner/lattice_primitives/visualizations/90.0.png create mode 100644 nav2_smac_planner/lattice_primitives/visualizations/all_trajectories.png diff --git a/nav2_behavior_tree/CMakeLists.txt b/nav2_behavior_tree/CMakeLists.txt index 2004b5c62b3..6aa40c5ed05 100644 --- a/nav2_behavior_tree/CMakeLists.txt +++ b/nav2_behavior_tree/CMakeLists.txt @@ -19,6 +19,7 @@ find_package(std_srvs REQUIRED) find_package(tf2 REQUIRED) find_package(tf2_ros REQUIRED) find_package(nav2_ros_common REQUIRED) +find_package(angles REQUIRED) nav2_package() @@ -33,6 +34,7 @@ target_include_directories(${library_name} "$") target_link_libraries(${library_name} PUBLIC ${action_msgs_TARGETS} + angles::angles behaviortree_cpp::behaviortree_cpp ${geometry_msgs_TARGETS} ${nav_msgs_TARGETS} @@ -275,6 +277,7 @@ foreach(bt_plugin ${plugin_libs}) ${std_msgs_TARGETS} ${std_srvs_TARGETS} nav2_ros_common::nav2_ros_common + angles::angles ) target_compile_definitions(${bt_plugin} PRIVATE BT_PLUGIN_EXPORT) endforeach() diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/remove_in_collision_goals_action.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/remove_in_collision_goals_action.hpp index a76394e2ffe..d402d37fde5 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/remove_in_collision_goals_action.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/remove_in_collision_goals_action.hpp @@ -73,6 +73,9 @@ class RemoveInCollisionGoals : public BtServiceNode BT::InputPort( "consider_unknown_as_obstacle", false, "Whether to consider unknown cost as obstacle"), + BT::InputPort( + "nb_goals_to_consider", 5, + "Number of goals to consider for collision checking"), BT::OutputPort("output_goals", "Goals with in-collision goals removed"), BT::InputPort>("input_waypoint_statuses", @@ -85,6 +88,7 @@ class RemoveInCollisionGoals : public BtServiceNode private: bool use_footprint_; bool consider_unknown_as_obstacle_; + int nb_goals_to_consider_; double cost_threshold_; nav_msgs::msg::Goals input_goals_; }; diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/remove_passed_goals_action.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/remove_passed_goals_action.hpp index a651318379f..d7271c3fbe1 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/remove_passed_goals_action.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/action/remove_passed_goals_action.hpp @@ -62,6 +62,9 @@ class RemovePassedGoals : public BT::ActionNodeBase "Goals with passed viapoints removed"), BT::InputPort("radius", 0.5, "radius to goal for it to be considered for removal"), BT::InputPort("robot_base_frame", "Robot base frame"), + BT::InputPort("nb_goals_to_consider", 1, "Number of waypoints to consider"), + BT::InputPort("yaw", 1.57, + "yaw threshold to goal for it to be considered for removal"), BT::InputPort>("input_waypoint_statuses", "Original waypoint_statuses to mark waypoint status from"), BT::OutputPort>("output_waypoint_statuses", @@ -74,10 +77,12 @@ class RemovePassedGoals : public BT::ActionNodeBase BT::NodeStatus tick() override; double viapoint_achieved_radius_; + double viapoint_achieved_yaw_; double transform_tolerance_; nav2::LifecycleNode::SharedPtr node_; std::shared_ptr tf_; std::string robot_base_frame_; + int nb_goals_to_consider_; }; } // namespace nav2_behavior_tree diff --git a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/is_path_valid_condition.hpp b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/is_path_valid_condition.hpp index 334eaefd6f0..f13ed225aaf 100644 --- a/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/is_path_valid_condition.hpp +++ b/nav2_behavior_tree/include/nav2_behavior_tree/plugins/condition/is_path_valid_condition.hpp @@ -73,7 +73,7 @@ class IsPathValidCondition : public BT::ConditionNode return { BT::InputPort("path", "Path to Check"), BT::InputPort("server_timeout"), - BT::InputPort("max_cost", 253, "Maximum cost of the path"), + BT::InputPort("max_cost", 254, "Maximum cost of the path"), BT::InputPort( "consider_unknown_as_obstacle", false, "Whether to consider unknown cost as obstacle") diff --git a/nav2_behavior_tree/plugins/action/remove_in_collision_goals_action.cpp b/nav2_behavior_tree/plugins/action/remove_in_collision_goals_action.cpp index 113c3a2f514..9c52804205b 100644 --- a/nav2_behavior_tree/plugins/action/remove_in_collision_goals_action.cpp +++ b/nav2_behavior_tree/plugins/action/remove_in_collision_goals_action.cpp @@ -39,6 +39,7 @@ void RemoveInCollisionGoals::on_tick() getInput("cost_threshold", cost_threshold_); getInput("input_goals", input_goals_); getInput("consider_unknown_as_obstacle", consider_unknown_as_obstacle_); + getInput("nb_goals_to_consider", nb_goals_to_consider_); if (input_goals_.goals.empty()) { setOutput("output_goals", input_goals_); @@ -48,8 +49,11 @@ void RemoveInCollisionGoals::on_tick() request_ = std::make_shared(); request_->use_footprint = use_footprint_; - for (const auto & goal : input_goals_.goals) { - request_->poses.push_back(goal); + for (size_t i = + 0; i < static_cast(nb_goals_to_consider_) && i < input_goals_.goals.size(); + ++i) + { + request_->poses.push_back(input_goals_.goals[i]); } } @@ -67,35 +71,31 @@ BT::NodeStatus RemoveInCollisionGoals::on_completion( // get the `waypoint_statuses` vector std::vector waypoint_statuses; auto waypoint_statuses_get_res = getInput("input_waypoint_statuses", waypoint_statuses); - if (!waypoint_statuses_get_res) { - RCLCPP_ERROR(node_->get_logger(), "Missing [input_waypoint_statuses] port input!"); - } - nav_msgs::msg::Goals valid_goal_poses; - for (size_t i = 0; i < response->costs.size(); ++i) { - if ((response->costs[i] == 255 && !consider_unknown_as_obstacle_) || - response->costs[i] < cost_threshold_) + for (int i = static_cast(response->costs.size()) - 1; i >= 0; --i) { + if ((response->costs[i] != 255 || consider_unknown_as_obstacle_) && + response->costs[i] >= cost_threshold_) { - valid_goal_poses.goals.push_back(input_goals_.goals[i]); - } else if (waypoint_statuses_get_res) { - using namespace nav2_util::geometry_utils; // NOLINT - auto cur_waypoint_index = - find_next_matching_goal_in_waypoint_statuses(waypoint_statuses, input_goals_.goals[i]); - if (cur_waypoint_index == -1) { - RCLCPP_ERROR(node_->get_logger(), "Failed to find matching goal in waypoint_statuses"); - return BT::NodeStatus::FAILURE; + if (waypoint_statuses_get_res) { + const int cur_waypoint_index = + nav2_util::geometry_utils::find_next_matching_goal_in_waypoint_statuses( + waypoint_statuses, input_goals_.goals[i]); + + if (cur_waypoint_index >= 0 && + static_cast(cur_waypoint_index) < waypoint_statuses.size()) { + waypoint_statuses[static_cast(cur_waypoint_index)].waypoint_status = + nav2_msgs::msg::WaypointStatus::SKIPPED; + } else { + RCLCPP_WARN( + node_->get_logger(), + "RemoveInCollisionGoals: No matching waypoint found for goal index %d; " + "skipping waypoint_statuses update.", i); + } } - waypoint_statuses[cur_waypoint_index].waypoint_status = - nav2_msgs::msg::WaypointStatus::SKIPPED; + input_goals_.goals.erase(input_goals_.goals.begin() + i); } } - // Inform if all goals have been removed - if (valid_goal_poses.goals.empty()) { - RCLCPP_INFO( - node_->get_logger(), - "All goals are in collision and have been removed from the list"); - } - setOutput("output_goals", valid_goal_poses); + setOutput("output_goals", input_goals_); // set `waypoint_statuses` output setOutput("output_waypoint_statuses", waypoint_statuses); diff --git a/nav2_behavior_tree/plugins/action/remove_passed_goals_action.cpp b/nav2_behavior_tree/plugins/action/remove_passed_goals_action.cpp index 335d5e60c37..1fe5a5bd3f6 100644 --- a/nav2_behavior_tree/plugins/action/remove_passed_goals_action.cpp +++ b/nav2_behavior_tree/plugins/action/remove_passed_goals_action.cpp @@ -18,6 +18,8 @@ #include "nav_msgs/msg/path.hpp" #include "nav2_util/geometry_utils.hpp" +#include "tf2/utils.h" +#include "angles/angles.h" #include "nav2_behavior_tree/plugins/action/remove_passed_goals_action.hpp" @@ -28,12 +30,14 @@ RemovePassedGoals::RemovePassedGoals( const std::string & name, const BT::NodeConfiguration & conf) : BT::ActionNodeBase(name, conf), - viapoint_achieved_radius_(0.5) + viapoint_achieved_radius_(0.5), + viapoint_achieved_yaw_(1.57) {} void RemovePassedGoals::initialize() { getInput("radius", viapoint_achieved_radius_); + getInput("yaw", viapoint_achieved_yaw_); // new input for yaw threshold tf_ = config().blackboard->get>("tf_buffer"); node_ = config().blackboard->get("node"); @@ -56,6 +60,9 @@ inline BT::NodeStatus RemovePassedGoals::tick() setOutput("output_goals", goal_poses); return BT::NodeStatus::SUCCESS; } + getInput("nb_goals_to_consider", nb_goals_to_consider_); + nb_goals_to_consider_ = std::min(nb_goals_to_consider_, + static_cast(goal_poses.goals.size())); using namespace nav2_util::geometry_utils; // NOLINT @@ -70,33 +77,47 @@ inline BT::NodeStatus RemovePassedGoals::tick() // get the `waypoint_statuses` vector std::vector waypoint_statuses; auto waypoint_statuses_get_res = getInput("input_waypoint_statuses", waypoint_statuses); - if (!waypoint_statuses_get_res) { - RCLCPP_ERROR_ONCE(node_->get_logger(), "Missing [input_waypoint_statuses] port input!"); - } - - double dist_to_goal; - while (goal_poses.goals.size() > 1) { - dist_to_goal = euclidean_distance(goal_poses.goals[0].pose, current_pose.pose); - if (dist_to_goal > viapoint_achieved_radius_) { + bool goal_reached = false; + int reached_goal_index = -1; + + // Iterate over the first `nb_goals_to_consider` goals + for (int i = 0; i < nb_goals_to_consider_; ++i) { + double dist_to_goal = euclidean_distance(goal_poses.goals[i].pose, current_pose.pose); + double yaw_goal = tf2::getYaw(goal_poses.goals[i].pose.orientation); + double yaw_current = tf2::getYaw(current_pose.pose.orientation); + double yaw_diff = std::fabs(angles::shortest_angular_distance(yaw_current, yaw_goal)); + if ((dist_to_goal <= viapoint_achieved_radius_ && yaw_diff <= viapoint_achieved_yaw_) + || dist_to_goal <= 0.2) // Added a small tolerance without yaw check to avoid robot stucked near collision (allow to achieve pose without orientation) + { + reached_goal_index = i; + goal_reached = true; break; } - - // mark waypoint statuses before the goal is erased from goals + } + if (goal_reached) { if (waypoint_statuses_get_res) { - auto cur_waypoint_index = - find_next_matching_goal_in_waypoint_statuses(waypoint_statuses, goal_poses.goals[0]); - if (cur_waypoint_index == -1) { - RCLCPP_ERROR_ONCE(node_->get_logger(), "Failed to find matching goal in waypoint_statuses"); - return BT::NodeStatus::FAILURE; + for (int i = 0; i <= reached_goal_index; ++i) { + auto cur_waypoint_index = + find_next_matching_goal_in_waypoint_statuses(waypoint_statuses, goal_poses.goals[i]); + if (cur_waypoint_index >= 0 && + cur_waypoint_index < static_cast(waypoint_statuses.size())) + { + waypoint_statuses.at(cur_waypoint_index).waypoint_status = + (i == reached_goal_index) ? nav2_msgs::msg::WaypointStatus::COMPLETED : + nav2_msgs::msg::WaypointStatus::SKIPPED; + } else { + RCLCPP_WARN(node_->get_logger(), + "RemovePassedGoals: No matching waypoint found for goal %d", i); + } } - waypoint_statuses[cur_waypoint_index].waypoint_status = - nav2_msgs::msg::WaypointStatus::COMPLETED; } - - goal_poses.goals.erase(goal_poses.goals.begin()); + // If the reached goal is NOT the last one, erase all COMPLETED and SKIPPED goals + if (reached_goal_index < static_cast(goal_poses.goals.size()) - 1) { + goal_poses.goals.erase(goal_poses.goals.begin(), + goal_poses.goals.begin() + reached_goal_index + 1); + } } - setOutput("output_goals", goal_poses); // set `waypoint_statuses` output setOutput("output_waypoint_statuses", waypoint_statuses); diff --git a/nav2_bringup/launch/cbf_supervisor_launch.py b/nav2_bringup/launch/cbf_supervisor_launch.py new file mode 100644 index 00000000000..74dc2f951f9 --- /dev/null +++ b/nav2_bringup/launch/cbf_supervisor_launch.py @@ -0,0 +1,27 @@ +from ament_index_python.packages import get_package_share_directory +from launch import LaunchDescription +from launch.actions import IncludeLaunchDescription, ExecuteProcess +from launch.launch_description_sources import PythonLaunchDescriptionSource +from launch.substitutions import PathJoinSubstitution, EnvironmentVariable +import os + +def generate_launch_description(): + launchDesc = LaunchDescription() + # If the Supervisor package is available on system (debian install) + # then include the supervisor launch file + pkg = get_package_share_directory("lll_supervisor") + launchDesc.add_action( + IncludeLaunchDescription( + PythonLaunchDescriptionSource( + PathJoinSubstitution( + [ + get_package_share_directory("lll_supervisor"), + "launch", + "supervisor.launch.py", + ] + ) + ) + ) + ) + + return launchDesc diff --git a/nav2_bringup/launch/tb4_simulation_launch.py b/nav2_bringup/launch/tb4_simulation_launch.py index 2dcde8ec82f..e369d8bad7b 100644 --- a/nav2_bringup/launch/tb4_simulation_launch.py +++ b/nav2_bringup/launch/tb4_simulation_launch.py @@ -31,7 +31,7 @@ # Define local map types MAP_POSES_DICT = { 'depot': { - 'x': -8.00, 'y': 0.00, 'z': 0.01, + 'x': -7.54, 'y': -0.3, 'z': 0.01, 'R': 0.00, 'P': 0.00, 'Y': 0.00 }, 'warehouse': { diff --git a/nav2_bringup/params/nav2_params.yaml b/nav2_bringup/params/nav2_params.yaml index e66d409478f..a1c33435c18 100644 --- a/nav2_bringup/params/nav2_params.yaml +++ b/nav2_bringup/params/nav2_params.yaml @@ -6,6 +6,7 @@ amcl: alpha4: 0.2 alpha5: 0.2 base_frame_id: "base_footprint" + introspection_mode: "disabled" beam_skip_distance: 0.5 beam_skip_error_threshold: 0.9 beam_skip_threshold: 0.3 @@ -30,7 +31,6 @@ amcl: sigma_hit: 0.2 tf_broadcast: true transform_tolerance: 1.0 - introspection_mode: "disabled" update_min_a: 0.2 update_min_d: 0.25 z_hit: 0.5 @@ -38,6 +38,15 @@ amcl: z_rand: 0.5 z_short: 0.05 scan_topic: scan + set_initial_pose: true + always_reset_initial_pose: true + first_map_only: false + initial_pose: + x: 7.65 + y: 7.65 + z: 0.0 + yaw: 0.0 + bt_navigator: ros__parameters: @@ -45,8 +54,10 @@ bt_navigator: robot_base_frame: base_link odom_topic: odom bt_loop_duration: 10 + #enable_stamped_cmd_vel: False filter_duration: 0.3 default_server_timeout: 20 + goal_reached_tol: 0.8 wait_for_service_timeout: 1000 introspection_mode: "disabled" navigators: ["navigate_to_pose", "navigate_through_poses"] @@ -64,10 +75,20 @@ bt_navigator: # nav2_bt_navigator/navigate_to_pose_w_replanning_and_recovery.xml # nav2_bt_navigator/navigate_through_poses_w_replanning_and_recovery.xml # They can be set here or via a RewrittenYaml remap from a parent launch file to Nav2. - + + #default_nav_through_poses_bt_xml: $(find-pkg-share nav2_bt_navigator)/behavior_trees/navigate_through_poses_w_replanning_and_recovery_copy.xml + default_nav_through_poses_bt_xml: $(find-pkg-share nav2_bt_navigator)/behavior_trees/navigate_through_poses_w_replanning_and_recovery_copy_test_replanning.xml + #default_nav_through_poses_bt_xml: $(find-pkg-share nav2_bt_navigator)/behavior_trees/navigate_through_poses_w_replanning_and_recovery_if_invalid_copy.xml # Pelna obsluga przeszkod + # plugin_lib_names is used to add custom BT plugins to the executor (vector of strings). # Built-in plugins are added automatically - # plugin_lib_names: [] + plugin_lib_names: + - nav2_publish_goals_action_bt_node + - nav2_is_collision_free_condition_bt_node + - nav2_remove_next_goal_action_bt_node + - nav2_pose_reached_condition_bt_node + - nav2_goals_truncation_action_bt_node + - nav2_goals_has_at_most_point_count_bt_node error_code_name_prefixes: - assisted_teleop @@ -86,7 +107,8 @@ bt_navigator: controller_server: ros__parameters: - controller_frequency: 20.0 + controller_frequency: 30.0 + #enable_stamped_cmd_vel: False costmap_update_timeout: 0.30 min_x_velocity_threshold: 0.001 min_y_velocity_threshold: 0.5 @@ -100,8 +122,9 @@ controller_server: # Progress checker parameters progress_checker: - plugin: "nav2_controller::SimpleProgressChecker" - required_movement_radius: 0.5 + plugin: "nav2_controller::PoseProgressChecker" + required_movement_radius: 0.1 + required_movement_angle: 0.3 movement_time_allowance: 10.0 # Goal checker parameters #precise_goal_checker: @@ -112,11 +135,237 @@ controller_server: general_goal_checker: stateful: True plugin: "nav2_controller::SimpleGoalChecker" - xy_goal_tolerance: 0.25 - yaw_goal_tolerance: 0.25 - FollowPath: + xy_goal_tolerance: 0.25 #0.0 #0.25 #0.0 + yaw_goal_tolerance: 0.25 #0.0 #0.25 #0.0 + + # change general goal checker for path following + # FollowPath: + # plugin: "nav2_mppi_controller::MPPIController" + # time_steps: 56 + # model_dt: 0.05 + # batch_size: 2000 + # ax_max: 3.0 + # ax_min: -3.0 + # ay_max: 3.0 + # ay_min: -3.0 + # az_max: 3.5 + # vx_std: 0.2 + # vy_std: 0.2 + # wz_std: 0.4 #0.9 + # vx_max: 0.3 + # vx_min: -0.1 + # vy_max: 0.5 + # wz_max: 0.3 + # iteration_count: 1 + # prune_distance: 1.7 + # enforce_path_inversion: True + # transform_tolerance: 0.1 + # temperature: 0.3 + # gamma: 0.015 + # motion_model: "DiffDrive" + # #publish_critics_stats: True + # #visualize: true + # #publish_optimal_trajectory: true + # regenerate_noises: true + # Visualization: + # publish_critics_stats: false + # publish_optimal_trajectory_msg: false + # publish_optimal_trajectory: false + # publish_transformed_path: false + # publish_trajectories_with_total_cost: true + # publish_trajectories_with_individual_cost: true + # publish_optimal_footprints: true + # publish_optimal_path: false + # trajectory_step: 5 + # time_step: 3 + # footprint_downsample_factor: 3 + # TrajectoryValidator: + # # The validator can be configured with additional parameters if needed. + # plugin: "mppi::DefaultOptimalTrajectoryValidator" + # collision_lookahead_time: 0.0 # Time to look ahead to check for collisions + # consider_footprint: false # Whether to consider the full robot's footprint (or circle) in trajectory validation collision checks + # AckermannConstraints: + # min_turning_r: 0.2 + # critics: [ #"ConstraintCritic", + # "CostCritic", "GoalCritic", + # "GoalAngleCritic", "PathAlignCritic", "PathFollowCritic", + # "PathAngleCritic", "PreferForwardCritic"] + # GoalCritic: + # enabled: true + # cost_power: 1 + # cost_weight: 5.0 + # threshold_to_consider: 1.4 + # GoalAngleCritic: + # enabled: true + # cost_power: 1 + # cost_weight: 3.0 + # threshold_to_consider: 0.5 + # PreferForwardCritic: + # enabled: true + # cost_power: 1 + # cost_weight: 5.0 + # threshold_to_consider: 0.5 + # CostCritic: + # enabled: true + # cost_power: 1 + # cost_weight: 3.81 + # near_collision_cost: 253 + # critical_cost: 300.0 + # consider_footprint: true + # collision_cost: 1000000.0 + # near_goal_distance: 0.2 + # trajectory_point_step: 2 + # PathAlignCritic: + # enabled: true + # cost_power: 1 + # cost_weight: 14.0 + # max_path_occupancy_ratio: 0.05 + # trajectory_point_step: 4 + # threshold_to_consider: 0.5 + # offset_from_furthest: 20 + # use_path_orientations: false + # PathFollowCritic: + # enabled: true + # cost_power: 1 + # cost_weight: 5.0 + # offset_from_furthest: 5 + # threshold_to_consider: 1.4 + # PathAngleCritic: + # enabled: true + # cost_power: 1 + # cost_weight: 2.0 + # offset_from_furthest: 4 + # threshold_to_consider: 0.5 + # max_angle_to_furthest: 1.0 + # mode: 0 + # WaypointCritic: + # enabled: false + # cost_power: 1 + # cost_weight: 5.0 + # position_tolerance: 0.1 + # yaw_tolerance: 0.1 + # waypoint_topic: "truncated_path" + # use_orientation: false + # max_waypoint_distance: 10.0 + MPPI: + plugin: "nav2_mppi_controller::MPPIController" + time_steps: 50 + model_dt: 0.1 + batch_size: 2000 + ax_max: 1.0 + ax_min: -1.0 + ay_max: 0.0 + ay_min: 0.0 + az_max: 1.0 + vx_std: 0.2 + vy_std: 0.0 + wz_std: 0.9 + vx_max: 0.3 + vx_min: -0.1 + vy_max: 0.0 + wz_max: 0.3 + iteration_count: 1 + prune_distance: 1.7 + transform_tolerance: 0.1 + enforce_path_inversion: True + enforce_path_rotation: True + minimum_rotation_angle: 1.0 + inversion_xy_tolerance: 0.5 + inversion_yaw_tolerance: 0.785 + temperature: 0.3 + gamma: 0.015 + motion_model: "DiffDrive" + reset_period: 1.0 # (only in Humble) + regenerate_noises: true + Visualization: + publish_critics_stats: true + publish_optimal_trajectory_msg: false + publish_optimal_trajectory: false + publish_transformed_path: true + publish_trajectories_with_total_cost: false + publish_trajectories_with_individual_cost: false + publish_optimal_footprints: false + publish_optimal_path: false + trajectory_step: 10 + time_step: 5 + footprint_downsample_factor: 3 + critics: ["ConstraintCritic", "CostCritic", "GoalCritic", "GoalAngleCritic", "PathAlignCritic", "PathFollowCritic", "PathAngleCritic", "PreferForwardCritic"] + ConstraintCritic: + enabled: true + cost_power: 1 + cost_weight: 4.0 + GoalCritic: + enabled: true + cost_power: 1 + cost_weight: 5.0 + threshold_to_consider: 1.4 + GoalAngleCritic: + enabled: true + cost_power: 1 + cost_weight: 3.0 + threshold_to_consider: 0.5 + PreferForwardCritic: + enabled: true + cost_power: 1 + cost_weight: 5.0 + threshold_to_consider: 0.5 + # ObstaclesCritic: + # enabled: true + # cost_power: 1 + # repulsion_weight: 1.5 + # critical_weight: 20.0 + # consider_footprint: false + # collision_cost: 10000.0 + # collision_margin_distance: 0.1 + # near_goal_distance: 0.5 + # inflation_radius: 0.55 # (only in Humble) + # cost_scaling_factor: 10.0 # (only in Humble) + CostCritic: + enabled: true + cost_power: 1 + cost_weight: 2.5 # PODNIESC WAGE COST CRITISC + near_collision_cost: 253 + critical_cost: 250.0 + consider_footprint: true + collision_cost: 1000000.0 + near_goal_distance: 1.0 + trajectory_point_step: 2 + PathAlignCritic: + enabled: true + cost_power: 1 + cost_weight: 14.0 + max_path_occupancy_ratio: 0.05 + trajectory_point_step: 4 + threshold_to_consider: 0.5 + offset_from_furthest: 20 + use_path_orientations: false + PathFollowCritic: + enabled: true + cost_power: 1 + cost_weight: 5.0 + offset_from_furthest: 5 + threshold_to_consider: 1.4 + PathAngleCritic: + enabled: true + cost_power: 1 + cost_weight: 2.0 + offset_from_furthest: 4 + threshold_to_consider: 0.5 + max_angle_to_furthest: 1.0 + mode: 0 + + FollowPathA: + # plugin: "nav2_rotation_shim_controller::RotationShimController" + # primary_controller: "nav2_mppi_controller::MPPIController" + # angular_dist_threshold: 2.4434 # 140 degrees + # forward_sampling_distance: 0.5 + # angular_disengage_threshold: 0.3925 + # rotate_to_heading_angular_vel: 0.3 + # max_angular_accel: 1.0 + # simulate_ahead_time: 1.0 + # rotate_to_goal_heading: false plugin: "nav2_mppi_controller::MPPIController" - time_steps: 56 + time_steps: 35 model_dt: 0.05 batch_size: 2000 ax_max: 3.0 @@ -126,55 +375,389 @@ controller_server: az_max: 3.5 vx_std: 0.2 vy_std: 0.2 - wz_std: 0.4 - vx_max: 0.5 - vx_min: -0.35 + wz_std: 0.9 + vx_max: 0.3 + vx_min: -0.1 vy_max: 0.5 - wz_max: 1.9 + wz_max: 0.3 iteration_count: 1 - prune_distance: 1.7 + prune_distance: 0.8 + enforce_path_inversion: True transform_tolerance: 0.1 temperature: 0.3 gamma: 0.015 motion_model: "DiffDrive" - visualize: true - publish_optimal_trajectory: true - regenerate_noises: true - TrajectoryVisualizer: - trajectory_step: 5 - time_step: 3 + publish_critics_stats: True + #visualize: false + #publish_optimal_trajectory: true + regenerate_noises: false + Visualization: + publish_critics_stats: false + publish_optimal_trajectory_msg: false + publish_optimal_trajectory: false + publish_transformed_path: false + publish_trajectories_with_total_cost: true + publish_trajectories_with_individual_cost: false + publish_optimal_footprints: true + publish_optimal_path: false + trajectory_step: 10 + time_step: 5 + footprint_downsample_factor: 3 TrajectoryValidator: # The validator can be configured with additional parameters if needed. + # plugin: "mppi::AutoFixingTrajectoryValidator" + # #consider_footprint: true + # consider_footprint: true + # collision_lookahead_time: 0.8 + # auto_fix_ttc_window: 0.8 + # auto_fix_safe_clearance: 0.04 # ≈ 0.8 * resolution (np. 5cm → 0.04) + # auto_fix_step_gain: 0.6 + # auto_fix_max_step: 0.05 + # auto_fix_max_total_displacement: 0.35 + # auto_fix_max_iters: 6 + # auto_fix_smooth_iters: 2 plugin: "mppi::DefaultOptimalTrajectoryValidator" - collision_lookahead_time: 2.0 # Time to look ahead to check for collisions - consider_footprint: false # Whether to consider the full robot's footprint (or circle) in trajectory validation collision checks + collision_lookahead_time: 0.0 # Time to look ahead to check for collisions + consider_footprint: true # Whether to consider the full robot's footprint (or circle) in trajectory validation collision checks + + #collision_lookahead_time: 0.1 # Time to look ahead to check for collisions + #consider_footprint: true # Whether to consider the full robot's footprint (or circle) in trajectory validation collision checks AckermannConstraints: min_turning_r: 0.2 - critics: - [ - "ConstraintCritic", - "CostCritic", - "GoalCritic", - "GoalAngleCritic", - "PathAlignCritic", - "PathFollowCritic", - "PathAngleCritic", - "PreferForwardCritic", - ] + critics: [ #"ConstraintCritic", + "ObstaclesCritic", "GoalCritic", + "GoalAngleCritic", "PathAlignCritic", "PathFollowCritic", + "PathAngleCritic", "PreferForwardCritic"] #,"WaypointCritic"] ConstraintCritic: + enabled: false + cost_power: 1 + cost_weight: 4.0 + GoalCritic: enabled: true cost_power: 1 + cost_weight: 5.0 + threshold_to_consider: 0.5 #0.5 # zmienione z 0.2 + GoalAngleCritic: + enabled: true + cost_power: 1 + cost_weight: 3.0 + threshold_to_consider: 0.3 #0.3 # zmienione z 0.1 + PreferForwardCritic: + enabled: false + cost_power: 1 + cost_weight: 5.0 + threshold_to_consider: 0.5 + CostCritic: + enabled: true + cost_power: 1 + cost_weight: 2.5 # PODNIESC WAGE COST CRITISC + near_collision_cost: 253 + critical_cost: 250.0 + consider_footprint: true + collision_cost: 1000000.0 + near_goal_distance: 0.0 + trajectory_point_step: 2 + PathAlignCritic: + enabled: false + cost_power: 1 + cost_weight: 14.0 + max_path_occupancy_ratio: 0.05 + trajectory_point_step: 4 + threshold_to_consider: 0.5 + offset_from_furthest: 20 + use_path_orientations: false + PathFollowCritic: + enabled: true + cost_power: 1 + cost_weight: 4.0 + offset_from_furthest: 3 + threshold_to_consider: 0.0 + PathAngleCritic: + enabled: true + cost_power: 1 + cost_weight: 3.0 + offset_from_furthest: 4 + threshold_to_consider: 0.0 + max_angle_to_furthest: 0.5 #1.2 + mode: 0 + ObstaclesCritic: + enabled: true + cost_power: 1 + repulsion_weight: 0.5 #0.5 0.8 + critical_weight: 15.0 + consider_footprint: true # FALSE # avoid obstacles based on radius not the footprint + collision_cost: 10000.0 + collision_margin_distance: 0.1 + near_goal_distance: 0.2 + cost_scaling_factor: 2.5 + inflation_radius: 1.0 + WaypointCritic: + enabled: false + cost_power: 1 + cost_weight: 5.0 + position_tolerance: 0.1 + yaw_tolerance: 0.1 + waypoint_topic: "truncated_path" + use_orientation: True + max_waypoint_distance: 10.0 + TwirlingCritic: + enabled: false + twirling_cost_power: 1 + twirling_cost_weight: 10.0 + + FollowPathB: + # plugin: "nav2_rotation_shim_controller::RotationShimController" + # primary_controller: "nav2_mppi_controller::MPPIController" + # angular_dist_threshold: 2.4434 # 140 degrees + # forward_sampling_distance: 0.5 + # angular_disengage_threshold: 0.3925 + # rotate_to_heading_angular_vel: 0.3 + # max_angular_accel: 1.0 + # simulate_ahead_time: 1.0 + # rotate_to_goal_heading: false + plugin: "nav2_mppi_controller::MPPIController" + # Dobrac prune distance, time_stemps i model_dt PRZYSPIESZENIA NIE DZIALA DLA NIZSZEGO MODEL DT + time_steps: 35 + model_dt: 0.15 #0.05 + batch_size: 2000 + ax_max: 1.0 + ax_min: -1.0 + ay_max: 1.0 + ay_min: -1.0 + az_max: 1.0 + vx_std: 0.2 + vy_std: 0.2 + wz_std: 0.9 + vx_max: 0.3 + vx_min: -0.1 + vy_max: 0.5 + wz_max: 0.3 + iteration_count: 1 + prune_distance: 1.0 + enforce_path_inversion: True + enforce_path_rotation: True + minimum_rotation_angle: 1.0 + inversion_xy_tolerance: 0.5 + inversion_yaw_tolerance: 0.785 + transform_tolerance: 0.1 + temperature: 0.3 + gamma: 0.015 + motion_model: "DiffDrive" + publish_critics_stats: false + #visualize: false + #publish_optimal_trajectory: true + regenerate_noises: true + Visualization: + publish_critics_stats: true + publish_optimal_trajectory_msg: false + publish_optimal_trajectory: false + publish_transformed_path: true + publish_trajectories_with_total_cost: false + publish_trajectories_with_individual_cost: false + publish_optimal_footprints: false + publish_optimal_path: false + trajectory_step: 10 + time_step: 5 + footprint_downsample_factor: 3 + TrajectoryValidator: + # The validator can be configured with additional parameters if needed. + # plugin: "mppi::AutoFixingTrajectoryValidator" + # #consider_footprint: true + # consider_footprint: true + # collision_lookahead_time: 0.8 + # auto_fix_ttc_window: 0.8 + # auto_fix_safe_clearance: 0.04 # ≈ 0.8 * resolution (np. 5cm → 0.04) + # auto_fix_step_gain: 0.6 + # auto_fix_max_step: 0.05 + # auto_fix_max_total_displacement: 0.35 + # auto_fix_max_iters: 6 + # auto_fix_smooth_iters: 2 + plugin: "mppi::DefaultOptimalTrajectoryValidator" + collision_lookahead_time: 0.0 # Time to look ahead to check for collisions + consider_footprint: true # Whether to consider the full robot's footprint (or circle) in trajectory validation collision checks + + #collision_lookahead_time: 0.1 # Time to look ahead to check for collisions + #consider_footprint: true # Whether to consider the full robot's footprint (or circle) in trajectory validation collision checks + AckermannConstraints: + min_turning_r: 0.2 + critics: [ #"ConstraintCritic", + "CostCritic", "GoalCritic", + "GoalAngleCritic", "PathAlignCritic", "PathFollowCritic", + "PathAngleCritic", "PreferForwardCritic"] #,"WaypointCritic"] + ConstraintCritic: + enabled: false + cost_power: 1 cost_weight: 4.0 GoalCritic: enabled: true cost_power: 1 cost_weight: 5.0 - threshold_to_consider: 1.4 + threshold_to_consider: 0.5 #0.5 # zmienione z 0.2 GoalAngleCritic: enabled: true cost_power: 1 cost_weight: 3.0 + threshold_to_consider: 0.3 #0.3 # zmienione z 0.1 + PreferForwardCritic: + enabled: true + cost_power: 1 + cost_weight: 5.0 threshold_to_consider: 0.5 + CostCritic: + enabled: true + cost_power: 1 + cost_weight: 2.5 # PODNIESC WAGE COST CRITISC + near_collision_cost: 253 + critical_cost: 250.0 + consider_footprint: true + collision_cost: 1000000.0 + near_goal_distance: 0.0 + trajectory_point_step: 2 + PathAlignCritic: + enabled: true + cost_power: 1 + cost_weight: 14.0 + max_path_occupancy_ratio: 0.05 + trajectory_point_step: 4 + threshold_to_consider: 0.3 + offset_from_furthest: 10 + use_path_orientations: false + visualize_furthest_point: false + PathFollowCritic: + enabled: true + cost_power: 1 + cost_weight: 4.0 + offset_from_furthest: 3 + threshold_to_consider: 0.5 + visualize_furthest_point: false + PathAngleCritic: + enabled: true + cost_power: 1 + cost_weight: 3.0 + offset_from_furthest: 4 + threshold_to_consider: 0.3 + max_angle_to_furthest: 0.5 #1.2 + visualize_furthest_point: false + forward_preference: true + mode: 0 + ObstaclesCritic: + enabled: true + cost_power: 1 + repulsion_weight: 0.5 #0.5 0.8 + critical_weight: 15.0 + consider_footprint: true # FALSE # avoid obstacles based on radius not the footprint + collision_cost: 10000.0 + collision_margin_distance: 0.1 + near_goal_distance: 0.2 + cost_scaling_factor: 2.5 + inflation_radius: 1.0 + WaypointCritic: + enabled: false + cost_power: 1 + cost_weight: 5.0 + position_tolerance: 0.1 + yaw_tolerance: 0.1 + waypoint_topic: "truncated_path" + use_orientation: True + max_waypoint_distance: 10.0 + TwirlingCritic: + enabled: false + twirling_cost_power: 1 + twirling_cost_weight: 10.0 + + FollowPathC: + # plugin: "nav2_rotation_shim_controller::RotationShimController" + # primary_controller: "nav2_mppi_controller::MPPIController" + # angular_dist_threshold: 2.4434 # 140 degrees + # forward_sampling_distance: 0.5 + # angular_disengage_threshold: 0.3925 + # rotate_to_heading_angular_vel: 0.3 + # max_angular_accel: 1.0 + # simulate_ahead_time: 1.0 + # rotate_to_goal_heading: false + plugin: "nav2_mppi_controller::MPPIController" + # Dobrac prune distance, time_stemps i model_dt PRZYSPIESZENIA NIE DZIALA DLA NIZSZEGO MODEL DT + time_steps: 35 #35 # 18 + model_dt: 0.15 #0.15 #0.05 + batch_size: 2000 + ax_max: 1.0 + ax_min: -1.0 + ay_max: 1.0 + ay_min: -1.0 + az_max: 1.0 + vx_std: 0.2 + vy_std: 0.2 + wz_std: 0.4 + vx_max: 0.3 + vx_min: -0.1 + vy_max: 0.5 + wz_max: 0.3 + iteration_count: 1 + prune_distance: 1.7 #1.7 #1.0 + enforce_path_inversion: True + enforce_path_rotation: True + minimum_rotation_angle: 1.0 + inversion_xy_tolerance: 0.5 + inversion_yaw_tolerance: 0.785 + transform_tolerance: 0.1 + temperature: 0.3 + gamma: 0.015 + motion_model: "DiffDrive" + publish_critics_stats: false + #visualize: false + #publish_optimal_trajectory: true + regenerate_noises: true + Visualization: + publish_critics_stats: true + publish_optimal_trajectory_msg: false + publish_optimal_trajectory: false + publish_transformed_path: true + publish_trajectories_with_total_cost: false + publish_trajectories_with_individual_cost: false + publish_optimal_footprints: false + publish_optimal_path: false + trajectory_step: 10 + time_step: 5 + footprint_downsample_factor: 3 + TrajectoryValidator: + # The validator can be configured with additional parameters if needed. + # plugin: "mppi::AutoFixingTrajectoryValidator" + # #consider_footprint: true + # consider_footprint: true + # collision_lookahead_time: 0.8 + # auto_fix_ttc_window: 0.8 + # auto_fix_safe_clearance: 0.04 # ≈ 0.8 * resolution (np. 5cm → 0.04) + # auto_fix_step_gain: 0.6 + # auto_fix_max_step: 0.05 + # auto_fix_max_total_displacement: 0.35 + # auto_fix_max_iters: 6 + # auto_fix_smooth_iters: 2 + plugin: "mppi::DefaultOptimalTrajectoryValidator" + collision_lookahead_time: 0.0 # Time to look ahead to check for collisions + consider_footprint: true # Whether to consider the full robot's footprint (or circle) in trajectory validation collision checks + + #collision_lookahead_time: 0.1 # Time to look ahead to check for collisions + #consider_footprint: true # Whether to consider the full robot's footprint (or circle) in trajectory validation collision checks + AckermannConstraints: + min_turning_r: 0.2 + critics: [ #"ConstraintCritic", + "CostCritic", "GoalCritic", + "GoalAngleCritic", "PathAlignCritic", "PathFollowCritic", + "PathAngleCritic", "PreferForwardCritic"] #,"WaypointCritic"] + ConstraintCritic: + enabled: false + cost_power: 1 + cost_weight: 4.0 + GoalCritic: + enabled: true + cost_power: 1 + cost_weight: 5.0 + threshold_to_consider: 0.5 #0.5 # zmienione z 0.2 + GoalAngleCritic: + enabled: true + cost_power: 1 + cost_weight: 3.0 + threshold_to_consider: 0.3 #0.3 # zmienione z 0.1 PreferForwardCritic: enabled: true cost_power: 1 @@ -183,10 +766,10 @@ controller_server: CostCritic: enabled: true cost_power: 1 - cost_weight: 3.81 + cost_weight: 2.5 # PODNIESC WAGE COST CRITISC near_collision_cost: 253 - critical_cost: 300.0 - consider_footprint: false + critical_cost: 250.0 + consider_footprint: true collision_cost: 1000000.0 near_goal_distance: 1.0 trajectory_point_step: 2 @@ -196,27 +779,51 @@ controller_server: cost_weight: 14.0 max_path_occupancy_ratio: 0.05 trajectory_point_step: 4 - threshold_to_consider: 0.5 - offset_from_furthest: 20 + threshold_to_consider: 0.0 + offset_from_furthest: 10 use_path_orientations: false + visualize_furthest_point: false PathFollowCritic: enabled: true cost_power: 1 - cost_weight: 5.0 - offset_from_furthest: 5 - threshold_to_consider: 1.4 + cost_weight: 4.0 + offset_from_furthest: 3 + threshold_to_consider: 0.0 + visualize_furthest_point: false PathAngleCritic: enabled: true cost_power: 1 - cost_weight: 2.0 + cost_weight: 3.0 offset_from_furthest: 4 - threshold_to_consider: 0.5 - max_angle_to_furthest: 1.0 + threshold_to_consider: 0.0 + max_angle_to_furthest: 0.5 #1.2 + visualize_furthest_point: false + forward_preference: true mode: 0 - # TwirlingCritic: - # enabled: true - # twirling_cost_power: 1 - # twirling_cost_weight: 10.0 + ObstaclesCritic: + enabled: true + cost_power: 1 + repulsion_weight: 0.5 #0.5 0.8 + critical_weight: 15.0 + consider_footprint: true # FALSE # avoid obstacles based on radius not the footprint + collision_cost: 10000.0 + collision_margin_distance: 0.1 + near_goal_distance: 0.2 + cost_scaling_factor: 2.5 + inflation_radius: 1.0 + WaypointCritic: + enabled: false + cost_power: 1 + cost_weight: 5.0 + position_tolerance: 0.1 + yaw_tolerance: 0.1 + waypoint_topic: "truncated_path" + use_orientation: True + max_waypoint_distance: 10.0 + TwirlingCritic: + enabled: false + twirling_cost_power: 1 + twirling_cost_weight: 10.0 local_costmap: local_costmap: @@ -229,9 +836,10 @@ local_costmap: width: 3 height: 3 resolution: 0.05 - robot_radius: 0.22 - plugins: ["voxel_layer", "inflation_layer"] - filters: ["keepout_filter"] + #robot_radius: 0.22 + footprint: "[ [0.28638, 0.274], [0.28638, -0.274], [-0.39762, -0.274], [-0.39762, 0.274] ]" + plugins: ["obstacle_layer", "inflation_layer"] + #filters: ["keepout_filter"] keepout_filter: plugin: "nav2_costmap_2d::KeepoutFilter" enabled: KEEPOUT_ZONE_ENABLED @@ -240,8 +848,31 @@ local_costmap: lethal_override_cost: 200 inflation_layer: plugin: "nav2_costmap_2d::InflationLayer" - cost_scaling_factor: 3.0 - inflation_radius: 0.70 + cost_scaling_factor: 2.5 + inflation_radius: 1.0 + + obstacle_layer: + plugin: "nav2_costmap_2d::ObstacleLayer" + enabled: True + footprint_clearing_enabled: True + observation_sources: scan + scan: + # A relative topic will be appended to the parent of the global_costmap namespace. + # For example: + # * User chosen namespace is `tb4`. + # * User chosen topic is `scan`. + # * Topic will be remapped to `/tb4/scan` without `global_costmap`. + # * Use global topic `/scan` if you do not wish the node namespace to apply + topic: scan + max_obstacle_height: 2.0 + clearing: True + marking: True + data_type: "LaserScan" + raytrace_max_range: 3.0 + raytrace_min_range: 0.0 + obstacle_max_range: 2.5 + obstacle_min_range: 0.0 + voxel_layer: plugin: "nav2_costmap_2d::VoxelLayer" enabled: True @@ -271,6 +902,8 @@ local_costmap: static_layer: plugin: "nav2_costmap_2d::StaticLayer" map_subscribe_transient_local: True + footprint_clearing_enabled: True + restore_cleared_footprint: True always_send_full_costmap: True introspection_mode: "disabled" @@ -281,11 +914,12 @@ global_costmap: publish_frequency: 1.0 global_frame: map robot_base_frame: base_link - robot_radius: 0.22 + #robot_radius: 0.22 + footprint: "[ [0.28638, 0.274], [0.28638, -0.274], [-0.39762, -0.274], [-0.39762, 0.274] ]" resolution: 0.05 track_unknown_space: true plugins: ["static_layer", "obstacle_layer", "inflation_layer"] - filters: ["keepout_filter", "speed_filter"] + #filters: ["keepout_filter", "speed_filter"] keepout_filter: plugin: "nav2_costmap_2d::KeepoutFilter" enabled: KEEPOUT_ZONE_ENABLED @@ -300,6 +934,7 @@ global_costmap: obstacle_layer: plugin: "nav2_costmap_2d::ObstacleLayer" enabled: True + footprint_clearing_enabled: True observation_sources: scan scan: # A relative topic will be appended to the parent of the global_costmap namespace. @@ -323,7 +958,7 @@ global_costmap: inflation_layer: plugin: "nav2_costmap_2d::InflationLayer" cost_scaling_factor: 3.0 - inflation_radius: 0.7 + inflation_radius: 0.55 always_send_full_costmap: True introspection_mode: "disabled" @@ -369,18 +1004,81 @@ map_saver: map_subscribe_transient_local: True introspection_mode: "disabled" +# planner_server: +# ros__parameters: +# expected_planner_frequency: 20.0 +# planner_plugins: ["GridBased"] +# costmap_update_timeout: 1.0 +# introspection_mode: "disabled" +# GridBased: +# plugin: "nav2_smac_planner::SmacPlanner2D" # In Iron and older versions, "/" was used instead of "::" +# tolerance: 0.5 # tolerance for planning if unable to reach exact pose, in meters +# downsample_costmap: false # whether or not to downsample the map +# downsampling_factor: 1 # multiplier for the resolution of the costmap layer (e.g. 2 on a 5cm costmap would be 10cm) +# allow_unknown: true # allow traveling in unknown space +# max_iterations: 1000000 # maximum total iterations to search for before failing (in case unreachable), set to -1 to disable +# max_on_approach_iterations: 1000 # maximum number of iterations to attempt to reach goal once in tolerance +# max_planning_time: 2.0 # max time in s for planner to plan, smooth +# cost_travel_multiplier: 1.0 # Cost multiplier to apply to search to steer away from high cost areas. Larger values will place in the center of aisles more exactly (if non-`FREE` cost potential field exists) but take slightly longer to compute. To optimize for speed, a value of 1.0 is reasonable. A reasonable tradeoff value is 2.0. A value of 0.0 effective disables steering away from obstacles and acts like a naive binary search A*. +# use_final_approach_orientation: false # Whether to set the final path pose at the goal's orientation to the requested orientation (false) or in line with the approach angle so the robot doesn't rotate to heading (true) +# smoother: +# max_iterations: 1000 +# w_smooth: 0.6 +# w_data: 0.2 +# tolerance: 1.0e-10 + planner_server: ros__parameters: - expected_planner_frequency: 20.0 planner_plugins: ["GridBased"] - costmap_update_timeout: 1.0 - introspection_mode: "disabled" + GridBased: - plugin: "nav2_navfn_planner::NavfnPlanner" - tolerance: 0.5 - use_astar: false - allow_unknown: true + plugin: "nav2_smac_planner::SmacPlanner2D" # In Iron and older versions, "/" was used instead of "::" + tolerance: 0.5 # tolerance for planning if unable to reach exact pose, in meters + downsample_costmap: false # whether or not to downsample the map + downsampling_factor: 1 # multiplier for the resolution of the costmap layer (e.g. 2 on a 5cm costmap would be 10cm) + allow_unknown: true # allow traveling in unknown space + max_iterations: 1000000 # maximum total iterations to search for before failing (in case unreachable), set to -1 to disable + max_on_approach_iterations: 1000 # maximum number of iterations to attempt to reach goal once in tolerance + max_planning_time: 2.0 # max time in s for planner to plan, smooth + cost_travel_multiplier: 1.0 # Cost multiplier to apply to search to steer away from high cost areas. Larger values will place in the center of aisles more exactly (if non-`FREE` cost potential field exists) but take slightly longer to compute. To optimize for speed, a value of 1.0 is reasonable. A reasonable tradeoff value is 2.0. A value of 0.0 effective disables steering away from obstacles and acts like a naive binary search A*. + use_final_approach_orientation: false # Whether to set the final path pose at the goal's orientation to the requested orientation (false) or in line with the approach angle so the robot doesn't rotate to heading (true) + smoother: + max_iterations: 1000 + w_smooth: 0.6 + w_data: 0.2 + tolerance: 1.0e-10 + # GridBased: + # plugin: "nav2_smac_planner::SmacPlannerLattice" # In Iron and older versions, "/" was used instead of "::" + # allow_unknown: true # Allow traveling in unknown space + # tolerance: 0.25 # dist-to-goal heuristic cost (distance) for valid tolerance endpoints if exact goal cannot be found. + # max_iterations: 1000000 # Maximum total iterations to search for before failing (in case unreachable), set to -1 to disable + # max_on_approach_iterations: 1000 # Maximum number of iterations after within tolerances to continue to try to find exact solution + # max_planning_time: 5.0 # Max time in s for planner to plan, smooth + # analytic_expansion_ratio: 3.5 # The ratio to attempt analytic expansions during search for final approach. + # analytic_expansion_max_length: 3.0 # For Hybrid/Lattice nodes The maximum length of the analytic expansion to be considered valid to prevent unsafe shortcutting + # analytic_expansion_max_cost: 200.0 # The maximum single cost for any part of an analytic expansion to contain and be valid, except when necessary on approach to goal + # analytic_expansion_max_cost_override: false # Whether or not to override the maximum cost setting if within critical distance to goal (ie probably required) + # reverse_penalty: 2.0 # Penalty to apply if motion is reversing, must be => 1 + # change_penalty: 0.05 # Penalty to apply if motion is changing directions (L to R), must be >= 0 + # non_straight_penalty: 1.05 # Penalty to apply if motion is non-straight, must be => 1 + # cost_penalty: 2.0 # Penalty to apply to higher cost areas when adding into the obstacle map dynamic programming distance expansion heuristic. This drives the robot more towards the center of passages. A value between 1.3 - 3.5 is reasonable. + # rotation_penalty: 0.0 # Penalty to apply to in-place rotations, if minimum control set contains them + # retrospective_penalty: 0.015 + # lattice_filepath: "/home/jakub/ros2_jazzy/src/aros_navigation/aros_navigation/config/lattice_planner.json" # The filepath to the state lattice graph + # lookup_table_size: 20.0 # Size of the dubin/reeds-sheep distance window to cache, in meters. + # cache_obstacle_heuristic: false # Cache the obstacle map dynamic programming distance expansion heuristic between subsequent replannings of the same goal location. Dramatically speeds up replanning performance (40x) if costmap is largely static. + # allow_reverse_expansion: false # If true, allows the robot to use the primitives to expand in the mirrored opposite direction of the current robot's orientation (to reverse). + # coarse_search_resolution: 1 # Number of bins to skip when doing a coarse search for the path. Only used for all_direction goal heading mode. + # goal_heading_mode: "DEFAULT" # DEFAULT, BIDIRECTIONAL, ALL_DIRECTION + # smooth_path: True # If true, does a simple and quick smoothing post-processing to the path + # smoother: + # max_iterations: 1000 + # w_smooth: 0.3 + # w_data: 0.2 + # tolerance: 1.0e-10 + # do_refinement: true + # refinement_num: 2 smoother_server: ros__parameters: smoother_plugins: ["simple_smoother", "route_smoother"] @@ -407,6 +1105,7 @@ behavior_server: global_costmap_topic: global_costmap/costmap_raw local_footprint_topic: local_costmap/published_footprint global_footprint_topic: global_costmap/published_footprint + #enable_stamped_cmd_vel: False cycle_frequency: 10.0 behavior_plugins: ["spin", "backup", "drive_on_heading", "assisted_teleop", "wait"] @@ -482,17 +1181,19 @@ velocity_smoother: odom_duration: 0.1 deadband_velocity: [0.0, 0.0, 0.0] velocity_timeout: 1.0 + #enable_stamped_cmd_vel: False collision_monitor: ros__parameters: base_frame_id: "base_footprint" odom_frame_id: "odom" - cmd_vel_in_topic: "cmd_vel_smoothed" + cmd_vel_in_topic: cmd_vel_smoothed #cmd_vel_smoothed #"cmd_vel_supervised" cmd_vel_out_topic: "cmd_vel" state_topic: "collision_monitor_state" transform_tolerance: 0.2 source_timeout: 1.0 base_shift_correction: True + #enable_stamped_cmd_vel: False stop_pub_timeout: 2.0 # Polygons represent zone around the robot for "stop", "slowdown" and "limit" action types, # and robot footprint for "approach" action type. @@ -505,14 +1206,14 @@ collision_monitor: simulation_time_step: 0.1 min_points: 6 visualize: False - enabled: True + enabled: False observation_sources: ["scan"] scan: type: "scan" topic: "scan" min_height: 0.15 max_height: 2.0 - enabled: True + enabled: False docking_server: ros__parameters: @@ -532,12 +1233,13 @@ docking_server: dock_plugins: ["simple_charging_dock"] simple_charging_dock: plugin: "opennav_docking::SimpleChargingDock" - docking_threshold: 0.05 + docking_threshold: 0.01 staging_x_offset: -0.7 - use_external_detection_pose: true + use_external_detection_pose: false use_battery_status: false # true use_stall_detection: false # true dock_direction: "forward" + rotate_to_dock: False external_detection_timeout: 1.0 external_detection_translation_x: -0.18 @@ -547,6 +1249,19 @@ docking_server: external_detection_rotation_yaw: 0.0 filter_coef: 0.1 + docks: ['home_dock','flex_dock1', 'flex_dock2'] + home_dock: + type: 'simple_charging_dock' + frame: map + pose: [9.65, 7.65, 0.0] + flex_dock1: + type: 'simple_charging_dock' + frame: map + pose: [10.0, 10.0, 0.0] + flex_dock2: + type: 'simple_charging_dock' + frame: map + pose: [30.0, 30.0, 0.0] # Dock instances # The following example illustrates configuring dock instances. # docks: ['home_dock'] # Input your docks here @@ -556,10 +1271,10 @@ docking_server: # pose: [0.0, 0.0, 0.0] controller: - k_phi: 3.0 - k_delta: 2.0 - v_linear_min: 0.15 - v_linear_max: 0.15 + k_phi: 5.0 + k_delta: 5.0 + v_linear_min: 0.03 + v_linear_max: 0.1 use_collision_detection: true costmap_topic: "local_costmap/costmap_raw" footprint_topic: "local_costmap/published_footprint" @@ -567,6 +1282,8 @@ docking_server: projection_time: 5.0 simulation_step: 0.1 dock_collision_threshold: 0.3 + rotate_to_heading_angular_vel: 0.5 + rotate_to_heading_max_angular_accel: 1.0 loopback_simulator: ros__parameters: diff --git a/nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery.xml b/nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery.xml deleted file mode 100644 index 6c32906f4f9..00000000000 --- a/nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_copy.xml b/nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_copy.xml new file mode 100644 index 00000000000..01c9d8100d2 --- /dev/null +++ b/nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_copy.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_copy_test_replanning.xml b/nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_copy_test_replanning.xml new file mode 100644 index 00000000000..210d0f7ec88 --- /dev/null +++ b/nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_copy_test_replanning.xml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_if_invalid.xml b/nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_if_invalid.xml new file mode 100644 index 00000000000..91192bf0480 --- /dev/null +++ b/nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_if_invalid.xml @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_if_invalid_copy copy 2.xml b/nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_if_invalid_copy copy 2.xml new file mode 100644 index 00000000000..290cb7607d1 --- /dev/null +++ b/nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_if_invalid_copy copy 2.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_if_invalid_copy copy.xml b/nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_if_invalid_copy copy.xml new file mode 100644 index 00000000000..627b183a8da --- /dev/null +++ b/nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_if_invalid_copy copy.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_if_invalid_copy.xml b/nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_if_invalid_copy.xml new file mode 100644 index 00000000000..1e8079d8035 --- /dev/null +++ b/nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery_if_invalid_copy.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nav2_costmap_2d/include/nav2_costmap_2d/footprint_collision_checker copy.hpp b/nav2_costmap_2d/include/nav2_costmap_2d/footprint_collision_checker copy.hpp new file mode 100644 index 00000000000..36b16755846 --- /dev/null +++ b/nav2_costmap_2d/include/nav2_costmap_2d/footprint_collision_checker copy.hpp @@ -0,0 +1,94 @@ +// Copyright (c) 2019 Intel Corporation +// +// 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. +// +// Modified by: Shivang Patel (shivaang14@gmail.com) + +#ifndef NAV2_COSTMAP_2D__FOOTPRINT_COLLISION_CHECKER_HPP_ +#define NAV2_COSTMAP_2D__FOOTPRINT_COLLISION_CHECKER_HPP_ + +#include +#include +#include +#include + +#include "rclcpp/rclcpp.hpp" +#include "geometry_msgs/msg/pose_stamped.hpp" +#include "geometry_msgs/msg/pose.hpp" +#include "nav2_costmap_2d/costmap_2d.hpp" +#include "nav2_util/robot_utils.hpp" + +namespace nav2_costmap_2d +{ +typedef std::vector Footprint; + +/** + * @class FootprintCollisionChecker + * @brief Checker for collision with a footprint on a costmap + */ +template +class FootprintCollisionChecker +{ +public: + /** + * @brief A constructor. + */ + FootprintCollisionChecker(); + /** + * @brief A constructor. + */ + explicit FootprintCollisionChecker(CostmapT costmap); + /** + * @brief Find the footprint cost in oriented footprint + * @param footprint The footprint to check + * @param check_full_area If true, checks the full area after perimeter check (default: false) + * @return The maximum cost found (perimeter only or full area depending on parameter) + */ + double footprintCost(const Footprint & footprint, bool check_full_area = false); + /** + * @brief Find the footprint cost a a post with an unoriented footprint + */ + double footprintCostAtPose( + double x, double y, double theta, const Footprint & footprint, + bool check_full_area = false); + /** + * @brief Get the cost for a line segment + */ + double lineCost(int x0, int x1, int y0, int y1) const; + /** + * @brief Get the map coordinates from a world point + */ + bool worldToMap(double wx, double wy, unsigned int & mx, unsigned int & my); + /** + * @brief Get the cost of a point + */ + double pointCost(int x, int y) const; + /** + * @brief Set the current costmap object to use for collision detection + */ + void setCostmap(CostmapT costmap); + /** + * @brief Get the current costmap object + */ + CostmapT getCostmap() + { + return costmap_; + } + +protected: + CostmapT costmap_; +}; + +} // namespace nav2_costmap_2d + +#endif // NAV2_COSTMAP_2D__FOOTPRINT_COLLISION_CHECKER_HPP_ diff --git a/nav2_costmap_2d/include/nav2_costmap_2d/obstacle_layer.hpp b/nav2_costmap_2d/include/nav2_costmap_2d/obstacle_layer.hpp index 32b17ca5d5b..9e035eb6aeb 100644 --- a/nav2_costmap_2d/include/nav2_costmap_2d/obstacle_layer.hpp +++ b/nav2_costmap_2d/include/nav2_costmap_2d/obstacle_layer.hpp @@ -220,6 +220,7 @@ class ObstacleLayer : public CostmapLayer std::vector transformed_footprint_; bool footprint_clearing_enabled_; + double footprint_clearing_padding_; /** * @brief Clear costmap layer info below the robot's footprint */ diff --git a/nav2_costmap_2d/include/nav2_costmap_2d/static_layer.hpp b/nav2_costmap_2d/include/nav2_costmap_2d/static_layer.hpp index 0333f03577b..6ffdbca4980 100644 --- a/nav2_costmap_2d/include/nav2_costmap_2d/static_layer.hpp +++ b/nav2_costmap_2d/include/nav2_costmap_2d/static_layer.hpp @@ -172,6 +172,7 @@ class StaticLayer : public CostmapLayer std::vector transformed_footprint_; bool footprint_clearing_enabled_; bool restore_cleared_footprint_; + double footprint_clearing_padding_; /** * @brief Clear costmap layer info below the robot's footprint */ diff --git a/nav2_costmap_2d/plugins/obstacle_layer.cpp b/nav2_costmap_2d/plugins/obstacle_layer.cpp index c09d60da61b..352a58f563d 100644 --- a/nav2_costmap_2d/plugins/obstacle_layer.cpp +++ b/nav2_costmap_2d/plugins/obstacle_layer.cpp @@ -89,6 +89,7 @@ void ObstacleLayer::onInitialize() declareParameter("max_obstacle_height", rclcpp::ParameterValue(2.0)); declareParameter("combination_method", rclcpp::ParameterValue(1)); declareParameter("observation_sources", rclcpp::ParameterValue(std::string(""))); + declareParameter("footprint_clearing_padding", rclcpp::ParameterValue(0.0)); auto node = node_.lock(); if (!node) { @@ -97,6 +98,7 @@ void ObstacleLayer::onInitialize() node->get_parameter(name_ + "." + "enabled", enabled_); node->get_parameter(name_ + "." + "footprint_clearing_enabled", footprint_clearing_enabled_); + node->get_parameter(name_ + "." + "footprint_clearing_padding", footprint_clearing_padding_); node->get_parameter(name_ + "." + "min_obstacle_height", min_obstacle_height_); node->get_parameter(name_ + "." + "max_obstacle_height", max_obstacle_height_); node->get_parameter("track_unknown_space", track_unknown_space); @@ -587,7 +589,14 @@ ObstacleLayer::updateFootprint( double * max_y) { if (!footprint_clearing_enabled_) {return;} - transformFootprint(robot_x, robot_y, robot_yaw, getFootprint(), transformed_footprint_); + + // Get the base footprint and apply padding before transformation + std::vector padded_footprint = getFootprint(); + if (footprint_clearing_padding_ > 0.0) { + padFootprint(padded_footprint, footprint_clearing_padding_); + } + // Transform the padded footprint to the robot's current pose + transformFootprint(robot_x, robot_y, robot_yaw, padded_footprint, transformed_footprint_); for (unsigned int i = 0; i < transformed_footprint_.size(); i++) { touch(transformed_footprint_[i].x, transformed_footprint_[i].y, min_x, min_y, max_x, max_y); diff --git a/nav2_costmap_2d/plugins/static_layer.cpp b/nav2_costmap_2d/plugins/static_layer.cpp index 94aa69f87f8..18a0eadb153 100644 --- a/nav2_costmap_2d/plugins/static_layer.cpp +++ b/nav2_costmap_2d/plugins/static_layer.cpp @@ -139,6 +139,7 @@ StaticLayer::getParameters() declareParameter("map_topic", rclcpp::ParameterValue("map")); declareParameter("footprint_clearing_enabled", rclcpp::ParameterValue(false)); declareParameter("restore_cleared_footprint", rclcpp::ParameterValue(true)); + declareParameter("footprint_clearing_padding", rclcpp::ParameterValue(0.0)); auto node = node_.lock(); if (!node) { @@ -149,6 +150,7 @@ StaticLayer::getParameters() node->get_parameter(name_ + "." + "subscribe_to_updates", subscribe_to_updates_); node->get_parameter(name_ + "." + "footprint_clearing_enabled", footprint_clearing_enabled_); node->get_parameter(name_ + "." + "restore_cleared_footprint", restore_cleared_footprint_); + node->get_parameter(name_ + "." + "footprint_clearing_padding", footprint_clearing_padding_); node->get_parameter(name_ + "." + "map_topic", map_topic_); map_topic_ = joinWithParentNamespace(map_topic_); node->get_parameter( @@ -386,8 +388,13 @@ StaticLayer::updateFootprint( double * max_y) { if (!footprint_clearing_enabled_) {return;} - - transformFootprint(robot_x, robot_y, robot_yaw, getFootprint(), transformed_footprint_); + // Get the base footprint and apply padding before transformation + std::vector padded_footprint = getFootprint(); + if (footprint_clearing_padding_ > 0.0) { + padFootprint(padded_footprint, footprint_clearing_padding_); + } + // Transform the padded footprint to the robot's current pose + transformFootprint(robot_x, robot_y, robot_yaw, padded_footprint, transformed_footprint_); for (unsigned int i = 0; i < transformed_footprint_.size(); i++) { touch(transformed_footprint_[i].x, transformed_footprint_[i].y, min_x, min_y, max_x, max_y); @@ -511,6 +518,8 @@ StaticLayer::dynamicParametersCallback( } else if (param_type == ParameterType::PARAMETER_DOUBLE) { if (param_name == name_ + "." + "transform_tolerance") { transform_tolerance_ = tf2::durationFromSec(parameter.as_double()); + } else if (param_name == name_ + "." + "footprint_clearing_padding") { + footprint_clearing_padding_ = parameter.as_double(); } } else if (param_type == ParameterType::PARAMETER_BOOL) { if (param_name == name_ + "." + "enabled" && enabled_ != parameter.as_bool()) { diff --git a/nav2_costmap_2d/src/footprint_collision_checker copy.cpp b/nav2_costmap_2d/src/footprint_collision_checker copy.cpp new file mode 100644 index 00000000000..da234601a05 --- /dev/null +++ b/nav2_costmap_2d/src/footprint_collision_checker copy.cpp @@ -0,0 +1,268 @@ +// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2025 Angsa Robotics +// +// 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. +// +// Modified by: Shivang Patel (shivaang14@gmail.com) + +#include +#include +#include +#include +#include + +#include "nav2_costmap_2d/footprint_collision_checker.hpp" + +#include "nav2_costmap_2d/cost_values.hpp" +#include "nav2_costmap_2d/exceptions.hpp" +#include "nav2_costmap_2d/footprint.hpp" +#include "nav2_util/line_iterator.hpp" + +using namespace std::chrono_literals; + +namespace nav2_costmap_2d +{ + +// Simple 2D point structure +struct Point2D +{ + int x, y; + Point2D(int x_val, int y_val) + : x(x_val), y(y_val) {} +}; + +// Simple rectangle structure +struct Rectangle +{ + int x, y, width, height; + Rectangle(int x_val, int y_val, int w, int h) + : x(x_val), y(y_val), width(w), height(h) {} +}; + +// Calculate bounding rectangle for a set of points +Rectangle calculateBoundingRect(const std::vector & points) +{ + if (points.empty()) { + return Rectangle(0, 0, 0, 0); + } + + int min_x = points[0].x; + int max_x = points[0].x; + int min_y = points[0].y; + int max_y = points[0].y; + + for (const auto & pt : points) { + min_x = std::min(min_x, pt.x); + max_x = std::max(max_x, pt.x); + min_y = std::min(min_y, pt.y); + max_y = std::max(max_y, pt.y); + } + + return Rectangle(min_x, min_y, max_x - min_x + 1, max_y - min_y + 1); +} + +// Point-in-polygon test using ray casting algorithm +bool isPointInPolygon(int x, int y, const std::vector & polygon) +{ + if (polygon.size() < 3) { + return false; + } + + bool inside = false; + size_t j = polygon.size() - 1; + + for (size_t i = 0; i < polygon.size(); i++) { + if (((polygon[i].y > y) != (polygon[j].y > y)) && + (x < + (polygon[j].x - polygon[i].x) * (y - polygon[i].y) / (polygon[j].y - polygon[i].y) + + polygon[i].x)) + { + inside = !inside; + } + j = i; + } + + return inside; +} + +template +FootprintCollisionChecker::FootprintCollisionChecker() +: costmap_(nullptr) +{ +} + +template +FootprintCollisionChecker::FootprintCollisionChecker( + CostmapT costmap) +: costmap_(costmap) +{ +} + +template +double FootprintCollisionChecker::footprintCost( + const Footprint & footprint, + bool check_full_area) +{ + if (footprint.empty()) { + return static_cast(NO_INFORMATION); + } + + // Pre-convert all footprint points to map coordinates once + std::vector polygon_points; + polygon_points.reserve(footprint.size()); + + for (const auto & point : footprint) { + unsigned int mx, my; + if (!worldToMap(point.x, point.y, mx, my)) { + return static_cast(NO_INFORMATION); + } + polygon_points.emplace_back(static_cast(mx), static_cast(my)); + } + + // Check perimeter using pre-converted coordinates + double perimeter_cost = 0.0; + for (size_t i = 0; i < polygon_points.size(); ++i) { + size_t next_i = (i + 1) % polygon_points.size(); + double line_cost = lineCost( + polygon_points[i].x, polygon_points[next_i].x, + polygon_points[i].y, polygon_points[next_i].y); + + perimeter_cost = std::max(perimeter_cost, line_cost); + + // Early termination if lethal obstacle found + if (perimeter_cost == static_cast(LETHAL_OBSTACLE)) { + return perimeter_cost; + } + } + + // If perimeter check found collision or full area check not requested, return perimeter result + if (perimeter_cost == static_cast(LETHAL_OBSTACLE) || !check_full_area) { + return perimeter_cost; + } + + // If no collision on perimeter and full area check requested, rasterize the full area + Rectangle bbox = calculateBoundingRect(polygon_points); + + // Clamp bounding box to costmap dimensions for safety + unsigned int costmap_width = costmap_->getSizeInCellsX(); + unsigned int costmap_height = costmap_->getSizeInCellsY(); + + int min_x = std::max(0, bbox.x); + int min_y = std::max(0, bbox.y); + int max_x = std::min(static_cast(costmap_width - 1), bbox.x + bbox.width - 1); + int max_y = std::min(static_cast(costmap_height - 1), bbox.y + bbox.height - 1); + + // Translate polygon points to bounding box coordinates + std::vector bbox_polygon; + bbox_polygon.reserve(polygon_points.size()); + for (const auto & pt : polygon_points) { + bbox_polygon.emplace_back(pt.x - bbox.x, pt.y - bbox.y); + } + + double max_cost = perimeter_cost; + + // Iterate through the clamped bounding box and check costs only for cells inside the polygon + for (int y = min_y; y <= max_y; ++y) { + for (int x = min_x; x <= max_x; ++x) { + // Convert to bounding box coordinates for polygon test + int bbox_x = x - bbox.x; + int bbox_y = y - bbox.y; + + if (isPointInPolygon(bbox_x, bbox_y, bbox_polygon)) { + double cell_cost = pointCost(x, y); + + // Early termination if lethal obstacle found + if (cell_cost == static_cast(LETHAL_OBSTACLE)) { + return cell_cost; + } + + max_cost = std::max(max_cost, cell_cost); + } + } + } + + return max_cost; +} + +template +double FootprintCollisionChecker::lineCost(int x0, int x1, int y0, int y1) const +{ + double line_cost = 0.0; + double point_cost = -1.0; + + for (nav2_util::LineIterator line(x0, y0, x1, y1); line.isValid(); line.advance()) { + point_cost = pointCost(line.getX(), line.getY()); // Score the current point + + // if in collision, no need to continue + if (point_cost == static_cast(LETHAL_OBSTACLE)) { + return point_cost; + } + + if (line_cost < point_cost) { + line_cost = point_cost; + } + } + + return line_cost; +} + +template +bool FootprintCollisionChecker::worldToMap( + double wx, double wy, unsigned int & mx, unsigned int & my) +{ + return costmap_->worldToMap(wx, wy, mx, my); +} + +template +double FootprintCollisionChecker::pointCost(int x, int y) const +{ + // Bounds checking to prevent segmentation faults + if (x < 0 || y < 0 || + x >= static_cast(costmap_->getSizeInCellsX()) || + y >= static_cast(costmap_->getSizeInCellsY())) + { + return static_cast(LETHAL_OBSTACLE); + } + + return static_cast(costmap_->getCost(x, y)); +} + +template +void FootprintCollisionChecker::setCostmap(CostmapT costmap) +{ + costmap_ = costmap; +} + +template +double FootprintCollisionChecker::footprintCostAtPose( + double x, double y, double theta, const Footprint & footprint, bool check_full_area) +{ + double cos_th = cos(theta); + double sin_th = sin(theta); + Footprint oriented_footprint; + oriented_footprint.reserve(footprint.size()); + geometry_msgs::msg::Point new_pt; + for (unsigned int i = 0; i < footprint.size(); ++i) { + new_pt.x = x + (footprint[i].x * cos_th - footprint[i].y * sin_th); + new_pt.y = y + (footprint[i].x * sin_th + footprint[i].y * cos_th); + oriented_footprint.push_back(new_pt); + } + + return footprintCost(oriented_footprint, check_full_area); +} + +// declare our valid template parameters +template class FootprintCollisionChecker>; +template class FootprintCollisionChecker; + +} // namespace nav2_costmap_2d diff --git a/nav2_costmap_2d/test/unit/footprint_collision_checker_test copy.cpp b/nav2_costmap_2d/test/unit/footprint_collision_checker_test copy.cpp new file mode 100644 index 00000000000..c11fbb14d52 --- /dev/null +++ b/nav2_costmap_2d/test/unit/footprint_collision_checker_test copy.cpp @@ -0,0 +1,793 @@ +// Copyright (c) 2020 Shivang Patel +// +// 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 +#include +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "nav2_costmap_2d/footprint_collision_checker.hpp" +#include "nav2_costmap_2d/footprint.hpp" +#include "nav2_costmap_2d/cost_values.hpp" + +// Test basic footprint collision checking with no obstacles +// Verifies that a diamond-shaped footprint placed on an empty costmap returns zero cost +TEST(collision_footprint, test_basic) +{ + // Create a 100x100 costmap with 0.1m resolution, all cells initialized to cost 0 + std::shared_ptr costmap_ = + std::make_shared(100, 100, 0.1, 0, 0, 0); + + // Create a diamond-shaped footprint + geometry_msgs::msg::Point p1; + p1.x = -0.5; // Left vertex + p1.y = 0.0; + geometry_msgs::msg::Point p2; + p2.x = 0.0; // Top vertex + p2.y = 0.5; + geometry_msgs::msg::Point p3; + p3.x = 0.5; // Right vertex + p3.y = 0.0; + geometry_msgs::msg::Point p4; + p4.x = 0.0; // Bottom vertex + p4.y = -0.5; + + nav2_costmap_2d::Footprint footprint = {p1, p2, p3, p4}; + + nav2_costmap_2d::FootprintCollisionChecker> + collision_checker(costmap_); + + // Check footprint cost at position (5.0, 5.0) with no rotation + // Should return 0 since no obstacles are present + auto value = collision_checker.footprintCostAtPose(5.0, 5.0, 0.0, footprint); + + EXPECT_NEAR(value, 0.0, 0.001); +} + +// Test point cost functionality +// Verifies that the pointCost method correctly returns the cost value at a specific grid cell +TEST(collision_footprint, test_point_cost) +{ + // Create empty costmap with all cells at cost 0 + std::shared_ptr costmap_ = + std::make_shared(100, 100, 0.1, 0, 0, 0); + + nav2_costmap_2d::FootprintCollisionChecker> + collision_checker(costmap_); + + // Test point cost at grid cell (50, 50) - should be 0 for empty costmap + auto value = collision_checker.pointCost(50, 50); + + EXPECT_NEAR(value, 0.0, 0.001); +} + +// Test world-to-map coordinate conversion and point cost retrieval +// Verifies that world coordinates are correctly converted to map coordinates +// and that costs can be retrieved and set at those locations +TEST(collision_footprint, test_world_to_map) +{ + std::shared_ptr costmap_ = + std::make_shared(100, 100, 0.1, 0, 0, 0); + + nav2_costmap_2d::FootprintCollisionChecker> + collision_checker(costmap_); + + unsigned int x, y; + + // Convert world coordinates (1.0, 1.0) to map coordinates + // With 0.1m resolution and origin at (0,0), this should map to grid cell (10, 10) + collision_checker.worldToMap(1.0, 1.0, x, y); + + // Check that the cost at the converted coordinates is 0 (empty space) + auto value = collision_checker.pointCost(x, y); + EXPECT_NEAR(value, 0.0, 0.001); + + // Set a cost of 200 at grid cell (50, 50) and verify it can be retrieved + costmap_->setCost(50, 50, 200); + collision_checker.worldToMap(5.0, 5.0, x, y); // World (5.0, 5.0) -> Grid (50, 50) + + EXPECT_NEAR(collision_checker.pointCost(x, y), 200.0, 0.001); +} + +// Test footprint collision detection when robot moves into obstacle areas +// Creates a costmap with obstacles everywhere except a safe zone, then tests +// footprint placement at different positions to verify collision detection +TEST(collision_footprint, test_footprint_at_pose_with_movement) +{ + // Create costmap with all cells set to cost 254 (high obstacle cost) + std::shared_ptr costmap_ = + std::make_shared(100, 100, 0.1, 0, 0, 254); + + // Create a safe zone (cost 0) in the center of the map from grid (40,40) to (60,60) + // This corresponds to world coordinates roughly (4.0,4.0) to (6.0,6.0) + for (unsigned int i = 40; i <= 60; ++i) { + for (unsigned int j = 40; j <= 60; ++j) { + costmap_->setCost(i, j, 0); + } + } + + // Create a 2x2 meter square footprint + geometry_msgs::msg::Point p1; + p1.x = -1.0; // Bottom-left + p1.y = 1.0; + geometry_msgs::msg::Point p2; + p2.x = 1.0; // Bottom-right + p2.y = 1.0; + geometry_msgs::msg::Point p3; + p3.x = 1.0; // Top-right + p3.y = -1.0; + geometry_msgs::msg::Point p4; + p4.x = -1.0; // Top-left + p4.y = -1.0; + + nav2_costmap_2d::Footprint footprint = {p1, p2, p3, p4}; + + nav2_costmap_2d::FootprintCollisionChecker> + collision_checker(costmap_); + + // Test footprint at center of safe zone - should have zero cost + auto value = collision_checker.footprintCostAtPose(5.0, 5.0, 0.0, footprint); + EXPECT_NEAR(value, 0.0, 0.001); + + // Test footprint moved slightly up - should hit obstacle area (cost 254) + auto up_value = collision_checker.footprintCostAtPose(5.0, 4.9, 0.0, footprint); + EXPECT_NEAR(up_value, 254.0, 0.001); + + // Test footprint moved slightly down - should hit obstacle area (cost 254) + auto down_value = collision_checker.footprintCostAtPose(5.0, 5.2, 0.0, footprint); + EXPECT_NEAR(down_value, 254.0, 0.001); +} + +// Test point and line cost detection with strategically placed obstacles +// Places obstacles at specific locations and verifies they are detected when the +// footprint perimeter intersects them during different robot movements +TEST(collision_footprint, test_point_and_line_cost) +{ + std::shared_ptr costmap_ = + std::make_shared(100, 100, 0.10000, 0, 0.0, 0.0); + + // Place obstacles at strategic locations: + costmap_->setCost(62, 50, 254); // Obstacle to the right of center + costmap_->setCost(39, 60, 254); // Obstacle to the left and up from center + + // Create a 2x2 meter square footprint + geometry_msgs::msg::Point p1; + p1.x = -1.0; + p1.y = 1.0; + geometry_msgs::msg::Point p2; + p2.x = 1.0; + p2.y = 1.0; + geometry_msgs::msg::Point p3; + p3.x = 1.0; + p3.y = -1.0; + geometry_msgs::msg::Point p4; + p4.x = -1.0; + p4.y = -1.0; + + nav2_costmap_2d::Footprint footprint = {p1, p2, p3, p4}; + + nav2_costmap_2d::FootprintCollisionChecker> + collision_checker(costmap_); + + // Test footprint at center position - should not hit obstacles + auto value = collision_checker.footprintCostAtPose(5.0, 5.0, 0.0, footprint); + EXPECT_NEAR(value, 0.0, 0.001); + + // Move footprint left - should detect obstacle at (39, 60) + auto left_value = collision_checker.footprintCostAtPose(4.9, 5.0, 0.0, footprint); + EXPECT_NEAR(left_value, 254.0, 0.001); + + // Move footprint right - should detect obstacle at (62, 50) + auto right_value = collision_checker.footprintCostAtPose(5.2, 5.0, 0.0, footprint); + EXPECT_NEAR(right_value, 254.0, 0.001); +} + +// Test utility function for footprints with insufficient points +// Verifies that distance calculation functions handle edge cases correctly +// when footprint has fewer than 3 points (minimum for a valid polygon) +TEST(collision_footprint, not_enough_points) +{ + geometry_msgs::msg::Point p1; + p1.x = 2.0; + p1.y = 2.0; + + geometry_msgs::msg::Point p2; + p2.x = -2.0; + p2.y = -2.0; + + // Create footprint with only 2 points (insufficient for polygon) + std::vector footprint = {p1, p2}; + double min_dist = 0.0; + double max_dist = 0.0; + + // calculateMinAndMaxDistances should handle this gracefully + std::tie(min_dist, max_dist) = nav2_costmap_2d::calculateMinAndMaxDistances(footprint); + EXPECT_EQ(min_dist, std::numeric_limits::max()); + EXPECT_EQ(max_dist, 0.0f); +} + +// Test conversion from Point to Point32 message types +// Verifies that geometry message type conversions preserve coordinate values +TEST(collision_footprint, to_point_32) { + geometry_msgs::msg::Point p; + p.x = 123.0; + p.y = 456.0; + p.z = 789.0; + + // Convert to Point32 format + geometry_msgs::msg::Point32 p32; + p32 = nav2_costmap_2d::toPoint32(p); + + // Verify all coordinates are preserved + EXPECT_NEAR(p.x, p32.x, 1e-5); + EXPECT_NEAR(p.y, p32.y, 1e-5); + EXPECT_NEAR(p.z, p32.z, 1e-5); +} + +// Test conversion from vector of Points to Polygon message type +// Verifies that footprint data can be converted to ROS message format +TEST(collision_footprint, to_polygon) { + geometry_msgs::msg::Point p1; + p1.x = 1.2; + p1.y = 3.4; + p1.z = 5.1; + + geometry_msgs::msg::Point p2; + p2.x = -5.6; + p2.y = -7.8; + p2.z = -9.1; + std::vector pts = {p1, p2}; + + // Convert to Polygon message format + geometry_msgs::msg::Polygon poly; + poly = nav2_costmap_2d::toPolygon(pts); + + // Verify structure and coordinate preservation + EXPECT_EQ(2u, sizeof(poly.points) / sizeof(poly.points[0])); + EXPECT_NEAR(poly.points[0].x, p1.x, 1e-5); + EXPECT_NEAR(poly.points[0].y, p1.y, 1e-5); + EXPECT_NEAR(poly.points[0].z, p1.z, 1e-5); + EXPECT_NEAR(poly.points[1].x, p2.x, 1e-5); + EXPECT_NEAR(poly.points[1].y, p2.y, 1e-5); + EXPECT_NEAR(poly.points[1].z, p2.z, 1e-5); +} + +// Test successful parsing of footprint from string representation +// Verifies that string-based footprint configuration works correctly +TEST(collision_footprint, make_footprint_from_string) { + std::vector footprint; + + // Parse a valid footprint string with scientific notation + bool result = nav2_costmap_2d::makeFootprintFromString( + "[[1, 2.2], [.3, -4e4], [-.3, -4e4], [-1, 2.2]]", footprint); + + EXPECT_EQ(result, true); + EXPECT_EQ(4u, footprint.size()); + + // Verify all coordinate values are parsed correctly + EXPECT_NEAR(footprint[0].x, 1.0, 1e-5); + EXPECT_NEAR(footprint[0].y, 2.2, 1e-5); + EXPECT_NEAR(footprint[1].x, 0.3, 1e-5); + EXPECT_NEAR(footprint[1].y, -4e4, 1e-5); + EXPECT_NEAR(footprint[2].x, -0.3, 1e-5); + EXPECT_NEAR(footprint[2].y, -4e4, 1e-5); + EXPECT_NEAR(footprint[3].x, -1.0, 1e-5); + EXPECT_NEAR(footprint[3].y, 2.2, 1e-5); +} + +// Test error handling for malformed footprint strings +// Verifies that parsing fails gracefully with invalid input +TEST(collision_footprint, make_footprint_from_string_parse_error) { + std::vector footprint; + + // Try to parse a malformed string (missing closing bracket) + bool result = nav2_costmap_2d::makeFootprintFromString( + "[[bad_string", footprint); + EXPECT_EQ(result, false); +} + +// Test error handling for footprints with too few points +// Verifies that polygons with insufficient vertices are rejected +TEST(collision_footprint, make_footprint_from_string_two_points_error) { + std::vector footprint; + + // Try to parse footprint with only 2 points (need at least 3 for polygon) + bool result = nav2_costmap_2d::makeFootprintFromString( + "[[1, 2.2], [.3, -4e4]", footprint); + EXPECT_EQ(result, false); +} + +// Test error handling for points with wrong number of coordinates +// Verifies that points with extra coordinates are rejected +TEST(collision_footprint, make_footprint_from_string_not_pairs) { + std::vector footprint; + + // Try to parse footprint with a point having 3 coordinates (should be 2) + bool result = nav2_costmap_2d::makeFootprintFromString( + "[[1, 2.2], [.3, -4e4], [-.3, -4e4], [-1, 2.2, 5.6]]", footprint); + EXPECT_EQ(result, false); +} + +// Test full area checking functionality when no obstacles are present +// Verifies that both perimeter-only and full-area checking return the same result +// when the footprint area is completely free of obstacles +TEST(collision_footprint, test_full_area_check_no_collision) { + std::shared_ptr costmap_ = + std::make_shared(100, 100, 0.1, 0, 0, 0); + + // Create a 2x2 meter rectangular footprint + geometry_msgs::msg::Point p1; + p1.x = -1.0; // Bottom-left + p1.y = -1.0; + geometry_msgs::msg::Point p2; + p2.x = 1.0; // Bottom-right + p2.y = -1.0; + geometry_msgs::msg::Point p3; + p3.x = 1.0; // Top-right + p3.y = 1.0; + geometry_msgs::msg::Point p4; + p4.x = -1.0; // Top-left + p4.y = 1.0; + + nav2_costmap_2d::Footprint footprint = {p1, p2, p3, p4}; + + nav2_costmap_2d::FootprintCollisionChecker> + collision_checker(costmap_); + + // Test perimeter only check at a valid position (5.0, 5.0) within the 10x10m costmap + auto perimeter_cost = collision_checker.footprintCostAtPose(5.0, 5.0, 0.0, footprint, false); + EXPECT_NEAR(perimeter_cost, 0.0, 0.001); + + // Test full area check at the same position + auto full_area_cost = collision_checker.footprintCostAtPose(5.0, 5.0, 0.0, footprint, true); + EXPECT_NEAR(full_area_cost, 0.0, 0.001); + + // Both should be the same when no obstacles present + EXPECT_NEAR(perimeter_cost, full_area_cost, 0.001); +} + +// Test that full area checking detects obstacles inside the footprint +// Obstacles that are inside the footprint but not on the perimeter +// should only be detected by full area checking +TEST(collision_footprint, test_full_area_check_interior_obstacle) { + std::shared_ptr costmap_ = + std::make_shared(100, 100, 0.1, 0, 0, 0); + + // Create a 2x2 meter rectangular footprint that will be centered at (5.0, 5.0) + // This maps to grid coordinates around (50, 50) with the 0.1m resolution + geometry_msgs::msg::Point p1; + p1.x = -1.0; + p1.y = -1.0; + geometry_msgs::msg::Point p2; + p2.x = 1.0; + p2.y = -1.0; + geometry_msgs::msg::Point p3; + p3.x = 1.0; + p3.y = 1.0; + geometry_msgs::msg::Point p4; + p4.x = -1.0; + p4.y = 1.0; + + nav2_costmap_2d::Footprint footprint = {p1, p2, p3, p4}; + + // Place an obstacle in the interior of the footprint (not on perimeter) + // Grid cell (50, 50) corresponds to world coordinates (5.0, 5.0) - center of footprint + costmap_->setCost(50, 50, 200); + + nav2_costmap_2d::FootprintCollisionChecker> + collision_checker(costmap_); + + // Test perimeter only check - should NOT detect interior obstacle + auto perimeter_cost = collision_checker.footprintCostAtPose(5.0, 5.0, 0.0, footprint, false); + EXPECT_NEAR(perimeter_cost, 0.0, 0.001); + + // Test full area check - SHOULD detect interior obstacle + auto full_area_cost = collision_checker.footprintCostAtPose(5.0, 5.0, 0.0, footprint, true); + EXPECT_NEAR(full_area_cost, 200.0, 0.001); +} + +// Test full area checking with lethal obstacles for early termination +// Verifies that when a lethal obstacle is found during full area checking, +// the algorithm returns immediately with lethal cost (performance optimization) +TEST(collision_footprint, test_full_area_check_lethal_obstacle) { + std::shared_ptr costmap_ = + std::make_shared(100, 100, 0.1, 0, 0, 0); + + // Create a triangular footprint (simpler shape for testing) + geometry_msgs::msg::Point p1; + p1.x = 0.0; // Top vertex + p1.y = 1.0; + geometry_msgs::msg::Point p2; + p2.x = -1.0; // Bottom-left vertex + p2.y = -1.0; + geometry_msgs::msg::Point p3; + p3.x = 1.0; // Bottom-right vertex + p3.y = -1.0; + + nav2_costmap_2d::Footprint footprint = {p1, p2, p3}; + + // Place a lethal obstacle in the interior of the triangle + // Grid cell (50, 49) should be inside the triangle when centered at (5.0, 5.0) + costmap_->setCost(50, 49, nav2_costmap_2d::LETHAL_OBSTACLE); + + nav2_costmap_2d::FootprintCollisionChecker> + collision_checker(costmap_); + + // Test full area check with lethal obstacle - should return lethal cost immediately + auto full_area_cost = collision_checker.footprintCostAtPose(5.0, 5.0, 0.0, footprint, true); + EXPECT_NEAR(full_area_cost, static_cast(nav2_costmap_2d::LETHAL_OBSTACLE), 0.001); +} + +// Test full area checking with rotated footprints +// Verifies that the full area checking works correctly when the footprint +// is rotated, ensuring the OpenCV polygon rasterization handles rotation properly +TEST(collision_footprint, test_full_area_check_rotated_footprint) { + std::shared_ptr costmap_ = + std::make_shared(100, 100, 0.1, 0, 0, 0); + + // Create a rectangular footprint that's taller than it is wide + // This will test rotation effects more clearly + geometry_msgs::msg::Point p1; + p1.x = -0.5; // Left edge + p1.y = -1.5; // Bottom edge (tall rectangle) + geometry_msgs::msg::Point p2; + p2.x = 0.5; // Right edge + p2.y = -1.5; + geometry_msgs::msg::Point p3; + p3.x = 0.5; + p3.y = 1.5; // Top edge + geometry_msgs::msg::Point p4; + p4.x = -0.5; + p4.y = 1.5; + + nav2_costmap_2d::Footprint footprint = {p1, p2, p3, p4}; + + // Place obstacle where it would be inside the footprint when rotated 90 degrees + // Grid cell (52, 50) should be inside the rotated footprint + costmap_->setCost(52, 50, 150); + + nav2_costmap_2d::FootprintCollisionChecker> + collision_checker(costmap_); + + // Test with 90-degree rotation (M_PI/2 radians) + // The tall rectangle becomes a wide rectangle, covering the obstacle + auto rotated_cost = collision_checker.footprintCostAtPose(5.0, 5.0, M_PI / 2, footprint, true); + EXPECT_GT(rotated_cost, 0.0); // Should detect the obstacle +} + +// Test the lineCost function directly +// Verifies that line cost calculation works correctly for line segments, +// including early termination when lethal obstacles are encountered +TEST(collision_footprint, test_line_cost_function) { + std::shared_ptr costmap_ = + std::make_shared(100, 100, 0.1, 0, 0, 0); + + nav2_costmap_2d::FootprintCollisionChecker> + collision_checker(costmap_); + + // Test line cost with no obstacles - should return 0 + auto line_cost = collision_checker.lineCost(10, 20, 10, 20); + EXPECT_NEAR(line_cost, 0.0, 0.001); + + // Add obstacle in the line path from (10,10) to (20,20) + costmap_->setCost(15, 15, 100); + line_cost = collision_checker.lineCost(10, 20, 10, 20); + EXPECT_NEAR(line_cost, 100.0, 0.001); + + // Test with lethal obstacle - should return immediately without checking rest of line + costmap_->setCost(12, 12, nav2_costmap_2d::LETHAL_OBSTACLE); + line_cost = collision_checker.lineCost(10, 20, 10, 20); + EXPECT_NEAR(line_cost, static_cast(nav2_costmap_2d::LETHAL_OBSTACLE), 0.001); +} + +// Test handling of footprints that extend outside the costmap boundaries +// Verifies that out-of-bounds footprint coordinates are handled correctly +// and return lethal cost (indicating collision/invalid placement) +TEST(collision_footprint, test_out_of_bounds_footprint) { + std::shared_ptr costmap_ = + std::make_shared(100, 100, 0.1, 0, 0, 0); + + // Create a large footprint that will extend beyond map boundaries + geometry_msgs::msg::Point p1; + p1.x = -2.0; // Will extend to negative world coordinates + p1.y = -2.0; + geometry_msgs::msg::Point p2; + p2.x = 2.0; + p2.y = -2.0; + geometry_msgs::msg::Point p3; + p3.x = 2.0; + p3.y = 2.0; + geometry_msgs::msg::Point p4; + p4.x = -2.0; + p4.y = 2.0; + + nav2_costmap_2d::Footprint footprint = {p1, p2, p3, p4}; + + nav2_costmap_2d::FootprintCollisionChecker> + collision_checker(costmap_); + + // Place footprint so part of it extends outside the map (negative world coordinates) + // With costmap starting at origin (0,0), placing at (-1.0, -1.0) will put parts out of bounds + auto out_of_bounds_cost = collision_checker.footprintCostAtPose(-1.0, -1.0, 0.0, footprint, true); + EXPECT_EQ(out_of_bounds_cost, static_cast(nav2_costmap_2d::LETHAL_OBSTACLE)); +} + +// Test the setCostmap and getCostmap functionality +// Verifies that the collision checker can switch between different costmaps +// and that cost queries are performed on the correct costmap +TEST(collision_footprint, test_set_costmap_function) { + // Create two different costmaps with different sizes + std::shared_ptr costmap1 = + std::make_shared(50, 50, 0.1, 0, 0, 0); + std::shared_ptr costmap2 = + std::make_shared(100, 100, 0.1, 0, 0, 0); + + // Set different costs at the same grid location in both costmaps + costmap1->setCost(25, 25, 100); + costmap2->setCost(25, 25, 200); + + // Initialize collision checker with first costmap + nav2_costmap_2d::FootprintCollisionChecker> + collision_checker(costmap1); + + // Test with first costmap - should return cost 100 + auto cost1 = collision_checker.pointCost(25, 25); + EXPECT_NEAR(cost1, 100.0, 0.001); + + // Switch to second costmap using setCostmap + collision_checker.setCostmap(costmap2); + auto cost2 = collision_checker.pointCost(25, 25); + EXPECT_NEAR(cost2, 200.0, 0.001); + + // Verify getCostmap function returns the correct costmap reference + auto retrieved_costmap = collision_checker.getCostmap(); + EXPECT_EQ(retrieved_costmap, costmap2); +} + +// Test full area checking with complex polygons (non-rectangular shapes) +// Verifies that the OpenCV polygon rasterization works correctly for +// more complex shapes like pentagons, ensuring the implementation is robust +TEST(collision_footprint, test_complex_polygon_full_area) { + std::shared_ptr costmap_ = + std::make_shared(100, 100, 0.1, 0, 0, 0); + + // Create a complex polygon (pentagon) to test non-trivial polygon rasterization + geometry_msgs::msg::Point p1; + p1.x = 0.0; // Top vertex + p1.y = 1.5; + geometry_msgs::msg::Point p2; + p2.x = 1.4; // Top-right vertex + p2.y = 0.5; + geometry_msgs::msg::Point p3; + p3.x = 0.9; // Bottom-right vertex + p3.y = -1.2; + geometry_msgs::msg::Point p4; + p4.x = -0.9; // Bottom-left vertex + p4.y = -1.2; + geometry_msgs::msg::Point p5; + p5.x = -1.4; // Top-left vertex + p5.y = 0.5; + + nav2_costmap_2d::Footprint footprint = {p1, p2, p3, p4, p5}; + + // Place obstacle in what should be the interior of the pentagon + costmap_->setCost(50, 50, 180); + + nav2_costmap_2d::FootprintCollisionChecker> + collision_checker(costmap_); + + // Test that full area check detects interior obstacle in complex polygon + auto perimeter_cost = collision_checker.footprintCostAtPose(5.0, 5.0, 0.0, footprint, false); + auto full_area_cost = collision_checker.footprintCostAtPose(5.0, 5.0, 0.0, footprint, true); + + // Perimeter should not detect interior obstacle + EXPECT_LT(perimeter_cost, 180.0); + // Full area should detect it (demonstrating the value of full area checking) + EXPECT_NEAR(full_area_cost, 180.0, 0.001); +} + +// Performance test for footprint collision checking +// Measures execution time for both perimeter and full-area checking methods +// Tests deterministic scenarios: no obstacles, vertex obstacle, edge obstacle, interior obstacle +TEST(collision_footprint, test_performance_benchmark) { + // Create a reasonably large costmap for performance testing + std::shared_ptr costmap_ = + std::make_shared(500, 500, 0.05, 0, 0, 0); + + // Create a rectangular footprint (2m x 1m) + geometry_msgs::msg::Point p1; + p1.x = -1.0; // Left edge + p1.y = -0.5; // Bottom edge + geometry_msgs::msg::Point p2; + p2.x = 1.0; // Right edge + p2.y = -0.5; + geometry_msgs::msg::Point p3; + p3.x = 1.0; + p3.y = 0.5; // Top edge + geometry_msgs::msg::Point p4; + p4.x = -1.0; + p4.y = 0.5; + + nav2_costmap_2d::Footprint footprint = {p1, p2, p3, p4}; + + nav2_costmap_2d::FootprintCollisionChecker> + collision_checker(costmap_); + + // Fixed test position (center of costmap) + const double test_x = 12.5; // Center of 25m x 25m map + const double test_y = 12.5; + const double test_theta = 0.0; + + // Convert footprint vertices to grid coordinates for obstacle placement + unsigned int grid_x, grid_y; + + const int num_iterations = 1000; + + // Define test scenarios + struct TestScenario + { + std::string name; + std::vector> obstacle_positions; + }; + + std::vector scenarios; + + // Scenario 0: Obstacle at a vertex of the polygon (fastest) + // Place obstacle at the first vertex (p1: left edge, bottom edge) + collision_checker.worldToMap(test_x - 1.0, test_y - 0.5, grid_x, grid_y); + scenarios.push_back({"Obstacle at vertex", {{grid_x, grid_y}}}); + + // Scenario 1: Obstacle on a line segment (edge) + // Place obstacle on the last line segment (p4 to p1: top edge, from right to left) + collision_checker.worldToMap(test_x - 0.5, test_y + 0.5, grid_x, grid_y); + scenarios.push_back({"Obstacle on edge", {{grid_x, grid_y}}}); + + // Scenario 2: Obstacle inside the polygon + // Place obstacle at the center of the rectangle + collision_checker.worldToMap(test_x, test_y, grid_x, grid_y); + scenarios.push_back({"Obstacle inside", {{grid_x, grid_y}}}); + + // Scenario 3: No obstacles (slowest) + scenarios.push_back({"No obstacles", {}}); + + std::cout << "\n=== FOOTPRINT COLLISION CHECKER PERFORMANCE BENCHMARK ===" << std::endl; + std::cout << "Test configuration:" << std::endl; + std::cout << " - Costmap size: 500x500 cells (25m x 25m @ 0.05m resolution)" << std::endl; + std::cout << " - Footprint: Rectangle 2m x 1m" << std::endl; + std::cout << " - Test position: (" << test_x << ", " << test_y << ", " << test_theta << ")" << + std::endl; + std::cout << " - Iterations per scenario: " << num_iterations << std::endl; + std::cout << " - Obstacle cost: 254" << std::endl; + + // Run tests for each scenario + std::vector full_area_times; // Store timing for early exit verification + + for (const auto & scenario : scenarios) { + // Clear costmap + for (unsigned int i = 0; i < costmap_->getSizeInCellsX(); ++i) { + for (unsigned int j = 0; j < costmap_->getSizeInCellsY(); ++j) { + costmap_->setCost(i, j, 0); + } + } + + // Place obstacles for this scenario + for (const auto & pos : scenario.obstacle_positions) { + costmap_->setCost(pos.first, pos.second, 254); + } + + // Test perimeter-only checking + auto start_perimeter = std::chrono::high_resolution_clock::now(); + double perimeter_result = 0.0; + for (int iter = 0; iter < num_iterations; ++iter) { + perimeter_result = collision_checker.footprintCostAtPose( + test_x, test_y, test_theta, footprint, false); + } + auto end_perimeter = std::chrono::high_resolution_clock::now(); + auto perimeter_duration = std::chrono::duration_cast( + end_perimeter - start_perimeter); + + // Test full-area checking + auto start_full_area = std::chrono::high_resolution_clock::now(); + double full_area_result = 0.0; + for (int iter = 0; iter < num_iterations; ++iter) { + full_area_result = collision_checker.footprintCostAtPose( + test_x, test_y, test_theta, footprint, true); + } + auto end_full_area = std::chrono::high_resolution_clock::now(); + auto full_area_duration = std::chrono::duration_cast( + end_full_area - start_full_area); + + // Calculate averages + double avg_perimeter_us = static_cast(perimeter_duration.count()) / num_iterations; + double avg_full_area_us = static_cast(full_area_duration.count()) / num_iterations; + + // Store full-area timing for early exit verification + full_area_times.push_back(avg_full_area_us); + + // Print results for this scenario + std::cout << "\nScenario: " << scenario.name << std::endl; + std::cout << " Perimeter checking:" << std::endl; + std::cout << " - Result: " << perimeter_result << std::endl; + std::cout << " - Average time: " << std::fixed << std::setprecision(2) + << avg_perimeter_us << " microseconds" << std::endl; + std::cout << " Full-area checking:" << std::endl; + std::cout << " - Result: " << full_area_result << std::endl; + std::cout << " - Average time: " << std::fixed << std::setprecision(2) + << avg_full_area_us << " microseconds" << std::endl; + std::cout << " Performance ratio (full-area / perimeter): " + << std::fixed << std::setprecision(2) + << (avg_perimeter_us > + 0 ? avg_full_area_us / avg_perimeter_us : 0.0) << "x" << std::endl; + + // Verify expected behavior for each scenario + if (scenario.name == "No obstacles") { + EXPECT_NEAR(perimeter_result, 0.0, 0.001); + EXPECT_NEAR(full_area_result, 0.0, 0.001); + } else if (scenario.name == "Obstacle at vertex" || scenario.name == "Obstacle on edge") { + // Both methods should detect obstacles on perimeter + EXPECT_NEAR(perimeter_result, 254.0, 0.001); + EXPECT_NEAR(full_area_result, 254.0, 0.001); + } else if (scenario.name == "Obstacle inside") { + // Only full-area should detect interior obstacles + EXPECT_LT(perimeter_result, 254.0); // Should not detect interior obstacle + EXPECT_NEAR(full_area_result, 254.0, 0.001); // Should detect interior obstacle + } + + // Performance sanity checks + EXPECT_LT(avg_perimeter_us, 1000.0); // Should be less than 1ms per call + EXPECT_LT(avg_full_area_us, 5000.0); // Should be less than 5ms per call + } + + // Verify early exit performance ordering for full-area checking + // Expected order (fastest to slowest due to early exit): + // 0: "Obstacle at vertex" - exits earliest when scanning perimeter (fastest) + // 1: "Obstacle on edge" - exits when scanning perimeter + // 2: "Obstacle inside" - exits when scanning interior + // 3: "No obstacles" - must scan entire area (slowest) + std::cout << "\nEarly exit performance verification (full-area checking):" << std::endl; + std::cout << " Obstacle at vertex: " << std::fixed << std::setprecision(2) + << full_area_times[0] << " ÎŒs (fastest)" << std::endl; + std::cout << " Obstacle on edge: " << std::fixed << std::setprecision(2) + << full_area_times[1] << " ÎŒs" << std::endl; + std::cout << " Obstacle inside: " << std::fixed << std::setprecision(2) + << full_area_times[2] << " ÎŒs" << std::endl; + std::cout << " No obstacles: " << std::fixed << std::setprecision(2) + << full_area_times[3] << " ÎŒs (slowest)" << std::endl; + + // Verify early exit behavior: obstacles found earlier should result in faster execution + // Expected order: vertex < edge < inside < no obstacles + // Allow some tolerance for measurement noise, but expect clear ordering + double tolerance_factor = 1.2; // Allow 20% tolerance for timing variations + + // Obstacle at vertex should be fastest (first vertex checked) + EXPECT_LT(full_area_times[0] * tolerance_factor, full_area_times[1]) // vertex < edge + << "Obstacle at vertex (" << full_area_times[0] << + " ÎŒs) should be faster than obstacle on edge (" + << full_area_times[1] << " ÎŒs) due to early exit"; + + EXPECT_LT(full_area_times[1] * tolerance_factor, full_area_times[2]) // edge < inside + << "Obstacle on edge (" << full_area_times[1] << " ÎŒs) should be faster than obstacle inside (" + << full_area_times[2] << " ÎŒs) due to early exit"; + + EXPECT_LT(full_area_times[2] * tolerance_factor, full_area_times[3]) // inside < no obstacles + << "Obstacle inside (" << full_area_times[2] << " ÎŒs) should be faster than no obstacles (" + << full_area_times[3] << " ÎŒs) due to early exit"; + + std::cout << " ✓ Early exit performance ordering verified" << std::endl; + std::cout << "============================================================\n" << std::endl; +} diff --git a/nav2_docking/opennav_docking/src/docking_server.cpp b/nav2_docking/opennav_docking/src/docking_server.cpp index 1208ade5521..de552804640 100644 --- a/nav2_docking/opennav_docking/src/docking_server.cpp +++ b/nav2_docking/opennav_docking/src/docking_server.cpp @@ -319,6 +319,7 @@ void DockingServer::dockRobot() // Approach the dock using control law if (approachDock(dock, dock_pose, dock_backward)) { // We are docked, wait for charging to begin + publishZeroVelocity(); RCLCPP_INFO( get_logger(), "Made contact with dock, waiting for charge to start (if applicable)."); if (waitForCharge(dock)) { diff --git a/nav2_mppi_controller/CMakeLists copy.txt b/nav2_mppi_controller/CMakeLists copy.txt new file mode 100644 index 00000000000..fe98d0d2af7 --- /dev/null +++ b/nav2_mppi_controller/CMakeLists copy.txt @@ -0,0 +1,199 @@ +cmake_minimum_required(VERSION 3.8) +project(nav2_mppi_controller) + +find_package(ament_cmake REQUIRED) +find_package(angles REQUIRED) +find_package(backward_ros REQUIRED) +find_package(geometry_msgs REQUIRED) +find_package(nav2_common REQUIRED) +find_package(nav2_core REQUIRED) +find_package(nav2_costmap_2d REQUIRED) +find_package(nav2_msgs REQUIRED) +find_package(nav2_util REQUIRED) +find_package(nav_msgs REQUIRED) +find_package(pluginlib REQUIRED) +find_package(rclcpp REQUIRED) +find_package(rclcpp_lifecycle REQUIRED) +find_package(std_msgs REQUIRED) +find_package(tf2 REQUIRED) +find_package(tf2_geometry_msgs REQUIRED) +find_package(tf2_ros REQUIRED) +find_package(visualization_msgs REQUIRED) +find_package(nav2_ros_common REQUIRED) +find_package(Eigen3 REQUIRED) + + +include_directories( + include + ${EIGEN3_INCLUDE_DIR} +) + +nav2_package() + +include(CheckCXXCompilerFlag) + +check_cxx_compiler_flag("-mfma" COMPILER_SUPPORTS_FMA) + +if(COMPILER_SUPPORTS_FMA) + add_compile_options(-mfma) +endif() + +# If building one the same hardware to be deployed on, try `-march=native`! + +add_library(mppi_controller SHARED + src/controller.cpp + src/collision_checker.cpp + src/critic_manager.cpp + src/noise_generator.cpp + src/optimizer.cpp + src/parameters_handler.cpp + src/path_handler.cpp + src/trajectory_visualizer.cpp +) +target_compile_options(mppi_controller PUBLIC -O3) +target_include_directories(mppi_controller + PUBLIC + "$" + "$") +target_link_libraries(mppi_controller PUBLIC + angles::angles + ${geometry_msgs_TARGETS} + nav2_core::nav2_core + nav2_ros_common::nav2_ros_common + nav2_costmap_2d::layers + nav2_costmap_2d::nav2_costmap_2d_core + ${nav2_msgs_TARGETS} + ${nav_msgs_TARGETS} + pluginlib::pluginlib + rclcpp::rclcpp + rclcpp_lifecycle::rclcpp_lifecycle + ${std_msgs_TARGETS} + tf2::tf2 + tf2_geometry_msgs::tf2_geometry_msgs + tf2_ros::tf2_ros + ${visualization_msgs_TARGETS} +) + +add_library(mppi_critics SHARED + src/critics/constraint_critic.cpp + src/critics/cost_critic.cpp + src/critics/goal_critic.cpp + src/critics/goal_angle_critic.cpp + src/critics/obstacles_critic.cpp + src/critics/path_align_critic.cpp + src/critics/path_angle_critic.cpp + src/critics/path_follow_critic.cpp + src/critics/prefer_forward_critic.cpp + src/critics/twirling_critic.cpp + src/critics/velocity_deadband_critic.cpp + src/critics/waypoint_critic.cpp +) +if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND APPLE) + # Apple Clang: use C++20 and optimization, omit -fconcepts + target_compile_features(mppi_critics PUBLIC cxx_std_20) + target_compile_options(mppi_critics PUBLIC -O3) +else() + target_compile_options(mppi_critics PUBLIC -fconcepts -O3) +endif() +target_include_directories(mppi_critics + PUBLIC + "$" + "$") +target_link_libraries(mppi_critics PUBLIC + angles::angles + ${geometry_msgs_TARGETS} + nav2_core::nav2_core + nav2_ros_common::nav2_ros_common + nav2_costmap_2d::layers + nav2_costmap_2d::nav2_costmap_2d_core + ${nav_msgs_TARGETS} + rclcpp::rclcpp + rclcpp_lifecycle::rclcpp_lifecycle + ${std_msgs_TARGETS} + tf2::tf2 + tf2_geometry_msgs::tf2_geometry_msgs + tf2_ros::tf2_ros + ${visualization_msgs_TARGETS} + mppi_controller +) +target_link_libraries(mppi_critics PRIVATE + pluginlib::pluginlib +) + +add_library(mppi_trajectory_validators SHARED + src/trajectory_validators/optimal_trajectory_validator.cpp +) +target_compile_options(mppi_trajectory_validators PUBLIC -O3) +target_include_directories(mppi_trajectory_validators + PUBLIC + "$" + "$" + "$") +target_link_libraries(mppi_trajectory_validators PUBLIC + angles::angles + ${geometry_msgs_TARGETS} + nav2_core::nav2_core + nav2_costmap_2d::layers + nav2_costmap_2d::nav2_costmap_2d_core + ${nav_msgs_TARGETS} + rclcpp::rclcpp + rclcpp_lifecycle::rclcpp_lifecycle + ${std_msgs_TARGETS} + tf2::tf2 + tf2_geometry_msgs::tf2_geometry_msgs + tf2_ros::tf2_ros + ${visualization_msgs_TARGETS} +) +target_link_libraries(mppi_trajectory_validators PRIVATE + pluginlib::pluginlib +) + +install(TARGETS mppi_controller mppi_critics mppi_trajectory_validators + EXPORT nav2_mppi_controller + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin +) + +install(DIRECTORY include/ + DESTINATION include/${PROJECT_NAME} +) + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + find_package(ament_cmake_gtest REQUIRED) + set(ament_cmake_copyright_FOUND TRUE) + ament_lint_auto_find_test_dependencies() + + ament_find_gtest() + + add_subdirectory(test) + add_subdirectory(benchmark) +endif() + +ament_export_libraries(${libraries}) +ament_export_dependencies( + angles + geometry_msgs + nav2_core + nav2_costmap_2d + nav_msgs + pluginlib + rclcpp + rclcpp_lifecycle + std_msgs + tf2 + tf2_geometry_msgs + tf2_ros + visualization_msgs + nav2_ros_common + Eigen3 +) +ament_export_include_directories(include/${PROJECT_NAME}) +ament_export_targets(nav2_mppi_controller) + +pluginlib_export_plugin_description_file(nav2_core mppic.xml) +pluginlib_export_plugin_description_file(nav2_mppi_controller critics.xml) +pluginlib_export_plugin_description_file(nav2_mppi_controller trajectory_validators.xml) + +ament_package() diff --git a/nav2_mppi_controller/CMakeLists.txt b/nav2_mppi_controller/CMakeLists.txt index c28c711dc50..f4abe8dd80a 100644 --- a/nav2_mppi_controller/CMakeLists.txt +++ b/nav2_mppi_controller/CMakeLists.txt @@ -22,6 +22,7 @@ find_package(visualization_msgs REQUIRED) find_package(nav2_ros_common REQUIRED) find_package(Eigen3 REQUIRED) + include_directories( include ${EIGEN3_INCLUDE_DIR} @@ -84,6 +85,7 @@ add_library(mppi_critics SHARED src/critics/prefer_forward_critic.cpp src/critics/twirling_critic.cpp src/critics/velocity_deadband_critic.cpp + src/critics/waypoint_critic.cpp ) if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND APPLE) # Apple Clang: use C++20 and optimization, omit -fconcepts @@ -118,6 +120,7 @@ target_link_libraries(mppi_critics PRIVATE add_library(mppi_trajectory_validators SHARED src/trajectory_validators/optimal_trajectory_validator.cpp + src/trajectory_validators/fixing_trajectory_validator.cpp ) target_compile_options(mppi_trajectory_validators PUBLIC -O3) target_include_directories(mppi_trajectory_validators diff --git a/nav2_mppi_controller/critics.xml b/nav2_mppi_controller/critics.xml index deae2c11cb9..e738b3455a4 100644 --- a/nav2_mppi_controller/critics.xml +++ b/nav2_mppi_controller/critics.xml @@ -45,5 +45,11 @@ mppi critic for restricting command velocities in deadband range + + + mppi critic for following sparse waypoints with position and orientation tolerances + + + diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/collision_checker.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/collision_checker.hpp new file mode 100644 index 00000000000..09fe82f0649 --- /dev/null +++ b/nav2_mppi_controller/include/nav2_mppi_controller/collision_checker.hpp @@ -0,0 +1,148 @@ +// Copyright (c) 2025, Angsa Robotics GmbH +// +// 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. Reserved. + +#ifndef NAV2_MPPI_CONTROLLER__COLLISION_CHECKER_HPP_ +#define NAV2_MPPI_CONTROLLER__COLLISION_CHECKER_HPP_ + +#include +#include +#include + +#include "nav2_costmap_2d/footprint_collision_checker.hpp" +#include "nav2_costmap_2d/costmap_2d_ros.hpp" +#include "nav2_costmap_2d/cost_values.hpp" +#include "rclcpp_lifecycle/lifecycle_node.hpp" + + +namespace nav2_mppi_controller +{ + +/** + * @struct CollisionResult + * @brief Result of collision checking + */ +struct CollisionResult +{ + bool in_collision; + std::vector center_cost; + std::vector footprint_cost; +}; + +/** + * @class nav2_mppi_controller::MPPICollisionChecker + * @brief A costmap grid collision checker + */ +class MPPICollisionChecker + : public nav2_costmap_2d::FootprintCollisionChecker +{ +public: + /** + * @brief A constructor for nav2_mppi_controller::MPPICollisionChecker + * @param costmap The costmap to collision check against + * @param node Node to extract clock and logger from + */ + MPPICollisionChecker( + std::shared_ptr costmap, + rclcpp_lifecycle::LifecycleNode::SharedPtr node); + + /** + * @brief A constructor for nav2_mppi_controller::MPPICollisionChecker + * for use when irregular bin intervals are appropriate + * @param costmap The costmap to collision check against + * @param angles The vector of possible angle bins to precompute for + * orientations for to speed up collision checking, in radians + */ + // MPPICollisionChecker( + // nav2_costmap_2d::Costmap2D * costmap, + // std::vector & angles); + + /** + * @brief Set the footprint to use with collision checker + * @param footprint The footprint to collision check against + * @param radius Whether or not the footprint is a circle and use radius collision checking + * @param inflation_layer_name Optional name of inflation layer for cost calculation + */ + void setFootprint( + const nav2_costmap_2d::Footprint & footprint, + const bool & radius, + const std::string & inflation_layer_name = ""); + + /** + * @brief Check if in collision with costmap and footprint at poses (batch processing) + * @param x Vector of X coordinates of poses to check against + * @param y Vector of Y coordinates of poses to check against + * @param yaw Vector of yaw angles of poses to check against in radians + * @param traverse_unknown Whether or not to traverse in unknown space + * @return CollisionResult struct with collision status and vectors of center cost, footprint cost, and area cost + */ + CollisionResult inCollision( + const std::vector & x, + const std::vector & y, + const std::vector & yaw, + const bool & traverse_unknown); + + /** + * @brief Get costmap ros object for inflation layer params + * @return Costmap ros + */ + std::shared_ptr getCostmapROS() {return costmap_ros_;} + + /** + * @brief Check if value outside the range + * @param max Maximum value of the range + * @param value the value to check if it is within the range + * @return boolean if in range or not + */ + bool outsideRange(const unsigned int & max, const float & value) const; + + /** + * @brief Create convex hull from a set of points + * @param points Input points to create convex hull from + * @return Convex hull as a footprint + */ + nav2_costmap_2d::Footprint createConvexHull( + const std::vector & points); + + /** + * @brief Find the circumscribed cost for collision checking optimization + * @param inflation_layer_name Optional name of inflation layer + * @return The circumscribed cost value + */ + float findCircumscribedCost(const std::string & inflation_layer_name = ""); + +private: + /** + * @brief Transform footprint to given orientation + * @param footprint The base footprint to transform + * @param yaw The yaw angle to transform to + * @return Transformed footprint + */ + nav2_costmap_2d::Footprint transformFootprint( + const nav2_costmap_2d::Footprint & footprint, + float yaw) const; + +protected: + std::shared_ptr costmap_ros_; + nav2_costmap_2d::Footprint unoriented_footprint_; + bool footprint_is_radius_{false}; + float possible_collision_cost_{-1}; + float circumscribed_radius_{-1.0f}; + float circumscribed_cost_{-1.0f}; + rclcpp::Logger logger_{rclcpp::get_logger("MPPICollisionChecker")}; + rclcpp::Clock::SharedPtr clock_; +}; + +} // namespace nav2_mppi_controller + +#endif // NAV2_MPPI_CONTROLLER__COLLISION_CHECKER_HPP_ \ No newline at end of file diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/controller.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/controller.hpp index 1c52df96f68..40bb7613c40 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/controller.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/controller.hpp @@ -101,31 +101,18 @@ class MPPIController : public nav2_core::Controller void setSpeedLimit(const double & speed_limit, const bool & percentage) override; protected: - /** - * @brief Visualize trajectories - * @param transformed_plan Transformed input plan - * @param cmd_stamp Command stamp - * @param optimal_trajectory Optimal trajectory, if already computed - */ - void visualize( - nav_msgs::msg::Path transformed_plan, - const builtin_interfaces::msg::Time & cmd_stamp, - const Eigen::ArrayXXf & optimal_trajectory); + std::string name_; nav2::LifecycleNode::WeakPtr parent_; rclcpp::Logger logger_{rclcpp::get_logger("MPPIController")}; std::shared_ptr costmap_ros_; std::shared_ptr tf_buffer_; - nav2::Publisher::SharedPtr opt_traj_pub_; std::unique_ptr parameters_handler_; Optimizer optimizer_; PathHandler path_handler_; TrajectoryVisualizer trajectory_visualizer_; - - bool visualize_; - bool publish_optimal_trajectory_; }; } // namespace nav2_mppi_controller diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/critic_data copy 2.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/critic_data copy 2.hpp new file mode 100644 index 00000000000..8476dde055c --- /dev/null +++ b/nav2_mppi_controller/include/nav2_mppi_controller/critic_data copy 2.hpp @@ -0,0 +1,58 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NAV2_MPPI_CONTROLLER__CRITIC_DATA_HPP_ +#define NAV2_MPPI_CONTROLLER__CRITIC_DATA_HPP_ + +#include + +#include +#include + +#include "geometry_msgs/msg/pose_stamped.hpp" +#include "nav2_core/goal_checker.hpp" +#include "nav2_mppi_controller/models/state.hpp" +#include "nav2_mppi_controller/models/trajectories.hpp" +#include "nav2_mppi_controller/models/path.hpp" +#include "nav2_mppi_controller/motion_models.hpp" + + +namespace mppi +{ + +/** + * @struct mppi::CriticData + * @brief Data to pass to critics for scoring, including state, trajectories, + * pruned path, global goal, costs, and important parameters to share + */ +struct CriticData +{ + const models::State & state; + const models::Trajectories & trajectories; + const models::Path & path; + const geometry_msgs::msg::Pose & goal; + + Eigen::ArrayXf & costs; + float & model_dt; + + bool fail_flag; + nav2_core::GoalChecker * goal_checker; + std::shared_ptr motion_model; + std::optional> path_pts_valid; + std::optional furthest_reached_path_point; +}; + +} // namespace mppi + +#endif // NAV2_MPPI_CONTROLLER__CRITIC_DATA_HPP_ diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/critic_data.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/critic_data.hpp index 8476dde055c..db849ec9536 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/critic_data.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/critic_data.hpp @@ -18,6 +18,8 @@ #include #include +#include +#include #include #include "geometry_msgs/msg/pose_stamped.hpp" @@ -44,9 +46,11 @@ struct CriticData const geometry_msgs::msg::Pose & goal; Eigen::ArrayXf & costs; + std::optional>> individual_critics_cost; float & model_dt; bool fail_flag; + std::optional> trajectories_in_collision; nav2_core::GoalChecker * goal_checker; std::shared_ptr motion_model; std::optional> path_pts_valid; @@ -55,4 +59,4 @@ struct CriticData } // namespace mppi -#endif // NAV2_MPPI_CONTROLLER__CRITIC_DATA_HPP_ +#endif // NAV2_MPPI_CONTROLLER__CRITIC_DATA_HPP_ \ No newline at end of file diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/critic_function.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/critic_function.hpp index 9b7d17aaea5..dda4a14f96e 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/critic_function.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/critic_function.hpp @@ -70,7 +70,9 @@ class CriticFunction ParametersHandler * param_handler) { parent_ = parent; - logger_ = parent_.lock()->get_logger(); + auto node = parent_.lock(); + logger_ = node->get_logger(); + clock_ = node->get_clock(); name_ = name; parent_name_ = parent_name; costmap_ros_ = costmap_ros; @@ -111,6 +113,7 @@ class CriticFunction ParametersHandler * parameters_handler_; rclcpp::Logger logger_{rclcpp::get_logger("MPPIController")}; + rclcpp::Clock::SharedPtr clock_; }; } // namespace mppi::critics diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/critic_manager.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/critic_manager.hpp index 237649bedde..447757d73db 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/critic_manager.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/critic_manager.hpp @@ -99,6 +99,7 @@ class CriticManager nav2::Publisher::SharedPtr critics_effect_pub_; bool publish_critics_stats_; + bool visualize_per_critic_costs_; rclcpp::Logger logger_{rclcpp::get_logger("MPPIController")}; }; diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/critics/cost_critic copy.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/critics/cost_critic copy.hpp new file mode 100644 index 00000000000..969cd9b94d8 --- /dev/null +++ b/nav2_mppi_controller/include/nav2_mppi_controller/critics/cost_critic copy.hpp @@ -0,0 +1,109 @@ +// Copyright (c) 2023 Robocc Brice Renaudeau +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NAV2_MPPI_CONTROLLER__CRITICS__COST_CRITIC_HPP_ +#define NAV2_MPPI_CONTROLLER__CRITICS__COST_CRITIC_HPP_ + +#include +#include + +#include "nav2_costmap_2d/inflation_layer.hpp" + +#include "nav2_mppi_controller/critic_function.hpp" +#include "nav2_mppi_controller/models/state.hpp" +#include "nav2_mppi_controller/tools/utils.hpp" +#include "nav2_mppi_controller/collision_checker.hpp" + +namespace mppi::critics +{ + +/** + * @class mppi::critics::CostCritic + * @brief Critic objective function for avoiding obstacles using costmap's inflated cost + */ +class CostCritic : public CriticFunction +{ +public: + /** + * @brief Initialize critic + */ + void initialize() override; + + /** + * @brief Evaluate cost related to obstacle avoidance + * + * @param costs [out] add obstacle cost values to this tensor + */ + void score(CriticData & data) override; + +protected: + /** + * @brief An implementation of worldToMap fully using floats + * @param wx Float world X coord + * @param wy Float world Y coord + * @param mx unsigned int map X coord + * @param my unsigned into map Y coord + * @return if successful + */ + inline bool worldToMapFloat(float wx, float wy, unsigned int & mx, unsigned int & my) const + { + if (wx < origin_x_ || wy < origin_y_) { + return false; + } + + mx = static_cast((wx - origin_x_) / resolution_); + my = static_cast((wy - origin_y_) / resolution_); + + if (mx < size_x_ && my < size_y_) { + return true; + } + return false; + } + + /** + * @brief A local implementation of getIndex + * @param mx unsigned int map X coord + * @param my unsigned into map Y coord + * @return Index + */ + inline unsigned int getIndex(unsigned int mx, unsigned int my) const + { + return my * size_x_ + mx; + } + + std::unique_ptr collision_checker_; + float possible_collision_cost_; + + bool consider_footprint_{true}; + bool is_tracking_unknown_{true}; + float collision_cost_{0.0f}; + float critical_cost_{0.0f}; + unsigned int near_collision_cost_{253}; + float weight_{0}; + unsigned int trajectory_point_step_; + + float origin_x_, origin_y_, resolution_; + unsigned int size_x_, size_y_; + + float near_goal_distance_; + std::string inflation_layer_name_; + + + + unsigned int power_{0}; +}; + +} // namespace mppi::critics + +#endif // NAV2_MPPI_CONTROLLER__CRITICS__COST_CRITIC_HPP_ diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/critics/cost_critic.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/critics/cost_critic.hpp index c285a14b664..3ebaf54400e 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/critics/cost_critic.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/critics/cost_critic.hpp @@ -48,38 +48,6 @@ class CostCritic : public CriticFunction void score(CriticData & data) override; protected: - /** - * @brief Checks if cost represents a collision - * @param cost Point cost at pose center - * @param x X of pose - * @param y Y of pose - * @param theta theta of pose - * @return bool if in collision - */ - inline bool inCollision(float cost, float x, float y, float theta) - { - // If consider_footprint_ check footprint scort for collision - float score_cost = cost; - if (consider_footprint_ && - (cost >= possible_collision_cost_ || possible_collision_cost_ < 1.0f)) - { - score_cost = static_cast(collision_checker_.footprintCostAtPose( - static_cast(x), static_cast(y), static_cast(theta), - costmap_ros_->getRobotFootprint())); - } - - switch (static_cast(score_cost)) { - case (nav2_costmap_2d::LETHAL_OBSTACLE): - return true; - case (nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE): - return consider_footprint_ ? false : true; - case (nav2_costmap_2d::NO_INFORMATION): - return is_tracking_unknown_ ? false : true; - } - - return false; - } - /** * @brief Find the min cost of the inflation decay function for which the robot MAY be * in collision in any orientation @@ -148,4 +116,4 @@ class CostCritic : public CriticFunction } // namespace mppi::critics -#endif // NAV2_MPPI_CONTROLLER__CRITICS__COST_CRITIC_HPP_ +#endif // NAV2_MPPI_CONTROLLER__CRITICS__COST_CRITIC_HPP_ \ No newline at end of file diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/critics/obstacles_critic copy 3.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/critics/obstacles_critic copy 3.hpp new file mode 100644 index 00000000000..b1c52f361d2 --- /dev/null +++ b/nav2_mppi_controller/include/nav2_mppi_controller/critics/obstacles_critic copy 3.hpp @@ -0,0 +1,108 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NAV2_MPPI_CONTROLLER__CRITICS__OBSTACLES_CRITIC_HPP_ +#define NAV2_MPPI_CONTROLLER__CRITICS__OBSTACLES_CRITIC_HPP_ + +#include +#include + +//#include "nav2_costmap_2d/footprint_collision_checker.hpp" +#include "nav2_costmap_2d/inflation_layer.hpp" +#include "nav2_mppi_controller/critic_function.hpp" +#include "nav2_mppi_controller/models/state.hpp" +#include "nav2_mppi_controller/tools/utils.hpp" +#include "nav2_mppi_controller/collision_checker.hpp" + +namespace mppi::critics +{ + +/** + * @class mppi::critics::ConstraintCritic + * @brief Critic objective function for avoiding obstacles, allowing it to deviate off + * the planned path. This is important to tune in tandem with PathAlign to make a balance + * between path-tracking and dynamic obstacle avoidance capabilities as desirable for a + * particular application + */ +class ObstaclesCritic : public CriticFunction +{ +public: + /** + * @brief Initialize critic + */ + void initialize() override; + + /** + * @brief Evaluate cost related to obstacle avoidance + * + * @param costs [out] add obstacle cost values to this tensor + */ + void score(CriticData & data) override; + +protected: + /** + * @brief Checks if cost represents a collision + * @param cost Costmap cost + * @return bool if in collision + */ + inline bool inCollision(float cost) const; + + /** + * @brief cost at a robot pose + * @param x X of pose + * @param y Y of pose + * @param theta theta of pose + * @return Collision information at pose + */ + //inline CollisionCost costAtPose(float x, float y, float theta); + + /** + * @brief Distance to obstacle from cost + * @param cost Costmap cost + * @return float Distance to the obstacle represented by cost + */ + inline float distanceToObstacle(const CollisionCost & cost); + + /** + * @brief Find the min cost of the inflation decay function for which the robot MAY be + * in collision in any orientation + * @param costmap Costmap2DROS to get minimum inscribed cost (e.g. 128 in inflation layer documentation) + * @return double circumscribed cost, any higher than this and need to do full footprint collision checking + * since some element of the robot could be in collision + */ + float findCircumscribedCost(std::shared_ptr costmap); + +protected: + // nav2_costmap_2d::FootprintCollisionChecker + // collision_checker_{nullptr}; + std::unique_ptr collision_checker_; + int trajectory_point_step_{1}; + + bool consider_footprint_{true}; + float collision_cost_{0}; + float inflation_scale_factor_{0}, inflation_radius_{0}; + + float possible_collision_cost_; + float collision_margin_distance_; + float near_goal_distance_; + float circumscribed_cost_{0}, circumscribed_radius_{0}; + + unsigned int power_{0}; + float repulsion_weight_, critical_weight_{0}; + std::string inflation_layer_name_; +}; + +} // namespace mppi::critics + +#endif // NAV2_MPPI_CONTROLLER__CRITICS__OBSTACLES_CRITIC_HPP_ diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_align_critic.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_align_critic.hpp index f7ad2fda6a9..ac29e1035a8 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_align_critic.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_align_critic.hpp @@ -18,6 +18,9 @@ #include "nav2_mppi_controller/critic_function.hpp" #include "nav2_mppi_controller/models/state.hpp" #include "nav2_mppi_controller/tools/utils.hpp" +#include "geometry_msgs/msg/pose_stamped.hpp" +#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" +#include "nav2_ros_common/publisher.hpp" namespace mppi::critics { @@ -52,6 +55,9 @@ class PathAlignCritic : public CriticFunction bool use_path_orientations_{false}; unsigned int power_{0}; float weight_{0}; + + bool visualize_furthest_point_{false}; + nav2::Publisher::SharedPtr furthest_point_pub_; }; } // namespace mppi::critics diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_angle_critic.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_angle_critic.hpp index b137270aba9..a265c21e864 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_angle_critic.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_angle_critic.hpp @@ -20,6 +20,9 @@ #include "nav2_mppi_controller/models/state.hpp" #include "nav2_mppi_controller/tools/utils.hpp" #include "nav2_core/controller_exceptions.hpp" +#include "geometry_msgs/msg/pose_stamped.hpp" +#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" +#include "nav2_ros_common/publisher.hpp" namespace mppi::critics { @@ -81,6 +84,8 @@ class PathAngleCritic : public CriticFunction unsigned int power_{0}; float weight_{0}; + bool visualize_furthest_point_{false}; + nav2::Publisher::SharedPtr furthest_point_pub_; }; } // namespace mppi::critics diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_follow_critic.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_follow_critic.hpp index 1ccd08c32fe..c1d3e157104 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_follow_critic.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/critics/path_follow_critic.hpp @@ -19,6 +19,10 @@ #include "nav2_mppi_controller/critic_function.hpp" #include "nav2_mppi_controller/models/state.hpp" #include "nav2_mppi_controller/tools/utils.hpp" +#include "geometry_msgs/msg/pose_stamped.hpp" +#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" +#include "nav2_ros_common/publisher.hpp" + namespace mppi::critics { @@ -53,6 +57,8 @@ class PathFollowCritic : public CriticFunction unsigned int power_{0}; float weight_{0}; + bool visualize_furthest_point_{false}; + nav2::Publisher::SharedPtr furthest_point_pub_; }; } // namespace mppi::critics diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/critics/waypoint_critic.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/critics/waypoint_critic.hpp new file mode 100644 index 00000000000..46fe0e2ed2a --- /dev/null +++ b/nav2_mppi_controller/include/nav2_mppi_controller/critics/waypoint_critic.hpp @@ -0,0 +1,112 @@ +// Copyright (c) 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NAV2_MPPI_CONTROLLER__CRITICS__WAYPOINT_CRITIC_HPP_ +#define NAV2_MPPI_CONTROLLER__CRITICS__WAYPOINT_CRITIC_HPP_ + +#include +#include +#include +#include + +#include "rclcpp/rclcpp.hpp" +#include "nav_msgs/msg/path.hpp" +#include "geometry_msgs/msg/pose_stamped.hpp" +#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" +#include "tf2/utils.h" + +#include "nav2_mppi_controller/critic_function.hpp" +#include "nav2_mppi_controller/models/state.hpp" +#include "nav2_mppi_controller/tools/utils.hpp" + +namespace mppi::critics +{ + +/** + * @class mppi::critics::WaypointCritic + * @brief Critic objective function for following sparse waypoints + * + * This critic subscribes to a nav_msgs/Path topic containing sparse waypoints + * and penalizes trajectories that don't pass through these waypoints with + * configurable position and orientation tolerances. + */ +class WaypointCritic : public CriticFunction +{ +public: + /** + * @brief Initialize critic + */ + void initialize() override; + + /** + * @brief Evaluate cost related to waypoint following + * + * @param costs [out] add waypoint cost values to this tensor + */ + void score(CriticData & data) override; + +protected: + /** + * @brief Callback for waypoint path subscription + * @param msg Received path message containing waypoints + */ + void waypointCallback(const nav_msgs::msg::Path::SharedPtr msg); + + /** + * @brief Get the current target waypoint (always the first one in the list) + * @return Index of target waypoint (0 for first), or -1 if no waypoints available + */ + int getTargetWaypoint(); + + /** + * @brief Calculate distance between two points + * @param x1 X coordinate of first point + * @param y1 Y coordinate of first point + * @param x2 X coordinate of second point + * @param y2 Y coordinate of second point + * @return Euclidean distance + */ + double calculateDistance(double x1, double y1, double x2, double y2); + + /** + * @brief Calculate angular difference between two angles + * @param angle1 First angle in radians + * @param angle2 Second angle in radians + * @return Angular difference in radians (-pi to pi) + */ + double calculateAngularDifference(double angle1, double angle2); + + // ROS subscription + rclcpp::Subscription::SharedPtr waypoint_sub_; + + // Waypoint data + std::vector waypoints_; + std::mutex waypoints_mutex_; + + // Parameters + unsigned int power_{1}; + float weight_{1.0f}; + float position_tolerance_{0.5f}; // meters + float yaw_tolerance_{0.5f}; // radians + std::string waypoint_topic_{"truncated_path"}; + bool use_orientation_{true}; // whether to consider orientation + float max_waypoint_distance_{10.0f}; // max distance to consider a waypoint + std::shared_ptr tf_buffer_; + std::shared_ptr tf_listener_; + std::string waypoints_frame_; +}; + +} // namespace mppi::critics + +#endif // NAV2_MPPI_CONTROLLER__CRITICS__WAYPOINT_CRITIC_HPP_ diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/fixing_trajectory_validator.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/fixing_trajectory_validator.hpp new file mode 100644 index 00000000000..93cb60f53f9 --- /dev/null +++ b/nav2_mppi_controller/include/nav2_mppi_controller/fixing_trajectory_validator.hpp @@ -0,0 +1,71 @@ +// include/nav2_mppi_controller/validators/auto_fixing_trajectory_validator.hpp +#ifndef NAV2_MPPI_CONTROLLER__AUTO_FIXING_TRAJECTORY_VALIDATOR_HPP_ +#define NAV2_MPPI_CONTROLLER__AUTO_FIXING_TRAJECTORY_VALIDATOR_HPP_ + +#include "nav2_mppi_controller/optimal_trajectory_validator.hpp" +#include "nav2_costmap_2d/footprint_collision_checker.hpp" +#include + +namespace mppi { + +class AutoFixingTrajectoryValidator : public OptimalTrajectoryValidator +{ +public: + using Ptr = std::shared_ptr; + AutoFixingTrajectoryValidator() = default; + ~AutoFixingTrajectoryValidator() override = default; + + void initialize( + const nav2::LifecycleNode::WeakPtr & parent, + const std::string & name, + const std::shared_ptr costmap, + ParametersHandler * param_handler, + const std::shared_ptr tf_buffer, + const models::OptimizerSettings & settings) override; + + ValidationResult validateTrajectory( + const Eigen::ArrayXXf & optimal_trajectory, + const models::ControlSequence & control_sequence, + 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) override; + + // Po SUCCESS moĆŒna pobrać naprawioną trajektorię (jeƛli byƂa korygowana) + bool hasFixedTrajectory() const { return has_fixed_; } + const Eigen::ArrayXXf & getFixedTrajectory() const { return fixed_traj_; } + +private: + // --- narzędzia kolizyjne / naprawcze --- + bool isCollisionAt(double x, double y, double theta) const; + bool isSafe(double x, double y, double theta) const; + double clearanceAt(double x, double y) const; + std::optional gradientAt(double x, double y) const; + + bool tryRepair(Eigen::ArrayXXf & traj, int start_idx, int end_idx) const; + void laplacianSmoothXY(Eigen::ArrayXXf & traj, const std::vector &locked, int iters) const; + void smoothYaw(Eigen::ArrayXXf & traj) const; + + // --- stan --- + nav2_costmap_2d::Costmap2D * costmap_{nullptr}; + std::unique_ptr> fp_checker_; + + // --- parametry --- + bool consider_footprint_{true}; + double safe_clearance_{0.12}; + double step_gain_{0.6}; + double max_step_{0.05}; + double max_total_disp_{0.25}; + int max_iters_{6}; + int smooth_iters_{2}; + float model_dt_{0.f}; + float auto_fix_ttc_window_{0.8f}; + + // --- wynik naprawy --- + mutable bool has_fixed_{false}; + mutable Eigen::ArrayXXf fixed_traj_; +}; + +} // namespace mppi + +#endif // NAV2_MPPI_CONTROLLER__AUTO_FIXING_TRAJECTORY_VALIDATOR_HPP_ diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/optimizer copy 2.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/optimizer copy 2.hpp new file mode 100644 index 00000000000..1914abde647 --- /dev/null +++ b/nav2_mppi_controller/include/nav2_mppi_controller/optimizer copy 2.hpp @@ -0,0 +1,295 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NAV2_MPPI_CONTROLLER__OPTIMIZER_HPP_ +#define NAV2_MPPI_CONTROLLER__OPTIMIZER_HPP_ + +#include + +#include +#include +#include + +#include "rclcpp_lifecycle/lifecycle_node.hpp" + +#include "nav2_costmap_2d/costmap_2d_ros.hpp" +#include "nav2_core/goal_checker.hpp" +#include "nav2_core/controller_exceptions.hpp" +#include "tf2_ros/buffer.hpp" +#include "pluginlib/class_loader.hpp" + +#include "geometry_msgs/msg/twist.hpp" +#include "geometry_msgs/msg/pose_stamped.hpp" +#include "geometry_msgs/msg/twist_stamped.hpp" +#include "nav_msgs/msg/path.hpp" +#include "nav2_util/geometry_utils.hpp" + +#include "nav2_mppi_controller/models/optimizer_settings.hpp" +#include "nav2_mppi_controller/motion_models.hpp" +#include "nav2_mppi_controller/critic_manager.hpp" +#include "nav2_mppi_controller/models/state.hpp" +#include "nav2_mppi_controller/models/trajectories.hpp" +#include "nav2_mppi_controller/models/path.hpp" +#include "nav2_mppi_controller/tools/noise_generator.hpp" +#include "nav2_mppi_controller/tools/parameters_handler.hpp" +#include "nav2_mppi_controller/tools/utils.hpp" +#include "nav2_mppi_controller/optimal_trajectory_validator.hpp" + +namespace mppi +{ + +/** + * @class mppi::Optimizer + * @brief Main algorithm optimizer of the MPPI Controller + */ +class Optimizer +{ +public: + /** + * @brief Constructor for mppi::Optimizer + */ + Optimizer() = default; + + /** + * @brief Destructor for mppi::Optimizer + */ + ~Optimizer() {shutdown();} + + + /** + * @brief Initializes optimizer on startup + * @param parent WeakPtr to node + * @param name Name of plugin + * @param costmap_ros Costmap2DROS object of environment + * @param dynamic_parameter_handler Parameter handler object + * @param tf_buffer TF buffer for transformations + */ + void initialize( + nav2::LifecycleNode::WeakPtr parent, const std::string & name, + std::shared_ptr costmap_ros, + std::shared_ptr tf_buffer, + ParametersHandler * dynamic_parameters_handler); + + /** + * @brief Shutdown for optimizer at process end + */ + void shutdown(); + + /** + * @brief Compute control using MPPI algorithm + * @param robot_pose Pose of the robot at given time + * @param robot_speed Speed of the robot at given time + * @param plan Path plan to track + * @param goal Given Goal pose to reach. + * @param goal_checker Object to check if goal is completed + * @return Tuple of [TwistStamped command, optimal trajectory] + */ + std::tuple evalControl( + const geometry_msgs::msg::PoseStamped & robot_pose, + const geometry_msgs::msg::Twist & robot_speed, const nav_msgs::msg::Path & plan, + const geometry_msgs::msg::Pose & goal, nav2_core::GoalChecker * goal_checker); + + /** + * @brief Get the trajectories generated in a cycle for visualization + * @return Set of trajectories evaluated in cycle + */ + models::Trajectories & getGeneratedTrajectories(); + + /** + * @brief Get the optimal trajectory for a cycle for visualization + * @return Optimal trajectory + */ + Eigen::ArrayXXf getOptimizedTrajectory(); + + /** + * @brief Get the optimal control sequence for a cycle for visualization + * @return Optimal control sequence + */ + const models::ControlSequence & getOptimalControlSequence(); + + /** + * @brief Set the maximum speed based on the speed limits callback + * @param speed_limit Limit of the speed for use + * @param percentage Whether the speed limit is absolute or relative + */ + void setSpeedLimit(double speed_limit, bool percentage); + + /** + * @brief Reset the optimization problem to initial conditions + * @param Whether to reset the constraints to its base values + */ + void reset(bool reset_dynamic_speed_limits = true); + + /** + * @brief Get the motion model time step + * @return Time step of the model + */ + const models::OptimizerSettings & getSettings() const + { + return settings_; + } + +protected: + /** + * @brief Main function to generate, score, and return trajectories + */ + void optimize(); + + /** + * @brief Prepare state information on new request for trajectory rollouts + * @param robot_pose Pose of the robot at given time + * @param robot_speed Speed of the robot at given time + * @param plan Path plan to track + * @param goal_checker Object to check if goal is completed + */ + void prepare( + const geometry_msgs::msg::PoseStamped & robot_pose, + const geometry_msgs::msg::Twist & robot_speed, + const nav_msgs::msg::Path & plan, + const geometry_msgs::msg::Pose & goal, nav2_core::GoalChecker * goal_checker); + + /** + * @brief Obtain the main controller's parameters + */ + void getParams(); + + /** + * @brief Set the motion model of the vehicle platform + * @param model Model string to use + */ + void setMotionModel(const std::string & model); + + /** + * @brief Shift the optimal control sequence after processing for + * next iterations initial conditions after execution + */ + void shiftControlSequence(); + + /** + * @brief updates generated trajectories with noised trajectories + * from the last cycle's optimal control + */ + void generateNoisedTrajectories(); + + /** + * @brief Apply hard vehicle constraints on control sequence + */ + void applyControlSequenceConstraints(); + + /** + * @brief Update velocities in state + * @param state fill state with velocities on each step + */ + void updateStateVelocities(models::State & state) const; + + /** + * @brief Update initial velocity in state + * @param state fill state + */ + void updateInitialStateVelocities(models::State & state) const; + + /** + * @brief predict velocities in state using model + * for time horizon equal to timesteps + * @param state fill state + */ + void propagateStateVelocitiesFromInitials(models::State & state) const; + + /** + * @brief Rollout velocities in state to poses + * @param trajectories to rollout + * @param state fill state + */ + void integrateStateVelocities( + models::Trajectories & trajectories, + const models::State & state) const; + + /** + * @brief Rollout velocities in state to poses + * @param trajectories to rollout + * @param state fill state + */ + void integrateStateVelocities( + Eigen::Array & trajectories, + const Eigen::ArrayXXf & state) const; + + /** + * @brief Update control sequence with state controls weighted by costs + * using softmax function + */ + void updateControlSequence(); + + /** + * @brief Convert control sequence to a twist command + * @param stamp Timestamp to use + * @return TwistStamped of command to send to robot base + */ + geometry_msgs::msg::TwistStamped + getControlFromSequenceAsTwist(const builtin_interfaces::msg::Time & stamp); + + /** + * @brief Whether the motion model is holonomic + * @return Bool if holonomic to populate `y` axis of state + */ + bool isHolonomic() const; + + /** + * @brief Using control frequencies and time step size, determine if trajectory + * offset should be used to populate initial state of the next cycle + */ + void setOffset(double controller_frequency); + + /** + * @brief Perform fallback behavior to try to recover from a set of trajectories in collision + * @param fail Whether the system failed to recover from + */ + bool fallback(bool fail); + +protected: + nav2::LifecycleNode::WeakPtr parent_; + std::shared_ptr costmap_ros_; + nav2_costmap_2d::Costmap2D * costmap_; + std::string name_; + std::shared_ptr tf_buffer_; + + std::shared_ptr motion_model_; + + ParametersHandler * parameters_handler_; + CriticManager critic_manager_; + NoiseGenerator noise_generator_; + + std::unique_ptr> validator_loader_; + OptimalTrajectoryValidator::Ptr trajectory_validator_; + + models::OptimizerSettings settings_; + + models::State state_; + models::ControlSequence control_sequence_; + std::array control_history_; + models::Trajectories generated_trajectories_; + models::Path path_; + geometry_msgs::msg::Pose goal_; + Eigen::ArrayXf costs_; + + CriticData critics_data_ = { + state_, generated_trajectories_, path_, goal_, + costs_, settings_.model_dt, false, nullptr, nullptr, + std::nullopt, std::nullopt}; /// Caution, keep references + + rclcpp::Logger logger_{rclcpp::get_logger("MPPIController")}; +}; + +} // namespace mppi + +#endif // NAV2_MPPI_CONTROLLER__OPTIMIZER_HPP_ diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/optimizer.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/optimizer.hpp index 1914abde647..4da02024bfa 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/optimizer.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/optimizer.hpp @@ -20,6 +20,8 @@ #include #include #include +#include +#include #include "rclcpp_lifecycle/lifecycle_node.hpp" @@ -118,6 +120,37 @@ class Optimizer */ const models::ControlSequence & getOptimalControlSequence(); + /** + * @brief Get the costs for trajectories for visualization + * @return Costs array + */ + const Eigen::ArrayXf & getCosts() const + { + return costs_; + } + + /** + * @brief Get the per-critic costs for trajectories for visualization + * @return Vector of (critic_name, costs) pairs + */ + const std::vector> & getCriticCosts() const + { + if (critics_data_.individual_critics_cost) { + return *critics_data_.individual_critics_cost; + } + static const std::vector> empty; + return empty; + } + + /** + * @brief Get the collision status for trajectories for visualization + * @return Optional vector of collision flags for each trajectory + */ + const std::optional> & getTrajectoriesInCollision() const + { + return critics_data_.trajectories_in_collision; + } + /** * @brief Set the maximum speed based on the speed limits callback * @param speed_limit Limit of the speed for use @@ -284,7 +317,7 @@ class Optimizer CriticData critics_data_ = { state_, generated_trajectories_, path_, goal_, - costs_, settings_.model_dt, false, nullptr, nullptr, + costs_, std::nullopt, settings_.model_dt, false, std::nullopt, nullptr, nullptr, std::nullopt, std::nullopt}; /// Caution, keep references rclcpp::Logger logger_{rclcpp::get_logger("MPPIController")}; @@ -292,4 +325,4 @@ class Optimizer } // namespace mppi -#endif // NAV2_MPPI_CONTROLLER__OPTIMIZER_HPP_ +#endif // NAV2_MPPI_CONTROLLER__OPTIMIZER_HPP_ \ No newline at end of file diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler copy.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler copy.hpp new file mode 100644 index 00000000000..bb5320ebe62 --- /dev/null +++ b/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler copy.hpp @@ -0,0 +1,159 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// Copyright (c) 2023 Dexory +// Copyright (c) 2023 Open Navigation LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NAV2_MPPI_CONTROLLER__TOOLS__PATH_HANDLER_HPP_ +#define NAV2_MPPI_CONTROLLER__TOOLS__PATH_HANDLER_HPP_ + +#include +#include +#include +#include + +#include "nav2_ros_common/lifecycle_node.hpp" +#include "tf2_ros/buffer.hpp" +#include "geometry_msgs/msg/pose_stamped.hpp" +#include "nav_msgs/msg/path.hpp" +#include "builtin_interfaces/msg/time.hpp" +#include "nav2_costmap_2d/costmap_2d_ros.hpp" +#include "nav2_util/geometry_utils.hpp" +#include "nav2_core/controller_exceptions.hpp" + +#include "nav2_mppi_controller/tools/parameters_handler.hpp" + +namespace mppi +{ + +using PathIterator = std::vector::iterator; +using PathRange = std::pair; + +/** + * @class mppi::PathHandler + * @brief Manager of incoming reference paths for transformation and processing + */ + +class PathHandler +{ +public: + /** + * @brief Constructor for mppi::PathHandler + */ + PathHandler() = default; + + /** + * @brief Destructor for mppi::PathHandler + */ + ~PathHandler() = default; + + /** + * @brief Initialize path handler on bringup + * @param parent WeakPtr to node + * @param name Name of plugin + * @param costmap_ros Costmap2DROS object of environment + * @param tf TF buffer for transformations + * @param dynamic_parameter_handler Parameter handler object + */ + void initialize( + nav2::LifecycleNode::WeakPtr parent, const std::string & name, + std::shared_ptr, + std::shared_ptr, ParametersHandler *); + + /** + * @brief Set new reference path + * @param Plan Path to use + */ + void setPath(const nav_msgs::msg::Path & plan); + + /** + * @brief Get reference path + * @return Path + */ + nav_msgs::msg::Path & getPath(); + + /** + * @brief transform global plan to local applying constraints, + * then prune global plan + * @param robot_pose Pose of robot + * @return global plan in local frame + */ + nav_msgs::msg::Path transformPath(const geometry_msgs::msg::PoseStamped & robot_pose); + + /** + * @brief Get the global goal pose transformed to the local frame + * @param stamp Time to get the goal pose at + * @return Transformed goal pose + */ + geometry_msgs::msg::PoseStamped getTransformedGoal(const builtin_interfaces::msg::Time & stamp); + +protected: + /** + * @brief Get largest dimension of costmap (radially) + * @return Max distance from center of costmap to edge + */ + double getMaxCostmapDist(); + + /** + * @brief Transform a pose to the global reference frame + * @param pose Current pose + * @return output poose in global reference frame + */ + geometry_msgs::msg::PoseStamped + transformToGlobalPlanFrame(const geometry_msgs::msg::PoseStamped & pose); + + /** + * @brief Get global plan within window of the local costmap size + * @param global_pose Robot pose + * @return plan transformed in the costmap frame and iterator to the first pose of the global + * plan (for pruning) + */ + std::pair getGlobalPlanConsideringBoundsInCostmapFrame( + const geometry_msgs::msg::PoseStamped & global_pose); + + /** + * @brief Prune a path to only interesting portions + * @param plan Plan to prune + * @param end Final path iterator + */ + void prunePlan(nav_msgs::msg::Path & plan, const PathIterator end); + + /** + * @brief Check if the robot pose is within the set inversion or rotation tolerances + * @param robot_pose Robot's current pose to check + * @return bool If the robot pose is within the tolerances for the next path constraint + */ + bool isWithinInversionTolerances(const geometry_msgs::msg::PoseStamped & robot_pose); + + std::string name_; + std::shared_ptr costmap_; + std::shared_ptr tf_buffer_; + ParametersHandler * parameters_handler_; + + nav_msgs::msg::Path global_plan_; + nav_msgs::msg::Path global_plan_up_to_inversion_; + rclcpp::Logger logger_{rclcpp::get_logger("MPPIController")}; + + double max_robot_pose_search_dist_{0}; + double prune_distance_{0}; + double transform_tolerance_{0}; + float inversion_xy_tolerance_{0.2}; + float inversion_yaw_tolerance_{0.4}; + float minimum_rotation_angle_{0.785}; + bool enforce_path_inversion_{false}; + bool enforce_path_rotation_{false}; + unsigned int inversion_locale_{0u}; +}; +} // namespace mppi + +#endif // NAV2_MPPI_CONTROLLER__TOOLS__PATH_HANDLER_HPP_ \ No newline at end of file diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler.hpp index 4233c91ef28..bb5320ebe62 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler.hpp @@ -129,9 +129,9 @@ class PathHandler void prunePlan(nav_msgs::msg::Path & plan, const PathIterator end); /** - * @brief Check if the robot pose is within the set inversion tolerances + * @brief Check if the robot pose is within the set inversion or rotation tolerances * @param robot_pose Robot's current pose to check - * @return bool If the robot pose is within the set inversion tolerances + * @return bool If the robot pose is within the tolerances for the next path constraint */ bool isWithinInversionTolerances(const geometry_msgs::msg::PoseStamped & robot_pose); @@ -148,10 +148,12 @@ class PathHandler double prune_distance_{0}; double transform_tolerance_{0}; float inversion_xy_tolerance_{0.2}; - float inversion_yaw_tolerance{0.4}; + float inversion_yaw_tolerance_{0.4}; + float minimum_rotation_angle_{0.785}; bool enforce_path_inversion_{false}; + bool enforce_path_rotation_{false}; unsigned int inversion_locale_{0u}; }; } // namespace mppi -#endif // NAV2_MPPI_CONTROLLER__TOOLS__PATH_HANDLER_HPP_ +#endif // NAV2_MPPI_CONTROLLER__TOOLS__PATH_HANDLER_HPP_ \ No newline at end of file diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler_mod.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler_mod.hpp new file mode 100644 index 00000000000..bb5320ebe62 --- /dev/null +++ b/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler_mod.hpp @@ -0,0 +1,159 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// Copyright (c) 2023 Dexory +// Copyright (c) 2023 Open Navigation LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NAV2_MPPI_CONTROLLER__TOOLS__PATH_HANDLER_HPP_ +#define NAV2_MPPI_CONTROLLER__TOOLS__PATH_HANDLER_HPP_ + +#include +#include +#include +#include + +#include "nav2_ros_common/lifecycle_node.hpp" +#include "tf2_ros/buffer.hpp" +#include "geometry_msgs/msg/pose_stamped.hpp" +#include "nav_msgs/msg/path.hpp" +#include "builtin_interfaces/msg/time.hpp" +#include "nav2_costmap_2d/costmap_2d_ros.hpp" +#include "nav2_util/geometry_utils.hpp" +#include "nav2_core/controller_exceptions.hpp" + +#include "nav2_mppi_controller/tools/parameters_handler.hpp" + +namespace mppi +{ + +using PathIterator = std::vector::iterator; +using PathRange = std::pair; + +/** + * @class mppi::PathHandler + * @brief Manager of incoming reference paths for transformation and processing + */ + +class PathHandler +{ +public: + /** + * @brief Constructor for mppi::PathHandler + */ + PathHandler() = default; + + /** + * @brief Destructor for mppi::PathHandler + */ + ~PathHandler() = default; + + /** + * @brief Initialize path handler on bringup + * @param parent WeakPtr to node + * @param name Name of plugin + * @param costmap_ros Costmap2DROS object of environment + * @param tf TF buffer for transformations + * @param dynamic_parameter_handler Parameter handler object + */ + void initialize( + nav2::LifecycleNode::WeakPtr parent, const std::string & name, + std::shared_ptr, + std::shared_ptr, ParametersHandler *); + + /** + * @brief Set new reference path + * @param Plan Path to use + */ + void setPath(const nav_msgs::msg::Path & plan); + + /** + * @brief Get reference path + * @return Path + */ + nav_msgs::msg::Path & getPath(); + + /** + * @brief transform global plan to local applying constraints, + * then prune global plan + * @param robot_pose Pose of robot + * @return global plan in local frame + */ + nav_msgs::msg::Path transformPath(const geometry_msgs::msg::PoseStamped & robot_pose); + + /** + * @brief Get the global goal pose transformed to the local frame + * @param stamp Time to get the goal pose at + * @return Transformed goal pose + */ + geometry_msgs::msg::PoseStamped getTransformedGoal(const builtin_interfaces::msg::Time & stamp); + +protected: + /** + * @brief Get largest dimension of costmap (radially) + * @return Max distance from center of costmap to edge + */ + double getMaxCostmapDist(); + + /** + * @brief Transform a pose to the global reference frame + * @param pose Current pose + * @return output poose in global reference frame + */ + geometry_msgs::msg::PoseStamped + transformToGlobalPlanFrame(const geometry_msgs::msg::PoseStamped & pose); + + /** + * @brief Get global plan within window of the local costmap size + * @param global_pose Robot pose + * @return plan transformed in the costmap frame and iterator to the first pose of the global + * plan (for pruning) + */ + std::pair getGlobalPlanConsideringBoundsInCostmapFrame( + const geometry_msgs::msg::PoseStamped & global_pose); + + /** + * @brief Prune a path to only interesting portions + * @param plan Plan to prune + * @param end Final path iterator + */ + void prunePlan(nav_msgs::msg::Path & plan, const PathIterator end); + + /** + * @brief Check if the robot pose is within the set inversion or rotation tolerances + * @param robot_pose Robot's current pose to check + * @return bool If the robot pose is within the tolerances for the next path constraint + */ + bool isWithinInversionTolerances(const geometry_msgs::msg::PoseStamped & robot_pose); + + std::string name_; + std::shared_ptr costmap_; + std::shared_ptr tf_buffer_; + ParametersHandler * parameters_handler_; + + nav_msgs::msg::Path global_plan_; + nav_msgs::msg::Path global_plan_up_to_inversion_; + rclcpp::Logger logger_{rclcpp::get_logger("MPPIController")}; + + double max_robot_pose_search_dist_{0}; + double prune_distance_{0}; + double transform_tolerance_{0}; + float inversion_xy_tolerance_{0.2}; + float inversion_yaw_tolerance_{0.4}; + float minimum_rotation_angle_{0.785}; + bool enforce_path_inversion_{false}; + bool enforce_path_rotation_{false}; + unsigned int inversion_locale_{0u}; +}; +} // namespace mppi + +#endif // NAV2_MPPI_CONTROLLER__TOOLS__PATH_HANDLER_HPP_ \ No newline at end of file diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler_mod_final.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler_mod_final.hpp new file mode 100644 index 00000000000..bb5320ebe62 --- /dev/null +++ b/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler_mod_final.hpp @@ -0,0 +1,159 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// Copyright (c) 2023 Dexory +// Copyright (c) 2023 Open Navigation LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NAV2_MPPI_CONTROLLER__TOOLS__PATH_HANDLER_HPP_ +#define NAV2_MPPI_CONTROLLER__TOOLS__PATH_HANDLER_HPP_ + +#include +#include +#include +#include + +#include "nav2_ros_common/lifecycle_node.hpp" +#include "tf2_ros/buffer.hpp" +#include "geometry_msgs/msg/pose_stamped.hpp" +#include "nav_msgs/msg/path.hpp" +#include "builtin_interfaces/msg/time.hpp" +#include "nav2_costmap_2d/costmap_2d_ros.hpp" +#include "nav2_util/geometry_utils.hpp" +#include "nav2_core/controller_exceptions.hpp" + +#include "nav2_mppi_controller/tools/parameters_handler.hpp" + +namespace mppi +{ + +using PathIterator = std::vector::iterator; +using PathRange = std::pair; + +/** + * @class mppi::PathHandler + * @brief Manager of incoming reference paths for transformation and processing + */ + +class PathHandler +{ +public: + /** + * @brief Constructor for mppi::PathHandler + */ + PathHandler() = default; + + /** + * @brief Destructor for mppi::PathHandler + */ + ~PathHandler() = default; + + /** + * @brief Initialize path handler on bringup + * @param parent WeakPtr to node + * @param name Name of plugin + * @param costmap_ros Costmap2DROS object of environment + * @param tf TF buffer for transformations + * @param dynamic_parameter_handler Parameter handler object + */ + void initialize( + nav2::LifecycleNode::WeakPtr parent, const std::string & name, + std::shared_ptr, + std::shared_ptr, ParametersHandler *); + + /** + * @brief Set new reference path + * @param Plan Path to use + */ + void setPath(const nav_msgs::msg::Path & plan); + + /** + * @brief Get reference path + * @return Path + */ + nav_msgs::msg::Path & getPath(); + + /** + * @brief transform global plan to local applying constraints, + * then prune global plan + * @param robot_pose Pose of robot + * @return global plan in local frame + */ + nav_msgs::msg::Path transformPath(const geometry_msgs::msg::PoseStamped & robot_pose); + + /** + * @brief Get the global goal pose transformed to the local frame + * @param stamp Time to get the goal pose at + * @return Transformed goal pose + */ + geometry_msgs::msg::PoseStamped getTransformedGoal(const builtin_interfaces::msg::Time & stamp); + +protected: + /** + * @brief Get largest dimension of costmap (radially) + * @return Max distance from center of costmap to edge + */ + double getMaxCostmapDist(); + + /** + * @brief Transform a pose to the global reference frame + * @param pose Current pose + * @return output poose in global reference frame + */ + geometry_msgs::msg::PoseStamped + transformToGlobalPlanFrame(const geometry_msgs::msg::PoseStamped & pose); + + /** + * @brief Get global plan within window of the local costmap size + * @param global_pose Robot pose + * @return plan transformed in the costmap frame and iterator to the first pose of the global + * plan (for pruning) + */ + std::pair getGlobalPlanConsideringBoundsInCostmapFrame( + const geometry_msgs::msg::PoseStamped & global_pose); + + /** + * @brief Prune a path to only interesting portions + * @param plan Plan to prune + * @param end Final path iterator + */ + void prunePlan(nav_msgs::msg::Path & plan, const PathIterator end); + + /** + * @brief Check if the robot pose is within the set inversion or rotation tolerances + * @param robot_pose Robot's current pose to check + * @return bool If the robot pose is within the tolerances for the next path constraint + */ + bool isWithinInversionTolerances(const geometry_msgs::msg::PoseStamped & robot_pose); + + std::string name_; + std::shared_ptr costmap_; + std::shared_ptr tf_buffer_; + ParametersHandler * parameters_handler_; + + nav_msgs::msg::Path global_plan_; + nav_msgs::msg::Path global_plan_up_to_inversion_; + rclcpp::Logger logger_{rclcpp::get_logger("MPPIController")}; + + double max_robot_pose_search_dist_{0}; + double prune_distance_{0}; + double transform_tolerance_{0}; + float inversion_xy_tolerance_{0.2}; + float inversion_yaw_tolerance_{0.4}; + float minimum_rotation_angle_{0.785}; + bool enforce_path_inversion_{false}; + bool enforce_path_rotation_{false}; + unsigned int inversion_locale_{0u}; +}; +} // namespace mppi + +#endif // NAV2_MPPI_CONTROLLER__TOOLS__PATH_HANDLER_HPP_ \ No newline at end of file diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler_original.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler_original.hpp new file mode 100644 index 00000000000..4233c91ef28 --- /dev/null +++ b/nav2_mppi_controller/include/nav2_mppi_controller/tools/path_handler_original.hpp @@ -0,0 +1,157 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// Copyright (c) 2023 Dexory +// Copyright (c) 2023 Open Navigation LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NAV2_MPPI_CONTROLLER__TOOLS__PATH_HANDLER_HPP_ +#define NAV2_MPPI_CONTROLLER__TOOLS__PATH_HANDLER_HPP_ + +#include +#include +#include +#include + +#include "nav2_ros_common/lifecycle_node.hpp" +#include "tf2_ros/buffer.hpp" +#include "geometry_msgs/msg/pose_stamped.hpp" +#include "nav_msgs/msg/path.hpp" +#include "builtin_interfaces/msg/time.hpp" +#include "nav2_costmap_2d/costmap_2d_ros.hpp" +#include "nav2_util/geometry_utils.hpp" +#include "nav2_core/controller_exceptions.hpp" + +#include "nav2_mppi_controller/tools/parameters_handler.hpp" + +namespace mppi +{ + +using PathIterator = std::vector::iterator; +using PathRange = std::pair; + +/** + * @class mppi::PathHandler + * @brief Manager of incoming reference paths for transformation and processing + */ + +class PathHandler +{ +public: + /** + * @brief Constructor for mppi::PathHandler + */ + PathHandler() = default; + + /** + * @brief Destructor for mppi::PathHandler + */ + ~PathHandler() = default; + + /** + * @brief Initialize path handler on bringup + * @param parent WeakPtr to node + * @param name Name of plugin + * @param costmap_ros Costmap2DROS object of environment + * @param tf TF buffer for transformations + * @param dynamic_parameter_handler Parameter handler object + */ + void initialize( + nav2::LifecycleNode::WeakPtr parent, const std::string & name, + std::shared_ptr, + std::shared_ptr, ParametersHandler *); + + /** + * @brief Set new reference path + * @param Plan Path to use + */ + void setPath(const nav_msgs::msg::Path & plan); + + /** + * @brief Get reference path + * @return Path + */ + nav_msgs::msg::Path & getPath(); + + /** + * @brief transform global plan to local applying constraints, + * then prune global plan + * @param robot_pose Pose of robot + * @return global plan in local frame + */ + nav_msgs::msg::Path transformPath(const geometry_msgs::msg::PoseStamped & robot_pose); + + /** + * @brief Get the global goal pose transformed to the local frame + * @param stamp Time to get the goal pose at + * @return Transformed goal pose + */ + geometry_msgs::msg::PoseStamped getTransformedGoal(const builtin_interfaces::msg::Time & stamp); + +protected: + /** + * @brief Get largest dimension of costmap (radially) + * @return Max distance from center of costmap to edge + */ + double getMaxCostmapDist(); + + /** + * @brief Transform a pose to the global reference frame + * @param pose Current pose + * @return output poose in global reference frame + */ + geometry_msgs::msg::PoseStamped + transformToGlobalPlanFrame(const geometry_msgs::msg::PoseStamped & pose); + + /** + * @brief Get global plan within window of the local costmap size + * @param global_pose Robot pose + * @return plan transformed in the costmap frame and iterator to the first pose of the global + * plan (for pruning) + */ + std::pair getGlobalPlanConsideringBoundsInCostmapFrame( + const geometry_msgs::msg::PoseStamped & global_pose); + + /** + * @brief Prune a path to only interesting portions + * @param plan Plan to prune + * @param end Final path iterator + */ + void prunePlan(nav_msgs::msg::Path & plan, const PathIterator end); + + /** + * @brief Check if the robot pose is within the set inversion tolerances + * @param robot_pose Robot's current pose to check + * @return bool If the robot pose is within the set inversion tolerances + */ + bool isWithinInversionTolerances(const geometry_msgs::msg::PoseStamped & robot_pose); + + std::string name_; + std::shared_ptr costmap_; + std::shared_ptr tf_buffer_; + ParametersHandler * parameters_handler_; + + nav_msgs::msg::Path global_plan_; + nav_msgs::msg::Path global_plan_up_to_inversion_; + rclcpp::Logger logger_{rclcpp::get_logger("MPPIController")}; + + double max_robot_pose_search_dist_{0}; + double prune_distance_{0}; + double transform_tolerance_{0}; + float inversion_xy_tolerance_{0.2}; + float inversion_yaw_tolerance{0.4}; + bool enforce_path_inversion_{false}; + unsigned int inversion_locale_{0u}; +}; +} // namespace mppi + +#endif // NAV2_MPPI_CONTROLLER__TOOLS__PATH_HANDLER_HPP_ diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/tools/trajectory_visualizer copy 2.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/tools/trajectory_visualizer copy 2.hpp new file mode 100644 index 00000000000..f57d9b6b2b2 --- /dev/null +++ b/nav2_mppi_controller/include/nav2_mppi_controller/tools/trajectory_visualizer copy 2.hpp @@ -0,0 +1,118 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NAV2_MPPI_CONTROLLER__TOOLS__TRAJECTORY_VISUALIZER_HPP_ +#define NAV2_MPPI_CONTROLLER__TOOLS__TRAJECTORY_VISUALIZER_HPP_ + +#include + +#include +#include + +#include "nav_msgs/msg/path.hpp" +#include "nav2_ros_common/lifecycle_node.hpp" +#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" + +#include "nav2_mppi_controller/tools/parameters_handler.hpp" +#include "nav2_mppi_controller/tools/utils.hpp" +#include "nav2_mppi_controller/models/trajectories.hpp" + +namespace mppi +{ + +/** + * @class mppi::TrajectoryVisualizer + * @brief Visualizes trajectories for debugging + */ +class TrajectoryVisualizer +{ +public: + /** + * @brief Constructor for mppi::TrajectoryVisualizer + */ + TrajectoryVisualizer() = default; + + /** + * @brief Configure trajectory visualizer + * @param parent WeakPtr to node + * @param name Name of plugin + * @param frame_id Frame to publish trajectories in + * @param dynamic_parameter_handler Parameter handler object + */ + void on_configure( + nav2::LifecycleNode::WeakPtr parent, const std::string & name, + const std::string & frame_id, ParametersHandler * parameters_handler); + + /** + * @brief Cleanup object on shutdown + */ + void on_cleanup(); + + /** + * @brief Activate object + */ + void on_activate(); + + /** + * @brief Deactivate object + */ + void on_deactivate(); + + /** + * @brief Add an optimal trajectory to visualize + * @param trajectory Optimal trajectory + */ + void add( + const Eigen::ArrayXXf & trajectory, const std::string & marker_namespace, + const builtin_interfaces::msg::Time & cmd_stamp); + + /** + * @brief Add candidate trajectories to visualize + * @param trajectories Candidate trajectories + */ + void add(const models::Trajectories & trajectories, const std::string & marker_namespace); + + /** + * @brief Visualize the plan + * @param plan Plan to visualize + */ + void visualize(const nav_msgs::msg::Path & plan); + + /** + * @brief Reset object + */ + void reset(); + +protected: + std::string frame_id_; + nav2::Publisher::SharedPtr + trajectories_publisher_; + nav2::Publisher::SharedPtr transformed_path_pub_; + nav2::Publisher::SharedPtr optimal_path_pub_; + + std::unique_ptr optimal_path_; + std::unique_ptr points_; + int marker_id_ = 0; + + ParametersHandler * parameters_handler_; + + size_t trajectory_step_{0}; + size_t time_step_{0}; + + rclcpp::Logger logger_{rclcpp::get_logger("MPPIController")}; +}; + +} // namespace mppi + +#endif // NAV2_MPPI_CONTROLLER__TOOLS__TRAJECTORY_VISUALIZER_HPP_ diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/tools/trajectory_visualizer.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/tools/trajectory_visualizer.hpp index f57d9b6b2b2..b0a1afadfff 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/tools/trajectory_visualizer.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/tools/trajectory_visualizer.hpp @@ -19,14 +19,20 @@ #include #include +#include +#include #include "nav_msgs/msg/path.hpp" +#include "nav2_msgs/msg/trajectory.hpp" #include "nav2_ros_common/lifecycle_node.hpp" +#include "nav2_costmap_2d/costmap_2d_ros.hpp" +#include "nav2_costmap_2d/footprint.hpp" #include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" #include "nav2_mppi_controller/tools/parameters_handler.hpp" #include "nav2_mppi_controller/tools/utils.hpp" #include "nav2_mppi_controller/models/trajectories.hpp" +#include "nav2_mppi_controller/models/control_sequence.hpp" namespace mppi { @@ -78,16 +84,50 @@ class TrajectoryVisualizer const builtin_interfaces::msg::Time & cmd_stamp); /** - * @brief Add candidate trajectories to visualize + * @brief Add candidate trajectories with costs to visualize * @param trajectories Candidate trajectories + * @param total_costs Total cost array for each trajectory + * @param individual_critics_cost Optional vector of (critic_name, cost_array) pairs for per-critic visualization + * @param cmd_stamp Timestamp for the markers + * @param trajectories_in_collision Optional collision flags for each trajectory */ - void add(const models::Trajectories & trajectories, const std::string & marker_namespace); + void add( + const models::Trajectories & trajectories, + const Eigen::ArrayXf & total_costs, + const std::vector> & individual_critics_cost = {}, + const builtin_interfaces::msg::Time & cmd_stamp = builtin_interfaces::msg::Time(), + const std::optional> & trajectories_in_collision = std::nullopt); + + /** + * @brief Visualize all trajectory data in one call + * @param plan Transformed plan to visualize + * @param optimal_trajectory Optimal trajectory + * @param control_sequence Control sequence for optimal trajectory + * @param model_dt Model time step + * @param stamp Timestamp for the visualization + * @param costmap_ros Costmap ROS pointer + * @param candidate_trajectories Generated candidate trajectories + * @param costs Total costs for each trajectory + * @param critic_costs Per-critic costs for each trajectory + * @param trajectories_in_collision Optional collision flags for each trajectory + */ + void visualize( + nav_msgs::msg::Path plan, + const Eigen::ArrayXXf & optimal_trajectory, + const models::ControlSequence & control_sequence, + float model_dt, + const builtin_interfaces::msg::Time & stamp, + std::shared_ptr costmap_ros, + const models::Trajectories & candidate_trajectories, + const Eigen::ArrayXf & costs, + const std::vector> & critic_costs, + const std::optional> & trajectories_in_collision = std::nullopt); /** - * @brief Visualize the plan - * @param plan Plan to visualize + * @brief Visualize without optimizer (for testing) + * @param plan Transformed plan to visualize */ - void visualize(const nav_msgs::msg::Path & plan); + void visualize(nav_msgs::msg::Path plan); /** * @brief Reset object @@ -95,11 +135,40 @@ class TrajectoryVisualizer void reset(); protected: + /** + * @brief Create trajectory markers with cost-based coloring + * @param trajectories Set of trajectories to visualize + * @param costs Cost array for each trajectory + * @param ns Namespace for the markers + * @param cmd_stamp Timestamp for the markers + * @param trajectories_in_collision Optional collision flags for each trajectory + */ + void createTrajectoryMarkers( + const models::Trajectories & trajectories, + const Eigen::ArrayXf & costs, + const std::string & ns, + const builtin_interfaces::msg::Time & cmd_stamp, + const std::optional> & trajectories_in_collision = std::nullopt); + + /** + * @brief Create footprint markers from trajectory + * @param trajectory Optimal trajectory + * @param header Message header + * @param costmap_ros Costmap ROS pointer for footprint and frame information + * @return MarkerArray containing footprint polygons + */ + visualization_msgs::msg::MarkerArray createFootprintMarkers( + const Eigen::ArrayXXf & trajectory, + const std_msgs::msg::Header & header, + std::shared_ptr costmap_ros); + std::string frame_id_; nav2::Publisher::SharedPtr trajectories_publisher_; nav2::Publisher::SharedPtr transformed_path_pub_; nav2::Publisher::SharedPtr optimal_path_pub_; + nav2::Publisher::SharedPtr optimal_footprints_pub_; + nav2::Publisher::SharedPtr optimal_trajectory_msg_pub_; std::unique_ptr optimal_path_; std::unique_ptr points_; @@ -109,10 +178,17 @@ class TrajectoryVisualizer size_t trajectory_step_{0}; size_t time_step_{0}; + bool publish_trajectories_with_total_cost_{false}; + bool publish_trajectories_with_individual_cost_{false}; + bool publish_optimal_footprints_{false}; + bool publish_optimal_trajectory_msg_{false}; + bool publish_transformed_path_{false}; + bool publish_optimal_path_{false}; + int footprint_downsample_factor_{3}; rclcpp::Logger logger_{rclcpp::get_logger("MPPIController")}; }; } // namespace mppi -#endif // NAV2_MPPI_CONTROLLER__TOOLS__TRAJECTORY_VISUALIZER_HPP_ +#endif // NAV2_MPPI_CONTROLLER__TOOLS__TRAJECTORY_VISUALIZER_HPP_ \ No newline at end of file diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils copy 2.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils copy 2.hpp new file mode 100644 index 00000000000..4a3cc6b7c06 --- /dev/null +++ b/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils copy 2.hpp @@ -0,0 +1,689 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// Copyright (c) 2023 Open Navigation LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NAV2_MPPI_CONTROLLER__TOOLS__UTILS_HPP_ +#define NAV2_MPPI_CONTROLLER__TOOLS__UTILS_HPP_ + +#include + +#include +#include +#include +#include +#include +#include + +#include "angles/angles.h" + +#include "tf2/utils.hpp" +#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" + +#include "geometry_msgs/msg/twist_stamped.hpp" +#include "nav_msgs/msg/path.hpp" +#include "nav2_msgs/msg/trajectory.hpp" +#include "visualization_msgs/msg/marker_array.hpp" + +#include "rclcpp/rclcpp.hpp" +#include "rclcpp_lifecycle/lifecycle_node.hpp" + +#include "nav2_ros_common/node_utils.hpp" +#include "nav2_core/goal_checker.hpp" + +#include "nav2_mppi_controller/models/optimizer_settings.hpp" +#include "nav2_mppi_controller/models/control_sequence.hpp" +#include "nav2_mppi_controller/models/path.hpp" +#include "builtin_interfaces/msg/time.hpp" +#include "nav2_mppi_controller/critic_data.hpp" + +#define M_PIF 3.141592653589793238462643383279502884e+00F +#define M_PIF_2 1.5707963267948966e+00F + +namespace mppi::utils +{ +/** + * @brief Convert data into pose + * @param x X position + * @param y Y position + * @param z Z position + * @return Pose object + */ +inline geometry_msgs::msg::Pose createPose(double x, double y, double z) +{ + geometry_msgs::msg::Pose pose; + pose.position.x = x; + pose.position.y = y; + pose.position.z = z; + pose.orientation.w = 1; + pose.orientation.x = 0; + pose.orientation.y = 0; + pose.orientation.z = 0; + return pose; +} + +/** + * @brief Convert data into scale + * @param x X scale + * @param y Y scale + * @param z Z scale + * @return Scale object + */ +inline geometry_msgs::msg::Vector3 createScale(double x, double y, double z) +{ + geometry_msgs::msg::Vector3 scale; + scale.x = x; + scale.y = y; + scale.z = z; + return scale; +} + +/** + * @brief Convert data into color + * @param r Red component + * @param g Green component + * @param b Blue component + * @param a Alpha component (transparency) + * @return Color object + */ +inline std_msgs::msg::ColorRGBA createColor(float r, float g, float b, float a) +{ + std_msgs::msg::ColorRGBA color; + color.r = r; + color.g = g; + color.b = b; + color.a = a; + return color; +} + +/** + * @brief Convert data into a Maarker + * @param id Marker ID + * @param pose Marker pose + * @param scale Marker scale + * @param color Marker color + * @param frame Reference frame to use + * @return Visualization Marker + */ +inline visualization_msgs::msg::Marker createMarker( + int id, const geometry_msgs::msg::Pose & pose, const geometry_msgs::msg::Vector3 & scale, + const std_msgs::msg::ColorRGBA & color, const std::string & frame_id, const std::string & ns) +{ + using visualization_msgs::msg::Marker; + Marker marker; + marker.header.frame_id = frame_id; + marker.header.stamp = rclcpp::Time(0, 0); + marker.ns = ns; + marker.id = id; + marker.type = Marker::SPHERE; + marker.action = Marker::ADD; + + marker.pose = pose; + marker.scale = scale; + marker.color = color; + return marker; +} + +/** + * @brief Convert data into TwistStamped + * @param vx X velocity + * @param wz Angular velocity + * @param stamp Timestamp + * @param frame Reference frame to use + */ +inline geometry_msgs::msg::TwistStamped toTwistStamped( + float vx, float wz, const builtin_interfaces::msg::Time & stamp, const std::string & frame) +{ + geometry_msgs::msg::TwistStamped twist; + twist.header.frame_id = frame; + twist.header.stamp = stamp; + twist.twist.linear.x = vx; + twist.twist.angular.z = wz; + + return twist; +} + +/** + * @brief Convert data into TwistStamped + * @param vx X velocity + * @param vy Y velocity + * @param wz Angular velocity + * @param stamp Timestamp + * @param frame Reference frame to use + */ +inline geometry_msgs::msg::TwistStamped toTwistStamped( + float vx, float vy, float wz, const builtin_interfaces::msg::Time & stamp, + const std::string & frame) +{ + auto twist = toTwistStamped(vx, wz, stamp, frame); + twist.twist.linear.y = vy; + + return twist; +} + +inline std::unique_ptr toTrajectoryMsg( + const Eigen::ArrayXXf & trajectory, + const models::ControlSequence & control_sequence, + const double & model_dt, + const std_msgs::msg::Header & header) +{ + auto trajectory_msg = std::make_unique(); + trajectory_msg->header = header; + trajectory_msg->points.resize(trajectory.rows()); + + for (int i = 0; i < trajectory.rows(); ++i) { + auto & curr_pt = trajectory_msg->points[i]; + curr_pt.time_from_start = rclcpp::Duration::from_seconds(i * model_dt); + curr_pt.pose.position.x = trajectory(i, 0); + curr_pt.pose.position.y = trajectory(i, 1); + tf2::Quaternion quat; + quat.setRPY(0.0, 0.0, trajectory(i, 2)); + curr_pt.pose.orientation = tf2::toMsg(quat); + curr_pt.velocity.linear.x = control_sequence.vx(i); + curr_pt.velocity.angular.z = control_sequence.wz(i); + if (control_sequence.vy.size() > 0) { + curr_pt.velocity.linear.y = control_sequence.vy(i); + } + } + + return trajectory_msg; +} + +/** + * @brief Convert path to a tensor + * @param path Path to convert + * @return Path tensor + */ +inline models::Path toTensor(const nav_msgs::msg::Path & path) +{ + auto result = models::Path{}; + result.reset(path.poses.size()); + + for (size_t i = 0; i < path.poses.size(); ++i) { + result.x(i) = path.poses[i].pose.position.x; + result.y(i) = path.poses[i].pose.position.y; + result.yaws(i) = tf2::getYaw(path.poses[i].pose.orientation); + } + + return result; +} + +/** + * @brief Get the last pose from a path + * @param path Reference to the path + * @return geometry_msgs::msg::Pose Last pose in the path + */ +inline geometry_msgs::msg::Pose getLastPathPose(const models::Path & path) +{ + const unsigned int path_last_idx = path.x.size() - 1; + + auto last_orientation = path.yaws(path_last_idx); + + tf2::Quaternion pose_orientation; + pose_orientation.setRPY(0.0, 0.0, last_orientation); + + geometry_msgs::msg::Pose pathPose; + pathPose.position.x = path.x(path_last_idx); + pathPose.position.y = path.y(path_last_idx); + pathPose.orientation.x = pose_orientation.x(); + pathPose.orientation.y = pose_orientation.y(); + pathPose.orientation.z = pose_orientation.z(); + pathPose.orientation.w = pose_orientation.w(); + + return pathPose; +} + +/** + * @brief normalize + * Normalizes the angle to be -M_PIF circle to +M_PIF circle + * It takes and returns radians. + * @param angles Angles to normalize + * @return normalized angles + */ +template +auto normalize_angles(const T & angles) +{ + return (angles + M_PIF).unaryExpr([&](const float x) { + float remainder = std::fmod(x, 2.0f * M_PIF); + return remainder < 0.0f ? remainder + M_PIF : remainder - M_PIF; + }); +} + +/** + * @brief shortest_angular_distance + * + * Given 2 angles, this returns the shortest angular + * difference. The inputs and outputs are of course radians. + * + * The result + * would always be -pi <= result <= pi. Adding the result + * to "from" will always get you an equivalent angle to "to". + * @param from Start angle + * @param to End angle + * @return Shortest distance between angles + */ +template +auto shortest_angular_distance( + const F & from, + const T & to) +{ + return normalize_angles(to - from); +} + +/** + * @brief Evaluate furthest point idx of data.path which is + * nearest to some trajectory in data.trajectories + * @param data Data to use + * @return Idx of furthest path point reached by a set of trajectories + */ +inline size_t findPathFurthestReachedPoint(const CriticData & data) +{ + int traj_cols = data.trajectories.x.cols(); + const auto traj_x = data.trajectories.x.col(traj_cols - 1); + const auto traj_y = data.trajectories.y.col(traj_cols - 1); + + const auto dx = (data.path.x.transpose()).replicate(traj_x.rows(), 1).colwise() - traj_x; + const auto dy = (data.path.y.transpose()).replicate(traj_y.rows(), 1).colwise() - traj_y; + + const auto dists = dx * dx + dy * dy; + + int max_id_by_trajectories = 0, min_id_by_path = 0; + float min_distance_by_path = std::numeric_limits::max(); + size_t n_rows = dists.rows(); + size_t n_cols = dists.cols(); + for (size_t i = 0; i != n_rows; i++) { + min_id_by_path = 0; + min_distance_by_path = std::numeric_limits::max(); + for (size_t j = max_id_by_trajectories; j != n_cols; j++) { + const float cur_dist = dists(i, j); + if (cur_dist < min_distance_by_path) { + min_distance_by_path = cur_dist; + min_id_by_path = j; + } + } + max_id_by_trajectories = std::max(max_id_by_trajectories, min_id_by_path); + } + return max_id_by_trajectories; +} + +/** + * @brief evaluate path furthest point if it is not set + * @param data Data to use + */ +inline void setPathFurthestPointIfNotSet(CriticData & data) +{ + if (!data.furthest_reached_path_point) { + data.furthest_reached_path_point = findPathFurthestReachedPoint(data); + } +} + +/** + * @brief evaluate path costs + * @param data Data to use + */ +inline void findPathCosts( + CriticData & data, + std::shared_ptr costmap_ros) +{ + auto * costmap = costmap_ros->getCostmap(); + unsigned int map_x, map_y; + const size_t path_segments_count = data.path.x.size() - 1; + data.path_pts_valid = std::vector(path_segments_count, false); + const bool tracking_unknown = costmap_ros->getLayeredCostmap()->isTrackingUnknown(); + for (unsigned int idx = 0; idx < path_segments_count; idx++) { + if (!costmap->worldToMap(data.path.x(idx), data.path.y(idx), map_x, map_y)) { + (*data.path_pts_valid)[idx] = false; + continue; + } + + switch (costmap->getCost(map_x, map_y)) { + case (nav2_costmap_2d::LETHAL_OBSTACLE): + (*data.path_pts_valid)[idx] = false; + continue; + case (nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE): + (*data.path_pts_valid)[idx] = false; + continue; + case (nav2_costmap_2d::NO_INFORMATION): + (*data.path_pts_valid)[idx] = tracking_unknown ? true : false; + continue; + } + + (*data.path_pts_valid)[idx] = true; + } +} + +/** + * @brief evaluate path costs if it is not set + * @param data Data to use + */ +inline void setPathCostsIfNotSet( + CriticData & data, + std::shared_ptr costmap_ros) +{ + if (!data.path_pts_valid) { + findPathCosts(data, costmap_ros); + } +} + +/** + * @brief evaluate angle from pose (have angle) to point (no angle) + * @param pose pose + * @param point_x Point to find angle relative to X axis + * @param point_y Point to find angle relative to Y axis + * @param forward_preference If reversing direction is valid + * @return Angle between two points + */ +inline float posePointAngle( + const geometry_msgs::msg::Pose & pose, double point_x, double point_y, bool forward_preference) +{ + float pose_x = pose.position.x; + float pose_y = pose.position.y; + float pose_yaw = tf2::getYaw(pose.orientation); + + float yaw = atan2f(point_y - pose_y, point_x - pose_x); + + // If no preference for forward, return smallest angle either in heading or 180 of heading + if (!forward_preference) { + return std::min( + fabs(angles::shortest_angular_distance(yaw, pose_yaw)), + fabs(angles::shortest_angular_distance(yaw, angles::normalize_angle(pose_yaw + M_PIF)))); + } + + return fabs(angles::shortest_angular_distance(yaw, pose_yaw)); +} + +/** + * @brief evaluate angle from pose (have angle) to point (no angle) + * @param pose pose + * @param point_x Point to find angle relative to X axis + * @param point_y Point to find angle relative to Y axis + * @param point_yaw Yaw of the point to consider along Z axis + * @return Angle between two points + */ +inline float posePointAngle( + const geometry_msgs::msg::Pose & pose, + double point_x, double point_y, double point_yaw) +{ + float pose_x = static_cast(pose.position.x); + float pose_y = static_cast(pose.position.y); + float pose_yaw = static_cast(tf2::getYaw(pose.orientation)); + + float yaw = atan2f(static_cast(point_y) - pose_y, static_cast(point_x) - pose_x); + + if (fabs(angles::shortest_angular_distance(yaw, static_cast(point_yaw))) > M_PIF_2) { + yaw = angles::normalize_angle(yaw + M_PIF); + } + + return fabs(angles::shortest_angular_distance(yaw, pose_yaw)); +} + +/** + * @brief Apply Savisky-Golay filter to optimal trajectory + * @param control_sequence Sequence to apply filter to + * @param control_history Recent set of controls for edge-case handling + * @param Settings Settings to use + */ +inline void savitskyGolayFilter( + models::ControlSequence & control_sequence, + std::array & control_history, + const models::OptimizerSettings & settings) +{ + // Savitzky-Golay Quadratic, 9-point Coefficients + Eigen::Array filter = {-21.0f, 14.0f, 39.0f, 54.0f, 59.0f, 54.0f, 39.0f, 14.0f, + -21.0f}; + filter /= 231.0f; + + // Too short to smooth meaningfully + const unsigned int num_sequences = control_sequence.vx.size() - 1; + if (num_sequences < 20) { + return; + } + + auto applyFilter = [&](const Eigen::Array & data) -> float { + return (data * filter).eval().sum(); + }; + + auto applyFilterOverAxis = + [&](Eigen::ArrayXf & sequence, const Eigen::ArrayXf & initial_sequence, + const float hist_0, const float hist_1, const float hist_2, const float hist_3) -> void + { + float pt_m4 = hist_0; + float pt_m3 = hist_1; + float pt_m2 = hist_2; + float pt_m1 = hist_3; + float pt = initial_sequence(0); + float pt_p1 = initial_sequence(1); + float pt_p2 = initial_sequence(2); + float pt_p3 = initial_sequence(3); + float pt_p4 = initial_sequence(4); + + for (unsigned int idx = 0; idx != num_sequences; idx++) { + sequence(idx) = applyFilter({pt_m4, pt_m3, pt_m2, pt_m1, pt, pt_p1, pt_p2, pt_p3, pt_p4}); + pt_m4 = pt_m3; + pt_m3 = pt_m2; + pt_m2 = pt_m1; + pt_m1 = pt; + pt = pt_p1; + pt_p1 = pt_p2; + pt_p2 = pt_p3; + pt_p3 = pt_p4; + + if (idx + 5 < num_sequences) { + pt_p4 = initial_sequence(idx + 5); + } else { + // Return the last point + pt_p4 = initial_sequence(num_sequences); + } + } + }; + + // Filter trajectories + const models::ControlSequence initial_control_sequence = control_sequence; + applyFilterOverAxis( + control_sequence.vx, initial_control_sequence.vx, control_history[0].vx, + control_history[1].vx, control_history[2].vx, control_history[3].vx); + applyFilterOverAxis( + control_sequence.vy, initial_control_sequence.vy, control_history[0].vy, + control_history[1].vy, control_history[2].vy, control_history[3].vy); + applyFilterOverAxis( + control_sequence.wz, initial_control_sequence.wz, control_history[0].wz, + control_history[1].wz, control_history[2].wz, control_history[3].wz); + + // Update control history + unsigned int offset = settings.shift_control_sequence ? 1 : 0; + control_history[0] = control_history[1]; + control_history[1] = control_history[2]; + control_history[2] = control_history[3]; + control_history[3] = { + control_sequence.vx(offset), + control_sequence.vy(offset), + control_sequence.wz(offset)}; +} + +/** + * @brief Find the iterator of the first pose at which there is an inversion on the path, + * @param path to check for inversion + * @return the first point after the inversion found in the path + */ +inline unsigned int findFirstPathInversion(nav_msgs::msg::Path & path) +{ + // At least 3 poses for a possible inversion + if (path.poses.size() < 3) { + return path.poses.size(); + } + + // Iterating through the path to determine the position of the path inversion + for (unsigned int idx = 1; idx < path.poses.size() - 1; ++idx) { + // We have two vectors for the dot product OA and AB. Determining the vectors. + float oa_x = path.poses[idx].pose.position.x - + path.poses[idx - 1].pose.position.x; + float oa_y = path.poses[idx].pose.position.y - + path.poses[idx - 1].pose.position.y; + float ab_x = path.poses[idx + 1].pose.position.x - + path.poses[idx].pose.position.x; + float ab_y = path.poses[idx + 1].pose.position.y - + path.poses[idx].pose.position.y; + + // Checking for the existence of cusp, in the path, using the dot product. + float dot_product = (oa_x * ab_x) + (oa_y * ab_y); + if (dot_product < 0.0f) { + return idx + 1; + } + } + + return path.poses.size(); +} + +/** + * @brief Find and remove poses after the first inversion in the path + * @param path to check for inversion + * @return The location of the inversion, return 0 if none exist + */ +inline unsigned int removePosesAfterFirstInversion(nav_msgs::msg::Path & path) +{ + nav_msgs::msg::Path cropped_path = path; + const unsigned int first_after_inversion = findFirstPathInversion(cropped_path); + if (first_after_inversion == path.poses.size()) { + return 0u; + } + + cropped_path.poses.erase( + cropped_path.poses.begin() + first_after_inversion, cropped_path.poses.end()); + path = cropped_path; + return first_after_inversion; +} + +/** + * @brief Compare to trajectory points to find closest path point along integrated distances + * @param vec Vect to check + * @return dist Distance to look for + * @return init Starting index to indec from + */ +inline unsigned int findClosestPathPt( + const std::vector & vec, const float dist, const unsigned int init = 0u) +{ + float distim1 = init != 0u ? vec[init] : 0.0f; // First is 0, no accumulated distance yet + float disti = 0.0f; + const unsigned int size = vec.size(); + for (unsigned int i = init + 1; i != size; i++) { + disti = vec[i]; + if (disti > dist) { + if (i > 0 && dist - distim1 < disti - dist) { + return i - 1; + } + return i; + } + distim1 = disti; + } + return size - 1; +} + +// A struct to hold pose data in floating point resolution +struct Pose2D +{ + float x, y, theta; +}; + +/** + * @brief Shift the columns of a 2D Eigen Array or scalar values of + * 1D Eigen Array by 1 place. + * @param e Eigen Array + * @param direction direction in which Array will be shifted. + * 1 for shift in right direction and -1 for left direction. + */ +inline void shiftColumnsByOnePlace(Eigen::Ref e, int direction) +{ + int size = e.size(); + if(size == 1) {return;} + if(abs(direction) != 1) { + throw std::logic_error("Invalid direction, only 1 and -1 are valid values."); + } + + if((e.cols() == 1 || e.rows() == 1) && size > 1) { + auto start_ptr = direction == 1 ? e.data() + size - 2 : e.data() + 1; + auto end_ptr = direction == 1 ? e.data() : e.data() + size - 1; + while(start_ptr != end_ptr) { + *(start_ptr + direction) = *start_ptr; + start_ptr -= direction; + } + *(start_ptr + direction) = *start_ptr; + } else { + auto start_ptr = direction == 1 ? e.data() + size - 2 * e.rows() : e.data() + e.rows(); + auto end_ptr = direction == 1 ? e.data() : e.data() + size - e.rows(); + auto span = e.rows(); + while(start_ptr != end_ptr) { + std::copy(start_ptr, start_ptr + span, start_ptr + direction * span); + start_ptr -= (direction * span); + } + std::copy(start_ptr, start_ptr + span, start_ptr + direction * span); + } +} + +/** + * @brief Normalize the yaws between points on the basis of final yaw angle + * of the trajectory. + * @param last_yaws Final yaw angles of the trajectories. + * @param yaw_between_points Yaw angles calculated between x and y coordinates of the trajectories. + * @return Normalized yaw between points. + */ +inline auto normalize_yaws_between_points( + const Eigen::Ref & last_yaws, + const Eigen::Ref & yaw_between_points) +{ + Eigen::ArrayXf yaws = utils::shortest_angular_distance( + last_yaws, yaw_between_points).abs(); + int size = yaws.size(); + Eigen::ArrayXf yaws_between_points_corrected(size); + for(int i = 0; i != size; i++) { + const float & yaw_between_point = yaw_between_points[i]; + yaws_between_points_corrected[i] = yaws[i] < M_PIF_2 ? + yaw_between_point : angles::normalize_angle(yaw_between_point + M_PIF); + } + return yaws_between_points_corrected; +} + +/** + * @brief Normalize the yaws between points on the basis of goal angle. + * @param goal_yaw Goal yaw angle. + * @param yaw_between_points Yaw angles calculated between x and y coordinates of the trajectories. + * @return Normalized yaw between points + */ +inline auto normalize_yaws_between_points( + const float goal_yaw, const Eigen::Ref & yaw_between_points) +{ + int size = yaw_between_points.size(); + Eigen::ArrayXf yaws_between_points_corrected(size); + for(int i = 0; i != size; i++) { + const float & yaw_between_point = yaw_between_points[i]; + yaws_between_points_corrected[i] = fabs( + angles::normalize_angle(yaw_between_point - goal_yaw)) < M_PIF_2 ? + yaw_between_point : angles::normalize_angle(yaw_between_point + M_PIF); + } + return yaws_between_points_corrected; +} + +/** + * @brief Clamps the input between the given lower and upper bounds. + * @param lower_bound Lower bound. + * @param upper_bound Upper bound. + * @return Clamped output. + */ +inline float clamp( + const float lower_bound, const float upper_bound, const float input) +{ + return std::min(upper_bound, std::max(input, lower_bound)); +} + +} // namespace mppi::utils + +#endif // NAV2_MPPI_CONTROLLER__TOOLS__UTILS_HPP_ diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils copy.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils copy.hpp new file mode 100644 index 00000000000..cb6c000f2db --- /dev/null +++ b/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils copy.hpp @@ -0,0 +1,824 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// Copyright (c) 2023 Open Navigation LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NAV2_MPPI_CONTROLLER__TOOLS__UTILS_HPP_ +#define NAV2_MPPI_CONTROLLER__TOOLS__UTILS_HPP_ + +#include + +#include +#include +#include +#include +#include +#include + +#include "angles/angles.h" + +#include "tf2/utils.hpp" +#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" + +#include "geometry_msgs/msg/twist_stamped.hpp" +#include "nav_msgs/msg/path.hpp" +#include "nav2_msgs/msg/trajectory.hpp" +#include "visualization_msgs/msg/marker_array.hpp" + +#include "rclcpp/rclcpp.hpp" +#include "rclcpp_lifecycle/lifecycle_node.hpp" + +#include "nav2_ros_common/node_utils.hpp" +#include "nav2_core/goal_checker.hpp" + +#include "nav2_mppi_controller/models/optimizer_settings.hpp" +#include "nav2_mppi_controller/models/control_sequence.hpp" +#include "nav2_mppi_controller/models/path.hpp" +#include "builtin_interfaces/msg/time.hpp" +#include "nav2_mppi_controller/critic_data.hpp" + +#define M_PIF 3.141592653589793238462643383279502884e+00F +#define M_PIF_2 1.5707963267948966e+00F + +namespace mppi::utils +{ +/** + * @brief Convert data into pose + * @param x X position + * @param y Y position + * @param z Z position + * @return Pose object + */ +inline geometry_msgs::msg::Pose createPose(double x, double y, double z) +{ + geometry_msgs::msg::Pose pose; + pose.position.x = x; + pose.position.y = y; + pose.position.z = z; + pose.orientation.w = 1; + pose.orientation.x = 0; + pose.orientation.y = 0; + pose.orientation.z = 0; + return pose; +} + +/** + * @brief Convert data into scale + * @param x X scale + * @param y Y scale + * @param z Z scale + * @return Scale object + */ +inline geometry_msgs::msg::Vector3 createScale(double x, double y, double z) +{ + geometry_msgs::msg::Vector3 scale; + scale.x = x; + scale.y = y; + scale.z = z; + return scale; +} + +/** + * @brief Convert data into color + * @param r Red component + * @param g Green component + * @param b Blue component + * @param a Alpha component (transparency) + * @return Color object + */ +inline std_msgs::msg::ColorRGBA createColor(float r, float g, float b, float a) +{ + std_msgs::msg::ColorRGBA color; + color.r = r; + color.g = g; + color.b = b; + color.a = a; + return color; +} + +/** + * @brief Convert data into a Maarker + * @param id Marker ID + * @param pose Marker pose + * @param scale Marker scale + * @param color Marker color + * @param frame Reference frame to use + * @return Visualization Marker + */ +inline visualization_msgs::msg::Marker createMarker( + int id, const geometry_msgs::msg::Pose & pose, const geometry_msgs::msg::Vector3 & scale, + const std_msgs::msg::ColorRGBA & color, const std::string & frame_id, const std::string & ns) +{ + using visualization_msgs::msg::Marker; + Marker marker; + marker.header.frame_id = frame_id; + marker.header.stamp = rclcpp::Time(0, 0); + marker.ns = ns; + marker.id = id; + marker.type = Marker::SPHERE; + marker.action = Marker::ADD; + + marker.pose = pose; + marker.scale = scale; + marker.color = color; + return marker; +} + +/** + * @brief Convert data into TwistStamped + * @param vx X velocity + * @param wz Angular velocity + * @param stamp Timestamp + * @param frame Reference frame to use + */ +inline geometry_msgs::msg::TwistStamped toTwistStamped( + float vx, float wz, const builtin_interfaces::msg::Time & stamp, const std::string & frame) +{ + geometry_msgs::msg::TwistStamped twist; + twist.header.frame_id = frame; + twist.header.stamp = stamp; + twist.twist.linear.x = vx; + twist.twist.angular.z = wz; + + return twist; +} + +/** + * @brief Convert data into TwistStamped + * @param vx X velocity + * @param vy Y velocity + * @param wz Angular velocity + * @param stamp Timestamp + * @param frame Reference frame to use + */ +inline geometry_msgs::msg::TwistStamped toTwistStamped( + float vx, float vy, float wz, const builtin_interfaces::msg::Time & stamp, + const std::string & frame) +{ + auto twist = toTwistStamped(vx, wz, stamp, frame); + twist.twist.linear.y = vy; + + return twist; +} + +inline std::unique_ptr toTrajectoryMsg( + const Eigen::ArrayXXf & trajectory, + const models::ControlSequence & control_sequence, + const double & model_dt, + const std_msgs::msg::Header & header) +{ + auto trajectory_msg = std::make_unique(); + trajectory_msg->header = header; + trajectory_msg->points.resize(trajectory.rows()); + + for (int i = 0; i < trajectory.rows(); ++i) { + auto & curr_pt = trajectory_msg->points[i]; + curr_pt.time_from_start = rclcpp::Duration::from_seconds(i * model_dt); + curr_pt.pose.position.x = trajectory(i, 0); + curr_pt.pose.position.y = trajectory(i, 1); + tf2::Quaternion quat; + quat.setRPY(0.0, 0.0, trajectory(i, 2)); + curr_pt.pose.orientation = tf2::toMsg(quat); + curr_pt.velocity.linear.x = control_sequence.vx(i); + curr_pt.velocity.angular.z = control_sequence.wz(i); + if (control_sequence.vy.size() > 0) { + curr_pt.velocity.linear.y = control_sequence.vy(i); + } + } + + return trajectory_msg; +} + +/** + * @brief Convert path to a tensor + * @param path Path to convert + * @return Path tensor + */ +inline models::Path toTensor(const nav_msgs::msg::Path & path) +{ + auto result = models::Path{}; + result.reset(path.poses.size()); + + for (size_t i = 0; i < path.poses.size(); ++i) { + result.x(i) = path.poses[i].pose.position.x; + result.y(i) = path.poses[i].pose.position.y; + result.yaws(i) = tf2::getYaw(path.poses[i].pose.orientation); + } + + return result; +} + +/** + * @brief Get the last pose from a path + * @param path Reference to the path + * @return geometry_msgs::msg::Pose Last pose in the path + */ +inline geometry_msgs::msg::Pose getLastPathPose(const models::Path & path) +{ + const unsigned int path_last_idx = path.x.size() - 1; + + auto last_orientation = path.yaws(path_last_idx); + + tf2::Quaternion pose_orientation; + pose_orientation.setRPY(0.0, 0.0, last_orientation); + + geometry_msgs::msg::Pose pathPose; + pathPose.position.x = path.x(path_last_idx); + pathPose.position.y = path.y(path_last_idx); + pathPose.orientation.x = pose_orientation.x(); + pathPose.orientation.y = pose_orientation.y(); + pathPose.orientation.z = pose_orientation.z(); + pathPose.orientation.w = pose_orientation.w(); + + return pathPose; +} + +/** + * @brief normalize + * Normalizes the angle to be -M_PIF circle to +M_PIF circle + * It takes and returns radians. + * @param angles Angles to normalize + * @return normalized angles + */ +template +auto normalize_angles(const T & angles) +{ + return (angles + M_PIF).unaryExpr([&](const float x) { + float remainder = std::fmod(x, 2.0f * M_PIF); + return remainder < 0.0f ? remainder + M_PIF : remainder - M_PIF; + }); +} + +/** + * @brief shortest_angular_distance + * + * Given 2 angles, this returns the shortest angular + * difference. The inputs and outputs are of course radians. + * + * The result + * would always be -pi <= result <= pi. Adding the result + * to "from" will always get you an equivalent angle to "to". + * @param from Start angle + * @param to End angle + * @return Shortest distance between angles + */ +template +auto shortest_angular_distance( + const F & from, + const T & to) +{ + return normalize_angles(to - from); +} + +/** + * @brief Evaluate furthest point idx of data.path which is + * nearest to some trajectory in data.trajectories + * @param data Data to use + * @return Idx of furthest path point reached by a set of trajectories + */ +inline size_t findPathFurthestReachedPoint(const CriticData & data) +{ + int traj_cols = data.trajectories.x.cols(); + const auto traj_x = data.trajectories.x.col(traj_cols - 1); + const auto traj_y = data.trajectories.y.col(traj_cols - 1); + + const auto dx = (data.path.x.transpose()).replicate(traj_x.rows(), 1).colwise() - traj_x; + const auto dy = (data.path.y.transpose()).replicate(traj_y.rows(), 1).colwise() - traj_y; + + const auto dists = dx * dx + dy * dy; + + int max_id_by_trajectories = 0, min_id_by_path = 0; + float min_distance_by_path = std::numeric_limits::max(); + size_t n_rows = dists.rows(); + size_t n_cols = dists.cols(); + for (size_t i = 0; i != n_rows; i++) { + min_id_by_path = 0; + min_distance_by_path = std::numeric_limits::max(); + for (size_t j = max_id_by_trajectories; j != n_cols; j++) { + const float cur_dist = dists(i, j); + if (cur_dist < min_distance_by_path) { + min_distance_by_path = cur_dist; + min_id_by_path = j; + } + } + max_id_by_trajectories = std::max(max_id_by_trajectories, min_id_by_path); + } + return max_id_by_trajectories; +} + +/** + * @brief evaluate path furthest point if it is not set + * @param data Data to use + */ +inline void setPathFurthestPointIfNotSet(CriticData & data) +{ + if (!data.furthest_reached_path_point) { + data.furthest_reached_path_point = findPathFurthestReachedPoint(data); + } +} + +/** + * @brief evaluate path costs + * @param data Data to use + */ +inline void findPathCosts( + CriticData & data, + std::shared_ptr costmap_ros) +{ + auto * costmap = costmap_ros->getCostmap(); + unsigned int map_x, map_y; + const size_t path_segments_count = data.path.x.size() - 1; + data.path_pts_valid = std::vector(path_segments_count, false); + const bool tracking_unknown = costmap_ros->getLayeredCostmap()->isTrackingUnknown(); + for (unsigned int idx = 0; idx < path_segments_count; idx++) { + if (!costmap->worldToMap(data.path.x(idx), data.path.y(idx), map_x, map_y)) { + (*data.path_pts_valid)[idx] = false; + continue; + } + + switch (costmap->getCost(map_x, map_y)) { + case (nav2_costmap_2d::LETHAL_OBSTACLE): + (*data.path_pts_valid)[idx] = false; + continue; + case (nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE): + (*data.path_pts_valid)[idx] = false; + continue; + case (nav2_costmap_2d::NO_INFORMATION): + (*data.path_pts_valid)[idx] = tracking_unknown ? true : false; + continue; + } + + (*data.path_pts_valid)[idx] = true; + } +} + +/** + * @brief evaluate path costs if it is not set + * @param data Data to use + */ +inline void setPathCostsIfNotSet( + CriticData & data, + std::shared_ptr costmap_ros) +{ + if (!data.path_pts_valid) { + findPathCosts(data, costmap_ros); + } +} + +/** + * @brief evaluate angle from pose (have angle) to point (no angle) + * @param pose pose + * @param point_x Point to find angle relative to X axis + * @param point_y Point to find angle relative to Y axis + * @param forward_preference If reversing direction is valid + * @return Angle between two points + */ +inline float posePointAngle( + const geometry_msgs::msg::Pose & pose, double point_x, double point_y, bool forward_preference) +{ + float pose_x = pose.position.x; + float pose_y = pose.position.y; + float pose_yaw = tf2::getYaw(pose.orientation); + + float yaw = atan2f(point_y - pose_y, point_x - pose_x); + + // If no preference for forward, return smallest angle either in heading or 180 of heading + if (!forward_preference) { + return std::min( + fabs(angles::shortest_angular_distance(yaw, pose_yaw)), + fabs(angles::shortest_angular_distance(yaw, angles::normalize_angle(pose_yaw + M_PIF)))); + } + + return fabs(angles::shortest_angular_distance(yaw, pose_yaw)); +} + +/** + * @brief evaluate angle from pose (have angle) to point (no angle) + * @param pose pose + * @param point_x Point to find angle relative to X axis + * @param point_y Point to find angle relative to Y axis + * @param point_yaw Yaw of the point to consider along Z axis + * @return Angle between two points + */ +inline float posePointAngle( + const geometry_msgs::msg::Pose & pose, + double point_x, double point_y, double point_yaw) +{ + float pose_x = static_cast(pose.position.x); + float pose_y = static_cast(pose.position.y); + float pose_yaw = static_cast(tf2::getYaw(pose.orientation)); + + float yaw = atan2f(static_cast(point_y) - pose_y, static_cast(point_x) - pose_x); + + if (fabs(angles::shortest_angular_distance(yaw, static_cast(point_yaw))) > M_PIF_2) { + yaw = angles::normalize_angle(yaw + M_PIF); + } + + return fabs(angles::shortest_angular_distance(yaw, pose_yaw)); +} + +/** + * @brief Apply Savisky-Golay filter to optimal trajectory + * @param control_sequence Sequence to apply filter to + * @param control_history Recent set of controls for edge-case handling + * @param Settings Settings to use + */ +inline void savitskyGolayFilter( + models::ControlSequence & control_sequence, + std::array & control_history, + const models::OptimizerSettings & settings) +{ + // Savitzky-Golay Quadratic, 9-point Coefficients + Eigen::Array filter = {-21.0f, 14.0f, 39.0f, 54.0f, 59.0f, 54.0f, 39.0f, 14.0f, + -21.0f}; + filter /= 231.0f; + + // Too short to smooth meaningfully + const unsigned int num_sequences = control_sequence.vx.size() - 1; + if (num_sequences < 20) { + return; + } + + auto applyFilter = [&](const Eigen::Array & data) -> float { + return (data * filter).eval().sum(); + }; + + auto applyFilterOverAxis = + [&](Eigen::ArrayXf & sequence, const Eigen::ArrayXf & initial_sequence, + const float hist_0, const float hist_1, const float hist_2, const float hist_3) -> void + { + float pt_m4 = hist_0; + float pt_m3 = hist_1; + float pt_m2 = hist_2; + float pt_m1 = hist_3; + float pt = initial_sequence(0); + float pt_p1 = initial_sequence(1); + float pt_p2 = initial_sequence(2); + float pt_p3 = initial_sequence(3); + float pt_p4 = initial_sequence(4); + + for (unsigned int idx = 0; idx != num_sequences; idx++) { + sequence(idx) = applyFilter({pt_m4, pt_m3, pt_m2, pt_m1, pt, pt_p1, pt_p2, pt_p3, pt_p4}); + pt_m4 = pt_m3; + pt_m3 = pt_m2; + pt_m2 = pt_m1; + pt_m1 = pt; + pt = pt_p1; + pt_p1 = pt_p2; + pt_p2 = pt_p3; + pt_p3 = pt_p4; + + if (idx + 5 < num_sequences) { + pt_p4 = initial_sequence(idx + 5); + } else { + // Return the last point + pt_p4 = initial_sequence(num_sequences); + } + } + }; + + // Filter trajectories + const models::ControlSequence initial_control_sequence = control_sequence; + applyFilterOverAxis( + control_sequence.vx, initial_control_sequence.vx, control_history[0].vx, + control_history[1].vx, control_history[2].vx, control_history[3].vx); + applyFilterOverAxis( + control_sequence.vy, initial_control_sequence.vy, control_history[0].vy, + control_history[1].vy, control_history[2].vy, control_history[3].vy); + applyFilterOverAxis( + control_sequence.wz, initial_control_sequence.wz, control_history[0].wz, + control_history[1].wz, control_history[2].wz, control_history[3].wz); + + // Update control history + unsigned int offset = settings.shift_control_sequence ? 1 : 0; + control_history[0] = control_history[1]; + control_history[1] = control_history[2]; + control_history[2] = control_history[3]; + control_history[3] = { + control_sequence.vx(offset), + control_sequence.vy(offset), + control_sequence.wz(offset)}; +} + +/** + * @brief Find the iterator of the first pose at which there is an inversion or in-place rotation in the path + * @param path to check for inversion or rotation + * @param rotation_threshold Minimum rotation angle to consider an in-place rotation (0 to disable rotation check) + * @return the first point after the inversion or rotation found in the path + */ +inline unsigned int findFirstPathInversion( + nav_msgs::msg::Path & path, + float rotation_threshold = 0.0f) +{ + if (path.poses.size() < 2) { + RCLCPP_INFO( + rclcpp::get_logger("mppi_utils"), + "[MPPI PathUtils] Path too short for constraints (size=%zu)", + path.poses.size()); + return path.poses.size(); + } + + unsigned int first_after_constraint = path.poses.size(); + + // TYLKO DO LOGÓW: maksymalna rotacja poniĆŒej progu (ĆŒeby wiedzieć, czy byƂo \"prawie\") + float max_rotation_below_threshold = 0.0f; + + // Check for in-place rotation first (if enabled) + if (rotation_threshold > 0.0f) { + for (unsigned int idx = 0; idx < path.poses.size() - 1; ++idx) { + float dx = path.poses[idx + 1].pose.position.x - path.poses[idx].pose.position.x; + float dy = path.poses[idx + 1].pose.position.y - path.poses[idx].pose.position.y; + float translation = sqrtf(dx * dx + dy * dy); + + // Check if poses are at roughly the same location + if (translation < 1e-3) { + float yaw1 = tf2::getYaw(path.poses[idx].pose.orientation); + float yaw2 = tf2::getYaw(path.poses[idx + 1].pose.orientation); + float rotation = fabs(angles::shortest_angular_distance(yaw1, yaw2)); + + // TYLKO DO LOGÓW: zapamiętaj największą rotację poniĆŒej progu + if (rotation < rotation_threshold && + rotation > max_rotation_below_threshold) + { + max_rotation_below_threshold = rotation; + } + + // Check if this meets the minimum rotation threshold + if (rotation > rotation_threshold) { + // Found start of in-place rotation, now find where it ends + unsigned int end_idx = idx + 1; + + // Continue while we have minimal translation (still rotating in place) + while (end_idx < path.poses.size() - 1) { + float next_dx = path.poses[end_idx + 1].pose.position.x - + path.poses[end_idx].pose.position.x; + float next_dy = path.poses[end_idx + 1].pose.position.y - + path.poses[end_idx].pose.position.y; + float next_translation = sqrtf(next_dx * next_dx + next_dy * next_dy); + + if (next_translation >= 1e-3) { + break; // End of in-place rotation sequence + } + end_idx++; + } + + first_after_constraint = end_idx; + + // LOG: wykryto rotation constraint + RCLCPP_INFO( + rclcpp::get_logger("mppi_utils"), + "[MPPI PathUtils] Rotation constraint detected: rotation=%.3f rad, " + "threshold=%.3f rad, start_idx=%u, constraint_idx=%u", + rotation, rotation_threshold, idx, end_idx); + + break; + } + } + } + } + + // Check for inversion (at least 3 poses needed) + if (path.poses.size() >= 3) { + for (unsigned int idx = 1; idx < path.poses.size() - 1; ++idx) { + float oa_x = path.poses[idx].pose.position.x - + path.poses[idx - 1].pose.position.x; + float oa_y = path.poses[idx].pose.position.y - + path.poses[idx - 1].pose.position.y; + float ab_x = path.poses[idx + 1].pose.position.x - + path.poses[idx].pose.position.x; + float ab_y = path.poses[idx + 1].pose.position.y - + path.poses[idx].pose.position.y; + + float dot_product = (oa_x * ab_x) + (oa_y * ab_y); + + float norm_oa = hypotf(oa_x, oa_y); + float norm_ab = hypotf(ab_x, ab_y); + + if (norm_oa < 1e-4f || norm_ab < 1e-4f) { + continue; + } + + float cos_ang = dot_product / (norm_oa * norm_ab); + cos_ang = std::max(-1.0f, std::min(1.0f, cos_ang)); + float angle = acosf(cos_ang); // [0, pi] + + if (rotation_threshold > 0.0f) { + if (angle > rotation_threshold) { + first_after_constraint = std::min(first_after_constraint, idx + 1); + + // LOG: constraint z powodu kąta (cusp/inversion po Ƃuku) + RCLCPP_INFO( + rclcpp::get_logger("mppi_utils"), + "[MPPI PathUtils] Inversion/cusp constraint detected by angle: " + "angle=%.3f rad (threshold=%.3f rad), at idx=%u (constraint_idx=%u)", + angle, rotation_threshold, idx, first_after_constraint); + + break; + } + } else { + if (dot_product < 0.0f) { + first_after_constraint = std::min(first_after_constraint, idx + 1); + + // LOG: klasyczny cusp przez dot_product<0 + RCLCPP_INFO( + rclcpp::get_logger("mppi_utils"), + "[MPPI PathUtils] Inversion cusp constraint detected by dot_product<0 " + "at idx=%u (constraint_idx=%u, dot=%.6f)", + idx, first_after_constraint, dot_product); + + break; + } + } + } + } + + // Podsumowanie: nic nie znalazƂ -> zobacz, czy rotacja byƂa \"prawie\" wystarczająca + if (first_after_constraint == path.poses.size()) { + if (rotation_threshold > 0.0f && max_rotation_below_threshold > 0.0f) { + float ratio = max_rotation_below_threshold / rotation_threshold; + + if (ratio >= 0.9f) { + // LOG: rotacja byƂa blisko progu (>= 90%) + RCLCPP_INFO( + rclcpp::get_logger("mppi_utils"), + "[MPPI PathUtils] Rotation almost met threshold: rotation=%.3f rad, " + "threshold=%.3f rad (%.1f%% of threshold) – no constraint applied", + max_rotation_below_threshold, rotation_threshold, ratio * 100.0f); + } else { + RCLCPP_INFO( + rclcpp::get_logger("mppi_utils"), + "[MPPI PathUtils] No constraint found. Max rotation below threshold: " + "rotation=%.3f rad, threshold=%.3f rad (%.1f%% of threshold)", + max_rotation_below_threshold, rotation_threshold, ratio * 100.0f); + } + } else { + RCLCPP_INFO( + rclcpp::get_logger("mppi_utils"), + "[MPPI PathUtils] No inversion / rotation constraint found " + "(path_size=%zu, rotation_threshold=%.3f)", + path.poses.size(), rotation_threshold); + } + } + + return first_after_constraint; +} + + +/** + * @brief Find and remove poses after the first inversion or in-place rotation in the path + * @param path Path to check for inversion or rotation + * @param rotation_threshold Minimum rotation angle to consider an in-place rotation (0 to disable rotation check) + * @return The location of the inversion/rotation, return 0 if none exist + */ +inline unsigned int removePosesAfterFirstInversion( + nav_msgs::msg::Path & path, + float rotation_threshold = 0.0f) +{ + nav_msgs::msg::Path cropped_path = path; + + const unsigned int first_after_constraint = findFirstPathInversion(cropped_path, rotation_threshold); + + if (first_after_constraint == path.poses.size()) { + return 0u; // No constraint found + } + + cropped_path.poses.erase( + cropped_path.poses.begin() + first_after_constraint, cropped_path.poses.end()); + path = cropped_path; + return first_after_constraint; +} + +/** + * @brief Compare to trajectory points to find closest path point along integrated distances + * @param vec Vect to check + * @return dist Distance to look for + * @return init Starting index to indec from + */ +inline unsigned int findClosestPathPt( + const std::vector & vec, const float dist, const unsigned int init = 0u) +{ + float distim1 = init != 0u ? vec[init] : 0.0f; // First is 0, no accumulated distance yet + float disti = 0.0f; + const unsigned int size = vec.size(); + for (unsigned int i = init + 1; i != size; i++) { + disti = vec[i]; + if (disti > dist) { + if (i > 0 && dist - distim1 < disti - dist) { + return i - 1; + } + return i; + } + distim1 = disti; + } + return size - 1; +} + +// A struct to hold pose data in floating point resolution +struct Pose2D +{ + float x, y, theta; +}; + +/** + * @brief Shift the columns of a 2D Eigen Array or scalar values of + * 1D Eigen Array by 1 place. + * @param e Eigen Array + * @param direction direction in which Array will be shifted. + * 1 for shift in right direction and -1 for left direction. + */ +inline void shiftColumnsByOnePlace(Eigen::Ref e, int direction) +{ + int size = e.size(); + if(size == 1) {return;} + if(abs(direction) != 1) { + throw std::logic_error("Invalid direction, only 1 and -1 are valid values."); + } + + if((e.cols() == 1 || e.rows() == 1) && size > 1) { + auto start_ptr = direction == 1 ? e.data() + size - 2 : e.data() + 1; + auto end_ptr = direction == 1 ? e.data() : e.data() + size - 1; + while(start_ptr != end_ptr) { + *(start_ptr + direction) = *start_ptr; + start_ptr -= direction; + } + *(start_ptr + direction) = *start_ptr; + } else { + auto start_ptr = direction == 1 ? e.data() + size - 2 * e.rows() : e.data() + e.rows(); + auto end_ptr = direction == 1 ? e.data() : e.data() + size - e.rows(); + auto span = e.rows(); + while(start_ptr != end_ptr) { + std::copy(start_ptr, start_ptr + span, start_ptr + direction * span); + start_ptr -= (direction * span); + } + std::copy(start_ptr, start_ptr + span, start_ptr + direction * span); + } +} + +/** + * @brief Normalize the yaws between points on the basis of final yaw angle + * of the trajectory. + * @param last_yaws Final yaw angles of the trajectories. + * @param yaw_between_points Yaw angles calculated between x and y coordinates of the trajectories. + * @return Normalized yaw between points. + */ +inline auto normalize_yaws_between_points( + const Eigen::Ref & last_yaws, + const Eigen::Ref & yaw_between_points) +{ + Eigen::ArrayXf yaws = utils::shortest_angular_distance( + last_yaws, yaw_between_points).abs(); + int size = yaws.size(); + Eigen::ArrayXf yaws_between_points_corrected(size); + for(int i = 0; i != size; i++) { + const float & yaw_between_point = yaw_between_points[i]; + yaws_between_points_corrected[i] = yaws[i] < M_PIF_2 ? + yaw_between_point : angles::normalize_angle(yaw_between_point + M_PIF); + } + return yaws_between_points_corrected; +} + +/** + * @brief Normalize the yaws between points on the basis of goal angle. + * @param goal_yaw Goal yaw angle. + * @param yaw_between_points Yaw angles calculated between x and y coordinates of the trajectories. + * @return Normalized yaw between points + */ +inline auto normalize_yaws_between_points( + const float goal_yaw, const Eigen::Ref & yaw_between_points) +{ + int size = yaw_between_points.size(); + Eigen::ArrayXf yaws_between_points_corrected(size); + for(int i = 0; i != size; i++) { + const float & yaw_between_point = yaw_between_points[i]; + yaws_between_points_corrected[i] = fabs( + angles::normalize_angle(yaw_between_point - goal_yaw)) < M_PIF_2 ? + yaw_between_point : angles::normalize_angle(yaw_between_point + M_PIF); + } + return yaws_between_points_corrected; +} + +/** + * @brief Clamps the input between the given lower and upper bounds. + * @param lower_bound Lower bound. + * @param upper_bound Upper bound. + * @return Clamped output. + */ +inline float clamp( + const float lower_bound, const float upper_bound, const float input) +{ + return std::min(upper_bound, std::max(input, lower_bound)); +} + +} // namespace mppi::utils + +#endif // NAV2_MPPI_CONTROLLER__TOOLS__UTILS_HPP_ diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils copy_mod.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils copy_mod.hpp new file mode 100644 index 00000000000..0ad511a351f --- /dev/null +++ b/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils copy_mod.hpp @@ -0,0 +1,757 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// Copyright (c) 2023 Open Navigation LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NAV2_MPPI_CONTROLLER__TOOLS__UTILS_HPP_ +#define NAV2_MPPI_CONTROLLER__TOOLS__UTILS_HPP_ + +#include + +#include +#include +#include +#include +#include +#include + +#include "angles/angles.h" + +#include "tf2/utils.hpp" +#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" + +#include "geometry_msgs/msg/twist_stamped.hpp" +#include "nav_msgs/msg/path.hpp" +#include "nav2_msgs/msg/trajectory.hpp" +#include "visualization_msgs/msg/marker_array.hpp" + +#include "rclcpp/rclcpp.hpp" +#include "rclcpp_lifecycle/lifecycle_node.hpp" + +#include "nav2_ros_common/node_utils.hpp" +#include "nav2_core/goal_checker.hpp" + +#include "nav2_mppi_controller/models/optimizer_settings.hpp" +#include "nav2_mppi_controller/models/control_sequence.hpp" +#include "nav2_mppi_controller/models/path.hpp" +#include "builtin_interfaces/msg/time.hpp" +#include "nav2_mppi_controller/critic_data.hpp" + +#define M_PIF 3.141592653589793238462643383279502884e+00F +#define M_PIF_2 1.5707963267948966e+00F + +namespace mppi::utils +{ +/** + * @brief Convert data into pose + * @param x X position + * @param y Y position + * @param z Z position + * @return Pose object + */ +inline geometry_msgs::msg::Pose createPose(double x, double y, double z) +{ + geometry_msgs::msg::Pose pose; + pose.position.x = x; + pose.position.y = y; + pose.position.z = z; + pose.orientation.w = 1; + pose.orientation.x = 0; + pose.orientation.y = 0; + pose.orientation.z = 0; + return pose; +} + +/** + * @brief Convert data into scale + * @param x X scale + * @param y Y scale + * @param z Z scale + * @return Scale object + */ +inline geometry_msgs::msg::Vector3 createScale(double x, double y, double z) +{ + geometry_msgs::msg::Vector3 scale; + scale.x = x; + scale.y = y; + scale.z = z; + return scale; +} + +/** + * @brief Convert data into color + * @param r Red component + * @param g Green component + * @param b Blue component + * @param a Alpha component (transparency) + * @return Color object + */ +inline std_msgs::msg::ColorRGBA createColor(float r, float g, float b, float a) +{ + std_msgs::msg::ColorRGBA color; + color.r = r; + color.g = g; + color.b = b; + color.a = a; + return color; +} + +/** + * @brief Convert data into a Maarker + * @param id Marker ID + * @param pose Marker pose + * @param scale Marker scale + * @param color Marker color + * @param frame Reference frame to use + * @return Visualization Marker + */ +inline visualization_msgs::msg::Marker createMarker( + int id, const geometry_msgs::msg::Pose & pose, const geometry_msgs::msg::Vector3 & scale, + const std_msgs::msg::ColorRGBA & color, const std::string & frame_id, const std::string & ns) +{ + using visualization_msgs::msg::Marker; + Marker marker; + marker.header.frame_id = frame_id; + marker.header.stamp = rclcpp::Time(0, 0); + marker.ns = ns; + marker.id = id; + marker.type = Marker::SPHERE; + marker.action = Marker::ADD; + + marker.pose = pose; + marker.scale = scale; + marker.color = color; + return marker; +} + +/** + * @brief Convert data into TwistStamped + * @param vx X velocity + * @param wz Angular velocity + * @param stamp Timestamp + * @param frame Reference frame to use + */ +inline geometry_msgs::msg::TwistStamped toTwistStamped( + float vx, float wz, const builtin_interfaces::msg::Time & stamp, const std::string & frame) +{ + geometry_msgs::msg::TwistStamped twist; + twist.header.frame_id = frame; + twist.header.stamp = stamp; + twist.twist.linear.x = vx; + twist.twist.angular.z = wz; + + return twist; +} + +/** + * @brief Convert data into TwistStamped + * @param vx X velocity + * @param vy Y velocity + * @param wz Angular velocity + * @param stamp Timestamp + * @param frame Reference frame to use + */ +inline geometry_msgs::msg::TwistStamped toTwistStamped( + float vx, float vy, float wz, const builtin_interfaces::msg::Time & stamp, + const std::string & frame) +{ + auto twist = toTwistStamped(vx, wz, stamp, frame); + twist.twist.linear.y = vy; + + return twist; +} + +inline std::unique_ptr toTrajectoryMsg( + const Eigen::ArrayXXf & trajectory, + const models::ControlSequence & control_sequence, + const double & model_dt, + const std_msgs::msg::Header & header) +{ + auto trajectory_msg = std::make_unique(); + trajectory_msg->header = header; + trajectory_msg->points.resize(trajectory.rows()); + + for (int i = 0; i < trajectory.rows(); ++i) { + auto & curr_pt = trajectory_msg->points[i]; + curr_pt.time_from_start = rclcpp::Duration::from_seconds(i * model_dt); + curr_pt.pose.position.x = trajectory(i, 0); + curr_pt.pose.position.y = trajectory(i, 1); + tf2::Quaternion quat; + quat.setRPY(0.0, 0.0, trajectory(i, 2)); + curr_pt.pose.orientation = tf2::toMsg(quat); + curr_pt.velocity.linear.x = control_sequence.vx(i); + curr_pt.velocity.angular.z = control_sequence.wz(i); + if (control_sequence.vy.size() > 0) { + curr_pt.velocity.linear.y = control_sequence.vy(i); + } + } + + return trajectory_msg; +} + +/** + * @brief Convert path to a tensor + * @param path Path to convert + * @return Path tensor + */ +inline models::Path toTensor(const nav_msgs::msg::Path & path) +{ + auto result = models::Path{}; + result.reset(path.poses.size()); + + for (size_t i = 0; i < path.poses.size(); ++i) { + result.x(i) = path.poses[i].pose.position.x; + result.y(i) = path.poses[i].pose.position.y; + result.yaws(i) = tf2::getYaw(path.poses[i].pose.orientation); + } + + return result; +} + +/** + * @brief Get the last pose from a path + * @param path Reference to the path + * @return geometry_msgs::msg::Pose Last pose in the path + */ +inline geometry_msgs::msg::Pose getLastPathPose(const models::Path & path) +{ + const unsigned int path_last_idx = path.x.size() - 1; + + auto last_orientation = path.yaws(path_last_idx); + + tf2::Quaternion pose_orientation; + pose_orientation.setRPY(0.0, 0.0, last_orientation); + + geometry_msgs::msg::Pose pathPose; + pathPose.position.x = path.x(path_last_idx); + pathPose.position.y = path.y(path_last_idx); + pathPose.orientation.x = pose_orientation.x(); + pathPose.orientation.y = pose_orientation.y(); + pathPose.orientation.z = pose_orientation.z(); + pathPose.orientation.w = pose_orientation.w(); + + return pathPose; +} + +/** + * @brief normalize + * Normalizes the angle to be -M_PIF circle to +M_PIF circle + * It takes and returns radians. + * @param angles Angles to normalize + * @return normalized angles + */ +template +auto normalize_angles(const T & angles) +{ + return (angles + M_PIF).unaryExpr([&](const float x) { + float remainder = std::fmod(x, 2.0f * M_PIF); + return remainder < 0.0f ? remainder + M_PIF : remainder - M_PIF; + }); +} + +/** + * @brief shortest_angular_distance + * + * Given 2 angles, this returns the shortest angular + * difference. The inputs and outputs are of course radians. + * + * The result + * would always be -pi <= result <= pi. Adding the result + * to "from" will always get you an equivalent angle to "to". + * @param from Start angle + * @param to End angle + * @return Shortest distance between angles + */ +template +auto shortest_angular_distance( + const F & from, + const T & to) +{ + return normalize_angles(to - from); +} + +/** + * @brief Evaluate furthest point idx of data.path which is + * nearest to some trajectory in data.trajectories + * @param data Data to use + * @return Idx of furthest path point reached by a set of trajectories + */ +inline size_t findPathFurthestReachedPoint(const CriticData & data) +{ + int traj_cols = data.trajectories.x.cols(); + const auto traj_x = data.trajectories.x.col(traj_cols - 1); + const auto traj_y = data.trajectories.y.col(traj_cols - 1); + + const auto dx = (data.path.x.transpose()).replicate(traj_x.rows(), 1).colwise() - traj_x; + const auto dy = (data.path.y.transpose()).replicate(traj_y.rows(), 1).colwise() - traj_y; + + const auto dists = dx * dx + dy * dy; + + int max_id_by_trajectories = 0, min_id_by_path = 0; + float min_distance_by_path = std::numeric_limits::max(); + size_t n_rows = dists.rows(); + size_t n_cols = dists.cols(); + for (size_t i = 0; i != n_rows; i++) { + min_id_by_path = 0; + min_distance_by_path = std::numeric_limits::max(); + for (size_t j = max_id_by_trajectories; j != n_cols; j++) { + const float cur_dist = dists(i, j); + if (cur_dist < min_distance_by_path) { + min_distance_by_path = cur_dist; + min_id_by_path = j; + } + } + max_id_by_trajectories = std::max(max_id_by_trajectories, min_id_by_path); + } + return max_id_by_trajectories; +} + +/** + * @brief evaluate path furthest point if it is not set + * @param data Data to use + */ +inline void setPathFurthestPointIfNotSet(CriticData & data) +{ + if (!data.furthest_reached_path_point) { + data.furthest_reached_path_point = findPathFurthestReachedPoint(data); + } +} + +/** + * @brief evaluate path costs + * @param data Data to use + */ +inline void findPathCosts( + CriticData & data, + std::shared_ptr costmap_ros) +{ + auto * costmap = costmap_ros->getCostmap(); + unsigned int map_x, map_y; + const size_t path_segments_count = data.path.x.size() - 1; + data.path_pts_valid = std::vector(path_segments_count, false); + const bool tracking_unknown = costmap_ros->getLayeredCostmap()->isTrackingUnknown(); + for (unsigned int idx = 0; idx < path_segments_count; idx++) { + if (!costmap->worldToMap(data.path.x(idx), data.path.y(idx), map_x, map_y)) { + (*data.path_pts_valid)[idx] = false; + continue; + } + + switch (costmap->getCost(map_x, map_y)) { + case (nav2_costmap_2d::LETHAL_OBSTACLE): + (*data.path_pts_valid)[idx] = false; + continue; + case (nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE): + (*data.path_pts_valid)[idx] = false; + continue; + case (nav2_costmap_2d::NO_INFORMATION): + (*data.path_pts_valid)[idx] = tracking_unknown ? true : false; + continue; + } + + (*data.path_pts_valid)[idx] = true; + } +} + +/** + * @brief evaluate path costs if it is not set + * @param data Data to use + */ +inline void setPathCostsIfNotSet( + CriticData & data, + std::shared_ptr costmap_ros) +{ + if (!data.path_pts_valid) { + findPathCosts(data, costmap_ros); + } +} + +/** + * @brief evaluate angle from pose (have angle) to point (no angle) + * @param pose pose + * @param point_x Point to find angle relative to X axis + * @param point_y Point to find angle relative to Y axis + * @param forward_preference If reversing direction is valid + * @return Angle between two points + */ +inline float posePointAngle( + const geometry_msgs::msg::Pose & pose, double point_x, double point_y, bool forward_preference) +{ + float pose_x = pose.position.x; + float pose_y = pose.position.y; + float pose_yaw = tf2::getYaw(pose.orientation); + + float yaw = atan2f(point_y - pose_y, point_x - pose_x); + + // If no preference for forward, return smallest angle either in heading or 180 of heading + if (!forward_preference) { + return std::min( + fabs(angles::shortest_angular_distance(yaw, pose_yaw)), + fabs(angles::shortest_angular_distance(yaw, angles::normalize_angle(pose_yaw + M_PIF)))); + } + + return fabs(angles::shortest_angular_distance(yaw, pose_yaw)); +} + +/** + * @brief evaluate angle from pose (have angle) to point (no angle) + * @param pose pose + * @param point_x Point to find angle relative to X axis + * @param point_y Point to find angle relative to Y axis + * @param point_yaw Yaw of the point to consider along Z axis + * @return Angle between two points + */ +inline float posePointAngle( + const geometry_msgs::msg::Pose & pose, + double point_x, double point_y, double point_yaw) +{ + float pose_x = static_cast(pose.position.x); + float pose_y = static_cast(pose.position.y); + float pose_yaw = static_cast(tf2::getYaw(pose.orientation)); + + float yaw = atan2f(static_cast(point_y) - pose_y, static_cast(point_x) - pose_x); + + if (fabs(angles::shortest_angular_distance(yaw, static_cast(point_yaw))) > M_PIF_2) { + yaw = angles::normalize_angle(yaw + M_PIF); + } + + return fabs(angles::shortest_angular_distance(yaw, pose_yaw)); +} + +/** + * @brief Apply Savisky-Golay filter to optimal trajectory + * @param control_sequence Sequence to apply filter to + * @param control_history Recent set of controls for edge-case handling + * @param Settings Settings to use + */ +inline void savitskyGolayFilter( + models::ControlSequence & control_sequence, + std::array & control_history, + const models::OptimizerSettings & settings) +{ + // Savitzky-Golay Quadratic, 9-point Coefficients + Eigen::Array filter = {-21.0f, 14.0f, 39.0f, 54.0f, 59.0f, 54.0f, 39.0f, 14.0f, + -21.0f}; + filter /= 231.0f; + + // Too short to smooth meaningfully + const unsigned int num_sequences = control_sequence.vx.size() - 1; + if (num_sequences < 20) { + return; + } + + auto applyFilter = [&](const Eigen::Array & data) -> float { + return (data * filter).eval().sum(); + }; + + auto applyFilterOverAxis = + [&](Eigen::ArrayXf & sequence, const Eigen::ArrayXf & initial_sequence, + const float hist_0, const float hist_1, const float hist_2, const float hist_3) -> void + { + float pt_m4 = hist_0; + float pt_m3 = hist_1; + float pt_m2 = hist_2; + float pt_m1 = hist_3; + float pt = initial_sequence(0); + float pt_p1 = initial_sequence(1); + float pt_p2 = initial_sequence(2); + float pt_p3 = initial_sequence(3); + float pt_p4 = initial_sequence(4); + + for (unsigned int idx = 0; idx != num_sequences; idx++) { + sequence(idx) = applyFilter({pt_m4, pt_m3, pt_m2, pt_m1, pt, pt_p1, pt_p2, pt_p3, pt_p4}); + pt_m4 = pt_m3; + pt_m3 = pt_m2; + pt_m2 = pt_m1; + pt_m1 = pt; + pt = pt_p1; + pt_p1 = pt_p2; + pt_p2 = pt_p3; + pt_p3 = pt_p4; + + if (idx + 5 < num_sequences) { + pt_p4 = initial_sequence(idx + 5); + } else { + // Return the last point + pt_p4 = initial_sequence(num_sequences); + } + } + }; + + // Filter trajectories + const models::ControlSequence initial_control_sequence = control_sequence; + applyFilterOverAxis( + control_sequence.vx, initial_control_sequence.vx, control_history[0].vx, + control_history[1].vx, control_history[2].vx, control_history[3].vx); + applyFilterOverAxis( + control_sequence.vy, initial_control_sequence.vy, control_history[0].vy, + control_history[1].vy, control_history[2].vy, control_history[3].vy); + applyFilterOverAxis( + control_sequence.wz, initial_control_sequence.wz, control_history[0].wz, + control_history[1].wz, control_history[2].wz, control_history[3].wz); + + // Update control history + unsigned int offset = settings.shift_control_sequence ? 1 : 0; + control_history[0] = control_history[1]; + control_history[1] = control_history[2]; + control_history[2] = control_history[3]; + control_history[3] = { + control_sequence.vx(offset), + control_sequence.vy(offset), + control_sequence.wz(offset)}; +} + +/** + * @brief Find the iterator of the first pose at which there is an inversion or in-place rotation in the path + * @param path to check for inversion or rotation + * @param rotation_threshold Minimum rotation angle to consider an in-place rotation (0 to disable rotation check) + * @return the first point after the inversion or rotation found in the path + */ +inline unsigned int findFirstPathInversion( + nav_msgs::msg::Path & path, + float rotation_threshold = 0.0f) +{ + if (path.poses.size() < 2) { + return path.poses.size(); + } + + unsigned int first_after_constraint = path.poses.size(); + + // Check for in-place rotation first (if enabled) + if (rotation_threshold > 0.0f) { + for (unsigned int idx = 0; idx < path.poses.size() - 1; ++idx) { + float dx = path.poses[idx + 1].pose.position.x - path.poses[idx].pose.position.x; + float dy = path.poses[idx + 1].pose.position.y - path.poses[idx].pose.position.y; + float translation = sqrtf(dx * dx + dy * dy); + + // Check if poses are at roughly the same location + if (translation < 1e-3) { + float yaw1 = tf2::getYaw(path.poses[idx].pose.orientation); + float yaw2 = tf2::getYaw(path.poses[idx + 1].pose.orientation); + float rotation = fabs(angles::shortest_angular_distance(yaw1, yaw2)); + + // Check if this meets the minimum rotation threshold + if (rotation > rotation_threshold) { + // Found start of in-place rotation, now find where it ends + unsigned int end_idx = idx + 1; + + // Continue while we have minimal translation (still rotating in place) + while (end_idx < path.poses.size() - 1) { + float next_dx = path.poses[end_idx + 1].pose.position.x - + path.poses[end_idx].pose.position.x; + float next_dy = path.poses[end_idx + 1].pose.position.y - + path.poses[end_idx].pose.position.y; + float next_translation = sqrtf(next_dx * next_dx + next_dy * next_dy); + + if (next_translation >= 1e-3) { + break; // End of in-place rotation sequence + } + end_idx++; + } + + first_after_constraint = end_idx; + break; + } + } + } + } + + // Check for inversion (at least 3 poses needed) + if (path.poses.size() >= 3) { + for (unsigned int idx = 1; idx < path.poses.size() - 1; ++idx) { + float oa_x = path.poses[idx].pose.position.x - + path.poses[idx - 1].pose.position.x; + float oa_y = path.poses[idx].pose.position.y - + path.poses[idx - 1].pose.position.y; + float ab_x = path.poses[idx + 1].pose.position.x - + path.poses[idx].pose.position.x; + float ab_y = path.poses[idx + 1].pose.position.y - + path.poses[idx].pose.position.y; + + float dot_product = (oa_x * ab_x) + (oa_y * ab_y); + + float norm_oa = hypotf(oa_x, oa_y); + float norm_ab = hypotf(ab_x, ab_y); + + if (norm_oa < 1e-4f || norm_ab < 1e-4f) { + continue; + } + + float cos_ang = dot_product / (norm_oa * norm_ab); + cos_ang = std::max(-1.0f, std::min(1.0f, cos_ang)); + float angle = acosf(cos_ang); // [0, pi] + + if (rotation_threshold > 0.0f) { + if (angle > rotation_threshold) { + first_after_constraint = std::min(first_after_constraint, idx + 1); + break; + } + } else { + if (dot_product < 0.0f) { + first_after_constraint = std::min(first_after_constraint, idx + 1); + break; + } + } + } + } + + return first_after_constraint; +} + +/** + * @brief Find and remove poses after the first inversion or in-place rotation in the path + * @param path Path to check for inversion or rotation + * @param rotation_threshold Minimum rotation angle to consider an in-place rotation (0 to disable rotation check) + * @return The location of the inversion/rotation, return 0 if none exist + */ +inline unsigned int removePosesAfterFirstInversion( + nav_msgs::msg::Path & path, + float rotation_threshold = 0.0f) +{ + nav_msgs::msg::Path cropped_path = path; + + const unsigned int first_after_constraint = findFirstPathInversion(cropped_path, rotation_threshold); + + if (first_after_constraint == path.poses.size()) { + return 0u; // No constraint found + } + + cropped_path.poses.erase( + cropped_path.poses.begin() + first_after_constraint, cropped_path.poses.end()); + path = cropped_path; + return first_after_constraint; +} + +/** + * @brief Compare to trajectory points to find closest path point along integrated distances + * @param vec Vect to check + * @return dist Distance to look for + * @return init Starting index to indec from + */ +inline unsigned int findClosestPathPt( + const std::vector & vec, const float dist, const unsigned int init = 0u) +{ + float distim1 = init != 0u ? vec[init] : 0.0f; // First is 0, no accumulated distance yet + float disti = 0.0f; + const unsigned int size = vec.size(); + for (unsigned int i = init + 1; i != size; i++) { + disti = vec[i]; + if (disti > dist) { + if (i > 0 && dist - distim1 < disti - dist) { + return i - 1; + } + return i; + } + distim1 = disti; + } + return size - 1; +} + +// A struct to hold pose data in floating point resolution +struct Pose2D +{ + float x, y, theta; +}; + +/** + * @brief Shift the columns of a 2D Eigen Array or scalar values of + * 1D Eigen Array by 1 place. + * @param e Eigen Array + * @param direction direction in which Array will be shifted. + * 1 for shift in right direction and -1 for left direction. + */ +inline void shiftColumnsByOnePlace(Eigen::Ref e, int direction) +{ + int size = e.size(); + if(size == 1) {return;} + if(abs(direction) != 1) { + throw std::logic_error("Invalid direction, only 1 and -1 are valid values."); + } + + if((e.cols() == 1 || e.rows() == 1) && size > 1) { + auto start_ptr = direction == 1 ? e.data() + size - 2 : e.data() + 1; + auto end_ptr = direction == 1 ? e.data() : e.data() + size - 1; + while(start_ptr != end_ptr) { + *(start_ptr + direction) = *start_ptr; + start_ptr -= direction; + } + *(start_ptr + direction) = *start_ptr; + } else { + auto start_ptr = direction == 1 ? e.data() + size - 2 * e.rows() : e.data() + e.rows(); + auto end_ptr = direction == 1 ? e.data() : e.data() + size - e.rows(); + auto span = e.rows(); + while(start_ptr != end_ptr) { + std::copy(start_ptr, start_ptr + span, start_ptr + direction * span); + start_ptr -= (direction * span); + } + std::copy(start_ptr, start_ptr + span, start_ptr + direction * span); + } +} + +/** + * @brief Normalize the yaws between points on the basis of final yaw angle + * of the trajectory. + * @param last_yaws Final yaw angles of the trajectories. + * @param yaw_between_points Yaw angles calculated between x and y coordinates of the trajectories. + * @return Normalized yaw between points. + */ +inline auto normalize_yaws_between_points( + const Eigen::Ref & last_yaws, + const Eigen::Ref & yaw_between_points) +{ + Eigen::ArrayXf yaws = utils::shortest_angular_distance( + last_yaws, yaw_between_points).abs(); + int size = yaws.size(); + Eigen::ArrayXf yaws_between_points_corrected(size); + for(int i = 0; i != size; i++) { + const float & yaw_between_point = yaw_between_points[i]; + yaws_between_points_corrected[i] = yaws[i] < M_PIF_2 ? + yaw_between_point : angles::normalize_angle(yaw_between_point + M_PIF); + } + return yaws_between_points_corrected; +} + +/** + * @brief Normalize the yaws between points on the basis of goal angle. + * @param goal_yaw Goal yaw angle. + * @param yaw_between_points Yaw angles calculated between x and y coordinates of the trajectories. + * @return Normalized yaw between points + */ +inline auto normalize_yaws_between_points( + const float goal_yaw, const Eigen::Ref & yaw_between_points) +{ + int size = yaw_between_points.size(); + Eigen::ArrayXf yaws_between_points_corrected(size); + for(int i = 0; i != size; i++) { + const float & yaw_between_point = yaw_between_points[i]; + yaws_between_points_corrected[i] = fabs( + angles::normalize_angle(yaw_between_point - goal_yaw)) < M_PIF_2 ? + yaw_between_point : angles::normalize_angle(yaw_between_point + M_PIF); + } + return yaws_between_points_corrected; +} + +/** + * @brief Clamps the input between the given lower and upper bounds. + * @param lower_bound Lower bound. + * @param upper_bound Upper bound. + * @return Clamped output. + */ +inline float clamp( + const float lower_bound, const float upper_bound, const float input) +{ + return std::min(upper_bound, std::max(input, lower_bound)); +} + +} // namespace mppi::utils + +#endif // NAV2_MPPI_CONTROLLER__TOOLS__UTILS_HPP_ diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils copy_original.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils copy_original.hpp new file mode 100644 index 00000000000..e2f6a925d36 --- /dev/null +++ b/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils copy_original.hpp @@ -0,0 +1,705 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// Copyright (c) 2023 Open Navigation LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NAV2_MPPI_CONTROLLER__TOOLS__UTILS_HPP_ +#define NAV2_MPPI_CONTROLLER__TOOLS__UTILS_HPP_ + +#include + +#include +#include +#include +#include +#include +#include + +#include "angles/angles.h" + +#include "tf2/utils.hpp" +#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" + +#include "geometry_msgs/msg/twist_stamped.hpp" +#include "nav_msgs/msg/path.hpp" +#include "nav2_msgs/msg/trajectory.hpp" +#include "visualization_msgs/msg/marker_array.hpp" + +#include "rclcpp/rclcpp.hpp" +#include "rclcpp_lifecycle/lifecycle_node.hpp" + +#include "nav2_ros_common/node_utils.hpp" +#include "nav2_core/goal_checker.hpp" + +#include "nav2_mppi_controller/models/optimizer_settings.hpp" +#include "nav2_mppi_controller/models/control_sequence.hpp" +#include "nav2_mppi_controller/models/path.hpp" +#include "builtin_interfaces/msg/time.hpp" +#include "nav2_mppi_controller/critic_data.hpp" + +#define M_PIF 3.141592653589793238462643383279502884e+00F +#define M_PIF_2 1.5707963267948966e+00F + +namespace mppi::utils +{ +/** + * @brief Convert data into pose + * @param x X position + * @param y Y position + * @param z Z position + * @return Pose object + */ +inline geometry_msgs::msg::Pose createPose(double x, double y, double z) +{ + geometry_msgs::msg::Pose pose; + pose.position.x = x; + pose.position.y = y; + pose.position.z = z; + pose.orientation.w = 1; + pose.orientation.x = 0; + pose.orientation.y = 0; + pose.orientation.z = 0; + return pose; +} + +/** + * @brief Convert data into scale + * @param x X scale + * @param y Y scale + * @param z Z scale + * @return Scale object + */ +inline geometry_msgs::msg::Vector3 createScale(double x, double y, double z) +{ + geometry_msgs::msg::Vector3 scale; + scale.x = x; + scale.y = y; + scale.z = z; + return scale; +} + +/** + * @brief Convert data into color + * @param r Red component + * @param g Green component + * @param b Blue component + * @param a Alpha component (transparency) + * @return Color object + */ +inline std_msgs::msg::ColorRGBA createColor(float r, float g, float b, float a) +{ + std_msgs::msg::ColorRGBA color; + color.r = r; + color.g = g; + color.b = b; + color.a = a; + return color; +} + +/** + * @brief Convert data into a Maarker + * @param id Marker ID + * @param pose Marker pose + * @param scale Marker scale + * @param color Marker color + * @param frame Reference frame to use + * @return Visualization Marker + */ +inline visualization_msgs::msg::Marker createMarker( + int id, const geometry_msgs::msg::Pose & pose, const geometry_msgs::msg::Vector3 & scale, + const std_msgs::msg::ColorRGBA & color, const std::string & frame_id, const std::string & ns) +{ + using visualization_msgs::msg::Marker; + Marker marker; + marker.header.frame_id = frame_id; + marker.header.stamp = rclcpp::Time(0, 0); + marker.ns = ns; + marker.id = id; + marker.type = Marker::SPHERE; + marker.action = Marker::ADD; + + marker.pose = pose; + marker.scale = scale; + marker.color = color; + return marker; +} + +/** + * @brief Convert data into TwistStamped + * @param vx X velocity + * @param wz Angular velocity + * @param stamp Timestamp + * @param frame Reference frame to use + */ +inline geometry_msgs::msg::TwistStamped toTwistStamped( + float vx, float wz, const builtin_interfaces::msg::Time & stamp, const std::string & frame) +{ + geometry_msgs::msg::TwistStamped twist; + twist.header.frame_id = frame; + twist.header.stamp = stamp; + twist.twist.linear.x = vx; + twist.twist.angular.z = wz; + + return twist; +} + +/** + * @brief Convert data into TwistStamped + * @param vx X velocity + * @param vy Y velocity + * @param wz Angular velocity + * @param stamp Timestamp + * @param frame Reference frame to use + */ +inline geometry_msgs::msg::TwistStamped toTwistStamped( + float vx, float vy, float wz, const builtin_interfaces::msg::Time & stamp, + const std::string & frame) +{ + auto twist = toTwistStamped(vx, wz, stamp, frame); + twist.twist.linear.y = vy; + + return twist; +} + +inline std::unique_ptr toTrajectoryMsg( + const Eigen::ArrayXXf & trajectory, + const models::ControlSequence & control_sequence, + const double & model_dt, + const std_msgs::msg::Header & header) +{ + auto trajectory_msg = std::make_unique(); + trajectory_msg->header = header; + trajectory_msg->points.resize(trajectory.rows()); + + for (int i = 0; i < trajectory.rows(); ++i) { + auto & curr_pt = trajectory_msg->points[i]; + curr_pt.time_from_start = rclcpp::Duration::from_seconds(i * model_dt); + curr_pt.pose.position.x = trajectory(i, 0); + curr_pt.pose.position.y = trajectory(i, 1); + tf2::Quaternion quat; + quat.setRPY(0.0, 0.0, trajectory(i, 2)); + curr_pt.pose.orientation = tf2::toMsg(quat); + curr_pt.velocity.linear.x = control_sequence.vx(i); + curr_pt.velocity.angular.z = control_sequence.wz(i); + if (control_sequence.vy.size() > 0) { + curr_pt.velocity.linear.y = control_sequence.vy(i); + } + } + + return trajectory_msg; +} + +/** + * @brief Convert path to a tensor + * @param path Path to convert + * @return Path tensor + */ +inline models::Path toTensor(const nav_msgs::msg::Path & path) +{ + auto result = models::Path{}; + result.reset(path.poses.size()); + + for (size_t i = 0; i < path.poses.size(); ++i) { + result.x(i) = path.poses[i].pose.position.x; + result.y(i) = path.poses[i].pose.position.y; + result.yaws(i) = tf2::getYaw(path.poses[i].pose.orientation); + } + + return result; +} + +/** + * @brief Get the last pose from a path + * @param path Reference to the path + * @return geometry_msgs::msg::Pose Last pose in the path + */ +inline geometry_msgs::msg::Pose getLastPathPose(const models::Path & path) +{ + const unsigned int path_last_idx = path.x.size() - 1; + + auto last_orientation = path.yaws(path_last_idx); + + tf2::Quaternion pose_orientation; + pose_orientation.setRPY(0.0, 0.0, last_orientation); + + geometry_msgs::msg::Pose pathPose; + pathPose.position.x = path.x(path_last_idx); + pathPose.position.y = path.y(path_last_idx); + pathPose.orientation.x = pose_orientation.x(); + pathPose.orientation.y = pose_orientation.y(); + pathPose.orientation.z = pose_orientation.z(); + pathPose.orientation.w = pose_orientation.w(); + + return pathPose; +} + +/** + * @brief normalize + * Normalizes the angle to be -M_PIF circle to +M_PIF circle + * It takes and returns radians. + * @param angles Angles to normalize + * @return normalized angles + */ +template +auto normalize_angles(const T & angles) +{ + return (angles + M_PIF).unaryExpr([&](const float x) { + float remainder = std::fmod(x, 2.0f * M_PIF); + return remainder < 0.0f ? remainder + M_PIF : remainder - M_PIF; + }); +} + +/** + * @brief shortest_angular_distance + * + * Given 2 angles, this returns the shortest angular + * difference. The inputs and outputs are of course radians. + * + * The result + * would always be -pi <= result <= pi. Adding the result + * to "from" will always get you an equivalent angle to "to". + * @param from Start angle + * @param to End angle + * @return Shortest distance between angles + */ +template +auto shortest_angular_distance( + const F & from, + const T & to) +{ + return normalize_angles(to - from); +} + +/** + * @brief Evaluate furthest point idx of data.path which is + * nearest to some trajectory in data.trajectories + * @param data Data to use + * @return Idx of furthest path point reached by a set of trajectories + */ +inline size_t findPathFurthestReachedPoint(const CriticData & data) +{ + int traj_cols = data.trajectories.x.cols(); + const auto traj_x = data.trajectories.x.col(traj_cols - 1); + const auto traj_y = data.trajectories.y.col(traj_cols - 1); + + const auto dx = (data.path.x.transpose()).replicate(traj_x.rows(), 1).colwise() - traj_x; + const auto dy = (data.path.y.transpose()).replicate(traj_y.rows(), 1).colwise() - traj_y; + + const auto dists = dx * dx + dy * dy; + + int max_id_by_trajectories = 0, min_id_by_path = 0; + float min_distance_by_path = std::numeric_limits::max(); + size_t n_rows = dists.rows(); + size_t n_cols = dists.cols(); + for (size_t i = 0; i != n_rows; i++) { + // Skip trajectories in collision + bool in_collision = data.trajectories_in_collision && + i < data.trajectories_in_collision->size() && + (*data.trajectories_in_collision)[i]; + if (in_collision) { + continue; + } + min_id_by_path = 0; + min_distance_by_path = std::numeric_limits::max(); + // Search all path points to find the true closest point + for (size_t j = 0; j != n_cols; j++) { + const float cur_dist = dists(i, j); + if (cur_dist < min_distance_by_path) { + min_distance_by_path = cur_dist; + min_id_by_path = j; + // If we've found the last path point as closest, can't do better for this trajectory + if (min_id_by_path >= static_cast(n_cols - 1)) { + break; + } + } + } + max_id_by_trajectories = std::max(max_id_by_trajectories, min_id_by_path); + // Early exit: if we've reached the last path point, we can't do better + if (max_id_by_trajectories >= static_cast(n_cols - 1)) { + break; + } + } + return max_id_by_trajectories; +} + +/** + * @brief evaluate path furthest point if it is not set + * @param data Data to use + */ +inline void setPathFurthestPointIfNotSet(CriticData & data) +{ + if (!data.furthest_reached_path_point) { + data.furthest_reached_path_point = findPathFurthestReachedPoint(data); + } +} + +/** + * @brief evaluate path costs + * @param data Data to use + */ +inline void findPathCosts( + CriticData & data, + std::shared_ptr costmap_ros) +{ + auto * costmap = costmap_ros->getCostmap(); + unsigned int map_x, map_y; + const size_t path_segments_count = data.path.x.size() - 1; + data.path_pts_valid = std::vector(path_segments_count, false); + const bool tracking_unknown = costmap_ros->getLayeredCostmap()->isTrackingUnknown(); + for (unsigned int idx = 0; idx < path_segments_count; idx++) { + if (!costmap->worldToMap(data.path.x(idx), data.path.y(idx), map_x, map_y)) { + (*data.path_pts_valid)[idx] = false; + continue; + } + + switch (costmap->getCost(map_x, map_y)) { + case (nav2_costmap_2d::LETHAL_OBSTACLE): + (*data.path_pts_valid)[idx] = false; + continue; + case (nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE): + (*data.path_pts_valid)[idx] = false; + continue; + case (nav2_costmap_2d::NO_INFORMATION): + (*data.path_pts_valid)[idx] = tracking_unknown ? true : false; + continue; + } + + (*data.path_pts_valid)[idx] = true; + } +} + +/** + * @brief evaluate path costs if it is not set + * @param data Data to use + */ +inline void setPathCostsIfNotSet( + CriticData & data, + std::shared_ptr costmap_ros) +{ + if (!data.path_pts_valid) { + findPathCosts(data, costmap_ros); + } +} + +/** + * @brief evaluate angle from pose (have angle) to point (no angle) + * @param pose pose + * @param point_x Point to find angle relative to X axis + * @param point_y Point to find angle relative to Y axis + * @param forward_preference If reversing direction is valid + * @return Angle between two points + */ +inline float posePointAngle( + const geometry_msgs::msg::Pose & pose, double point_x, double point_y, bool forward_preference) +{ + float pose_x = pose.position.x; + float pose_y = pose.position.y; + float pose_yaw = tf2::getYaw(pose.orientation); + + float yaw = atan2f(point_y - pose_y, point_x - pose_x); + + // If no preference for forward, return smallest angle either in heading or 180 of heading + if (!forward_preference) { + return std::min( + fabs(angles::shortest_angular_distance(yaw, pose_yaw)), + fabs(angles::shortest_angular_distance(yaw, angles::normalize_angle(pose_yaw + M_PIF)))); + } + + return fabs(angles::shortest_angular_distance(yaw, pose_yaw)); +} + +/** + * @brief evaluate angle from pose (have angle) to point (no angle) + * @param pose pose + * @param point_x Point to find angle relative to X axis + * @param point_y Point to find angle relative to Y axis + * @param point_yaw Yaw of the point to consider along Z axis + * @return Angle between two points + */ +inline float posePointAngle( + const geometry_msgs::msg::Pose & pose, + double point_x, double point_y, double point_yaw) +{ + float pose_x = static_cast(pose.position.x); + float pose_y = static_cast(pose.position.y); + float pose_yaw = static_cast(tf2::getYaw(pose.orientation)); + + float yaw = atan2f(static_cast(point_y) - pose_y, static_cast(point_x) - pose_x); + + if (fabs(angles::shortest_angular_distance(yaw, static_cast(point_yaw))) > M_PIF_2) { + yaw = angles::normalize_angle(yaw + M_PIF); + } + + return fabs(angles::shortest_angular_distance(yaw, pose_yaw)); +} + +/** + * @brief Apply Savisky-Golay filter to optimal trajectory + * @param control_sequence Sequence to apply filter to + * @param control_history Recent set of controls for edge-case handling + * @param Settings Settings to use + */ +inline void savitskyGolayFilter( + models::ControlSequence & control_sequence, + std::array & control_history, + const models::OptimizerSettings & settings) +{ + // Savitzky-Golay Quadratic, 9-point Coefficients + Eigen::Array filter = {-21.0f, 14.0f, 39.0f, 54.0f, 59.0f, 54.0f, 39.0f, 14.0f, + -21.0f}; + filter /= 231.0f; + + // Too short to smooth meaningfully + const unsigned int num_sequences = control_sequence.vx.size() - 1; + if (num_sequences < 20) { + return; + } + + auto applyFilter = [&](const Eigen::Array & data) -> float { + return (data * filter).eval().sum(); + }; + + auto applyFilterOverAxis = + [&](Eigen::ArrayXf & sequence, const Eigen::ArrayXf & initial_sequence, + const float hist_0, const float hist_1, const float hist_2, const float hist_3) -> void + { + float pt_m4 = hist_0; + float pt_m3 = hist_1; + float pt_m2 = hist_2; + float pt_m1 = hist_3; + float pt = initial_sequence(0); + float pt_p1 = initial_sequence(1); + float pt_p2 = initial_sequence(2); + float pt_p3 = initial_sequence(3); + float pt_p4 = initial_sequence(4); + + for (unsigned int idx = 0; idx != num_sequences; idx++) { + sequence(idx) = applyFilter({pt_m4, pt_m3, pt_m2, pt_m1, pt, pt_p1, pt_p2, pt_p3, pt_p4}); + pt_m4 = pt_m3; + pt_m3 = pt_m2; + pt_m2 = pt_m1; + pt_m1 = pt; + pt = pt_p1; + pt_p1 = pt_p2; + pt_p2 = pt_p3; + pt_p3 = pt_p4; + + if (idx + 5 < num_sequences) { + pt_p4 = initial_sequence(idx + 5); + } else { + // Return the last point + pt_p4 = initial_sequence(num_sequences); + } + } + }; + + // Filter trajectories + const models::ControlSequence initial_control_sequence = control_sequence; + applyFilterOverAxis( + control_sequence.vx, initial_control_sequence.vx, control_history[0].vx, + control_history[1].vx, control_history[2].vx, control_history[3].vx); + applyFilterOverAxis( + control_sequence.vy, initial_control_sequence.vy, control_history[0].vy, + control_history[1].vy, control_history[2].vy, control_history[3].vy); + applyFilterOverAxis( + control_sequence.wz, initial_control_sequence.wz, control_history[0].wz, + control_history[1].wz, control_history[2].wz, control_history[3].wz); + + // Update control history + unsigned int offset = settings.shift_control_sequence ? 1 : 0; + control_history[0] = control_history[1]; + control_history[1] = control_history[2]; + control_history[2] = control_history[3]; + control_history[3] = { + control_sequence.vx(offset), + control_sequence.vy(offset), + control_sequence.wz(offset)}; +} + +/** + * @brief Find the iterator of the first pose at which there is an inversion on the path, + * @param path to check for inversion + * @return the first point after the inversion found in the path + */ +inline unsigned int findFirstPathInversion(nav_msgs::msg::Path & path) +{ + // At least 3 poses for a possible inversion + if (path.poses.size() < 3) { + return path.poses.size(); + } + + // Iterating through the path to determine the position of the path inversion + for (unsigned int idx = 1; idx < path.poses.size() - 1; ++idx) { + // We have two vectors for the dot product OA and AB. Determining the vectors. + float oa_x = path.poses[idx].pose.position.x - + path.poses[idx - 1].pose.position.x; + float oa_y = path.poses[idx].pose.position.y - + path.poses[idx - 1].pose.position.y; + float ab_x = path.poses[idx + 1].pose.position.x - + path.poses[idx].pose.position.x; + float ab_y = path.poses[idx + 1].pose.position.y - + path.poses[idx].pose.position.y; + + // Checking for the existence of cusp, in the path, using the dot product. + float dot_product = (oa_x * ab_x) + (oa_y * ab_y); + if (dot_product < 0.0f) { + return idx + 1; + } + } + + return path.poses.size(); +} + +/** + * @brief Find and remove poses after the first inversion in the path + * @param path to check for inversion + * @return The location of the inversion, return 0 if none exist + */ +inline unsigned int removePosesAfterFirstInversion(nav_msgs::msg::Path & path) +{ + nav_msgs::msg::Path cropped_path = path; + const unsigned int first_after_inversion = findFirstPathInversion(cropped_path); + if (first_after_inversion == path.poses.size()) { + return 0u; + } + + cropped_path.poses.erase( + cropped_path.poses.begin() + first_after_inversion, cropped_path.poses.end()); + path = cropped_path; + return first_after_inversion; +} + +/** + * @brief Compare to trajectory points to find closest path point along integrated distances + * @param vec Vect to check + * @return dist Distance to look for + * @return init Starting index to indec from + */ +inline unsigned int findClosestPathPt( + const std::vector & vec, const float dist, const unsigned int init = 0u) +{ + float distim1 = init != 0u ? vec[init] : 0.0f; // First is 0, no accumulated distance yet + float disti = 0.0f; + const unsigned int size = vec.size(); + for (unsigned int i = init + 1; i != size; i++) { + disti = vec[i]; + if (disti > dist) { + if (i > 0 && dist - distim1 < disti - dist) { + return i - 1; + } + return i; + } + distim1 = disti; + } + return size - 1; +} + +// A struct to hold pose data in floating point resolution +struct Pose2D +{ + float x, y, theta; +}; + +/** + * @brief Shift the columns of a 2D Eigen Array or scalar values of + * 1D Eigen Array by 1 place. + * @param e Eigen Array + * @param direction direction in which Array will be shifted. + * 1 for shift in right direction and -1 for left direction. + */ +inline void shiftColumnsByOnePlace(Eigen::Ref e, int direction) +{ + int size = e.size(); + if(size == 1) {return;} + if(abs(direction) != 1) { + throw std::logic_error("Invalid direction, only 1 and -1 are valid values."); + } + + if((e.cols() == 1 || e.rows() == 1) && size > 1) { + auto start_ptr = direction == 1 ? e.data() + size - 2 : e.data() + 1; + auto end_ptr = direction == 1 ? e.data() : e.data() + size - 1; + while(start_ptr != end_ptr) { + *(start_ptr + direction) = *start_ptr; + start_ptr -= direction; + } + *(start_ptr + direction) = *start_ptr; + } else { + auto start_ptr = direction == 1 ? e.data() + size - 2 * e.rows() : e.data() + e.rows(); + auto end_ptr = direction == 1 ? e.data() : e.data() + size - e.rows(); + auto span = e.rows(); + while(start_ptr != end_ptr) { + std::copy(start_ptr, start_ptr + span, start_ptr + direction * span); + start_ptr -= (direction * span); + } + std::copy(start_ptr, start_ptr + span, start_ptr + direction * span); + } +} + +/** + * @brief Normalize the yaws between points on the basis of final yaw angle + * of the trajectory. + * @param last_yaws Final yaw angles of the trajectories. + * @param yaw_between_points Yaw angles calculated between x and y coordinates of the trajectories. + * @return Normalized yaw between points. + */ +inline auto normalize_yaws_between_points( + const Eigen::Ref & last_yaws, + const Eigen::Ref & yaw_between_points) +{ + Eigen::ArrayXf yaws = utils::shortest_angular_distance( + last_yaws, yaw_between_points).abs(); + int size = yaws.size(); + Eigen::ArrayXf yaws_between_points_corrected(size); + for(int i = 0; i != size; i++) { + const float & yaw_between_point = yaw_between_points[i]; + yaws_between_points_corrected[i] = yaws[i] < M_PIF_2 ? + yaw_between_point : angles::normalize_angle(yaw_between_point + M_PIF); + } + return yaws_between_points_corrected; +} + +/** + * @brief Normalize the yaws between points on the basis of goal angle. + * @param goal_yaw Goal yaw angle. + * @param yaw_between_points Yaw angles calculated between x and y coordinates of the trajectories. + * @return Normalized yaw between points + */ +inline auto normalize_yaws_between_points( + const float goal_yaw, const Eigen::Ref & yaw_between_points) +{ + int size = yaw_between_points.size(); + Eigen::ArrayXf yaws_between_points_corrected(size); + for(int i = 0; i != size; i++) { + const float & yaw_between_point = yaw_between_points[i]; + yaws_between_points_corrected[i] = fabs( + angles::normalize_angle(yaw_between_point - goal_yaw)) < M_PIF_2 ? + yaw_between_point : angles::normalize_angle(yaw_between_point + M_PIF); + } + return yaws_between_points_corrected; +} + +/** + * @brief Clamps the input between the given lower and upper bounds. + * @param lower_bound Lower bound. + * @param upper_bound Upper bound. + * @return Clamped output. + */ +inline float clamp( + const float lower_bound, const float upper_bound, const float input) +{ + return std::min(upper_bound, std::max(input, lower_bound)); +} + +} // namespace mppi::utils + +#endif // NAV2_MPPI_CONTROLLER__TOOLS__UTILS_HPP_ diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils.hpp index 4a3cc6b7c06..0ad511a351f 100644 --- a/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils.hpp +++ b/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils.hpp @@ -511,56 +511,124 @@ inline void savitskyGolayFilter( } /** - * @brief Find the iterator of the first pose at which there is an inversion on the path, - * @param path to check for inversion - * @return the first point after the inversion found in the path + * @brief Find the iterator of the first pose at which there is an inversion or in-place rotation in the path + * @param path to check for inversion or rotation + * @param rotation_threshold Minimum rotation angle to consider an in-place rotation (0 to disable rotation check) + * @return the first point after the inversion or rotation found in the path */ -inline unsigned int findFirstPathInversion(nav_msgs::msg::Path & path) +inline unsigned int findFirstPathInversion( + nav_msgs::msg::Path & path, + float rotation_threshold = 0.0f) { - // At least 3 poses for a possible inversion - if (path.poses.size() < 3) { + if (path.poses.size() < 2) { return path.poses.size(); } - // Iterating through the path to determine the position of the path inversion - for (unsigned int idx = 1; idx < path.poses.size() - 1; ++idx) { - // We have two vectors for the dot product OA and AB. Determining the vectors. - float oa_x = path.poses[idx].pose.position.x - - path.poses[idx - 1].pose.position.x; - float oa_y = path.poses[idx].pose.position.y - - path.poses[idx - 1].pose.position.y; - float ab_x = path.poses[idx + 1].pose.position.x - - path.poses[idx].pose.position.x; - float ab_y = path.poses[idx + 1].pose.position.y - - path.poses[idx].pose.position.y; - - // Checking for the existence of cusp, in the path, using the dot product. - float dot_product = (oa_x * ab_x) + (oa_y * ab_y); - if (dot_product < 0.0f) { - return idx + 1; + unsigned int first_after_constraint = path.poses.size(); + + // Check for in-place rotation first (if enabled) + if (rotation_threshold > 0.0f) { + for (unsigned int idx = 0; idx < path.poses.size() - 1; ++idx) { + float dx = path.poses[idx + 1].pose.position.x - path.poses[idx].pose.position.x; + float dy = path.poses[idx + 1].pose.position.y - path.poses[idx].pose.position.y; + float translation = sqrtf(dx * dx + dy * dy); + + // Check if poses are at roughly the same location + if (translation < 1e-3) { + float yaw1 = tf2::getYaw(path.poses[idx].pose.orientation); + float yaw2 = tf2::getYaw(path.poses[idx + 1].pose.orientation); + float rotation = fabs(angles::shortest_angular_distance(yaw1, yaw2)); + + // Check if this meets the minimum rotation threshold + if (rotation > rotation_threshold) { + // Found start of in-place rotation, now find where it ends + unsigned int end_idx = idx + 1; + + // Continue while we have minimal translation (still rotating in place) + while (end_idx < path.poses.size() - 1) { + float next_dx = path.poses[end_idx + 1].pose.position.x - + path.poses[end_idx].pose.position.x; + float next_dy = path.poses[end_idx + 1].pose.position.y - + path.poses[end_idx].pose.position.y; + float next_translation = sqrtf(next_dx * next_dx + next_dy * next_dy); + + if (next_translation >= 1e-3) { + break; // End of in-place rotation sequence + } + end_idx++; + } + + first_after_constraint = end_idx; + break; + } + } } } - return path.poses.size(); + // Check for inversion (at least 3 poses needed) + if (path.poses.size() >= 3) { + for (unsigned int idx = 1; idx < path.poses.size() - 1; ++idx) { + float oa_x = path.poses[idx].pose.position.x - + path.poses[idx - 1].pose.position.x; + float oa_y = path.poses[idx].pose.position.y - + path.poses[idx - 1].pose.position.y; + float ab_x = path.poses[idx + 1].pose.position.x - + path.poses[idx].pose.position.x; + float ab_y = path.poses[idx + 1].pose.position.y - + path.poses[idx].pose.position.y; + + float dot_product = (oa_x * ab_x) + (oa_y * ab_y); + + float norm_oa = hypotf(oa_x, oa_y); + float norm_ab = hypotf(ab_x, ab_y); + + if (norm_oa < 1e-4f || norm_ab < 1e-4f) { + continue; + } + + float cos_ang = dot_product / (norm_oa * norm_ab); + cos_ang = std::max(-1.0f, std::min(1.0f, cos_ang)); + float angle = acosf(cos_ang); // [0, pi] + + if (rotation_threshold > 0.0f) { + if (angle > rotation_threshold) { + first_after_constraint = std::min(first_after_constraint, idx + 1); + break; + } + } else { + if (dot_product < 0.0f) { + first_after_constraint = std::min(first_after_constraint, idx + 1); + break; + } + } + } + } + + return first_after_constraint; } /** - * @brief Find and remove poses after the first inversion in the path - * @param path to check for inversion - * @return The location of the inversion, return 0 if none exist + * @brief Find and remove poses after the first inversion or in-place rotation in the path + * @param path Path to check for inversion or rotation + * @param rotation_threshold Minimum rotation angle to consider an in-place rotation (0 to disable rotation check) + * @return The location of the inversion/rotation, return 0 if none exist */ -inline unsigned int removePosesAfterFirstInversion(nav_msgs::msg::Path & path) +inline unsigned int removePosesAfterFirstInversion( + nav_msgs::msg::Path & path, + float rotation_threshold = 0.0f) { nav_msgs::msg::Path cropped_path = path; - const unsigned int first_after_inversion = findFirstPathInversion(cropped_path); - if (first_after_inversion == path.poses.size()) { - return 0u; + + const unsigned int first_after_constraint = findFirstPathInversion(cropped_path, rotation_threshold); + + if (first_after_constraint == path.poses.size()) { + return 0u; // No constraint found } cropped_path.poses.erase( - cropped_path.poses.begin() + first_after_inversion, cropped_path.poses.end()); + cropped_path.poses.begin() + first_after_constraint, cropped_path.poses.end()); path = cropped_path; - return first_after_inversion; + return first_after_constraint; } /** diff --git a/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils_mod_final.hpp b/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils_mod_final.hpp new file mode 100644 index 00000000000..0ad511a351f --- /dev/null +++ b/nav2_mppi_controller/include/nav2_mppi_controller/tools/utils_mod_final.hpp @@ -0,0 +1,757 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// Copyright (c) 2023 Open Navigation LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NAV2_MPPI_CONTROLLER__TOOLS__UTILS_HPP_ +#define NAV2_MPPI_CONTROLLER__TOOLS__UTILS_HPP_ + +#include + +#include +#include +#include +#include +#include +#include + +#include "angles/angles.h" + +#include "tf2/utils.hpp" +#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" + +#include "geometry_msgs/msg/twist_stamped.hpp" +#include "nav_msgs/msg/path.hpp" +#include "nav2_msgs/msg/trajectory.hpp" +#include "visualization_msgs/msg/marker_array.hpp" + +#include "rclcpp/rclcpp.hpp" +#include "rclcpp_lifecycle/lifecycle_node.hpp" + +#include "nav2_ros_common/node_utils.hpp" +#include "nav2_core/goal_checker.hpp" + +#include "nav2_mppi_controller/models/optimizer_settings.hpp" +#include "nav2_mppi_controller/models/control_sequence.hpp" +#include "nav2_mppi_controller/models/path.hpp" +#include "builtin_interfaces/msg/time.hpp" +#include "nav2_mppi_controller/critic_data.hpp" + +#define M_PIF 3.141592653589793238462643383279502884e+00F +#define M_PIF_2 1.5707963267948966e+00F + +namespace mppi::utils +{ +/** + * @brief Convert data into pose + * @param x X position + * @param y Y position + * @param z Z position + * @return Pose object + */ +inline geometry_msgs::msg::Pose createPose(double x, double y, double z) +{ + geometry_msgs::msg::Pose pose; + pose.position.x = x; + pose.position.y = y; + pose.position.z = z; + pose.orientation.w = 1; + pose.orientation.x = 0; + pose.orientation.y = 0; + pose.orientation.z = 0; + return pose; +} + +/** + * @brief Convert data into scale + * @param x X scale + * @param y Y scale + * @param z Z scale + * @return Scale object + */ +inline geometry_msgs::msg::Vector3 createScale(double x, double y, double z) +{ + geometry_msgs::msg::Vector3 scale; + scale.x = x; + scale.y = y; + scale.z = z; + return scale; +} + +/** + * @brief Convert data into color + * @param r Red component + * @param g Green component + * @param b Blue component + * @param a Alpha component (transparency) + * @return Color object + */ +inline std_msgs::msg::ColorRGBA createColor(float r, float g, float b, float a) +{ + std_msgs::msg::ColorRGBA color; + color.r = r; + color.g = g; + color.b = b; + color.a = a; + return color; +} + +/** + * @brief Convert data into a Maarker + * @param id Marker ID + * @param pose Marker pose + * @param scale Marker scale + * @param color Marker color + * @param frame Reference frame to use + * @return Visualization Marker + */ +inline visualization_msgs::msg::Marker createMarker( + int id, const geometry_msgs::msg::Pose & pose, const geometry_msgs::msg::Vector3 & scale, + const std_msgs::msg::ColorRGBA & color, const std::string & frame_id, const std::string & ns) +{ + using visualization_msgs::msg::Marker; + Marker marker; + marker.header.frame_id = frame_id; + marker.header.stamp = rclcpp::Time(0, 0); + marker.ns = ns; + marker.id = id; + marker.type = Marker::SPHERE; + marker.action = Marker::ADD; + + marker.pose = pose; + marker.scale = scale; + marker.color = color; + return marker; +} + +/** + * @brief Convert data into TwistStamped + * @param vx X velocity + * @param wz Angular velocity + * @param stamp Timestamp + * @param frame Reference frame to use + */ +inline geometry_msgs::msg::TwistStamped toTwistStamped( + float vx, float wz, const builtin_interfaces::msg::Time & stamp, const std::string & frame) +{ + geometry_msgs::msg::TwistStamped twist; + twist.header.frame_id = frame; + twist.header.stamp = stamp; + twist.twist.linear.x = vx; + twist.twist.angular.z = wz; + + return twist; +} + +/** + * @brief Convert data into TwistStamped + * @param vx X velocity + * @param vy Y velocity + * @param wz Angular velocity + * @param stamp Timestamp + * @param frame Reference frame to use + */ +inline geometry_msgs::msg::TwistStamped toTwistStamped( + float vx, float vy, float wz, const builtin_interfaces::msg::Time & stamp, + const std::string & frame) +{ + auto twist = toTwistStamped(vx, wz, stamp, frame); + twist.twist.linear.y = vy; + + return twist; +} + +inline std::unique_ptr toTrajectoryMsg( + const Eigen::ArrayXXf & trajectory, + const models::ControlSequence & control_sequence, + const double & model_dt, + const std_msgs::msg::Header & header) +{ + auto trajectory_msg = std::make_unique(); + trajectory_msg->header = header; + trajectory_msg->points.resize(trajectory.rows()); + + for (int i = 0; i < trajectory.rows(); ++i) { + auto & curr_pt = trajectory_msg->points[i]; + curr_pt.time_from_start = rclcpp::Duration::from_seconds(i * model_dt); + curr_pt.pose.position.x = trajectory(i, 0); + curr_pt.pose.position.y = trajectory(i, 1); + tf2::Quaternion quat; + quat.setRPY(0.0, 0.0, trajectory(i, 2)); + curr_pt.pose.orientation = tf2::toMsg(quat); + curr_pt.velocity.linear.x = control_sequence.vx(i); + curr_pt.velocity.angular.z = control_sequence.wz(i); + if (control_sequence.vy.size() > 0) { + curr_pt.velocity.linear.y = control_sequence.vy(i); + } + } + + return trajectory_msg; +} + +/** + * @brief Convert path to a tensor + * @param path Path to convert + * @return Path tensor + */ +inline models::Path toTensor(const nav_msgs::msg::Path & path) +{ + auto result = models::Path{}; + result.reset(path.poses.size()); + + for (size_t i = 0; i < path.poses.size(); ++i) { + result.x(i) = path.poses[i].pose.position.x; + result.y(i) = path.poses[i].pose.position.y; + result.yaws(i) = tf2::getYaw(path.poses[i].pose.orientation); + } + + return result; +} + +/** + * @brief Get the last pose from a path + * @param path Reference to the path + * @return geometry_msgs::msg::Pose Last pose in the path + */ +inline geometry_msgs::msg::Pose getLastPathPose(const models::Path & path) +{ + const unsigned int path_last_idx = path.x.size() - 1; + + auto last_orientation = path.yaws(path_last_idx); + + tf2::Quaternion pose_orientation; + pose_orientation.setRPY(0.0, 0.0, last_orientation); + + geometry_msgs::msg::Pose pathPose; + pathPose.position.x = path.x(path_last_idx); + pathPose.position.y = path.y(path_last_idx); + pathPose.orientation.x = pose_orientation.x(); + pathPose.orientation.y = pose_orientation.y(); + pathPose.orientation.z = pose_orientation.z(); + pathPose.orientation.w = pose_orientation.w(); + + return pathPose; +} + +/** + * @brief normalize + * Normalizes the angle to be -M_PIF circle to +M_PIF circle + * It takes and returns radians. + * @param angles Angles to normalize + * @return normalized angles + */ +template +auto normalize_angles(const T & angles) +{ + return (angles + M_PIF).unaryExpr([&](const float x) { + float remainder = std::fmod(x, 2.0f * M_PIF); + return remainder < 0.0f ? remainder + M_PIF : remainder - M_PIF; + }); +} + +/** + * @brief shortest_angular_distance + * + * Given 2 angles, this returns the shortest angular + * difference. The inputs and outputs are of course radians. + * + * The result + * would always be -pi <= result <= pi. Adding the result + * to "from" will always get you an equivalent angle to "to". + * @param from Start angle + * @param to End angle + * @return Shortest distance between angles + */ +template +auto shortest_angular_distance( + const F & from, + const T & to) +{ + return normalize_angles(to - from); +} + +/** + * @brief Evaluate furthest point idx of data.path which is + * nearest to some trajectory in data.trajectories + * @param data Data to use + * @return Idx of furthest path point reached by a set of trajectories + */ +inline size_t findPathFurthestReachedPoint(const CriticData & data) +{ + int traj_cols = data.trajectories.x.cols(); + const auto traj_x = data.trajectories.x.col(traj_cols - 1); + const auto traj_y = data.trajectories.y.col(traj_cols - 1); + + const auto dx = (data.path.x.transpose()).replicate(traj_x.rows(), 1).colwise() - traj_x; + const auto dy = (data.path.y.transpose()).replicate(traj_y.rows(), 1).colwise() - traj_y; + + const auto dists = dx * dx + dy * dy; + + int max_id_by_trajectories = 0, min_id_by_path = 0; + float min_distance_by_path = std::numeric_limits::max(); + size_t n_rows = dists.rows(); + size_t n_cols = dists.cols(); + for (size_t i = 0; i != n_rows; i++) { + min_id_by_path = 0; + min_distance_by_path = std::numeric_limits::max(); + for (size_t j = max_id_by_trajectories; j != n_cols; j++) { + const float cur_dist = dists(i, j); + if (cur_dist < min_distance_by_path) { + min_distance_by_path = cur_dist; + min_id_by_path = j; + } + } + max_id_by_trajectories = std::max(max_id_by_trajectories, min_id_by_path); + } + return max_id_by_trajectories; +} + +/** + * @brief evaluate path furthest point if it is not set + * @param data Data to use + */ +inline void setPathFurthestPointIfNotSet(CriticData & data) +{ + if (!data.furthest_reached_path_point) { + data.furthest_reached_path_point = findPathFurthestReachedPoint(data); + } +} + +/** + * @brief evaluate path costs + * @param data Data to use + */ +inline void findPathCosts( + CriticData & data, + std::shared_ptr costmap_ros) +{ + auto * costmap = costmap_ros->getCostmap(); + unsigned int map_x, map_y; + const size_t path_segments_count = data.path.x.size() - 1; + data.path_pts_valid = std::vector(path_segments_count, false); + const bool tracking_unknown = costmap_ros->getLayeredCostmap()->isTrackingUnknown(); + for (unsigned int idx = 0; idx < path_segments_count; idx++) { + if (!costmap->worldToMap(data.path.x(idx), data.path.y(idx), map_x, map_y)) { + (*data.path_pts_valid)[idx] = false; + continue; + } + + switch (costmap->getCost(map_x, map_y)) { + case (nav2_costmap_2d::LETHAL_OBSTACLE): + (*data.path_pts_valid)[idx] = false; + continue; + case (nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE): + (*data.path_pts_valid)[idx] = false; + continue; + case (nav2_costmap_2d::NO_INFORMATION): + (*data.path_pts_valid)[idx] = tracking_unknown ? true : false; + continue; + } + + (*data.path_pts_valid)[idx] = true; + } +} + +/** + * @brief evaluate path costs if it is not set + * @param data Data to use + */ +inline void setPathCostsIfNotSet( + CriticData & data, + std::shared_ptr costmap_ros) +{ + if (!data.path_pts_valid) { + findPathCosts(data, costmap_ros); + } +} + +/** + * @brief evaluate angle from pose (have angle) to point (no angle) + * @param pose pose + * @param point_x Point to find angle relative to X axis + * @param point_y Point to find angle relative to Y axis + * @param forward_preference If reversing direction is valid + * @return Angle between two points + */ +inline float posePointAngle( + const geometry_msgs::msg::Pose & pose, double point_x, double point_y, bool forward_preference) +{ + float pose_x = pose.position.x; + float pose_y = pose.position.y; + float pose_yaw = tf2::getYaw(pose.orientation); + + float yaw = atan2f(point_y - pose_y, point_x - pose_x); + + // If no preference for forward, return smallest angle either in heading or 180 of heading + if (!forward_preference) { + return std::min( + fabs(angles::shortest_angular_distance(yaw, pose_yaw)), + fabs(angles::shortest_angular_distance(yaw, angles::normalize_angle(pose_yaw + M_PIF)))); + } + + return fabs(angles::shortest_angular_distance(yaw, pose_yaw)); +} + +/** + * @brief evaluate angle from pose (have angle) to point (no angle) + * @param pose pose + * @param point_x Point to find angle relative to X axis + * @param point_y Point to find angle relative to Y axis + * @param point_yaw Yaw of the point to consider along Z axis + * @return Angle between two points + */ +inline float posePointAngle( + const geometry_msgs::msg::Pose & pose, + double point_x, double point_y, double point_yaw) +{ + float pose_x = static_cast(pose.position.x); + float pose_y = static_cast(pose.position.y); + float pose_yaw = static_cast(tf2::getYaw(pose.orientation)); + + float yaw = atan2f(static_cast(point_y) - pose_y, static_cast(point_x) - pose_x); + + if (fabs(angles::shortest_angular_distance(yaw, static_cast(point_yaw))) > M_PIF_2) { + yaw = angles::normalize_angle(yaw + M_PIF); + } + + return fabs(angles::shortest_angular_distance(yaw, pose_yaw)); +} + +/** + * @brief Apply Savisky-Golay filter to optimal trajectory + * @param control_sequence Sequence to apply filter to + * @param control_history Recent set of controls for edge-case handling + * @param Settings Settings to use + */ +inline void savitskyGolayFilter( + models::ControlSequence & control_sequence, + std::array & control_history, + const models::OptimizerSettings & settings) +{ + // Savitzky-Golay Quadratic, 9-point Coefficients + Eigen::Array filter = {-21.0f, 14.0f, 39.0f, 54.0f, 59.0f, 54.0f, 39.0f, 14.0f, + -21.0f}; + filter /= 231.0f; + + // Too short to smooth meaningfully + const unsigned int num_sequences = control_sequence.vx.size() - 1; + if (num_sequences < 20) { + return; + } + + auto applyFilter = [&](const Eigen::Array & data) -> float { + return (data * filter).eval().sum(); + }; + + auto applyFilterOverAxis = + [&](Eigen::ArrayXf & sequence, const Eigen::ArrayXf & initial_sequence, + const float hist_0, const float hist_1, const float hist_2, const float hist_3) -> void + { + float pt_m4 = hist_0; + float pt_m3 = hist_1; + float pt_m2 = hist_2; + float pt_m1 = hist_3; + float pt = initial_sequence(0); + float pt_p1 = initial_sequence(1); + float pt_p2 = initial_sequence(2); + float pt_p3 = initial_sequence(3); + float pt_p4 = initial_sequence(4); + + for (unsigned int idx = 0; idx != num_sequences; idx++) { + sequence(idx) = applyFilter({pt_m4, pt_m3, pt_m2, pt_m1, pt, pt_p1, pt_p2, pt_p3, pt_p4}); + pt_m4 = pt_m3; + pt_m3 = pt_m2; + pt_m2 = pt_m1; + pt_m1 = pt; + pt = pt_p1; + pt_p1 = pt_p2; + pt_p2 = pt_p3; + pt_p3 = pt_p4; + + if (idx + 5 < num_sequences) { + pt_p4 = initial_sequence(idx + 5); + } else { + // Return the last point + pt_p4 = initial_sequence(num_sequences); + } + } + }; + + // Filter trajectories + const models::ControlSequence initial_control_sequence = control_sequence; + applyFilterOverAxis( + control_sequence.vx, initial_control_sequence.vx, control_history[0].vx, + control_history[1].vx, control_history[2].vx, control_history[3].vx); + applyFilterOverAxis( + control_sequence.vy, initial_control_sequence.vy, control_history[0].vy, + control_history[1].vy, control_history[2].vy, control_history[3].vy); + applyFilterOverAxis( + control_sequence.wz, initial_control_sequence.wz, control_history[0].wz, + control_history[1].wz, control_history[2].wz, control_history[3].wz); + + // Update control history + unsigned int offset = settings.shift_control_sequence ? 1 : 0; + control_history[0] = control_history[1]; + control_history[1] = control_history[2]; + control_history[2] = control_history[3]; + control_history[3] = { + control_sequence.vx(offset), + control_sequence.vy(offset), + control_sequence.wz(offset)}; +} + +/** + * @brief Find the iterator of the first pose at which there is an inversion or in-place rotation in the path + * @param path to check for inversion or rotation + * @param rotation_threshold Minimum rotation angle to consider an in-place rotation (0 to disable rotation check) + * @return the first point after the inversion or rotation found in the path + */ +inline unsigned int findFirstPathInversion( + nav_msgs::msg::Path & path, + float rotation_threshold = 0.0f) +{ + if (path.poses.size() < 2) { + return path.poses.size(); + } + + unsigned int first_after_constraint = path.poses.size(); + + // Check for in-place rotation first (if enabled) + if (rotation_threshold > 0.0f) { + for (unsigned int idx = 0; idx < path.poses.size() - 1; ++idx) { + float dx = path.poses[idx + 1].pose.position.x - path.poses[idx].pose.position.x; + float dy = path.poses[idx + 1].pose.position.y - path.poses[idx].pose.position.y; + float translation = sqrtf(dx * dx + dy * dy); + + // Check if poses are at roughly the same location + if (translation < 1e-3) { + float yaw1 = tf2::getYaw(path.poses[idx].pose.orientation); + float yaw2 = tf2::getYaw(path.poses[idx + 1].pose.orientation); + float rotation = fabs(angles::shortest_angular_distance(yaw1, yaw2)); + + // Check if this meets the minimum rotation threshold + if (rotation > rotation_threshold) { + // Found start of in-place rotation, now find where it ends + unsigned int end_idx = idx + 1; + + // Continue while we have minimal translation (still rotating in place) + while (end_idx < path.poses.size() - 1) { + float next_dx = path.poses[end_idx + 1].pose.position.x - + path.poses[end_idx].pose.position.x; + float next_dy = path.poses[end_idx + 1].pose.position.y - + path.poses[end_idx].pose.position.y; + float next_translation = sqrtf(next_dx * next_dx + next_dy * next_dy); + + if (next_translation >= 1e-3) { + break; // End of in-place rotation sequence + } + end_idx++; + } + + first_after_constraint = end_idx; + break; + } + } + } + } + + // Check for inversion (at least 3 poses needed) + if (path.poses.size() >= 3) { + for (unsigned int idx = 1; idx < path.poses.size() - 1; ++idx) { + float oa_x = path.poses[idx].pose.position.x - + path.poses[idx - 1].pose.position.x; + float oa_y = path.poses[idx].pose.position.y - + path.poses[idx - 1].pose.position.y; + float ab_x = path.poses[idx + 1].pose.position.x - + path.poses[idx].pose.position.x; + float ab_y = path.poses[idx + 1].pose.position.y - + path.poses[idx].pose.position.y; + + float dot_product = (oa_x * ab_x) + (oa_y * ab_y); + + float norm_oa = hypotf(oa_x, oa_y); + float norm_ab = hypotf(ab_x, ab_y); + + if (norm_oa < 1e-4f || norm_ab < 1e-4f) { + continue; + } + + float cos_ang = dot_product / (norm_oa * norm_ab); + cos_ang = std::max(-1.0f, std::min(1.0f, cos_ang)); + float angle = acosf(cos_ang); // [0, pi] + + if (rotation_threshold > 0.0f) { + if (angle > rotation_threshold) { + first_after_constraint = std::min(first_after_constraint, idx + 1); + break; + } + } else { + if (dot_product < 0.0f) { + first_after_constraint = std::min(first_after_constraint, idx + 1); + break; + } + } + } + } + + return first_after_constraint; +} + +/** + * @brief Find and remove poses after the first inversion or in-place rotation in the path + * @param path Path to check for inversion or rotation + * @param rotation_threshold Minimum rotation angle to consider an in-place rotation (0 to disable rotation check) + * @return The location of the inversion/rotation, return 0 if none exist + */ +inline unsigned int removePosesAfterFirstInversion( + nav_msgs::msg::Path & path, + float rotation_threshold = 0.0f) +{ + nav_msgs::msg::Path cropped_path = path; + + const unsigned int first_after_constraint = findFirstPathInversion(cropped_path, rotation_threshold); + + if (first_after_constraint == path.poses.size()) { + return 0u; // No constraint found + } + + cropped_path.poses.erase( + cropped_path.poses.begin() + first_after_constraint, cropped_path.poses.end()); + path = cropped_path; + return first_after_constraint; +} + +/** + * @brief Compare to trajectory points to find closest path point along integrated distances + * @param vec Vect to check + * @return dist Distance to look for + * @return init Starting index to indec from + */ +inline unsigned int findClosestPathPt( + const std::vector & vec, const float dist, const unsigned int init = 0u) +{ + float distim1 = init != 0u ? vec[init] : 0.0f; // First is 0, no accumulated distance yet + float disti = 0.0f; + const unsigned int size = vec.size(); + for (unsigned int i = init + 1; i != size; i++) { + disti = vec[i]; + if (disti > dist) { + if (i > 0 && dist - distim1 < disti - dist) { + return i - 1; + } + return i; + } + distim1 = disti; + } + return size - 1; +} + +// A struct to hold pose data in floating point resolution +struct Pose2D +{ + float x, y, theta; +}; + +/** + * @brief Shift the columns of a 2D Eigen Array or scalar values of + * 1D Eigen Array by 1 place. + * @param e Eigen Array + * @param direction direction in which Array will be shifted. + * 1 for shift in right direction and -1 for left direction. + */ +inline void shiftColumnsByOnePlace(Eigen::Ref e, int direction) +{ + int size = e.size(); + if(size == 1) {return;} + if(abs(direction) != 1) { + throw std::logic_error("Invalid direction, only 1 and -1 are valid values."); + } + + if((e.cols() == 1 || e.rows() == 1) && size > 1) { + auto start_ptr = direction == 1 ? e.data() + size - 2 : e.data() + 1; + auto end_ptr = direction == 1 ? e.data() : e.data() + size - 1; + while(start_ptr != end_ptr) { + *(start_ptr + direction) = *start_ptr; + start_ptr -= direction; + } + *(start_ptr + direction) = *start_ptr; + } else { + auto start_ptr = direction == 1 ? e.data() + size - 2 * e.rows() : e.data() + e.rows(); + auto end_ptr = direction == 1 ? e.data() : e.data() + size - e.rows(); + auto span = e.rows(); + while(start_ptr != end_ptr) { + std::copy(start_ptr, start_ptr + span, start_ptr + direction * span); + start_ptr -= (direction * span); + } + std::copy(start_ptr, start_ptr + span, start_ptr + direction * span); + } +} + +/** + * @brief Normalize the yaws between points on the basis of final yaw angle + * of the trajectory. + * @param last_yaws Final yaw angles of the trajectories. + * @param yaw_between_points Yaw angles calculated between x and y coordinates of the trajectories. + * @return Normalized yaw between points. + */ +inline auto normalize_yaws_between_points( + const Eigen::Ref & last_yaws, + const Eigen::Ref & yaw_between_points) +{ + Eigen::ArrayXf yaws = utils::shortest_angular_distance( + last_yaws, yaw_between_points).abs(); + int size = yaws.size(); + Eigen::ArrayXf yaws_between_points_corrected(size); + for(int i = 0; i != size; i++) { + const float & yaw_between_point = yaw_between_points[i]; + yaws_between_points_corrected[i] = yaws[i] < M_PIF_2 ? + yaw_between_point : angles::normalize_angle(yaw_between_point + M_PIF); + } + return yaws_between_points_corrected; +} + +/** + * @brief Normalize the yaws between points on the basis of goal angle. + * @param goal_yaw Goal yaw angle. + * @param yaw_between_points Yaw angles calculated between x and y coordinates of the trajectories. + * @return Normalized yaw between points + */ +inline auto normalize_yaws_between_points( + const float goal_yaw, const Eigen::Ref & yaw_between_points) +{ + int size = yaw_between_points.size(); + Eigen::ArrayXf yaws_between_points_corrected(size); + for(int i = 0; i != size; i++) { + const float & yaw_between_point = yaw_between_points[i]; + yaws_between_points_corrected[i] = fabs( + angles::normalize_angle(yaw_between_point - goal_yaw)) < M_PIF_2 ? + yaw_between_point : angles::normalize_angle(yaw_between_point + M_PIF); + } + return yaws_between_points_corrected; +} + +/** + * @brief Clamps the input between the given lower and upper bounds. + * @param lower_bound Lower bound. + * @param upper_bound Upper bound. + * @return Clamped output. + */ +inline float clamp( + const float lower_bound, const float upper_bound, const float input) +{ + return std::min(upper_bound, std::max(input, lower_bound)); +} + +} // namespace mppi::utils + +#endif // NAV2_MPPI_CONTROLLER__TOOLS__UTILS_HPP_ diff --git a/nav2_mppi_controller/src/collision_checker.cpp b/nav2_mppi_controller/src/collision_checker.cpp new file mode 100644 index 00000000000..351bc06a575 --- /dev/null +++ b/nav2_mppi_controller/src/collision_checker.cpp @@ -0,0 +1,394 @@ +// Copyright (c) 2025, Angsa Robotics GmbH +// +// 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. Reserved. + +#include +#include + +#include "nav2_mppi_controller/collision_checker.hpp" +#include "nav2_costmap_2d/inflation_layer.hpp" + + +namespace nav2_mppi_controller +{ + +MPPICollisionChecker::MPPICollisionChecker( + std::shared_ptr costmap_ros, + rclcpp_lifecycle::LifecycleNode::SharedPtr node) +: FootprintCollisionChecker(costmap_ros ? costmap_ros->getCostmap() : nullptr) +{ + if (node) { + clock_ = node->get_clock(); + logger_ = node->get_logger(); + } + + if (costmap_ros) { + costmap_ros_ = costmap_ros; + } +} + +void MPPICollisionChecker::setFootprint( + const nav2_costmap_2d::Footprint & footprint, + const bool & radius, + const std::string & inflation_layer_name) +{ + // Calculate the circumscribed cost automatically + possible_collision_cost_ = findCircumscribedCost(inflation_layer_name); + + if (possible_collision_cost_ <= 0.0f) { + RCLCPP_ERROR_THROTTLE( + logger_, *clock_, 1000, + "Inflation layer either not found or inflation is not set sufficiently for " + "optimized non-circular collision checking capabilities. It is HIGHLY recommended to set" + " the inflation radius to be at MINIMUM half of the robot's largest cross-section. See " + "github.com/ros-planning/navigation2/tree/main/nav2_mppi_controller#potential-fields" + " for full instructions. This will substantially impact run-time performance."); + } + + footprint_is_radius_ = radius; + + // Use radius, no caching required + if (radius) { + return; + } + + // Store the unoriented footprint for on-the-fly transformation + unoriented_footprint_ = footprint; +} + +CollisionResult MPPICollisionChecker::inCollision( + const std::vector & x, + const std::vector & y, + const std::vector & yaw, + const bool & traverse_unknown) +{ + CollisionResult result; + result.in_collision = false; + + // Check if all vectors have the same size + if (x.size() != y.size() || x.size() != yaw.size()) { + RCLCPP_ERROR(logger_, "Unexpected size of input vectors for collision checking"); + result.in_collision = true; + return result; + } + + // Initialize result vectors + result.center_cost.resize(x.size(), -1.0f); + result.footprint_cost.resize(x.size(), -1.0f); + + // Step 1: Check all poses for bounds and get center costs + std::vector needs_footprint_check(x.size(), false); + + for (size_t i = 0; i < x.size(); ++i) { + // Check to make sure cell is inside the map + if (outsideRange(costmap_->getSizeInCellsX(), x[i]) || + outsideRange(costmap_->getSizeInCellsY(), y[i])) + { + result.in_collision = true; + return result; + } + + // Get center cost for this pose + float current_center_cost = static_cast(costmap_->getCost( + static_cast(x[i] + 0.5f), static_cast(y[i] + 0.5f))); + + result.center_cost[i] = current_center_cost; + + if (current_center_cost == nav2_costmap_2d::NO_INFORMATION && !traverse_unknown) { + result.in_collision = true; + return result; + } + + if (current_center_cost >= nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE) { + result.in_collision = true; + return result; + } + + // For footprint-based collision checking, mark poses that need further checking + if (!footprint_is_radius_) { + // Skip if center cost is below collision threshold + if (current_center_cost >= possible_collision_cost_ || possible_collision_cost_ <= 0.0f) { + needs_footprint_check[i] = true; + } + } + } + + if (footprint_is_radius_) { + return result; // No further checks needed for radius-based checking + } + + // Step 2: If using footprint, check footprint costs for poses that need it + std::vector needs_swept_area_check(x.size(), false); + // Cache computed footprints to avoid recomputation in Step 4 + std::vector cached_footprints(x.size()); + + for (size_t i = 0; i < x.size(); ++i) { + // Skip poses that don't need footprint checking + if (!needs_footprint_check[i]) { + continue; + } + + // Transform footprint to current orientation and world position + double wx, wy; + costmap_->mapToWorld(static_cast(x[i]), static_cast(y[i]), wx, wy); + + // Transform the footprint to the given orientation + nav2_costmap_2d::Footprint oriented_footprint = transformFootprint(unoriented_footprint_, + yaw[i]); + + // Translate to world position + nav2_costmap_2d::Footprint current_footprint; + current_footprint.reserve(oriented_footprint.size()); + geometry_msgs::msg::Point new_pt; + for (unsigned int j = 0; j < oriented_footprint.size(); ++j) { + new_pt.x = wx + oriented_footprint[j].x; + new_pt.y = wy + oriented_footprint[j].y; + current_footprint.push_back(new_pt); + } + + // Cache the computed footprint for potential reuse in Step 4 + cached_footprints[i] = current_footprint; + + // Check footprint perimeter + float footprint_cost = static_cast(footprintCost(current_footprint, false)); + + // Store footprint cost in result + result.footprint_cost[i] = footprint_cost; + + if (footprint_cost == nav2_costmap_2d::NO_INFORMATION && !traverse_unknown) { + result.in_collision = true; + return result; + } + + if (footprint_cost >= nav2_costmap_2d::LETHAL_OBSTACLE) { + result.in_collision = true; + return result; + } + + // Mark for swept area checking if footprint cost is INSCRIBED_INFLATED_OBSTACLE + if (footprint_cost == nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE) { + needs_swept_area_check[i] = true; + } + } + + // Step 3: Check swept area for consecutive poses with footprint cost INSCRIBED_INFLATED_OBSTACLE + // Find consecutive sequences of poses that need swept area checking + std::vector> consecutive_sequences; + std::vector current_sequence; + + for (size_t i = 0; i < x.size(); ++i) { + if (needs_swept_area_check[i]) { + current_sequence.push_back(i); + // Limit sequence to maximum of 5 poses to avoid an unprecise swept area check + if (current_sequence.size() >= 5) { + consecutive_sequences.push_back(current_sequence); + current_sequence.clear(); + } + } else { + if (!current_sequence.empty()) { + // Only add sequences with more than one pose + if (current_sequence.size() > 1) { + consecutive_sequences.push_back(current_sequence); + } + current_sequence.clear(); + } + } + } + + // Don't forget the last sequence if it ends at the last pose + if (!current_sequence.empty()) { + // Only add sequences with more than one pose + if (current_sequence.size() > 1) { + consecutive_sequences.push_back(current_sequence); + } + } + + // Step 4: Check swept area using convex hull for each consecutive sequence + for (const auto & sequence : consecutive_sequences) { + // Collect all footprint points from consecutive poses using cached footprints + std::vector all_points; + + for (size_t seq_idx : sequence) { + // Use cached footprint instead of recomputing + const nav2_costmap_2d::Footprint & current_footprint = cached_footprints[seq_idx]; + + for (const auto & footprint_pt : current_footprint) { + all_points.push_back(footprint_pt); + } + } + + // Create convex hull from all collected points + nav2_costmap_2d::Footprint convex_hull = createConvexHull(all_points); + + // Check swept area cost using full area checking + float swept_area_cost = static_cast(footprintCost(convex_hull, true)); + + if (swept_area_cost == nav2_costmap_2d::NO_INFORMATION && !traverse_unknown) { + result.in_collision = true; + return result; + } + + if (swept_area_cost >= nav2_costmap_2d::LETHAL_OBSTACLE) { + result.in_collision = true; + return result; + } + } + + return result; +} + +nav2_costmap_2d::Footprint MPPICollisionChecker::createConvexHull( + const std::vector & points) +{ + if (points.size() <= 1) { + return points; + } + + // Create a copy of points for sorting + std::vector sorted_points = points; + + // Sort points lexicographically (first by x, then by y) + std::sort(sorted_points.begin(), sorted_points.end(), + [](const geometry_msgs::msg::Point & a, const geometry_msgs::msg::Point & b) { + return a.x < b.x || (a.x == b.x && a.y < b.y); + }); + + // Remove duplicate points + sorted_points.erase( + std::unique(sorted_points.begin(), sorted_points.end(), + [](const geometry_msgs::msg::Point & a, const geometry_msgs::msg::Point & b) { + return std::abs(a.x - b.x) < 1e-9 && std::abs(a.y - b.y) < 1e-9; + }), + sorted_points.end()); + + if (sorted_points.size() <= 1) { + return sorted_points; + } + + // Andrew's monotone chain algorithm + std::vector hull; + + // Helper function to compute cross product + auto cross = [](const geometry_msgs::msg::Point & O, + const geometry_msgs::msg::Point & A, + const geometry_msgs::msg::Point & B) { + return (A.x - O.x) * (B.y - O.y) - (A.y - O.y) * (B.x - O.x); + }; + + // Build lower hull + for (size_t i = 0; i < sorted_points.size(); ++i) { + while (hull.size() >= 2 && cross(hull[hull.size() - 2], hull[hull.size() - 1], + sorted_points[i]) <= 0) + { + hull.pop_back(); + } + hull.push_back(sorted_points[i]); + } + + // Build upper hull + size_t t = hull.size() + 1; + for (int i = static_cast(sorted_points.size()) - 2; i >= 0; --i) { + while (hull.size() >= t && cross(hull[hull.size() - 2], hull[hull.size() - 1], + sorted_points[i]) <= 0) + { + hull.pop_back(); + } + hull.push_back(sorted_points[i]); + } + + // Remove last point because it's the same as the first one + if (hull.size() > 1) { + hull.pop_back(); + } + + return hull; +} + +bool MPPICollisionChecker::outsideRange(const unsigned int & max, const float & value) const +{ + return value < 0.0f || value > max; +} + +nav2_costmap_2d::Footprint MPPICollisionChecker::transformFootprint( + const nav2_costmap_2d::Footprint & footprint, + float yaw) const +{ + double sin_th = sin(yaw); + double cos_th = cos(yaw); + nav2_costmap_2d::Footprint oriented_footprint; + oriented_footprint.reserve(footprint.size()); + + geometry_msgs::msg::Point new_pt; + for (unsigned int i = 0; i < footprint.size(); ++i) { + new_pt.x = footprint[i].x * cos_th - footprint[i].y * sin_th; + new_pt.y = footprint[i].x * sin_th + footprint[i].y * cos_th; + oriented_footprint.push_back(new_pt); + } + + return oriented_footprint; +} + +float MPPICollisionChecker::findCircumscribedCost(const std::string & inflation_layer_name) +{ + if (!costmap_ros_) { + RCLCPP_ERROR(logger_, "Costmap ROS is not available for circumscribed cost calculation"); + return -1.0f; + } + + double result = -1.0; + const double circum_radius = costmap_ros_->getLayeredCostmap()->getCircumscribedRadius(); + + if (static_cast(circum_radius) == circumscribed_radius_) { + // early return if footprint size is unchanged + return circumscribed_cost_; + } + + // check if the costmap has an inflation layer + const auto inflation_layer = nav2_costmap_2d::InflationLayer::getInflationLayer( + costmap_ros_, + inflation_layer_name); + + if (inflation_layer != nullptr) { + const double resolution = costmap_ros_->getCostmap()->getResolution(); + double inflation_radius = inflation_layer->getInflationRadius(); + + if (inflation_radius < circum_radius) { + RCLCPP_ERROR( + logger_, + "The inflation radius (%f) is smaller than the circumscribed radius (%f) " + "If this is an SE2-collision checking plugin, it cannot use costmap potential " + "field to speed up collision checking by only checking the full footprint " + "when robot is within possibly-inscribed radius of an obstacle. This may " + "significantly slow down planning times!", + inflation_radius, circum_radius); + result = 0.0; + } else { + result = inflation_layer->computeCost(circum_radius / resolution); + } + } else { + RCLCPP_WARN( + logger_, + "No inflation layer found in costmap configuration. " + "If this is an SE2-collision checking plugin, it cannot use costmap potential " + "field to speed up collision checking by only checking the full footprint " + "when robot is within possibly-inscribed radius of an obstacle. This may " + "significantly slow down planning times and not avoid anything but absolute collisions!"); + } + + circumscribed_radius_ = static_cast(circum_radius); + circumscribed_cost_ = static_cast(result); + + return circumscribed_cost_; +} + +} // namespace nav2_mppi_controller \ No newline at end of file diff --git a/nav2_mppi_controller/src/controller copy 2.cpp b/nav2_mppi_controller/src/controller copy 2.cpp new file mode 100644 index 00000000000..a9fab10c85e --- /dev/null +++ b/nav2_mppi_controller/src/controller copy 2.cpp @@ -0,0 +1,157 @@ +// 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 +#include +#include "nav2_mppi_controller/controller.hpp" +#include "nav2_mppi_controller/tools/utils.hpp" + +// #define BENCHMARK_TESTING + +namespace nav2_mppi_controller +{ + +void MPPIController::configure( + const nav2::LifecycleNode::WeakPtr & parent, + std::string name, const std::shared_ptr tf, + const std::shared_ptr costmap_ros) +{ + parent_ = parent; + costmap_ros_ = costmap_ros; + tf_buffer_ = tf; + name_ = name; + parameters_handler_ = std::make_unique(parent, name_); + + auto node = parent_.lock(); + // Get high-level controller parameters + auto getParam = parameters_handler_->getParamGetter(name_); + getParam(visualize_, "visualize", false); + + getParam(publish_optimal_trajectory_, "publish_optimal_trajectory", false); + + // Configure composed objects + optimizer_.initialize(parent_, name_, costmap_ros_, tf_buffer_, parameters_handler_.get()); + path_handler_.initialize(parent_, name_, costmap_ros_, tf_buffer_, parameters_handler_.get()); + trajectory_visualizer_.on_configure( + parent_, name_, + costmap_ros_->getGlobalFrameID(), parameters_handler_.get()); + + if (publish_optimal_trajectory_) { + opt_traj_pub_ = node->create_publisher( + "~/optimal_trajectory"); + } + + RCLCPP_INFO(logger_, "Configured MPPI Controller: %s", name_.c_str()); +} + +void MPPIController::cleanup() +{ + optimizer_.shutdown(); + trajectory_visualizer_.on_cleanup(); + parameters_handler_.reset(); + opt_traj_pub_.reset(); + RCLCPP_INFO(logger_, "Cleaned up MPPI Controller: %s", name_.c_str()); +} + +void MPPIController::activate() +{ + auto node = parent_.lock(); + trajectory_visualizer_.on_activate(); + parameters_handler_->start(); + if (opt_traj_pub_) { + opt_traj_pub_->on_activate(); + } + RCLCPP_INFO(logger_, "Activated MPPI Controller: %s", name_.c_str()); +} + +void MPPIController::deactivate() +{ + trajectory_visualizer_.on_deactivate(); + if (opt_traj_pub_) { + opt_traj_pub_->on_deactivate(); + } + RCLCPP_INFO(logger_, "Deactivated MPPI Controller: %s", name_.c_str()); +} + +void MPPIController::reset() +{ + optimizer_.reset(false /*Don't reset zone-based speed limits between requests*/); +} + +geometry_msgs::msg::TwistStamped MPPIController::computeVelocityCommands( + const geometry_msgs::msg::PoseStamped & robot_pose, + const geometry_msgs::msg::Twist & robot_speed, + nav2_core::GoalChecker * goal_checker) +{ +#ifdef BENCHMARK_TESTING + auto start = std::chrono::system_clock::now(); +#endif + + std::lock_guard param_lock(*parameters_handler_->getLock()); + geometry_msgs::msg::Pose goal = path_handler_.getTransformedGoal(robot_pose.header.stamp).pose; + + nav_msgs::msg::Path transformed_plan = path_handler_.transformPath(robot_pose); + + nav2_costmap_2d::Costmap2D * costmap = costmap_ros_->getCostmap(); + std::unique_lock costmap_lock(*(costmap->getMutex())); + + auto [cmd, optimal_trajectory] = + optimizer_.evalControl(robot_pose, robot_speed, transformed_plan, goal, goal_checker); + +#ifdef BENCHMARK_TESTING + auto end = std::chrono::system_clock::now(); + auto duration = std::chrono::duration_cast(end - start).count(); + RCLCPP_INFO(logger_, "Control loop execution time: %ld [ms]", duration); +#endif + + if (publish_optimal_trajectory_ && opt_traj_pub_->get_subscription_count() > 0) { + auto trajectory_msg = utils::toTrajectoryMsg( + optimal_trajectory, + optimizer_.getOptimalControlSequence(), + optimizer_.getSettings().model_dt, + cmd.header); + opt_traj_pub_->publish(std::move(trajectory_msg)); + } + + if (visualize_) { + visualize(std::move(transformed_plan), cmd.header.stamp, optimal_trajectory); + } + + return cmd; +} + +void MPPIController::visualize( + nav_msgs::msg::Path transformed_plan, + const builtin_interfaces::msg::Time & cmd_stamp, + const Eigen::ArrayXXf & optimal_trajectory) +{ + trajectory_visualizer_.add(optimizer_.getGeneratedTrajectories(), "Candidate Trajectories"); + trajectory_visualizer_.add(optimal_trajectory, "Optimal Trajectory", cmd_stamp); + trajectory_visualizer_.visualize(std::move(transformed_plan)); +} + +void MPPIController::setPlan(const nav_msgs::msg::Path & path) +{ + path_handler_.setPath(path); +} + +void MPPIController::setSpeedLimit(const double & speed_limit, const bool & percentage) +{ + optimizer_.setSpeedLimit(speed_limit, percentage); +} + +} // namespace nav2_mppi_controller + +#include "pluginlib/class_list_macros.hpp" +PLUGINLIB_EXPORT_CLASS(nav2_mppi_controller::MPPIController, nav2_core::Controller) diff --git a/nav2_mppi_controller/src/controller.cpp b/nav2_mppi_controller/src/controller.cpp index a9fab10c85e..1988b65d6c1 100644 --- a/nav2_mppi_controller/src/controller.cpp +++ b/nav2_mppi_controller/src/controller.cpp @@ -16,8 +16,9 @@ #include #include "nav2_mppi_controller/controller.hpp" #include "nav2_mppi_controller/tools/utils.hpp" - -// #define BENCHMARK_TESTING +#include "nav2_costmap_2d/footprint.hpp" +#include "visualization_msgs/msg/marker_array.hpp" +#include "std_msgs/msg/header.hpp" namespace nav2_mppi_controller { @@ -36,9 +37,6 @@ void MPPIController::configure( auto node = parent_.lock(); // Get high-level controller parameters auto getParam = parameters_handler_->getParamGetter(name_); - getParam(visualize_, "visualize", false); - - getParam(publish_optimal_trajectory_, "publish_optimal_trajectory", false); // Configure composed objects optimizer_.initialize(parent_, name_, costmap_ros_, tf_buffer_, parameters_handler_.get()); @@ -47,11 +45,6 @@ void MPPIController::configure( parent_, name_, costmap_ros_->getGlobalFrameID(), parameters_handler_.get()); - if (publish_optimal_trajectory_) { - opt_traj_pub_ = node->create_publisher( - "~/optimal_trajectory"); - } - RCLCPP_INFO(logger_, "Configured MPPI Controller: %s", name_.c_str()); } @@ -60,7 +53,6 @@ void MPPIController::cleanup() optimizer_.shutdown(); trajectory_visualizer_.on_cleanup(); parameters_handler_.reset(); - opt_traj_pub_.reset(); RCLCPP_INFO(logger_, "Cleaned up MPPI Controller: %s", name_.c_str()); } @@ -69,18 +61,12 @@ void MPPIController::activate() auto node = parent_.lock(); trajectory_visualizer_.on_activate(); parameters_handler_->start(); - if (opt_traj_pub_) { - opt_traj_pub_->on_activate(); - } RCLCPP_INFO(logger_, "Activated MPPI Controller: %s", name_.c_str()); } void MPPIController::deactivate() { trajectory_visualizer_.on_deactivate(); - if (opt_traj_pub_) { - opt_traj_pub_->on_deactivate(); - } RCLCPP_INFO(logger_, "Deactivated MPPI Controller: %s", name_.c_str()); } @@ -94,9 +80,9 @@ geometry_msgs::msg::TwistStamped MPPIController::computeVelocityCommands( const geometry_msgs::msg::Twist & robot_speed, nav2_core::GoalChecker * goal_checker) { -#ifdef BENCHMARK_TESTING + #ifdef BENCHMARK_TESTING auto start = std::chrono::system_clock::now(); -#endif + #endif std::lock_guard param_lock(*parameters_handler_->getLock()); geometry_msgs::msg::Pose goal = path_handler_.getTransformedGoal(robot_pose.header.stamp).pose; @@ -109,38 +95,28 @@ geometry_msgs::msg::TwistStamped MPPIController::computeVelocityCommands( auto [cmd, optimal_trajectory] = optimizer_.evalControl(robot_pose, robot_speed, transformed_plan, goal, goal_checker); -#ifdef BENCHMARK_TESTING + + #ifdef BENCHMARK_TESTING auto end = std::chrono::system_clock::now(); auto duration = std::chrono::duration_cast(end - start).count(); RCLCPP_INFO(logger_, "Control loop execution time: %ld [ms]", duration); -#endif - - if (publish_optimal_trajectory_ && opt_traj_pub_->get_subscription_count() > 0) { - auto trajectory_msg = utils::toTrajectoryMsg( - optimal_trajectory, - optimizer_.getOptimalControlSequence(), - optimizer_.getSettings().model_dt, - cmd.header); - opt_traj_pub_->publish(std::move(trajectory_msg)); - } - - if (visualize_) { - visualize(std::move(transformed_plan), cmd.header.stamp, optimal_trajectory); - } + #endif + + trajectory_visualizer_.visualize( + std::move(transformed_plan), + optimal_trajectory, + optimizer_.getOptimalControlSequence(), + optimizer_.getSettings().model_dt, + cmd.header.stamp, + costmap_ros_, + optimizer_.getGeneratedTrajectories(), + optimizer_.getCosts(), + optimizer_.getCriticCosts(), + optimizer_.getTrajectoriesInCollision()); return cmd; } -void MPPIController::visualize( - nav_msgs::msg::Path transformed_plan, - const builtin_interfaces::msg::Time & cmd_stamp, - const Eigen::ArrayXXf & optimal_trajectory) -{ - trajectory_visualizer_.add(optimizer_.getGeneratedTrajectories(), "Candidate Trajectories"); - trajectory_visualizer_.add(optimal_trajectory, "Optimal Trajectory", cmd_stamp); - trajectory_visualizer_.visualize(std::move(transformed_plan)); -} - void MPPIController::setPlan(const nav_msgs::msg::Path & path) { path_handler_.setPath(path); @@ -154,4 +130,4 @@ void MPPIController::setSpeedLimit(const double & speed_limit, const bool & perc } // namespace nav2_mppi_controller #include "pluginlib/class_list_macros.hpp" -PLUGINLIB_EXPORT_CLASS(nav2_mppi_controller::MPPIController, nav2_core::Controller) +PLUGINLIB_EXPORT_CLASS(nav2_mppi_controller::MPPIController, nav2_core::Controller) \ No newline at end of file diff --git a/nav2_mppi_controller/src/critic_manager.cpp b/nav2_mppi_controller/src/critic_manager.cpp index 305140e2e13..aaa5790548c 100644 --- a/nav2_mppi_controller/src/critic_manager.cpp +++ b/nav2_mppi_controller/src/critic_manager.cpp @@ -37,7 +37,11 @@ void CriticManager::getParams() auto node = parent_.lock(); auto getParam = parameters_handler_->getParamGetter(name_); getParam(critic_names_, "critics", std::vector{}, ParameterType::Static); - getParam(publish_critics_stats_, "publish_critics_stats", false, ParameterType::Static); + // Read visualization parameters from Visualization namespace + auto getVisualizerParam = parameters_handler_->getParamGetter(name_ + ".Visualization"); + getVisualizerParam(publish_critics_stats_, "publish_critics_stats", false, ParameterType::Static); + getVisualizerParam(visualize_per_critic_costs_, "publish_trajectories_with_individual_cost", + false, ParameterType::Static); } void CriticManager::loadCritics() @@ -83,6 +87,15 @@ void CriticManager::evalTrajectoriesScores( stats_msg->costs_sum.reserve(critics_.size()); } + // Initialize per-critic costs tracking only if requested + if (visualize_per_critic_costs_) { + if (!data.individual_critics_cost) { + data.individual_critics_cost = std::vector>(); + } + data.individual_critics_cost->clear(); + data.individual_critics_cost->reserve(critics_.size()); + } + for (size_t i = 0; i < critics_.size(); ++i) { if (data.fail_flag) { break; @@ -90,21 +103,30 @@ void CriticManager::evalTrajectoriesScores( // Store costs before critic evaluation Eigen::ArrayXf costs_before; - if (publish_critics_stats_) { + if (visualize_per_critic_costs_ || publish_critics_stats_) { costs_before = data.costs; } critics_[i]->score(data); - // Calculate statistics if publishing is enabled - if (publish_critics_stats_) { - stats_msg->critics.push_back(critic_names_[i]); + // Calculate cost contribution from this critic + if (visualize_per_critic_costs_ || publish_critics_stats_) { // Calculate sum of costs added by this individual critic Eigen::ArrayXf cost_diff = data.costs - costs_before; - float costs_sum = cost_diff.sum(); - stats_msg->costs_sum.push_back(costs_sum); - stats_msg->changed.push_back(costs_sum != 0.0f); + if (visualize_per_critic_costs_) { + data.individual_critics_cost->emplace_back(critic_names_[i], cost_diff); + } + + // Calculate statistics if publishing is enabled + if (publish_critics_stats_) { + stats_msg->critics.push_back(critic_names_[i]); + + // Calculate sum of costs added by this individual critic + float costs_sum = cost_diff.sum(); + stats_msg->costs_sum.push_back(costs_sum); + stats_msg->changed.push_back(costs_sum != 0.0f); + } } } diff --git a/nav2_mppi_controller/src/critics/cost_critic copy 2.cpp b/nav2_mppi_controller/src/critics/cost_critic copy 2.cpp new file mode 100644 index 00000000000..2ab49793515 --- /dev/null +++ b/nav2_mppi_controller/src/critics/cost_critic copy 2.cpp @@ -0,0 +1,284 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// Copyright (c) 2023 Open Navigation LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include "nav2_mppi_controller/critics/cost_critic.hpp" +#include "nav2_core/controller_exceptions.hpp" +#include +#include + +namespace mppi::critics +{ + +void CostCritic::initialize() +{ + auto getParentParam = parameters_handler_->getParamGetter(parent_name_); + auto getParam = parameters_handler_->getParamGetter(name_); + getParam(consider_footprint_, "consider_footprint", false); + getParam(power_, "cost_power", 1); + getParam(weight_, "cost_weight", 3.81f); + getParam(critical_cost_, "critical_cost", 300.0f); + getParam(near_collision_cost_, "near_collision_cost", 253); + getParam(collision_cost_, "collision_cost", 1000000.0f); + getParam(near_goal_distance_, "near_goal_distance", 0.5f); + getParam(inflation_layer_name_, "inflation_layer_name", std::string("")); + getParam(trajectory_point_step_, "trajectory_point_step", 2); + getParam(precollision_lookahead_time_, "precollision_lookahead_time", 1.0f); + getParam(precollision_penalty_, "precollision_penalty", 100.0f); + + // Normalized by cost value to put in same regime as other weights + weight_ /= 254.0f; + + // Normalize weight when parameter is changed dynamically as well + auto weightDynamicCb = [&]( + const rclcpp::Parameter & weight, rcl_interfaces::msg::SetParametersResult & /*result*/) { + weight_ = weight.as_double() / 254.0f; + }; + parameters_handler_->addParamCallback(name_ + ".cost_weight", weightDynamicCb); + + collision_checker_.setCostmap(costmap_); + possible_collision_cost_ = findCircumscribedCost(costmap_ros_); + + if (possible_collision_cost_ < 1.0f) { + RCLCPP_ERROR( + logger_, + "Inflation layer either not found or inflation is not set sufficiently for " + "optimized non-circular collision checking capabilities. It is HIGHLY recommended to set" + " the inflation radius to be at MINIMUM half of the robot's largest cross-section. See " + "github.com/ros-planning/navigation2/tree/main/nav2_smac_planner#potential-fields" + " for full instructions. This will substantially impact run-time performance."); + } + + if (costmap_ros_->getUseRadius() == consider_footprint_) { + RCLCPP_WARN( + logger_, + "Inconsistent configuration in collision checking. Please verify the robot's shape settings " + "in both the costmap and the cost critic."); + if (costmap_ros_->getUseRadius()) { + throw nav2_core::ControllerException( + "Considering footprint in collision checking but no robot footprint provided in the " + "costmap."); + } + } + + if(near_collision_cost_ > 253) { + RCLCPP_WARN(logger_, "Near collision cost is set higher than INSCRIBED_INFLATED_OBSTACLE"); + } + + RCLCPP_INFO( + logger_, + "InflationCostCritic instantiated with %d power and %f / %f weights. " + "Critic will collision check based on %s cost.", + power_, critical_cost_, weight_, consider_footprint_ ? + "footprint" : "circular"); +} + +float CostCritic::findCircumscribedCost( + std::shared_ptr costmap) +{ + double result = -1.0; + const double circum_radius = costmap->getLayeredCostmap()->getCircumscribedRadius(); + if (static_cast(circum_radius) == circumscribed_radius_) { + // early return if footprint size is unchanged + return circumscribed_cost_; + } + + // check if the costmap has an inflation layer + const auto inflation_layer = nav2_costmap_2d::InflationLayer::getInflationLayer( + costmap, + inflation_layer_name_); + if (inflation_layer != nullptr) { + const double resolution = costmap->getCostmap()->getResolution(); + double inflation_radius = inflation_layer->getInflationRadius(); + if (inflation_radius < circum_radius) { + RCLCPP_ERROR( + rclcpp::get_logger("computeCircumscribedCost"), + "The inflation radius (%f) is smaller than the circumscribed radius (%f) " + "If this is an SE2-collision checking plugin, it cannot use costmap potential " + "field to speed up collision checking by only checking the full footprint " + "when robot is within possibly-inscribed radius of an obstacle. This may " + "significantly slow down planning times!", + inflation_radius, circum_radius); + result = 0.0; + return result; + } + result = inflation_layer->computeCost(circum_radius / resolution); + } else { + RCLCPP_WARN( + logger_, + "No inflation layer found in costmap configuration. " + "If this is an SE2-collision checking plugin, it cannot use costmap potential " + "field to speed up collision checking by only checking the full footprint " + "when robot is within possibly-inscribed radius of an obstacle. This may " + "significantly slow down planning times and not avoid anything but absolute collisions!"); + } + + circumscribed_radius_ = static_cast(circum_radius); + circumscribed_cost_ = static_cast(result); + + return circumscribed_cost_; +} + +void CostCritic::score(CriticData & data) +{ + if (!enabled_) { + return; + } + + // Setup cost information for various parts of the critic + is_tracking_unknown_ = costmap_ros_->getLayeredCostmap()->isTrackingUnknown(); + auto * costmap = collision_checker_.getCostmap(); + origin_x_ = static_cast(costmap->getOriginX()); + origin_y_ = static_cast(costmap->getOriginY()); + resolution_ = static_cast(costmap->getResolution()); + size_x_ = costmap->getSizeInCellsX(); + size_y_ = costmap->getSizeInCellsY(); + + if (consider_footprint_) { + // footprint may have changed since initialization if user has dynamic footprints + possible_collision_cost_ = findCircumscribedCost(costmap_ros_); + } + + // If near the goal, don't apply the preferential term since the goal is near obstacles + bool near_goal = false; + if (data.state.local_path_length < near_goal_distance_) { + near_goal = true; + } + + Eigen::ArrayXf repulsive_cost(data.costs.rows()); + repulsive_cost.setZero(); + bool all_trajectories_collide = true; + + int strided_traj_cols = floor((data.trajectories.x.cols() - 1) / trajectory_point_step_) + 1; + int strided_traj_rows = data.trajectories.x.rows(); + int outer_stride = strided_traj_rows * trajectory_point_step_; + + // Initialize trajectories collision tracking + if (!data.trajectories_in_collision) { + data.trajectories_in_collision = std::vector(strided_traj_rows, false); + } + + const auto traj_x = Eigen::Map>(data.trajectories.x.data(), strided_traj_rows, strided_traj_cols, + Eigen::Stride<-1, -1>(outer_stride, 1)); + const auto traj_y = Eigen::Map>(data.trajectories.y.data(), strided_traj_rows, strided_traj_cols, + Eigen::Stride<-1, -1>(outer_stride, 1)); + const auto traj_yaw = Eigen::Map>(data.trajectories.yaws.data(), strided_traj_rows, strided_traj_cols, + Eigen::Stride<-1, -1>(outer_stride, 1)); + + for (int i = 0; i < strided_traj_rows; ++i) { + bool trajectory_collide = false; + float pose_cost = 0.0f; + float & traj_cost = repulsive_cost(i); + + for (int j = 0; j < strided_traj_cols; j++) { + float Tx = traj_x(i, j); + float Ty = traj_y(i, j); + unsigned int x_i = 0u, y_i = 0u; + + // The getCost doesn't use orientation + // The footprintCostAtPose will always return "INSCRIBED" if footprint is over it + // So the center point has more information than the footprint + if (!worldToMapFloat(Tx, Ty, x_i, y_i)) { + pose_cost = 255.0f; // NO_INFORMATION in float + } else { + pose_cost = static_cast(costmap->getCost(getIndex(x_i, y_i))); + if (pose_cost < 1.0f) { + continue; // In free space + } + } + + if (inCollision(pose_cost, Tx, Ty, traj_yaw(i, j))) { + traj_cost = collision_cost_; + trajectory_collide = true; + break; + } + + // Let near-collision trajectory points be punished severely + // Note that we collision check based on the footprint actual, + // but score based on the center-point cost regardless + if (pose_cost >= static_cast(near_collision_cost_)) { + traj_cost += critical_cost_; + } else if (!near_goal) { // Generally prefer trajectories further from obstacles + traj_cost += pose_cost; + } + } + + if (!trajectory_collide && + precollision_lookahead_time_ > 0.0f && + precollision_penalty_ > 0.0f) + { + // how many columns ahead correspond to precollision_lookahead_time_ + const unsigned int max_preview_cols = std::min( + (unsigned int)std::ceil( + precollision_lookahead_time_ / + (data.model_dt * static_cast(trajectory_point_step_))), + (unsigned int)strided_traj_cols); + + bool collide_within_window = false; + + for (unsigned int pj = 0; pj < max_preview_cols; ++pj) { + const float Px = traj_x(i, pj); + const float Py = traj_y(i, pj); + const float Pyaw = traj_yaw(i, pj); + + const double fp_cost = collision_checker_.footprintCostAtPose( + (double)Px, + (double)Py, + (double)Pyaw, + costmap_ros_->getRobotFootprint()); + + // Jeƛli footprint w tej prĂłbce jest juĆŒ w strefie naprawdę niebezpiecznej, + // to ta trajektoria "uderzy" w przeszkodę w czasie krĂłtszym niĆŒ + // precollision_lookahead_time_. + // Uznajemy ją za zƂą. + if (fp_cost == (double)nav2_costmap_2d::LETHAL_OBSTACLE || + fp_cost == (double)nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE) + { + collide_within_window = true; + break; + } + } + + if (collide_within_window) { + // kara staƂa za to, ĆŒe kolizja jest < precollision_lookahead_time_ + repulsive_cost(i) += precollision_penalty_; + } + } + + + (*data.trajectories_in_collision)[i] = trajectory_collide; + all_trajectories_collide &= trajectory_collide; + } + + if (power_ > 1u) { + data.costs += (repulsive_cost * + (weight_ / static_cast(strided_traj_cols))).pow(power_); + } else { + data.costs += repulsive_cost * (weight_ / static_cast(strided_traj_cols)); + } + + data.fail_flag = all_trajectories_collide; +} + +} // namespace mppi::critics + +#include + +PLUGINLIB_EXPORT_CLASS( + mppi::critics::CostCritic, + mppi::critics::CriticFunction) diff --git a/nav2_mppi_controller/src/critics/cost_critic copy 3.cpp b/nav2_mppi_controller/src/critics/cost_critic copy 3.cpp new file mode 100644 index 00000000000..74ce0df31e0 --- /dev/null +++ b/nav2_mppi_controller/src/critics/cost_critic copy 3.cpp @@ -0,0 +1,297 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// Copyright (c) 2023 Open Navigation LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include "nav2_mppi_controller/critics/cost_critic.hpp" +#include "nav2_core/controller_exceptions.hpp" + +namespace mppi::critics +{ + +void CostCritic::initialize() +{ + auto getParentParam = parameters_handler_->getParamGetter(parent_name_); + auto getParam = parameters_handler_->getParamGetter(name_); + getParam(consider_footprint_, "consider_footprint", false); + getParam(power_, "cost_power", 1); + getParam(weight_, "cost_weight", 3.81f); + getParam(critical_cost_, "critical_cost", 300.0f); + getParam(near_collision_cost_, "near_collision_cost", 253); + getParam(collision_cost_, "collision_cost", 1000000.0f); + getParam(near_goal_distance_, "near_goal_distance", 0.5f); + getParam(inflation_layer_name_, "inflation_layer_name", std::string("")); + getParam(trajectory_point_step_, "trajectory_point_step", 2); + + // Normalized by cost value to put in same regime as other weights + weight_ /= 254.0f; + + // Normalize weight when parameter is changed dynamically as well + auto weightDynamicCb = [&]( + const rclcpp::Parameter & weight, rcl_interfaces::msg::SetParametersResult & /*result*/) { + weight_ = weight.as_double() / 254.0f; + }; + parameters_handler_->addParamCallback(name_ + ".cost_weight", weightDynamicCb); + + collision_checker_.setCostmap(costmap_); + possible_collision_cost_ = findCircumscribedCost(costmap_ros_); + + if (possible_collision_cost_ < 1.0f) { + RCLCPP_ERROR( + logger_, + "Inflation layer either not found or inflation is not set sufficiently for " + "optimized non-circular collision checking capabilities. It is HIGHLY recommended to set" + " the inflation radius to be at MINIMUM half of the robot's largest cross-section. See " + "github.com/ros-planning/navigation2/tree/main/nav2_smac_planner#potential-fields" + " for full instructions. This will substantially impact run-time performance."); + } + + if (costmap_ros_->getUseRadius() == consider_footprint_) { + RCLCPP_WARN( + logger_, + "Inconsistent configuration in collision checking. Please verify the robot's shape settings " + "in both the costmap and the cost critic."); + if (costmap_ros_->getUseRadius()) { + throw nav2_core::ControllerException( + "Considering footprint in collision checking but no robot footprint provided in the " + "costmap."); + } + } + + if(near_collision_cost_ > 253) { + RCLCPP_WARN(logger_, "Near collision cost is set higher than INSCRIBED_INFLATED_OBSTACLE"); + } + + RCLCPP_INFO( + logger_, + "InflationCostCritic instantiated with %d power and %f / %f weights. " + "Critic will collision check based on %s cost.", + power_, critical_cost_, weight_, consider_footprint_ ? + "footprint" : "circular"); +} + +float CostCritic::findCircumscribedCost( + std::shared_ptr costmap) +{ + double result = -1.0; + const double circum_radius = costmap->getLayeredCostmap()->getCircumscribedRadius() + 0.2; + if (static_cast(circum_radius) == circumscribed_radius_) { + // early return if footprint size is unchanged + return circumscribed_cost_; + } + + // check if the costmap has an inflation layer + const auto inflation_layer = nav2_costmap_2d::InflationLayer::getInflationLayer( + costmap, + inflation_layer_name_); + if (inflation_layer != nullptr) { + const double resolution = costmap->getCostmap()->getResolution(); + double inflation_radius = inflation_layer->getInflationRadius(); + if (inflation_radius < circum_radius) { + RCLCPP_ERROR( + rclcpp::get_logger("computeCircumscribedCost"), + "The inflation radius (%f) is smaller than the circumscribed radius (%f) " + "If this is an SE2-collision checking plugin, it cannot use costmap potential " + "field to speed up collision checking by only checking the full footprint " + "when robot is within possibly-inscribed radius of an obstacle. This may " + "significantly slow down planning times!", + inflation_radius, circum_radius); + result = 0.0; + return result; + } + result = inflation_layer->computeCost(circum_radius / resolution); + } else { + RCLCPP_WARN( + logger_, + "No inflation layer found in costmap configuration. " + "If this is an SE2-collision checking plugin, it cannot use costmap potential " + "field to speed up collision checking by only checking the full footprint " + "when robot is within possibly-inscribed radius of an obstacle. This may " + "significantly slow down planning times and not avoid anything but absolute collisions!"); + } + + circumscribed_radius_ = static_cast(circum_radius); + circumscribed_cost_ = static_cast(result); + + return circumscribed_cost_; +} + +void CostCritic::score(CriticData & data) +{ + if (!enabled_) { + return; + } + + // static bool s_footprint_active = false; + // bool any_use_footprint_this_tick = false; + + // Setup cost information for various parts of the critic + is_tracking_unknown_ = costmap_ros_->getLayeredCostmap()->isTrackingUnknown(); + auto * costmap = collision_checker_.getCostmap(); + origin_x_ = static_cast(costmap->getOriginX()); + origin_y_ = static_cast(costmap->getOriginY()); + resolution_ = static_cast(costmap->getResolution()); + size_x_ = costmap->getSizeInCellsX(); + size_y_ = costmap->getSizeInCellsY(); + + if (consider_footprint_) { + // footprint may have changed since initialization if user has dynamic footprints + possible_collision_cost_ = findCircumscribedCost(costmap_ros_); + } + + // If near the goal, don't apply the preferential term since the goal is near obstacles + bool near_goal = false; + if (data.state.local_path_length < near_goal_distance_) { + near_goal = true; + } + + Eigen::ArrayXf repulsive_cost(data.costs.rows()); + repulsive_cost.setZero(); + bool all_trajectories_collide = true; + + int strided_traj_cols = floor((data.trajectories.x.cols() - 1) / trajectory_point_step_) + 1; + int strided_traj_rows = data.trajectories.x.rows(); + int outer_stride = strided_traj_rows * trajectory_point_step_; + + // Initialize trajectories collision tracking + if (!data.trajectories_in_collision) { + data.trajectories_in_collision = std::vector(strided_traj_rows, false); + } + + const auto traj_x = Eigen::Map>(data.trajectories.x.data(), strided_traj_rows, strided_traj_cols, + Eigen::Stride<-1, -1>(outer_stride, 1)); + const auto traj_y = Eigen::Map>(data.trajectories.y.data(), strided_traj_rows, strided_traj_cols, + Eigen::Stride<-1, -1>(outer_stride, 1)); + const auto traj_yaw = Eigen::Map>(data.trajectories.yaws.data(), strided_traj_rows, strided_traj_cols, + Eigen::Stride<-1, -1>(outer_stride, 1)); + + for (int i = 0; i < strided_traj_rows; ++i) { + bool trajectory_collide = false; + float pose_cost = 0.0f; + float & traj_cost = repulsive_cost(i); + + for (int j = 0; j < strided_traj_cols; j++) { + float Tx = traj_x(i, j); + float Ty = traj_y(i, j); + unsigned int x_i = 0u, y_i = 0u; + + // The getCost doesn't use orientation + // The footprintCostAtPose will always return "INSCRIBED" if footprint is over it + // So the center point has more information than the footprint + if (!worldToMapFloat(Tx, Ty, x_i, y_i)) { + pose_cost = 0.0f; // NO_INFORMATION in float + } else { + pose_cost = static_cast(costmap->getCost(getIndex(x_i, y_i))); + if (pose_cost < 1.0f) { + continue; // In free space + } + } + int footprint_every_n_points_ = 2; + float cost_for_scoring = pose_cost; + // bool use_footprint_cost = consider_footprint_ && + // (pose_cost >= possible_collision_cost_ || possible_collision_cost_ < 1.0f); + bool use_footprint_cost = false; + if (consider_footprint_) { + const bool near_obstacle = (pose_cost >= possible_collision_cost_) || (possible_collision_cost_ < 1.0f); + bool sample_this_point = false; + if (footprint_every_n_points_ <= 0) { + sample_this_point = (j == strided_traj_cols - 1); // tylko koƄcowy punkt + } else { + sample_this_point = (j % footprint_every_n_points_ == 0) || (j == strided_traj_cols - 1); + } + use_footprint_cost = near_obstacle && sample_this_point; + } + + //any_use_footprint_this_tick = any_use_footprint_this_tick || use_footprint_cost; + + if (use_footprint_cost) { + cost_for_scoring = static_cast(collision_checker_.footprintCostAtPose( + static_cast(Tx), static_cast(Ty), static_cast(traj_yaw(i, j)), + costmap_ros_->getRobotFootprint())); + } + + // Check for collision based on the appropriate cost + bool in_collision = false; + + switch (static_cast(cost_for_scoring)) { + case (nav2_costmap_2d::LETHAL_OBSTACLE): + in_collision = true; + break; + case (nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE): + in_collision = use_footprint_cost ? false : true; + break; + case (nav2_costmap_2d::NO_INFORMATION): + in_collision = is_tracking_unknown_ ? false : true; + break; + default: + in_collision = false; + } + + + if (in_collision) { + traj_cost = collision_cost_; + trajectory_collide = true; + break; + } + + // Let near-collision trajectory points be punished severely + // Note that we collision check based on the footprint actual, + // but score based on the center-point cost regardless + if (cost_for_scoring >= near_collision_cost_ && !near_goal) { + traj_cost += critical_cost_; + } else if (!near_goal) { // Generally prefer trajectories further from obstacles + traj_cost += cost_for_scoring; + } + } + + (*data.trajectories_in_collision)[i] = trajectory_collide; + all_trajectories_collide &= trajectory_collide; + } + + if (power_ > 1u) { + data.costs += (repulsive_cost * + (weight_ / static_cast(strided_traj_cols))).pow(power_); + } else { + data.costs += repulsive_cost * (weight_ / static_cast(strided_traj_cols)); + } + + // if (any_use_footprint_this_tick != s_footprint_active) { + // if (any_use_footprint_this_tick) { + // RCLCPP_INFO(logger_, "[CostCritic] WLACZONO FOOTPRINT COST"); + // RCLCPP_INFO(logger_, + // "[CostCritic] possible_collision_cost_ = %.1f", + // possible_collision_cost_); + // } else { + // RCLCPP_INFO(logger_, "[CostCritic] WYLACZONO FOOTPRINT COST"); + // RCLCPP_INFO(logger_, + // "[CostCritic] possible_collision_cost_ = %.1f", + // possible_collision_cost_); + // } + // s_footprint_active = any_use_footprint_this_tick; + // } + + + data.fail_flag = all_trajectories_collide; +} + +} // namespace mppi::critics + +#include + +PLUGINLIB_EXPORT_CLASS( + mppi::critics::CostCritic, + mppi::critics::CriticFunction) diff --git a/nav2_mppi_controller/src/critics/cost_critic copy.cpp b/nav2_mppi_controller/src/critics/cost_critic copy.cpp new file mode 100644 index 00000000000..5c5d803ca3a --- /dev/null +++ b/nav2_mppi_controller/src/critics/cost_critic copy.cpp @@ -0,0 +1,198 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// Copyright (c) 2023 Open Navigation LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include "nav2_mppi_controller/critics/cost_critic.hpp" +#include "nav2_core/controller_exceptions.hpp" +#include "nav2_costmap_2d/cost_values.hpp" +#include "geometry_msgs/msg/point.hpp" + +namespace mppi::critics +{ + +void CostCritic::initialize() +{ + auto getParentParam = parameters_handler_->getParamGetter(parent_name_); + auto getParam = parameters_handler_->getParamGetter(name_); + getParam(consider_footprint_, "consider_footprint", false); + getParam(power_, "cost_power", 1); + getParam(weight_, "cost_weight", 3.81f); + getParam(critical_cost_, "critical_cost", 300.0f); + getParam(near_collision_cost_, "near_collision_cost", 253); + getParam(collision_cost_, "collision_cost", 1000000.0f); + getParam(near_goal_distance_, "near_goal_distance", 0.5f); + getParam(inflation_layer_name_, "inflation_layer_name", std::string("")); + getParam(trajectory_point_step_, "trajectory_point_step", 2); + + // Normalized by cost value to put in same regime as other weights + weight_ /= 254.0f; + + // Normalize weight when parameter is changed dynamically as well + auto weightDynamicCb = [&]( + const rclcpp::Parameter & weight, rcl_interfaces::msg::SetParametersResult & /*result*/) { + weight_ = weight.as_double() / 254.0f; + }; + parameters_handler_->addParamCallback(name_ + ".cost_weight", weightDynamicCb); + + collision_checker_ = std::make_unique( + costmap_ros_, parent_.lock()); + + if (costmap_ros_->getUseRadius() == consider_footprint_) { + RCLCPP_WARN( + logger_, + "Inconsistent configuration in collision checking. Please verify the robot's shape settings " + "in both the costmap and the cost critic."); + if (costmap_ros_->getUseRadius()) { + throw nav2_core::ControllerException( + "Considering footprint in collision checking but no robot footprint provided in the " + "costmap."); + } + } + + if(near_collision_cost_ > 253) { + RCLCPP_WARN(logger_, "Near collision cost is set higher than INSCRIBED_INFLATED_OBSTACLE"); + } + + RCLCPP_INFO( + logger_, + "InflationCostCritic instantiated with %d power and %f / %f weights. " + "Critic will collision check based on %s cost.", + power_, critical_cost_, weight_, consider_footprint_ ? + "footprint" : "circular"); +} + +void CostCritic::score(CriticData & data) +{ + if (!enabled_) { + return; + } + + // Setup cost information for various parts of the critic + is_tracking_unknown_ = costmap_ros_->getLayeredCostmap()->isTrackingUnknown(); + auto * costmap = collision_checker_->getCostmap(); + origin_x_ = static_cast(costmap->getOriginX()); + origin_y_ = static_cast(costmap->getOriginY()); + resolution_ = static_cast(costmap->getResolution()); + size_x_ = costmap->getSizeInCellsX(); + size_y_ = costmap->getSizeInCellsY(); + + bool near_goal = false; + if (data.state.local_path_length < near_goal_distance_) { + near_goal = true; + } + + Eigen::ArrayXf repulsive_cost(data.costs.rows()); + repulsive_cost.setZero(); + bool all_trajectories_collide = true; + + int strided_traj_cols = floor((data.trajectories.x.cols() - 1) / trajectory_point_step_) + 1; + int strided_traj_rows = data.trajectories.x.rows(); + int outer_stride = strided_traj_rows * trajectory_point_step_; + + // Initialize trajectories collision tracking + if (!data.trajectories_in_collision) { + data.trajectories_in_collision = std::vector(strided_traj_rows, false); + } + + const auto traj_x = Eigen::Map>(data.trajectories.x.data(), strided_traj_rows, strided_traj_cols, + Eigen::Stride<-1, -1>(outer_stride, 1)); + const auto traj_y = Eigen::Map>(data.trajectories.y.data(), strided_traj_rows, strided_traj_cols, + Eigen::Stride<-1, -1>(outer_stride, 1)); + const auto traj_yaw = Eigen::Map>(data.trajectories.yaws.data(), strided_traj_rows, strided_traj_cols, + Eigen::Stride<-1, -1>(outer_stride, 1)); + + collision_checker_->setFootprint( + costmap_ros_->getRobotFootprint(), + costmap_ros_->getUseRadius(), + inflation_layer_name_); + + for (int i = 0; i < strided_traj_rows; ++i) { + bool trajectory_collide = false; + float & traj_cost = repulsive_cost(i); + // Prepare vectors for batch collision checking + std::vector x_coords, y_coords, yaw_angles; + x_coords.reserve(strided_traj_cols); + y_coords.reserve(strided_traj_cols); + yaw_angles.reserve(strided_traj_cols); + + // Convert world coordinates to map coordinates and collect yaw angles + for (int j = 0; j < strided_traj_cols; j++) { + float Tx = traj_x(i, j); + float Ty = traj_y(i, j); + unsigned int x_i = 0u, y_i = 0u; + + if (!worldToMapFloat(Tx, Ty, x_i, y_i)) { + // If any point is outside the map, mark trajectory as colliding + traj_cost = collision_cost_; + trajectory_collide = true; + break; + } + + x_coords.push_back(static_cast(x_i)); + y_coords.push_back(static_cast(y_i)); + yaw_angles.push_back(traj_yaw(i, j)); + } + // Skip batch collision checking if trajectory already marked as colliding + if (trajectory_collide) { + continue; + } + + // Perform batch collision checking + auto collision_result = collision_checker_->inCollision( + x_coords, y_coords, yaw_angles, is_tracking_unknown_); + + if (collision_result.in_collision) { + traj_cost = collision_cost_; + trajectory_collide = true; + } else { + for (size_t k = 0; k < collision_result.footprint_cost.size(); ++k) { + // For optimization purposes, we don't always have the footprint cost + // but we still use it when available to staw away from collision more conservatively + float cost = collision_result.footprint_cost[k] != -1.0f ? + collision_result.footprint_cost[k] : collision_result.center_cost[k]; + + if (cost >= static_cast(near_collision_cost_)) { + traj_cost += critical_cost_; + } else if (!near_goal) { // Generally prefer trajectories further from obstacles + traj_cost += cost; + } + } + } + + + (*data.trajectories_in_collision)[i] = trajectory_collide; + all_trajectories_collide &= trajectory_collide; + } + + if (power_ > 1u) { + data.costs += (repulsive_cost * + (weight_ / static_cast(strided_traj_cols))).pow(power_); + } else { + data.costs += repulsive_cost * (weight_ / static_cast(strided_traj_cols)); + } + + data.fail_flag = all_trajectories_collide; +} + +} // namespace mppi::critics + +#include + +PLUGINLIB_EXPORT_CLASS( + mppi::critics::CostCritic, + mppi::critics::CriticFunction) diff --git a/nav2_mppi_controller/src/critics/cost_critic.cpp b/nav2_mppi_controller/src/critics/cost_critic.cpp index 6b15d992ca8..6d2e869614e 100644 --- a/nav2_mppi_controller/src/critics/cost_critic.cpp +++ b/nav2_mppi_controller/src/critics/cost_critic.cpp @@ -85,7 +85,7 @@ float CostCritic::findCircumscribedCost( std::shared_ptr costmap) { double result = -1.0; - const double circum_radius = costmap->getLayeredCostmap()->getCircumscribedRadius(); + const double circum_radius = costmap->getLayeredCostmap()->getCircumscribedRadius() /*+ 0.2*/; if (static_cast(circum_radius) == circumscribed_radius_) { // early return if footprint size is unchanged return circumscribed_cost_; @@ -133,6 +133,9 @@ void CostCritic::score(CriticData & data) return; } + static bool s_footprint_active = false; + bool any_use_footprint_this_tick = false; + // Setup cost information for various parts of the critic is_tracking_unknown_ = costmap_ros_->getLayeredCostmap()->isTrackingUnknown(); auto * costmap = collision_checker_.getCostmap(); @@ -161,6 +164,11 @@ void CostCritic::score(CriticData & data) int strided_traj_rows = data.trajectories.x.rows(); int outer_stride = strided_traj_rows * trajectory_point_step_; + // Initialize trajectories collision tracking + if (!data.trajectories_in_collision) { + data.trajectories_in_collision = std::vector(strided_traj_rows, false); + } + const auto traj_x = Eigen::Map>(data.trajectories.x.data(), strided_traj_rows, strided_traj_cols, Eigen::Stride<-1, -1>(outer_stride, 1)); @@ -185,7 +193,7 @@ void CostCritic::score(CriticData & data) // The footprintCostAtPose will always return "INSCRIBED" if footprint is over it // So the center point has more information than the footprint if (!worldToMapFloat(Tx, Ty, x_i, y_i)) { - pose_cost = 255.0f; // NO_INFORMATION in float + pose_cost = 0.0f; // NO_INFORMATION in float } else { pose_cost = static_cast(costmap->getCost(getIndex(x_i, y_i))); if (pose_cost < 1.0f) { @@ -193,7 +201,36 @@ void CostCritic::score(CriticData & data) } } - if (inCollision(pose_cost, Tx, Ty, traj_yaw(i, j))) { + float cost_for_scoring = pose_cost; + bool use_footprint_cost = consider_footprint_ && + (pose_cost >= possible_collision_cost_ || possible_collision_cost_ < 1.0f); + any_use_footprint_this_tick = any_use_footprint_this_tick || use_footprint_cost; + + if (use_footprint_cost) { + cost_for_scoring = static_cast(collision_checker_.footprintCostAtPose( + static_cast(Tx), static_cast(Ty), static_cast(traj_yaw(i, j)), + costmap_ros_->getRobotFootprint())); + } + + // Check for collision based on the appropriate cost + bool in_collision = false; + + switch (static_cast(cost_for_scoring)) { + case (nav2_costmap_2d::LETHAL_OBSTACLE): + in_collision = true; + break; + case (nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE): + in_collision = use_footprint_cost ? false : true; + break; + case (nav2_costmap_2d::NO_INFORMATION): + in_collision = false; + break; + default: + in_collision = false; + } + + + if (in_collision) { traj_cost = collision_cost_; trajectory_collide = true; break; @@ -202,13 +239,14 @@ void CostCritic::score(CriticData & data) // Let near-collision trajectory points be punished severely // Note that we collision check based on the footprint actual, // but score based on the center-point cost regardless - if (pose_cost >= static_cast(near_collision_cost_)) { + if (cost_for_scoring >= near_collision_cost_ && !near_goal) { traj_cost += critical_cost_; } else if (!near_goal) { // Generally prefer trajectories further from obstacles - traj_cost += pose_cost; + traj_cost += cost_for_scoring; } } + (*data.trajectories_in_collision)[i] = trajectory_collide; all_trajectories_collide &= trajectory_collide; } @@ -219,6 +257,18 @@ void CostCritic::score(CriticData & data) data.costs += repulsive_cost * (weight_ / static_cast(strided_traj_cols)); } + if (any_use_footprint_this_tick != s_footprint_active) { + static constexpr const char* RESET = "\033[0m"; + static constexpr const char* GREEN = "\033[32;1m"; + if (any_use_footprint_this_tick) { + RCLCPP_INFO(logger_, "%s[CostCritic] TURN ON FOOTPRINT COST%s",GREEN, RESET); + } else { + RCLCPP_INFO(logger_, "%s[CostCritic] TURN OFF FOOTPRINT COST%s",GREEN, RESET); + } + s_footprint_active = any_use_footprint_this_tick; + } + + data.fail_flag = all_trajectories_collide; } diff --git a/nav2_mppi_controller/src/critics/cost_critic_clear.cpp b/nav2_mppi_controller/src/critics/cost_critic_clear.cpp new file mode 100644 index 00000000000..9ab364dcd0b --- /dev/null +++ b/nav2_mppi_controller/src/critics/cost_critic_clear.cpp @@ -0,0 +1,281 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// Copyright (c) 2023 Open Navigation LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include "nav2_mppi_controller/critics/cost_critic.hpp" +#include "nav2_core/controller_exceptions.hpp" + +namespace mppi::critics +{ + +void CostCritic::initialize() +{ + auto getParentParam = parameters_handler_->getParamGetter(parent_name_); + auto getParam = parameters_handler_->getParamGetter(name_); + getParam(consider_footprint_, "consider_footprint", false); + getParam(power_, "cost_power", 1); + getParam(weight_, "cost_weight", 3.81f); + getParam(critical_cost_, "critical_cost", 300.0f); + getParam(near_collision_cost_, "near_collision_cost", 253); + getParam(collision_cost_, "collision_cost", 1000000.0f); + getParam(near_goal_distance_, "near_goal_distance", 0.5f); + getParam(inflation_layer_name_, "inflation_layer_name", std::string("")); + getParam(trajectory_point_step_, "trajectory_point_step", 2); + + // Normalized by cost value to put in same regime as other weights + weight_ /= 254.0f; + + // Normalize weight when parameter is changed dynamically as well + auto weightDynamicCb = [&]( + const rclcpp::Parameter & weight, rcl_interfaces::msg::SetParametersResult & /*result*/) { + weight_ = weight.as_double() / 254.0f; + }; + parameters_handler_->addParamCallback(name_ + ".cost_weight", weightDynamicCb); + + collision_checker_.setCostmap(costmap_); + possible_collision_cost_ = findCircumscribedCost(costmap_ros_); + + if (possible_collision_cost_ < 1.0f) { + RCLCPP_ERROR( + logger_, + "Inflation layer either not found or inflation is not set sufficiently for " + "optimized non-circular collision checking capabilities. It is HIGHLY recommended to set" + " the inflation radius to be at MINIMUM half of the robot's largest cross-section. See " + "github.com/ros-planning/navigation2/tree/main/nav2_smac_planner#potential-fields" + " for full instructions. This will substantially impact run-time performance."); + } + + if (costmap_ros_->getUseRadius() == consider_footprint_) { + RCLCPP_WARN( + logger_, + "Inconsistent configuration in collision checking. Please verify the robot's shape settings " + "in both the costmap and the cost critic."); + if (costmap_ros_->getUseRadius()) { + throw nav2_core::ControllerException( + "Considering footprint in collision checking but no robot footprint provided in the " + "costmap."); + } + } + + if(near_collision_cost_ > 253) { + RCLCPP_WARN(logger_, "Near collision cost is set higher than INSCRIBED_INFLATED_OBSTACLE"); + } + + RCLCPP_INFO( + logger_, + "InflationCostCritic instantiated with %d power and %f / %f weights. " + "Critic will collision check based on %s cost.", + power_, critical_cost_, weight_, consider_footprint_ ? + "footprint" : "circular"); +} + +float CostCritic::findCircumscribedCost( + std::shared_ptr costmap) +{ + double result = -1.0; + const double circum_radius = costmap->getLayeredCostmap()->getCircumscribedRadius(); + if (static_cast(circum_radius) == circumscribed_radius_) { + // early return if footprint size is unchanged + return circumscribed_cost_; + } + + // check if the costmap has an inflation layer + const auto inflation_layer = nav2_costmap_2d::InflationLayer::getInflationLayer( + costmap, + inflation_layer_name_); + if (inflation_layer != nullptr) { + const double resolution = costmap->getCostmap()->getResolution(); + double inflation_radius = inflation_layer->getInflationRadius(); + if (inflation_radius < circum_radius) { + RCLCPP_ERROR( + rclcpp::get_logger("computeCircumscribedCost"), + "The inflation radius (%f) is smaller than the circumscribed radius (%f) " + "If this is an SE2-collision checking plugin, it cannot use costmap potential " + "field to speed up collision checking by only checking the full footprint " + "when robot is within possibly-inscribed radius of an obstacle. This may " + "significantly slow down planning times!", + inflation_radius, circum_radius); + result = 0.0; + return result; + } + result = inflation_layer->computeCost(circum_radius / resolution); + } else { + RCLCPP_WARN( + logger_, + "No inflation layer found in costmap configuration. " + "If this is an SE2-collision checking plugin, it cannot use costmap potential " + "field to speed up collision checking by only checking the full footprint " + "when robot is within possibly-inscribed radius of an obstacle. This may " + "significantly slow down planning times and not avoid anything but absolute collisions!"); + } + + circumscribed_radius_ = static_cast(circum_radius); + circumscribed_cost_ = static_cast(result); + + return circumscribed_cost_; +} + +void CostCritic::score(CriticData & data) +{ + if (!enabled_) { + return; + } + + static bool s_footprint_active = false; + bool any_use_footprint_this_tick = false; + + // Setup cost information for various parts of the critic + is_tracking_unknown_ = costmap_ros_->getLayeredCostmap()->isTrackingUnknown(); + auto * costmap = collision_checker_.getCostmap(); + origin_x_ = static_cast(costmap->getOriginX()); + origin_y_ = static_cast(costmap->getOriginY()); + resolution_ = static_cast(costmap->getResolution()); + size_x_ = costmap->getSizeInCellsX(); + size_y_ = costmap->getSizeInCellsY(); + + if (consider_footprint_) { + // footprint may have changed since initialization if user has dynamic footprints + possible_collision_cost_ = findCircumscribedCost(costmap_ros_); + } + + // If near the goal, don't apply the preferential term since the goal is near obstacles + bool near_goal = false; + if (data.state.local_path_length < near_goal_distance_) { + near_goal = true; + } + + Eigen::ArrayXf repulsive_cost(data.costs.rows()); + repulsive_cost.setZero(); + bool all_trajectories_collide = true; + + int strided_traj_cols = floor((data.trajectories.x.cols() - 1) / trajectory_point_step_) + 1; + int strided_traj_rows = data.trajectories.x.rows(); + int outer_stride = strided_traj_rows * trajectory_point_step_; + + // Initialize trajectories collision tracking + if (!data.trajectories_in_collision) { + data.trajectories_in_collision = std::vector(strided_traj_rows, false); + } + + const auto traj_x = Eigen::Map>(data.trajectories.x.data(), strided_traj_rows, strided_traj_cols, + Eigen::Stride<-1, -1>(outer_stride, 1)); + const auto traj_y = Eigen::Map>(data.trajectories.y.data(), strided_traj_rows, strided_traj_cols, + Eigen::Stride<-1, -1>(outer_stride, 1)); + const auto traj_yaw = Eigen::Map>(data.trajectories.yaws.data(), strided_traj_rows, strided_traj_cols, + Eigen::Stride<-1, -1>(outer_stride, 1)); + + for (int i = 0; i < strided_traj_rows; ++i) { + bool trajectory_collide = false; + float pose_cost = 0.0f; + float & traj_cost = repulsive_cost(i); + + for (int j = 0; j < strided_traj_cols; j++) { + float Tx = traj_x(i, j); + float Ty = traj_y(i, j); + unsigned int x_i = 0u, y_i = 0u; + + // The getCost doesn't use orientation + // The footprintCostAtPose will always return "INSCRIBED" if footprint is over it + // So the center point has more information than the footprint + if (!worldToMapFloat(Tx, Ty, x_i, y_i)) { + pose_cost = 0.0f; // NO_INFORMATION in float + } else { + pose_cost = static_cast(costmap->getCost(getIndex(x_i, y_i))); + if (pose_cost < 1.0f) { + continue; // In free space + } + } + + float cost_for_scoring = pose_cost; + bool use_footprint_cost = consider_footprint_ && + (pose_cost >= possible_collision_cost_ || possible_collision_cost_ < 1.0f); + any_use_footprint_this_tick = any_use_footprint_this_tick || use_footprint_cost; + + if (use_footprint_cost) { + cost_for_scoring = static_cast(collision_checker_.footprintCostAtPose( + static_cast(Tx), static_cast(Ty), static_cast(traj_yaw(i, j)), + costmap_ros_->getRobotFootprint())); + } + + // Check for collision based on the appropriate cost + bool in_collision = false; + + switch (static_cast(cost_for_scoring)) { + case (nav2_costmap_2d::LETHAL_OBSTACLE): + in_collision = true; + break; + case (nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE): + in_collision = use_footprint_cost ? false : true; + break; + case (nav2_costmap_2d::NO_INFORMATION): + in_collision = is_tracking_unknown_ ? false : true; + break; + default: + in_collision = false; + } + + + if (in_collision) { + traj_cost = collision_cost_; + trajectory_collide = true; + break; + } + + // Let near-collision trajectory points be punished severely + // Note that we collision check based on the footprint actual, + // but score based on the center-point cost regardless + if (cost_for_scoring >= near_collision_cost_ && !near_goal) { + traj_cost += critical_cost_; + } else if (!near_goal) { // Generally prefer trajectories further from obstacles + traj_cost += cost_for_scoring; + } + } + + (*data.trajectories_in_collision)[i] = trajectory_collide; + all_trajectories_collide &= trajectory_collide; + } + + if (power_ > 1u) { + data.costs += (repulsive_cost * + (weight_ / static_cast(strided_traj_cols))).pow(power_); + } else { + data.costs += repulsive_cost * (weight_ / static_cast(strided_traj_cols)); + } + + if (any_use_footprint_this_tick != s_footprint_active) { + static constexpr const char* RESET = "\033[0m"; + static constexpr const char* GREEN = "\033[32;1m"; + if (any_use_footprint_this_tick) { + RCLCPP_INFO(logger_, "%s[CostCritic] TURN ON FOOTPRINT COST%s",GREEN, RESET); + } else { + RCLCPP_INFO(logger_, "%s[CostCritic] TURN OFF FOOTPRINT COST%s",GREEN, RESET); + } + s_footprint_active = any_use_footprint_this_tick; + } + + + data.fail_flag = all_trajectories_collide; +} + +} // namespace mppi::critics + +#include + +PLUGINLIB_EXPORT_CLASS( + mppi::critics::CostCritic, + mppi::critics::CriticFunction) diff --git a/nav2_mppi_controller/src/critics/obstacles_critic copy 3.cpp b/nav2_mppi_controller/src/critics/obstacles_critic copy 3.cpp new file mode 100644 index 00000000000..0577db0a76f --- /dev/null +++ b/nav2_mppi_controller/src/critics/obstacles_critic copy 3.cpp @@ -0,0 +1,249 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// Copyright (c) 2023 Open Navigation LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include "nav2_mppi_controller/critics/obstacles_critic.hpp" +#include "nav2_costmap_2d/inflation_layer.hpp" +#include "nav2_core/controller_exceptions.hpp" + +namespace mppi::critics +{ + +void ObstaclesCritic::initialize() +{ + auto getParentParam = parameters_handler_->getParamGetter(parent_name_); + auto getParam = parameters_handler_->getParamGetter(name_); + getParam(consider_footprint_, "consider_footprint", false); + getParam(power_, "cost_power", 1); + getParam(repulsion_weight_, "repulsion_weight", 1.5f); + getParam(critical_weight_, "critical_weight", 20.0f); + getParam(collision_cost_, "collision_cost", 100000.0f); + getParam(collision_margin_distance_, "collision_margin_distance", 0.10f); + getParam(near_goal_distance_, "near_goal_distance", 0.5f); + getParam(inflation_layer_name_, "inflation_layer_name", std::string("")); + + collision_checker_.setCostmap(costmap_); + possible_collision_cost_ = findCircumscribedCost(costmap_ros_); + + if (possible_collision_cost_ < 1.0f) { + RCLCPP_ERROR( + logger_, + "Inflation layer either not found or inflation is not set sufficiently for " + "optimized non-circular collision checking capabilities. It is HIGHLY recommended to set" + " the inflation radius to be at MINIMUM half of the robot's largest cross-section. See " + "github.com/ros-planning/navigation2/tree/main/nav2_smac_planner#potential-fields" + " for full instructions. This will substantially impact run-time performance."); + } + + if (costmap_ros_->getUseRadius() == consider_footprint_) { + RCLCPP_WARN( + logger_, + "Inconsistent configuration in collision checking. Please verify the robot's shape settings " + "in both the costmap and the obstacle critic."); + if (costmap_ros_->getUseRadius()) { + throw nav2_core::ControllerException( + "Considering footprint in collision checking but no robot footprint provided in the " + "costmap."); + } + } + + RCLCPP_INFO( + logger_, + "ObstaclesCritic instantiated with %d power and %f / %f weights. " + "Critic will collision check based on %s cost.", + power_, critical_weight_, repulsion_weight_, consider_footprint_ ? + "footprint" : "circular"); +} + +float ObstaclesCritic::findCircumscribedCost( + std::shared_ptr costmap) +{ + double result = -1.0; + const double circum_radius = costmap->getLayeredCostmap()->getCircumscribedRadius(); + if (static_cast(circum_radius) == circumscribed_radius_) { + // early return if footprint size is unchanged + return circumscribed_cost_; + } + + // check if the costmap has an inflation layer + const auto inflation_layer = nav2_costmap_2d::InflationLayer::getInflationLayer( + costmap, + inflation_layer_name_); + if (inflation_layer != nullptr) { + const double resolution = costmap->getCostmap()->getResolution(); + result = inflation_layer->computeCost(circum_radius / resolution); + inflation_scale_factor_ = static_cast(inflation_layer->getCostScalingFactor()); + inflation_radius_ = static_cast(inflation_layer->getInflationRadius()); + } else { + RCLCPP_WARN( + logger_, + "No inflation layer found in costmap configuration. " + "If this is an SE2-collision checking plugin, it cannot use costmap potential " + "field to speed up collision checking by only checking the full footprint " + "when robot is within possibly-inscribed radius of an obstacle. This may " + "significantly slow down planning times and not avoid anything but absolute collisions!"); + } + + circumscribed_radius_ = static_cast(circum_radius); + circumscribed_cost_ = static_cast(result); + + return circumscribed_cost_; +} + +float ObstaclesCritic::distanceToObstacle(const CollisionCost & cost) +{ + const float scale_factor = inflation_scale_factor_; + const float min_radius = costmap_ros_->getLayeredCostmap()->getInscribedRadius(); + float dist_to_obj = (scale_factor * min_radius - log(cost.cost) + log(253.0f)) / scale_factor; + + // If not footprint collision checking, the cost is using the center point cost and + // needs the radius subtracted to obtain the closest distance to the object + if (!cost.using_footprint) { + dist_to_obj -= min_radius; + } + + return dist_to_obj; +} + +void ObstaclesCritic::score(CriticData & data) +{ + if (!enabled_) { + return; + } + + if (consider_footprint_) { + // footprint may have changed since initialization if user has dynamic footprints + possible_collision_cost_ = findCircumscribedCost(costmap_ros_); + } + + // If near the goal, don't apply the preferential term since the goal is near obstacles + bool near_goal = false; + if (data.state.local_path_length < near_goal_distance_) { + near_goal = true; + } + + Eigen::ArrayXf raw_cost = Eigen::ArrayXf::Zero(data.costs.size()); + Eigen::ArrayXf repulsive_cost = Eigen::ArrayXf::Zero(data.costs.size()); + + const unsigned int traj_len = data.trajectories.x.cols(); + const unsigned int batch_size = data.trajectories.x.rows(); + bool all_trajectories_collide = true; + + for(unsigned int i = 0; i != batch_size; i++) { + bool trajectory_collide = false; + float traj_cost = 0.0f; + const auto & traj = data.trajectories; + CollisionCost pose_cost; + raw_cost(i) = 0.0f; + repulsive_cost(i) = 0.0f; + + for(unsigned int j = 0; j != traj_len; j++) { + pose_cost = costAtPose(traj.x(i, j), traj.y(i, j), traj.yaws(i, j)); + if (pose_cost.cost < 1.0f) {continue;} // In free space + + if (inCollision(pose_cost.cost)) { + trajectory_collide = true; + break; + } + + // Cannot process repulsion if inflation layer does not exist + if (inflation_radius_ == 0.0f || inflation_scale_factor_ == 0.0f) { + continue; + } + + const float dist_to_obj = distanceToObstacle(pose_cost); + + // Let near-collision trajectory points be punished severely + if (dist_to_obj < collision_margin_distance_) { + traj_cost += (collision_margin_distance_ - dist_to_obj); + } + + // Generally prefer trajectories further from obstacles + if (!near_goal) { + repulsive_cost[i] += inflation_radius_ - dist_to_obj; + } + } + + if (!trajectory_collide) {all_trajectories_collide = false;} + raw_cost(i) = trajectory_collide ? collision_cost_ : traj_cost; + } + + // Normalize repulsive cost by trajectory length & lowest score to not overweight importance + // This is a preferential cost, not collision cost, to be tuned relative to desired behaviors + auto repulsive_cost_normalized = (repulsive_cost - repulsive_cost.minCoeff()) / traj_len; + + if (power_ > 1u) { + data.costs += + ((critical_weight_ * raw_cost) + (repulsion_weight_ * repulsive_cost_normalized)).pow(power_); + } else { + data.costs += (critical_weight_ * raw_cost) + (repulsion_weight_ * repulsive_cost_normalized); + } + + data.fail_flag = all_trajectories_collide; +} + +/** + * @brief Checks if cost represents a collision + * @param cost Costmap cost + * @return bool if in collision + */ +bool ObstaclesCritic::inCollision(float cost) const +{ + bool is_tracking_unknown = + costmap_ros_->getLayeredCostmap()->isTrackingUnknown(); + + using namespace nav2_costmap_2d; // NOLINT + switch (static_cast(cost)) { + case (LETHAL_OBSTACLE): + return true; + case (INSCRIBED_INFLATED_OBSTACLE): + return consider_footprint_ ? false : true; + case (NO_INFORMATION): + return is_tracking_unknown ? false : true; + } + + return false; +} + +CollisionCost ObstaclesCritic::costAtPose(float x, float y, float theta) +{ + CollisionCost collision_cost; + float & cost = collision_cost.cost; + collision_cost.using_footprint = false; + unsigned int x_i, y_i; + if (!collision_checker_.worldToMap(x, y, x_i, y_i)) { + cost = nav2_costmap_2d::NO_INFORMATION; + return collision_cost; + } + cost = collision_checker_.pointCost(x_i, y_i); + + if (consider_footprint_ && + (cost >= possible_collision_cost_ || possible_collision_cost_ < 1.0f)) + { + cost = static_cast(collision_checker_.footprintCostAtPose( + x, y, theta, costmap_ros_->getRobotFootprint())); + collision_cost.using_footprint = true; + } + + return collision_cost; +} + +} // namespace mppi::critics + +#include + +PLUGINLIB_EXPORT_CLASS( + mppi::critics::ObstaclesCritic, + mppi::critics::CriticFunction) diff --git a/nav2_mppi_controller/src/critics/obstacles_critic copy.cpp b/nav2_mppi_controller/src/critics/obstacles_critic copy.cpp new file mode 100644 index 00000000000..387dfd64776 --- /dev/null +++ b/nav2_mppi_controller/src/critics/obstacles_critic copy.cpp @@ -0,0 +1,373 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// Copyright (c) 2023 Open Navigation LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include "nav2_mppi_controller/critics/obstacles_critic.hpp" +#include "nav2_costmap_2d/inflation_layer.hpp" +#include "nav2_core/controller_exceptions.hpp" + +namespace mppi::critics +{ + +void ObstaclesCritic::initialize() +{ + auto getParentParam = parameters_handler_->getParamGetter(parent_name_); + auto getParam = parameters_handler_->getParamGetter(name_); + getParam(consider_footprint_, "consider_footprint", false); + getParam(power_, "cost_power", 1); + getParam(repulsion_weight_, "repulsion_weight", 1.5f); + getParam(critical_weight_, "critical_weight", 20.0f); + getParam(collision_cost_, "collision_cost", 100000.0f); + getParam(collision_margin_distance_, "collision_margin_distance", 0.10f); + getParam(near_goal_distance_, "near_goal_distance", 0.5f); + getParam(inflation_layer_name_, "inflation_layer_name", std::string("")); + getParam(trajectory_point_step_, "trajectory_point_step", 2); + + //collision_checker_.setCostmap(costmap_); + collision_checker_ = std::make_unique( + costmap_ros_, parent_.lock()); + possible_collision_cost_ = findCircumscribedCost(costmap_ros_); + + if (possible_collision_cost_ < 1.0f) { + RCLCPP_ERROR( + logger_, + "Inflation layer either not found or inflation is not set sufficiently for " + "optimized non-circular collision checking capabilities. It is HIGHLY recommended to set" + " the inflation radius to be at MINIMUM half of the robot's largest cross-section. See " + "github.com/ros-planning/navigation2/tree/main/nav2_smac_planner#potential-fields" + " for full instructions. This will substantially impact run-time performance."); + } + + if (costmap_ros_->getUseRadius() == consider_footprint_) { + RCLCPP_WARN( + logger_, + "Inconsistent configuration in collision checking. Please verify the robot's shape settings " + "in both the costmap and the obstacle critic."); + if (costmap_ros_->getUseRadius()) { + throw nav2_core::ControllerException( + "Considering footprint in collision checking but no robot footprint provided in the " + "costmap."); + } + } + + RCLCPP_INFO( + logger_, + "ObstaclesCritic instantiated with %d power and %f / %f weights. " + "Critic will collision check based on %s cost.", + power_, critical_weight_, repulsion_weight_, consider_footprint_ ? + "footprint" : "circular"); + + RCLCPP_WARN(logger_, + "Inflation: radius=%.3f scale=%.3f, possible_collision_cost=%.1f, inscribed=%.3f", + inflation_radius_, inflation_scale_factor_, possible_collision_cost_, + costmap_ros_->getLayeredCostmap()->getInscribedRadius()); + +} + +float ObstaclesCritic::findCircumscribedCost( + std::shared_ptr costmap) +{ + double result = -1.0; + const double circum_radius = costmap->getLayeredCostmap()->getCircumscribedRadius(); + if (static_cast(circum_radius) == circumscribed_radius_) { + // early return if footprint size is unchanged + return circumscribed_cost_; + } + + // check if the costmap has an inflation layer + const auto inflation_layer = nav2_costmap_2d::InflationLayer::getInflationLayer( + costmap, + inflation_layer_name_); + if (inflation_layer != nullptr) { + const double resolution = costmap->getCostmap()->getResolution(); + result = inflation_layer->computeCost(circum_radius / resolution); + inflation_scale_factor_ = static_cast(inflation_layer->getCostScalingFactor()); + inflation_radius_ = static_cast(inflation_layer->getInflationRadius()); + } else { + RCLCPP_WARN( + logger_, + "No inflation layer found in costmap configuration. " + "If this is an SE2-collision checking plugin, it cannot use costmap potential " + "field to speed up collision checking by only checking the full footprint " + "when robot is within possibly-inscribed radius of an obstacle. This may " + "significantly slow down planning times and not avoid anything but absolute collisions!"); + } + + circumscribed_radius_ = static_cast(circum_radius); + circumscribed_cost_ = static_cast(result); + + return circumscribed_cost_; +} + +float ObstaclesCritic::distanceToObstacle(const CollisionCost & cost) +{ + const float scale_factor = inflation_scale_factor_; + const float min_radius = costmap_ros_->getLayeredCostmap()->getInscribedRadius(); + float dist_to_obj = (scale_factor * min_radius - log(cost.cost) + log(253.0f)) / scale_factor; + + // If not footprint collision checking, the cost is using the center point cost and + // needs the radius subtracted to obtain the closest distance to the object + if (!cost.using_footprint) { + dist_to_obj -= min_radius; + } + + return dist_to_obj; +} + +void ObstaclesCritic::score(CriticData & data) +{ + if (!enabled_) { + return; + } + + collision_checker_->setFootprint( + costmap_ros_->getRobotFootprint(), + costmap_ros_->getUseRadius(), + inflation_layer_name_); + + const auto & fp = costmap_ros_->getRobotFootprint(); + RCLCPP_WARN(logger_, + "[DBG] setFootprint: use_radius=%d, fp_pts=%zu, infl_radius=%.3f, infl_scale=%.3f, circ_cost=%.1f, inscribed=%.3f", + costmap_ros_->getUseRadius(), fp.size(), + inflation_radius_, inflation_scale_factor_, possible_collision_cost_, + costmap_ros_->getLayeredCostmap()->getInscribedRadius()); + if (!fp.empty()) { + float minx=fp[0].x, maxx=fp[0].x, miny=fp[0].y, maxy=fp[0].y; + for (auto &p: fp) { minx=std::min(minx,(float)p.x); maxx=std::max(maxx,(float)p.x); + miny=std::min(miny,(float)p.y); maxy=std::max(maxy,(float)p.y); } + RCLCPP_WARN(logger_, "[DBG] footprint bounds: x[%.3f, %.3f], y[%.3f, %.3f]", minx, maxx, miny, maxy); + } + + if (consider_footprint_) { + // footprint may have changed since initialization if user has dynamic footprints + possible_collision_cost_ = findCircumscribedCost(costmap_ros_); + } + + Eigen::ArrayXf raw_cost = Eigen::ArrayXf::Zero(data.costs.size()); + Eigen::ArrayXf repulsive_cost = Eigen::ArrayXf::Zero(data.costs.size()); + + //const unsigned int traj_len = data.trajectories.x.cols(); + //const unsigned int batch_size = data.trajectories.x.rows(); + const int traj_cols_full = data.trajectories.x.cols(); + const int traj_rows = data.trajectories.x.rows(); + const int strided_cols = std::floor((traj_cols_full - 1) / trajectory_point_step_) + 1; + + bool all_trajectories_collide = true; + + // Initialize trajectories collision tracking + // if (!data.trajectories_in_collision) { + // data.trajectories_in_collision = std::vector(batch_size, false); + // } + if (data.trajectories_in_collision && data.trajectories_in_collision->size() != (size_t)traj_rows) { + data.trajectories_in_collision->assign(traj_rows, false); + } + + + + // for(unsigned int i = 0; i != batch_size; i++) { + for (int i = 0; i < traj_rows; ++i) { + bool trajectory_collide = false; + // float traj_cost = 0.0f; + // const auto & traj = data.trajectories; + // CollisionCost pose_cost; + // raw_cost(i) = 0.0f; + // repulsive_cost(i) = 0.0f; + float& near_collision_acc = raw_cost(i); + float& repulse_acc = repulsive_cost(i); + near_collision_acc = 0.0f; + repulse_acc = 0.0f; + + + + // for(unsigned int j = 0; j != traj_len; j++) { + // pose_cost = costAtPose(traj.x(i, j), traj.y(i, j), traj.yaws(i, j)); + // if (pose_cost.cost < 1.0f) {continue;} // In free space + + // if (inCollision(pose_cost.cost)) { + // trajectory_collide = true; + // break; + // } + + // // Cannot process repulsion if inflation layer does not exist + // if (inflation_radius_ == 0.0f || inflation_scale_factor_ == 0.0f) { + // continue; + // } + + // const float dist_to_obj = distanceToObstacle(pose_cost); + + // // Let near-collision trajectory points be punished severely + // if (dist_to_obj < collision_margin_distance_) { + // traj_cost += (collision_margin_distance_ - dist_to_obj); + // } + + // // Generally prefer trajectories further from obstacles + // if (!near_goal) { + // repulsive_cost[i] += inflation_radius_ - dist_to_obj; + // } + // } + + // 1) Zbierz prĂłbki w stride (dokƂadnie jak w CostCritic) + std::vector xs; xs.reserve(strided_cols); + std::vector ys; ys.reserve(strided_cols); + std::vector yaws; yaws.reserve(strided_cols); + + for (int j = 0; j < strided_cols; ++j) { + const int col = j * trajectory_point_step_; + if (col >= traj_cols_full) break; + xs.push_back(data.trajectories.x(i, col)); + ys.push_back(data.trajectories.y(i, col)); + yaws.push_back(data.trajectories.yaws(i, col)); + } + + // 2) Jedno batched sprawdzenie + auto result = collision_checker_->inCollision( + xs, ys, yaws, + /* treat_unknown_as_obstacle = */ false); + + if (i == 0) { + auto vminmax = [](const std::vector& v){ + if (v.empty()) return std::make_pair(0.f, 0.f); + float mn=v[0], mx=v[0]; + for (auto& z: v) { mn=std::min(mn,z); mx=std::max(mx,z); } + return std::make_pair(mn,mx); + }; + auto cc = vminmax(result.center_cost); + auto fc = vminmax(result.footprint_cost); + RCLCPP_WARN(logger_, + "[DBG] inCollision: in=%d, N=%zu, center[min=%.1f max=%.1f], footprint[min=%.1f max=%.1f]", + (int)result.in_collision, result.center_cost.size(), + cc.first, cc.second, fc.first, fc.second); + + // pokaĆŒ teĆŒ kilka prĂłbek z trajektorii + for (size_t s = 0; s < std::min(5, xs.size()); ++s) { + RCLCPP_WARN(logger_, "[DBG] sample[%zu]: x=%.3f y=%.3f yaw=%.3f", s, xs[s], ys[s], yaws[s]); + } + } + + if (result.in_collision) { + trajectory_collide = true; + near_collision_acc = collision_cost_; + } else { + // 3) „Near-collision” preferencja dystansu – identycznie jak u Ciebie, + // ale karmione danymi z 'result' + for (size_t k = 0; k < result.center_cost.size(); ++k) { + const bool have_fp = (k < result.footprint_cost.size() && result.footprint_cost[k] >= 0.0f); + float cost = have_fp ? result.footprint_cost[k] : result.center_cost[k]; + if (cost < 1.0f) continue; // free + + float dist = distanceToObstacle(CollisionCost{cost, have_fp}); + + // „twardy” element: kara tuĆŒ przed kolizją + if (dist < collision_margin_distance_) { + near_collision_acc += (collision_margin_distance_ - dist); + } + // „miękki” element: delikatna preferencja dalej od przeszkĂłd + if (inflation_radius_ > 0.f && inflation_scale_factor_ > 0.f && data.state.local_path_length >= near_goal_distance_) { + repulse_acc += std::max(0.f, inflation_radius_ - dist); + } + } + } + + // if (!trajectory_collide) {all_trajectories_collide = false;} + // (*data.trajectories_in_collision)[i] = trajectory_collide; + // raw_cost(i) = trajectory_collide ? collision_cost_ : traj_cost; + if (!trajectory_collide) { all_trajectories_collide = false; } + if (data.trajectories_in_collision) { + (*data.trajectories_in_collision)[i] = trajectory_collide; + } + } + + // Normalize repulsive cost by trajectory length & lowest score to not overweight importance + // This is a preferential cost, not collision cost, to be tuned relative to desired behaviors + //auto repulsive_cost_normalized = (repulsive_cost - repulsive_cost.minCoeff()) / traj_len; + auto repulsive_cost_normalized = (repulsive_cost - repulsive_cost.minCoeff()) / std::max(1, strided_cols); + + if (power_ > 1u) { + data.costs += + ((critical_weight_ * raw_cost) + (repulsion_weight_ * repulsive_cost_normalized)).pow(power_); + } else { + data.costs += (critical_weight_ * raw_cost) + (repulsion_weight_ * repulsive_cost_normalized); + } + + static int dbg_counter = 0; + if (++dbg_counter % 20 == 0) { + int coll_count = 0; + for (int i = 0; i < traj_rows; ++i) { + if (data.trajectories_in_collision && (*data.trajectories_in_collision)[i]) coll_count++; + } + RCLCPP_WARN(logger_, + "MPPI ObstaclesCritic: rows=%d, samples/row≈%d, collide_rows=%d, raw_cost_min=%.3f max=%.3f rep_min=%.3f max=%.3f", + traj_rows, strided_cols, + coll_count, + raw_cost.minCoeff(), raw_cost.maxCoeff(), + repulsive_cost.minCoeff(), repulsive_cost.maxCoeff()); + } + + data.fail_flag = all_trajectories_collide; +} + +/** + * @brief Checks if cost represents a collision + * @param cost Costmap cost + * @return bool if in collision + */ +bool ObstaclesCritic::inCollision(float cost) const +{ + bool is_tracking_unknown = + costmap_ros_->getLayeredCostmap()->isTrackingUnknown(); + + using namespace nav2_costmap_2d; // NOLINT + switch (static_cast(cost)) { + case (LETHAL_OBSTACLE): + return true; + case (INSCRIBED_INFLATED_OBSTACLE): + return consider_footprint_ ? false : true; + case (NO_INFORMATION): + return is_tracking_unknown ? false : true; + } + + return false; +} + +// CollisionCost ObstaclesCritic::costAtPose(float x, float y, float theta) +// { +// CollisionCost collision_cost; +// float & cost = collision_cost.cost; +// collision_cost.using_footprint = false; +// unsigned int x_i, y_i; +// if (!collision_checker_.worldToMap(x, y, x_i, y_i)) { +// cost = nav2_costmap_2d::NO_INFORMATION; +// return collision_cost; +// } +// cost = collision_checker_.pointCost(x_i, y_i); + +// if (consider_footprint_ && +// (cost >= possible_collision_cost_ || possible_collision_cost_ < 1.0f)) +// { +// cost = static_cast(collision_checker_.footprintCostAtPose( +// x, y, theta, costmap_ros_->getRobotFootprint())); +// collision_cost.using_footprint = true; +// } + +// return collision_cost; +// } + +} // namespace mppi::critics + +#include + +PLUGINLIB_EXPORT_CLASS( + mppi::critics::ObstaclesCritic, + mppi::critics::CriticFunction) diff --git a/nav2_mppi_controller/src/critics/obstacles_critic.cpp b/nav2_mppi_controller/src/critics/obstacles_critic.cpp index 0577db0a76f..2a02150e9e1 100644 --- a/nav2_mppi_controller/src/critics/obstacles_critic.cpp +++ b/nav2_mppi_controller/src/critics/obstacles_critic.cpp @@ -141,6 +141,12 @@ void ObstaclesCritic::score(CriticData & data) const unsigned int batch_size = data.trajectories.x.rows(); bool all_trajectories_collide = true; + // Initialize trajectories collision tracking + if (!data.trajectories_in_collision) { + data.trajectories_in_collision = std::vector(batch_size, false); + } + + for(unsigned int i = 0; i != batch_size; i++) { bool trajectory_collide = false; float traj_cost = 0.0f; @@ -177,6 +183,7 @@ void ObstaclesCritic::score(CriticData & data) } if (!trajectory_collide) {all_trajectories_collide = false;} + (*data.trajectories_in_collision)[i] = trajectory_collide; raw_cost(i) = trajectory_collide ? collision_cost_ : traj_cost; } diff --git a/nav2_mppi_controller/src/critics/path_align_critic.cpp b/nav2_mppi_controller/src/critics/path_align_critic.cpp index befa9e42eb9..028130de611 100644 --- a/nav2_mppi_controller/src/critics/path_align_critic.cpp +++ b/nav2_mppi_controller/src/critics/path_align_critic.cpp @@ -30,6 +30,17 @@ void PathAlignCritic::initialize() threshold_to_consider_, "threshold_to_consider", 0.5f); getParam(use_path_orientations_, "use_path_orientations", false); + getParam(visualize_furthest_point_, "visualize_furthest_point", false); + + if (visualize_furthest_point_) { + auto node = parent_.lock(); + if (node) { + furthest_point_pub_ = node->create_publisher( + "PathAlignCritic/furthest_reached_path_point", 1); + furthest_point_pub_->on_activate(); + } + } + RCLCPP_INFO( logger_, @@ -48,6 +59,23 @@ void PathAlignCritic::score(CriticData & data) // Up to furthest only, closest path point is always 0 from path handler const size_t path_segments_count = *data.furthest_reached_path_point; float path_segments_flt = static_cast(path_segments_count); + + // Visualize target pose if enabled + if (visualize_furthest_point_ && path_segments_count > 0 && + furthest_point_pub_->get_subscription_count() > 0) + { + auto furthest_point = std::make_unique(); + furthest_point->header.frame_id = costmap_ros_->getGlobalFrameID(); + furthest_point->header.stamp = clock_->now(); + furthest_point->pose.position.x = data.path.x(path_segments_count); + furthest_point->pose.position.y = data.path.y(path_segments_count); + furthest_point->pose.position.z = 0.0; + tf2::Quaternion quat; + quat.setRPY(0.0, 0.0, data.path.yaws(path_segments_count)); + furthest_point->pose.orientation = tf2::toMsg(quat); + furthest_point_pub_->publish(std::move(furthest_point)); + } + if (path_segments_count < offset_from_furthest_) { return; } diff --git a/nav2_mppi_controller/src/critics/path_angle_critic.cpp b/nav2_mppi_controller/src/critics/path_angle_critic.cpp index 6f42d33b042..68901f1c2af 100644 --- a/nav2_mppi_controller/src/critics/path_angle_critic.cpp +++ b/nav2_mppi_controller/src/critics/path_angle_critic.cpp @@ -41,6 +41,7 @@ void PathAngleCritic::initialize() getParam( max_angle_to_furthest_, "max_angle_to_furthest", 0.785398f); + getParam(visualize_furthest_point_, "visualize_furthest_point", false); int mode = 0; getParam(mode, "mode", mode); @@ -53,6 +54,15 @@ void PathAngleCritic::initialize() "don't allow for reversing! Setting mode to forward preference."); } + if (visualize_furthest_point_) { + auto node = parent_.lock(); + if (node) { + furthest_point_pub_ = node->create_publisher( + "PathAngleCritic/furthest_reached_path_point", 1); + furthest_point_pub_->on_activate(); + } + } + RCLCPP_INFO( logger_, "PathAngleCritic instantiated with %d power and %f weight. Mode set to: %s", @@ -74,6 +84,21 @@ void PathAngleCritic::score(CriticData & data) const float goal_y = data.path.y(offsetted_idx); const float goal_yaw = data.path.yaws(offsetted_idx); const geometry_msgs::msg::Pose & pose = data.state.pose.pose; + + // Visualize target pose if enabled + if (visualize_furthest_point_ && furthest_point_pub_->get_subscription_count() > 0) { + auto furthest_point = std::make_unique(); + furthest_point->header.frame_id = costmap_ros_->getGlobalFrameID(); + furthest_point->header.stamp = clock_->now(); + furthest_point->pose.position.x = goal_x; + furthest_point->pose.position.y = goal_y; + furthest_point->pose.position.z = 0.0; + tf2::Quaternion quat; + quat.setRPY(0.0, 0.0, goal_yaw); + furthest_point->pose.orientation = tf2::toMsg(quat); + furthest_point_pub_->publish(std::move(furthest_point)); + } + switch (mode_) { case PathAngleMode::FORWARD_PREFERENCE: diff --git a/nav2_mppi_controller/src/critics/path_follow_critic.cpp b/nav2_mppi_controller/src/critics/path_follow_critic.cpp index 240c49725d6..e62a465a270 100644 --- a/nav2_mppi_controller/src/critics/path_follow_critic.cpp +++ b/nav2_mppi_controller/src/critics/path_follow_critic.cpp @@ -29,6 +29,17 @@ void PathFollowCritic::initialize() getParam(offset_from_furthest_, "offset_from_furthest", 6); getParam(power_, "cost_power", 1); getParam(weight_, "cost_weight", 5.0f); + getParam(visualize_furthest_point_, "visualize_furthest_point", false); + + if (visualize_furthest_point_) { + auto node = parent_.lock(); + if (node) { + furthest_point_pub_ = node->create_publisher( + "PathFollowCritic/furthest_reached_path_point", 1); + furthest_point_pub_->on_activate(); + } + } + } void PathFollowCritic::score(CriticData & data) @@ -60,6 +71,20 @@ void PathFollowCritic::score(CriticData & data) const auto path_x = data.path.x(offsetted_idx); const auto path_y = data.path.y(offsetted_idx); + // Visualize target pose if enabled + if (visualize_furthest_point_ && furthest_point_pub_->get_subscription_count() > 0) { + auto furthest_point = std::make_unique(); + furthest_point->header.frame_id = costmap_ros_->getGlobalFrameID(); + furthest_point->header.stamp = clock_->now(); + furthest_point->pose.position.x = path_x; + furthest_point->pose.position.y = path_y; + furthest_point->pose.position.z = 0.0; + tf2::Quaternion quat; + quat.setRPY(0.0, 0.0, data.path.yaws(offsetted_idx)); + furthest_point->pose.orientation = tf2::toMsg(quat); + furthest_point_pub_->publish(std::move(furthest_point)); + } + const int && rightmost_idx = data.trajectories.x.cols() - 1; const auto last_x = data.trajectories.x.col(rightmost_idx); diff --git a/nav2_mppi_controller/src/critics/waypoint_critic.cpp b/nav2_mppi_controller/src/critics/waypoint_critic.cpp new file mode 100644 index 00000000000..ca8d5f1c3b6 --- /dev/null +++ b/nav2_mppi_controller/src/critics/waypoint_critic.cpp @@ -0,0 +1,207 @@ +// Copyright (c) 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "nav2_mppi_controller/critics/waypoint_critic.hpp" +#include +#include +#include +#include + +namespace mppi::critics +{ + +void WaypointCritic::initialize() +{ + auto getParentParam = parameters_handler_->getParamGetter(parent_name_); + auto getParam = parameters_handler_->getParamGetter(name_); + + getParam(power_, "cost_power", 1); + getParam(weight_, "cost_weight", 1.0f); + getParam(position_tolerance_, "position_tolerance", 0.1f); + getParam(yaw_tolerance_, "yaw_tolerance", 0.1f); + getParam(waypoint_topic_, "waypoint_topic", std::string("truncated_path")); + getParam(use_orientation_, "use_orientation", true); + getParam(max_waypoint_distance_, "max_waypoint_distance", 10.0f); + + RCLCPP_INFO( + logger_, + "WaypointCritic instantiated with %d power, %f weight, " + "position tolerance: %f, yaw tolerance: %f, topic: %s", + power_, weight_, position_tolerance_, yaw_tolerance_, waypoint_topic_.c_str()); + + // Create subscription to waypoint topic + auto node = parent_.lock(); + if (node) { + waypoint_sub_ = node->create_subscription( + waypoint_topic_, + std::bind(&WaypointCritic::waypointCallback, this, std::placeholders::_1), + rclcpp::QoS(10).reliable()); + tf_buffer_ = std::make_shared(node->get_clock()); + tf_listener_ = std::make_shared(*tf_buffer_, node, false); + + RCLCPP_INFO(logger_, "WaypointCritic subscribed to topic: %s", waypoint_topic_.c_str()); + } else { + RCLCPP_ERROR(logger_, "Failed to get parent node for WaypointCritic subscription"); + } +} + +void WaypointCritic::waypointCallback(const nav_msgs::msg::Path::SharedPtr msg) +{ + std::lock_guard lock(waypoints_mutex_); + waypoints_ = msg->poses; + waypoints_frame_ = msg->header.frame_id; + RCLCPP_INFO( + logger_, + "WaypointCritic received %zu waypoints", + waypoints_.size()); +} + +void WaypointCritic::score(CriticData & data) +{ + if (!enabled_) { + return; + } + + std::lock_guard lock(waypoints_mutex_); + + // If no waypoints available, don't apply any cost + if (waypoints_.empty()) { + return; + } + + // Get current robot pose + const auto & robot_x = data.state.pose.pose.position.x; + const auto & robot_y = data.state.pose.pose.position.y; + + // Always target the first waypoint (assuming completed waypoints are removed by publisher) + int target_waypoint_idx = getTargetWaypoint(); + + if (target_waypoint_idx < 0) { + return; + } + + const std::string target_frame = data.state.pose.header.frame_id; + geometry_msgs::msg::PoseStamped wp_in = waypoints_[target_waypoint_idx]; + if (wp_in.header.frame_id.empty()) { + wp_in.header.frame_id = waypoints_frame_; + } + geometry_msgs::msg::PoseStamped wp_tf; + if (wp_in.header.frame_id == target_frame || !tf_buffer_) { + wp_tf = wp_in; + } else { + if (!tf_buffer_->canTransform( + target_frame, wp_in.header.frame_id, wp_in.header.stamp, + tf2::durationFromSec(0.0))) + { + RCLCPP_WARN(logger_, "TF %s->%s not available yet", + wp_in.header.frame_id.c_str(), target_frame.c_str()); + return; + } + tf_buffer_->transform(wp_in, wp_tf, target_frame, tf2::durationFromSec(0.0)); + } + const float target_x = static_cast(wp_tf.pose.position.x); + const float target_y = static_cast(wp_tf.pose.position.y); + const float target_yaw = static_cast(tf2::getYaw(wp_tf.pose.orientation)); + + +// const auto & plan = data.path; +// if (plan.x.size() == 0) { +// return; +// } +// RCLCPP_INFO( +// logger_, +// "data.path: size=%zu frame=%s", +// static_cast(plan.x.size()), +// data.state.pose.header.frame_id.c_str()); +// const float target_x = plan.x(0); +// const float target_y = plan.y(0); +// const float target_yaw = (plan.yaws.size() > 0) ? plan.yaws(0) : 0.0f; + + + // Check if waypoint is within reasonable distance + double waypoint_distance = calculateDistance(robot_x, robot_y, target_x, target_y); + if (waypoint_distance > max_waypoint_distance_) { + return; + } + + + // Calculate position costs for all trajectories + const auto delta_x = data.trajectories.x - target_x; + const auto delta_y = data.trajectories.y - target_y; + // RCLCPP_INFO( + // logger_, + // "Traj mean: x=%.3f y=%.3f | target=(%.3f, %.3f)", + // static_cast(data.trajectories.x.mean()), + // static_cast(data.trajectories.y.mean()), + // target_x, target_y); + + // Calculate euclidean distance cost + auto position_distances = (delta_x.square() + delta_y.square()).sqrt(); + + // Apply position tolerance - penalize trajectories that are farther than tolerance + auto position_costs = (position_distances - position_tolerance_).cwiseMax(0.0f); + + // If orientation is considered, add yaw cost + Eigen::ArrayXf total_costs = position_costs; + + if (use_orientation_) { + // Calculate yaw differences for all trajectories using utils function + auto yaw_differences = utils::shortest_angular_distance(data.trajectories.yaws, target_yaw); + + // Apply yaw tolerance + auto yaw_costs = (yaw_differences.abs() - yaw_tolerance_).cwiseMax(0.0f); + + // Combine position and yaw costs + total_costs = position_costs + yaw_costs; + } + + // Apply power and weight + if (power_ > 1u) { + data.costs += (total_costs.rowwise().mean() * weight_).pow(power_); + } else { + data.costs += (total_costs.rowwise().mean() * weight_).eval(); + } +} + +int WaypointCritic::getTargetWaypoint() +{ + if (waypoints_.empty()) { + return -1; + } + + // Always target the first waypoint in the list + // (assuming the publisher removes completed waypoints) + return 0; +} + +double WaypointCritic::calculateDistance(double x1, double y1, double x2, double y2) +{ + double dx = x2 - x1; + double dy = y2 - y1; + return sqrt(dx * dx + dy * dy); +} + +double WaypointCritic::calculateAngularDifference(double angle1, double angle2) +{ + double diff = angle2 - angle1; + while (diff > M_PI) diff -= 2.0 * M_PI; + while (diff < -M_PI) diff += 2.0 * M_PI; + return diff; +} + +} // namespace mppi::critics + +#include + +PLUGINLIB_EXPORT_CLASS(mppi::critics::WaypointCritic, mppi::critics::CriticFunction) \ No newline at end of file diff --git a/nav2_mppi_controller/src/optimizer copy 2.cpp b/nav2_mppi_controller/src/optimizer copy 2.cpp new file mode 100644 index 00000000000..013b29de502 --- /dev/null +++ b/nav2_mppi_controller/src/optimizer copy 2.cpp @@ -0,0 +1,586 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// Copyright (c) 2025 Open Navigation LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "nav2_mppi_controller/optimizer.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "nav2_core/controller_exceptions.hpp" +#include "nav2_costmap_2d/costmap_filters/filter_values.hpp" +#include "nav2_ros_common/node_utils.hpp" + +namespace mppi +{ + +void Optimizer::initialize( + nav2::LifecycleNode::WeakPtr parent, const std::string & name, + std::shared_ptr costmap_ros, + std::shared_ptr tf_buffer, + ParametersHandler * param_handler) +{ + parent_ = parent; + name_ = name; + costmap_ros_ = costmap_ros; + costmap_ = costmap_ros_->getCostmap(); + parameters_handler_ = param_handler; + tf_buffer_ = tf_buffer; + + auto node = parent_.lock(); + logger_ = node->get_logger(); + + getParams(); + + critic_manager_.on_configure(parent_, name_, costmap_ros_, parameters_handler_); + noise_generator_.initialize(settings_, isHolonomic(), name_, parameters_handler_); + + // This may throw an exception if not valid and fail initialization + nav2::declare_parameter_if_not_declared( + node, name_ + ".TrajectoryValidator.plugin", + rclcpp::ParameterValue("mppi::DefaultOptimalTrajectoryValidator")); + std::string validator_plugin_type = nav2::get_plugin_type_param( + node, name_ + ".TrajectoryValidator"); + validator_loader_ = std::make_unique>( + "nav2_mppi_controller", "mppi::OptimalTrajectoryValidator"); + trajectory_validator_ = validator_loader_->createUniqueInstance(validator_plugin_type); + trajectory_validator_->initialize( + parent_, name_ + ".TrajectoryValidator", + costmap_ros_, parameters_handler_, tf_buffer, settings_); + RCLCPP_INFO(logger_, "Loaded trajectory validator plugin: %s", validator_plugin_type.c_str()); + + reset(); +} + +void Optimizer::shutdown() +{ + noise_generator_.shutdown(); +} + +void Optimizer::getParams() +{ + std::string motion_model_name; + + auto & s = settings_; + auto getParam = parameters_handler_->getParamGetter(name_); + auto getParentParam = parameters_handler_->getParamGetter(""); + getParam(s.model_dt, "model_dt", 0.05f); + getParam(s.time_steps, "time_steps", 56); + getParam(s.batch_size, "batch_size", 1000); + getParam(s.iteration_count, "iteration_count", 1); + getParam(s.temperature, "temperature", 0.3f); + getParam(s.gamma, "gamma", 0.015f); + getParam(s.base_constraints.vx_max, "vx_max", 0.5f); + getParam(s.base_constraints.vx_min, "vx_min", -0.35f); + getParam(s.base_constraints.vy, "vy_max", 0.5f); + getParam(s.base_constraints.wz, "wz_max", 1.9f); + getParam(s.base_constraints.ax_max, "ax_max", 3.0f); + getParam(s.base_constraints.ax_min, "ax_min", -3.0f); + getParam(s.base_constraints.ay_max, "ay_max", 3.0f); + getParam(s.base_constraints.ay_min, "ay_min", -3.0f); + getParam(s.base_constraints.az_max, "az_max", 3.5f); + getParam(s.sampling_std.vx, "vx_std", 0.2f); + getParam(s.sampling_std.vy, "vy_std", 0.2f); + getParam(s.sampling_std.wz, "wz_std", 0.4f); + getParam(s.retry_attempt_limit, "retry_attempt_limit", 1); + + s.base_constraints.ax_max = fabs(s.base_constraints.ax_max); + if (s.base_constraints.ax_min > 0.0) { + s.base_constraints.ax_min = -1.0 * s.base_constraints.ax_min; + RCLCPP_WARN( + logger_, + "Sign of the parameter ax_min is incorrect, consider setting it negative."); + } + + if (s.base_constraints.ay_min > 0.0) { + s.base_constraints.ay_min = -1.0 * s.base_constraints.ay_min; + RCLCPP_WARN( + logger_, + "Sign of the parameter ay_min is incorrect, consider setting it negative."); + } + + + getParam(motion_model_name, "motion_model", std::string("DiffDrive")); + + s.constraints = s.base_constraints; + + setMotionModel(motion_model_name); + parameters_handler_->addPostCallback([this]() {reset();}); + + double controller_frequency; + getParentParam(controller_frequency, "controller_frequency", 0.0, ParameterType::Static); + setOffset(controller_frequency); +} + +void Optimizer::setOffset(double controller_frequency) +{ + const double controller_period = 1.0 / controller_frequency; + constexpr double eps = 1e-6; + + if ((controller_period + eps) < settings_.model_dt) { + RCLCPP_WARN( + logger_, + "Controller period is less then model dt, consider setting it equal"); + } else if (abs(controller_period - settings_.model_dt) < eps) { + RCLCPP_INFO( + logger_, + "Controller period is equal to model dt. Control sequence " + "shifting is ON"); + settings_.shift_control_sequence = true; + } else { + throw nav2_core::ControllerException( + "Controller period more then model dt, set it equal to model dt"); + } +} + +void Optimizer::reset(bool reset_dynamic_speed_limits) +{ + state_.reset(settings_.batch_size, settings_.time_steps); + control_sequence_.reset(settings_.time_steps); + control_history_[0] = {0.0f, 0.0f, 0.0f}; + control_history_[1] = {0.0f, 0.0f, 0.0f}; + control_history_[2] = {0.0f, 0.0f, 0.0f}; + control_history_[3] = {0.0f, 0.0f, 0.0f}; + + if (reset_dynamic_speed_limits) { + settings_.constraints = settings_.base_constraints; + } + + costs_.setZero(settings_.batch_size); + generated_trajectories_.reset(settings_.batch_size, settings_.time_steps); + + noise_generator_.reset(settings_, isHolonomic()); + motion_model_->initialize(settings_.constraints, settings_.model_dt); + trajectory_validator_->initialize( + parent_, name_ + ".TrajectoryValidator", + costmap_ros_, parameters_handler_, tf_buffer_, settings_); + + RCLCPP_INFO(logger_, "Optimizer reset"); +} + +bool Optimizer::isHolonomic() const +{ + return motion_model_->isHolonomic(); +} + +std::tuple Optimizer::evalControl( + const geometry_msgs::msg::PoseStamped & robot_pose, + const geometry_msgs::msg::Twist & robot_speed, + const nav_msgs::msg::Path & plan, + const geometry_msgs::msg::Pose & goal, + nav2_core::GoalChecker * goal_checker) +{ + prepare(robot_pose, robot_speed, plan, goal, goal_checker); + Eigen::ArrayXXf optimal_trajectory; + bool trajectory_valid = true; + + do { + optimize(); + optimal_trajectory = getOptimizedTrajectory(); + switch (trajectory_validator_->validateTrajectory( + optimal_trajectory, control_sequence_, robot_pose, robot_speed, plan, goal)) + { + case mppi::ValidationResult::SOFT_RESET: + trajectory_valid = false; + RCLCPP_WARN(logger_, "Soft reset triggered by trajectory validator"); + break; + case mppi::ValidationResult::FAILURE: + throw nav2_core::NoValidControl( + "Trajectory validator failed to validate trajectory, hard reset triggered."); + case mppi::ValidationResult::SUCCESS: + default: + trajectory_valid = true; + break; + } + } while (fallback(critics_data_.fail_flag || !trajectory_valid)); + + utils::savitskyGolayFilter(control_sequence_, control_history_, settings_); + auto control = getControlFromSequenceAsTwist(plan.header.stamp); + + if (settings_.shift_control_sequence) { + shiftControlSequence(); + } + + return std::make_tuple(control, optimal_trajectory); +} + +void Optimizer::optimize() +{ + for (size_t i = 0; i < settings_.iteration_count; ++i) { + generateNoisedTrajectories(); + critic_manager_.evalTrajectoriesScores(critics_data_); + updateControlSequence(); + } +} + +bool Optimizer::fallback(bool fail) +{ + static size_t counter = 0; + + if (!fail) { + counter = 0; + return false; + } + + reset(); + + if (++counter > settings_.retry_attempt_limit) { + counter = 0; + throw nav2_core::NoValidControl("Optimizer fail to compute path"); + } + + return true; +} + +void Optimizer::prepare( + const geometry_msgs::msg::PoseStamped & robot_pose, + const geometry_msgs::msg::Twist & robot_speed, + const nav_msgs::msg::Path & plan, + const geometry_msgs::msg::Pose & goal, + nav2_core::GoalChecker * goal_checker) +{ + state_.pose = robot_pose; + state_.speed = robot_speed; + state_.local_path_length = nav2_util::geometry_utils::calculate_path_length(plan); + path_ = utils::toTensor(plan); + costs_.setZero(); + goal_ = goal; + + critics_data_.fail_flag = false; + critics_data_.goal_checker = goal_checker; + critics_data_.motion_model = motion_model_; + critics_data_.furthest_reached_path_point.reset(); + critics_data_.path_pts_valid.reset(); +} + +void Optimizer::shiftControlSequence() +{ + auto size = control_sequence_.vx.size(); + utils::shiftColumnsByOnePlace(control_sequence_.vx, -1); + utils::shiftColumnsByOnePlace(control_sequence_.wz, -1); + control_sequence_.vx(size - 1) = control_sequence_.vx(size - 2); + control_sequence_.wz(size - 1) = control_sequence_.wz(size - 2); + + if (isHolonomic()) { + utils::shiftColumnsByOnePlace(control_sequence_.vy, -1); + control_sequence_.vy(size - 1) = control_sequence_.vy(size - 2); + } +} + +void Optimizer::generateNoisedTrajectories() +{ + noise_generator_.setNoisedControls(state_, control_sequence_); + noise_generator_.generateNextNoises(); + updateStateVelocities(state_); + integrateStateVelocities(generated_trajectories_, state_); +} + +void Optimizer::applyControlSequenceConstraints() +{ + auto & s = settings_; + + float max_delta_vx = s.model_dt * s.constraints.ax_max; + float min_delta_vx = s.model_dt * s.constraints.ax_min; + float max_delta_vy = s.model_dt * s.constraints.ay_max; + float min_delta_vy = s.model_dt * s.constraints.ay_min; + float max_delta_wz = s.model_dt * s.constraints.az_max; + float vx_last = utils::clamp(s.constraints.vx_min, s.constraints.vx_max, control_sequence_.vx(0)); + float wz_last = utils::clamp(-s.constraints.wz, s.constraints.wz, control_sequence_.wz(0)); + control_sequence_.vx(0) = vx_last; + control_sequence_.wz(0) = wz_last; + float vy_last = 0; + if (isHolonomic()) { + vy_last = utils::clamp(-s.constraints.vy, s.constraints.vy, control_sequence_.vy(0)); + control_sequence_.vy(0) = vy_last; + } + + for (unsigned int i = 1; i != control_sequence_.vx.size(); i++) { + float & vx_curr = control_sequence_.vx(i); + vx_curr = utils::clamp(s.constraints.vx_min, s.constraints.vx_max, vx_curr); + if(vx_last > 0) { + vx_curr = utils::clamp(vx_last + min_delta_vx, vx_last + max_delta_vx, vx_curr); + } else { + vx_curr = utils::clamp(vx_last - max_delta_vx, vx_last - min_delta_vx, vx_curr); + } + vx_last = vx_curr; + + float & wz_curr = control_sequence_.wz(i); + wz_curr = utils::clamp(-s.constraints.wz, s.constraints.wz, wz_curr); + wz_curr = utils::clamp(wz_last - max_delta_wz, wz_last + max_delta_wz, wz_curr); + wz_last = wz_curr; + + if (isHolonomic()) { + float & vy_curr = control_sequence_.vy(i); + vy_curr = utils::clamp(-s.constraints.vy, s.constraints.vy, vy_curr); + if(vy_last > 0) { + vy_curr = utils::clamp(vy_last + min_delta_vy, vy_last + max_delta_vy, vy_curr); + } else { + vy_curr = utils::clamp(vy_last - max_delta_vy, vy_last - min_delta_vy, vy_curr); + } + vy_last = vy_curr; + } + } + + motion_model_->applyConstraints(control_sequence_); +} + +void Optimizer::updateStateVelocities( + models::State & state) const +{ + updateInitialStateVelocities(state); + propagateStateVelocitiesFromInitials(state); +} + +void Optimizer::updateInitialStateVelocities( + models::State & state) const +{ + state.vx.col(0) = static_cast(state.speed.linear.x); + state.wz.col(0) = static_cast(state.speed.angular.z); + + if (isHolonomic()) { + state.vy.col(0) = static_cast(state.speed.linear.y); + } +} + +void Optimizer::propagateStateVelocitiesFromInitials( + models::State & state) const +{ + motion_model_->predict(state); +} + +void Optimizer::integrateStateVelocities( + Eigen::Array & trajectory, + const Eigen::ArrayXXf & sequence) const +{ + float initial_yaw = static_cast(tf2::getYaw(state_.pose.pose.orientation)); + + const auto vx = sequence.col(0); + const auto wz = sequence.col(1); + + auto traj_x = trajectory.col(0); + auto traj_y = trajectory.col(1); + auto traj_yaws = trajectory.col(2); + + const size_t n_size = traj_yaws.size(); + if (n_size == 0) { + return; + } + + float last_yaw = initial_yaw; + for(size_t i = 0; i != n_size; i++) { + last_yaw += wz(i) * settings_.model_dt; + traj_yaws(i) = last_yaw; + } + + Eigen::ArrayXf yaw_cos = traj_yaws.cos(); + Eigen::ArrayXf yaw_sin = traj_yaws.sin(); + utils::shiftColumnsByOnePlace(yaw_cos, 1); + utils::shiftColumnsByOnePlace(yaw_sin, 1); + yaw_cos(0) = cosf(initial_yaw); + yaw_sin(0) = sinf(initial_yaw); + + auto dx = (vx * yaw_cos).eval(); + auto dy = (vx * yaw_sin).eval(); + + if (isHolonomic()) { + auto vy = sequence.col(2); + dx = (dx - vy * yaw_sin).eval(); + dy = (dy + vy * yaw_cos).eval(); + } + + float last_x = state_.pose.pose.position.x; + float last_y = state_.pose.pose.position.y; + for(size_t i = 0; i != n_size; i++) { + last_x += dx(i) * settings_.model_dt; + last_y += dy(i) * settings_.model_dt; + traj_x(i) = last_x; + traj_y(i) = last_y; + } +} + +void Optimizer::integrateStateVelocities( + models::Trajectories & trajectories, + const models::State & state) const +{ + auto initial_yaw = static_cast(tf2::getYaw(state.pose.pose.orientation)); + const size_t n_cols = trajectories.yaws.cols(); + + Eigen::ArrayXf last_yaws = Eigen::ArrayXf::Constant(trajectories.yaws.rows(), initial_yaw); + for (size_t i = 0; i != n_cols; i++) { + last_yaws += state.wz.col(i) * settings_.model_dt; + trajectories.yaws.col(i) = last_yaws; + } + + Eigen::ArrayXXf yaw_cos = trajectories.yaws.cos(); + Eigen::ArrayXXf yaw_sin = trajectories.yaws.sin(); + utils::shiftColumnsByOnePlace(yaw_cos, 1); + utils::shiftColumnsByOnePlace(yaw_sin, 1); + yaw_cos.col(0) = cosf(initial_yaw); + yaw_sin.col(0) = sinf(initial_yaw); + + auto dx = (state.vx * yaw_cos).eval(); + auto dy = (state.vx * yaw_sin).eval(); + + if (isHolonomic()) { + dx -= state.vy * yaw_sin; + dy += state.vy * yaw_cos; + } + + Eigen::ArrayXf last_x = Eigen::ArrayXf::Constant(trajectories.x.rows(), + state.pose.pose.position.x); + Eigen::ArrayXf last_y = Eigen::ArrayXf::Constant(trajectories.y.rows(), + state.pose.pose.position.y); + + for (size_t i = 0; i != n_cols; i++) { + last_x += dx.col(i) * settings_.model_dt; + last_y += dy.col(i) * settings_.model_dt; + trajectories.x.col(i) = last_x; + trajectories.y.col(i) = last_y; + } +} + +Eigen::ArrayXXf Optimizer::getOptimizedTrajectory() +{ + const bool is_holo = isHolonomic(); + Eigen::ArrayXXf sequence = Eigen::ArrayXXf(settings_.time_steps, is_holo ? 3 : 2); + Eigen::Array trajectories = + Eigen::Array(settings_.time_steps, 3); + + sequence.col(0) = control_sequence_.vx; + sequence.col(1) = control_sequence_.wz; + + if (is_holo) { + sequence.col(2) = control_sequence_.vy; + } + + integrateStateVelocities(trajectories, sequence); + return trajectories; +} + +const models::ControlSequence & Optimizer::getOptimalControlSequence() +{ + return control_sequence_; +} + +void Optimizer::updateControlSequence() +{ + const bool is_holo = isHolonomic(); + auto & s = settings_; + + auto vx_T = control_sequence_.vx.transpose(); + auto bounded_noises_vx = state_.cvx.rowwise() - vx_T; + const float gamma_vx = s.gamma / (s.sampling_std.vx * s.sampling_std.vx); + costs_ += (gamma_vx * (bounded_noises_vx.rowwise() * vx_T).rowwise().sum()).eval(); + + if (s.sampling_std.wz > 0.0f) { + auto wz_T = control_sequence_.wz.transpose(); + auto bounded_noises_wz = state_.cwz.rowwise() - wz_T; + const float gamma_wz = s.gamma / (s.sampling_std.wz * s.sampling_std.wz); + costs_ += (gamma_wz * (bounded_noises_wz.rowwise() * wz_T).rowwise().sum()).eval(); + } + + if (is_holo) { + auto vy_T = control_sequence_.vy.transpose(); + auto bounded_noises_vy = state_.cvy.rowwise() - vy_T; + const float gamma_vy = s.gamma / (s.sampling_std.vy * s.sampling_std.vy); + costs_ += (gamma_vy * (bounded_noises_vy.rowwise() * vy_T).rowwise().sum()).eval(); + } + + auto costs_normalized = costs_ - costs_.minCoeff(); + const float inv_temp = 1.0f / s.temperature; + auto softmaxes = (-inv_temp * costs_normalized).exp().eval(); + softmaxes /= softmaxes.sum(); + + auto softmax_mat = softmaxes.matrix(); + control_sequence_.vx = state_.cvx.transpose().matrix() * softmax_mat; + control_sequence_.wz = state_.cwz.transpose().matrix() * softmax_mat; + + if (is_holo) { + control_sequence_.vy = state_.cvy.transpose().matrix() * softmax_mat; + } + + applyControlSequenceConstraints(); +} + +geometry_msgs::msg::TwistStamped Optimizer::getControlFromSequenceAsTwist( + const builtin_interfaces::msg::Time & stamp) +{ + unsigned int offset = settings_.shift_control_sequence ? 1 : 0; + + auto vx = control_sequence_.vx(offset); + auto wz = control_sequence_.wz(offset); + + if (isHolonomic()) { + auto vy = control_sequence_.vy(offset); + return utils::toTwistStamped(vx, vy, wz, stamp, costmap_ros_->getBaseFrameID()); + } + + return utils::toTwistStamped(vx, wz, stamp, costmap_ros_->getBaseFrameID()); +} + +void Optimizer::setMotionModel(const std::string & model) +{ + if (model == "DiffDrive") { + motion_model_ = std::make_shared(); + } else if (model == "Omni") { + motion_model_ = std::make_shared(); + } else if (model == "Ackermann") { + motion_model_ = std::make_shared(parameters_handler_, name_); + } else { + throw nav2_core::ControllerException( + std::string( + "Model " + model + " is not valid! Valid options are DiffDrive, Omni, " + "or Ackermann")); + } + motion_model_->initialize(settings_.constraints, settings_.model_dt); +} + +void Optimizer::setSpeedLimit(double speed_limit, bool percentage) +{ + auto & s = settings_; + if (speed_limit == nav2_costmap_2d::NO_SPEED_LIMIT) { + s.constraints.vx_max = s.base_constraints.vx_max; + s.constraints.vx_min = s.base_constraints.vx_min; + s.constraints.vy = s.base_constraints.vy; + s.constraints.wz = s.base_constraints.wz; + } else { + if (percentage) { + // Speed limit is expressed in % from maximum speed of robot + double ratio = speed_limit / 100.0; + s.constraints.vx_max = s.base_constraints.vx_max * ratio; + s.constraints.vx_min = s.base_constraints.vx_min * ratio; + s.constraints.vy = s.base_constraints.vy * ratio; + s.constraints.wz = s.base_constraints.wz * ratio; + } else { + // Speed limit is expressed in absolute value + double ratio = speed_limit / s.base_constraints.vx_max; + s.constraints.vx_max = s.base_constraints.vx_max * ratio; + s.constraints.vx_min = s.base_constraints.vx_min * ratio; + s.constraints.vy = s.base_constraints.vy * ratio; + s.constraints.wz = s.base_constraints.wz * ratio; + } + } +} + +models::Trajectories & Optimizer::getGeneratedTrajectories() +{ + return generated_trajectories_; +} + +} // namespace mppi diff --git a/nav2_mppi_controller/src/optimizer.cpp b/nav2_mppi_controller/src/optimizer.cpp index 013b29de502..b12c7dd6925 100644 --- a/nav2_mppi_controller/src/optimizer.cpp +++ b/nav2_mppi_controller/src/optimizer.cpp @@ -267,6 +267,7 @@ void Optimizer::prepare( critics_data_.motion_model = motion_model_; critics_data_.furthest_reached_path_point.reset(); critics_data_.path_pts_valid.reset(); + critics_data_.trajectories_in_collision.reset(); } void Optimizer::shiftControlSequence() diff --git a/nav2_mppi_controller/src/path_handler copy 2.cpp b/nav2_mppi_controller/src/path_handler copy 2.cpp new file mode 100644 index 00000000000..b49e8991306 --- /dev/null +++ b/nav2_mppi_controller/src/path_handler copy 2.cpp @@ -0,0 +1,247 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// Copyright (c) 2023 Dexory +// Copyright (c) 2023 Open Navigation LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "nav2_mppi_controller/tools/path_handler.hpp" +#include "nav2_mppi_controller/tools/utils.hpp" +#include "nav2_costmap_2d/costmap_2d_ros.hpp" +#include "nav2_util/robot_utils.hpp" + +namespace mppi +{ + +void PathHandler::initialize( + nav2::LifecycleNode::WeakPtr parent, const std::string & name, + std::shared_ptr costmap, + std::shared_ptr buffer, ParametersHandler * param_handler) +{ + name_ = name; + costmap_ = costmap; + tf_buffer_ = buffer; + auto node = parent.lock(); + logger_ = node->get_logger(); + parameters_handler_ = param_handler; + + auto getParam = parameters_handler_->getParamGetter(name_); + getParam(max_robot_pose_search_dist_, "max_robot_pose_search_dist", getMaxCostmapDist()); + getParam(prune_distance_, "prune_distance", 1.5); + getParam(transform_tolerance_, "transform_tolerance", 0.1); + getParam(enforce_path_inversion_, "enforce_path_inversion", false); + getParam(enforce_path_rotation_, "enforce_path_rotation", false); + + if (enforce_path_inversion_ || enforce_path_rotation_) { + getParam(inversion_xy_tolerance_, "inversion_xy_tolerance", 0.2); + getParam(inversion_yaw_tolerance_, "inversion_yaw_tolerance", 0.4); + getParam(minimum_rotation_angle_, "minimum_rotation_angle", 0.785); + inversion_locale_ = 0u; + } +} + +std::pair +PathHandler::getGlobalPlanConsideringBoundsInCostmapFrame( + const geometry_msgs::msg::PoseStamped & global_pose) +{ + using nav2_util::geometry_utils::euclidean_distance; + + auto begin = global_plan_up_to_inversion_.poses.begin(); + + // Limit the search for the closest pose up to max_robot_pose_search_dist on the path + auto closest_pose_upper_bound = + nav2_util::geometry_utils::first_after_integrated_distance( + global_plan_up_to_inversion_.poses.begin(), global_plan_up_to_inversion_.poses.end(), + max_robot_pose_search_dist_); + + // Find closest point to the robot + auto closest_point = nav2_util::geometry_utils::min_by( + begin, closest_pose_upper_bound, + [&global_pose](const geometry_msgs::msg::PoseStamped & ps) { + return euclidean_distance(global_pose, ps); + }); + + nav_msgs::msg::Path transformed_plan; + transformed_plan.header.frame_id = costmap_->getGlobalFrameID(); + transformed_plan.header.stamp = global_pose.header.stamp; + + auto pruned_plan_end = + nav2_util::geometry_utils::first_after_integrated_distance( + closest_point, global_plan_up_to_inversion_.poses.end(), prune_distance_); + + unsigned int mx, my; + // Find the furthest relevant pose on the path to consider within costmap + // bounds + // Transforming it to the costmap frame in the same loop + for (auto global_plan_pose = closest_point; global_plan_pose != pruned_plan_end; + ++global_plan_pose) + { + // Transform from global plan frame to costmap frame + geometry_msgs::msg::PoseStamped costmap_plan_pose; + global_plan_pose->header.stamp = global_pose.header.stamp; + global_plan_pose->header.frame_id = global_plan_.header.frame_id; + nav2_util::transformPoseInTargetFrame(*global_plan_pose, costmap_plan_pose, *tf_buffer_, + costmap_->getGlobalFrameID(), transform_tolerance_); + + // Check if pose is inside the costmap + if (!costmap_->getCostmap()->worldToMap( + costmap_plan_pose.pose.position.x, costmap_plan_pose.pose.position.y, mx, my)) + { + return {transformed_plan, closest_point}; + } + + // Filling the transformed plan to return with the transformed pose + transformed_plan.poses.push_back(costmap_plan_pose); + } + + return {transformed_plan, closest_point}; +} + +geometry_msgs::msg::PoseStamped PathHandler::transformToGlobalPlanFrame( + const geometry_msgs::msg::PoseStamped & pose) +{ + if (global_plan_up_to_inversion_.poses.empty()) { + throw nav2_core::InvalidPath("Received plan with zero length"); + } + + geometry_msgs::msg::PoseStamped robot_pose; + if (!nav2_util::transformPoseInTargetFrame(pose, robot_pose, *tf_buffer_, + global_plan_up_to_inversion_.header.frame_id, transform_tolerance_)) + { + throw nav2_core::ControllerTFError( + "Unable to transform robot pose into global plan's frame"); + } + + return robot_pose; +} + +nav_msgs::msg::Path PathHandler::transformPath( + const geometry_msgs::msg::PoseStamped & robot_pose) +{ + // Find relevant bounds of path to use + geometry_msgs::msg::PoseStamped global_pose = + transformToGlobalPlanFrame(robot_pose); + auto [transformed_plan, lower_bound] = getGlobalPlanConsideringBoundsInCostmapFrame(global_pose); + + prunePlan(global_plan_up_to_inversion_, lower_bound); + + if ((enforce_path_inversion_ || enforce_path_rotation_) && inversion_locale_ != 0u) { + if (isWithinInversionTolerances(global_pose)) { + // Robot has reached the inversion/rotation point, unlock the rest of the path + prunePlan(global_plan_, global_plan_.poses.begin() + inversion_locale_); + global_plan_up_to_inversion_ = global_plan_; + + double max_yaw_delta = 0.0; + + for (size_t i = 1; i < global_plan_.poses.size(); i++) { + double yaw_prev = tf2::getYaw(global_plan_.poses[i-1].pose.orientation); + double yaw_curr = tf2::getYaw(global_plan_.poses[i].pose.orientation); + double diff = fabs(angles::shortest_angular_distance(yaw_prev, yaw_curr)); + if (diff > max_yaw_delta) { + max_yaw_delta = diff; + } + } + + // Recompute locale on the updated path + float rotation_threshold = enforce_path_rotation_ ? minimum_rotation_angle_ : 0.0f; + inversion_locale_ = utils::removePosesAfterFirstInversion( + global_plan_up_to_inversion_, rotation_threshold); + + RCLCPP_INFO( + logger_, + "[MPPI PathHandler] setPath: rotation_threshold=%.3f, inversion_locale=%u, path_size=%zu", + rotation_threshold, + inversion_locale_, + global_plan_up_to_inversion_.poses.size()); + + } + } + else{ + // RCLCPP_INFO( + // logger_, + // "[MPPI PathHandler] step: enforce_path_inversion=%s, enforce_path_rotation=%s, inversion_locale=%u", + // enforce_path_inversion_ ? "true" : "false", + // enforce_path_rotation_ ? "true" : "false", + // inversion_locale_); + + } + + if (transformed_plan.poses.empty()) { + throw nav2_core::InvalidPath("Resulting plan has 0 poses in it."); + } + + return transformed_plan; +} + +double PathHandler::getMaxCostmapDist() +{ + const auto & costmap = costmap_->getCostmap(); + return static_cast(std::max(costmap->getSizeInCellsX(), costmap->getSizeInCellsY())) * + costmap->getResolution() * 0.50; +} + +void PathHandler::setPath(const nav_msgs::msg::Path & plan) +{ + global_plan_ = plan; + global_plan_up_to_inversion_ = plan; + if (!enforce_path_inversion_ && !enforce_path_rotation_) { + inversion_locale_ = 0u; + return; + } + + // Find and restrict to the first rotation or inversion constraint + float rotation_threshold = enforce_path_rotation_ ? minimum_rotation_angle_ : 0.0f; + inversion_locale_ = utils::removePosesAfterFirstInversion( + global_plan_up_to_inversion_, rotation_threshold); +} + +nav_msgs::msg::Path & PathHandler::getPath() {return global_plan_;} + +void PathHandler::prunePlan(nav_msgs::msg::Path & plan, const PathIterator end) +{ + plan.poses.erase(plan.poses.begin(), end); +} + +geometry_msgs::msg::PoseStamped PathHandler::getTransformedGoal( + const builtin_interfaces::msg::Time & stamp) +{ + auto goal = global_plan_.poses.back(); + goal.header.frame_id = global_plan_.header.frame_id; + goal.header.stamp = stamp; + if (goal.header.frame_id.empty()) { + throw nav2_core::ControllerTFError("Goal pose has an empty frame_id"); + } + geometry_msgs::msg::PoseStamped transformed_goal; + if (!nav2_util::transformPoseInTargetFrame(goal, transformed_goal, *tf_buffer_, + costmap_->getGlobalFrameID(), transform_tolerance_)) + { + throw nav2_core::ControllerTFError("Unable to transform goal pose into costmap frame"); + } + return transformed_goal; +} + +bool PathHandler::isWithinInversionTolerances(const geometry_msgs::msg::PoseStamped & robot_pose) +{ + // Keep full path if we are within tolerance of the inversion pose + const auto last_pose = global_plan_up_to_inversion_.poses.back(); + float distance = hypotf( + robot_pose.pose.position.x - last_pose.pose.position.x, + robot_pose.pose.position.y - last_pose.pose.position.y); + + float angle_distance = angles::shortest_angular_distance( + tf2::getYaw(robot_pose.pose.orientation), + tf2::getYaw(last_pose.pose.orientation)); + + return distance <= inversion_xy_tolerance_ && fabs(angle_distance) <= inversion_yaw_tolerance_; +} + +} // namespace mppi \ No newline at end of file diff --git a/nav2_mppi_controller/src/path_handler copy.cpp b/nav2_mppi_controller/src/path_handler copy.cpp new file mode 100644 index 00000000000..b49e8991306 --- /dev/null +++ b/nav2_mppi_controller/src/path_handler copy.cpp @@ -0,0 +1,247 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// Copyright (c) 2023 Dexory +// Copyright (c) 2023 Open Navigation LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "nav2_mppi_controller/tools/path_handler.hpp" +#include "nav2_mppi_controller/tools/utils.hpp" +#include "nav2_costmap_2d/costmap_2d_ros.hpp" +#include "nav2_util/robot_utils.hpp" + +namespace mppi +{ + +void PathHandler::initialize( + nav2::LifecycleNode::WeakPtr parent, const std::string & name, + std::shared_ptr costmap, + std::shared_ptr buffer, ParametersHandler * param_handler) +{ + name_ = name; + costmap_ = costmap; + tf_buffer_ = buffer; + auto node = parent.lock(); + logger_ = node->get_logger(); + parameters_handler_ = param_handler; + + auto getParam = parameters_handler_->getParamGetter(name_); + getParam(max_robot_pose_search_dist_, "max_robot_pose_search_dist", getMaxCostmapDist()); + getParam(prune_distance_, "prune_distance", 1.5); + getParam(transform_tolerance_, "transform_tolerance", 0.1); + getParam(enforce_path_inversion_, "enforce_path_inversion", false); + getParam(enforce_path_rotation_, "enforce_path_rotation", false); + + if (enforce_path_inversion_ || enforce_path_rotation_) { + getParam(inversion_xy_tolerance_, "inversion_xy_tolerance", 0.2); + getParam(inversion_yaw_tolerance_, "inversion_yaw_tolerance", 0.4); + getParam(minimum_rotation_angle_, "minimum_rotation_angle", 0.785); + inversion_locale_ = 0u; + } +} + +std::pair +PathHandler::getGlobalPlanConsideringBoundsInCostmapFrame( + const geometry_msgs::msg::PoseStamped & global_pose) +{ + using nav2_util::geometry_utils::euclidean_distance; + + auto begin = global_plan_up_to_inversion_.poses.begin(); + + // Limit the search for the closest pose up to max_robot_pose_search_dist on the path + auto closest_pose_upper_bound = + nav2_util::geometry_utils::first_after_integrated_distance( + global_plan_up_to_inversion_.poses.begin(), global_plan_up_to_inversion_.poses.end(), + max_robot_pose_search_dist_); + + // Find closest point to the robot + auto closest_point = nav2_util::geometry_utils::min_by( + begin, closest_pose_upper_bound, + [&global_pose](const geometry_msgs::msg::PoseStamped & ps) { + return euclidean_distance(global_pose, ps); + }); + + nav_msgs::msg::Path transformed_plan; + transformed_plan.header.frame_id = costmap_->getGlobalFrameID(); + transformed_plan.header.stamp = global_pose.header.stamp; + + auto pruned_plan_end = + nav2_util::geometry_utils::first_after_integrated_distance( + closest_point, global_plan_up_to_inversion_.poses.end(), prune_distance_); + + unsigned int mx, my; + // Find the furthest relevant pose on the path to consider within costmap + // bounds + // Transforming it to the costmap frame in the same loop + for (auto global_plan_pose = closest_point; global_plan_pose != pruned_plan_end; + ++global_plan_pose) + { + // Transform from global plan frame to costmap frame + geometry_msgs::msg::PoseStamped costmap_plan_pose; + global_plan_pose->header.stamp = global_pose.header.stamp; + global_plan_pose->header.frame_id = global_plan_.header.frame_id; + nav2_util::transformPoseInTargetFrame(*global_plan_pose, costmap_plan_pose, *tf_buffer_, + costmap_->getGlobalFrameID(), transform_tolerance_); + + // Check if pose is inside the costmap + if (!costmap_->getCostmap()->worldToMap( + costmap_plan_pose.pose.position.x, costmap_plan_pose.pose.position.y, mx, my)) + { + return {transformed_plan, closest_point}; + } + + // Filling the transformed plan to return with the transformed pose + transformed_plan.poses.push_back(costmap_plan_pose); + } + + return {transformed_plan, closest_point}; +} + +geometry_msgs::msg::PoseStamped PathHandler::transformToGlobalPlanFrame( + const geometry_msgs::msg::PoseStamped & pose) +{ + if (global_plan_up_to_inversion_.poses.empty()) { + throw nav2_core::InvalidPath("Received plan with zero length"); + } + + geometry_msgs::msg::PoseStamped robot_pose; + if (!nav2_util::transformPoseInTargetFrame(pose, robot_pose, *tf_buffer_, + global_plan_up_to_inversion_.header.frame_id, transform_tolerance_)) + { + throw nav2_core::ControllerTFError( + "Unable to transform robot pose into global plan's frame"); + } + + return robot_pose; +} + +nav_msgs::msg::Path PathHandler::transformPath( + const geometry_msgs::msg::PoseStamped & robot_pose) +{ + // Find relevant bounds of path to use + geometry_msgs::msg::PoseStamped global_pose = + transformToGlobalPlanFrame(robot_pose); + auto [transformed_plan, lower_bound] = getGlobalPlanConsideringBoundsInCostmapFrame(global_pose); + + prunePlan(global_plan_up_to_inversion_, lower_bound); + + if ((enforce_path_inversion_ || enforce_path_rotation_) && inversion_locale_ != 0u) { + if (isWithinInversionTolerances(global_pose)) { + // Robot has reached the inversion/rotation point, unlock the rest of the path + prunePlan(global_plan_, global_plan_.poses.begin() + inversion_locale_); + global_plan_up_to_inversion_ = global_plan_; + + double max_yaw_delta = 0.0; + + for (size_t i = 1; i < global_plan_.poses.size(); i++) { + double yaw_prev = tf2::getYaw(global_plan_.poses[i-1].pose.orientation); + double yaw_curr = tf2::getYaw(global_plan_.poses[i].pose.orientation); + double diff = fabs(angles::shortest_angular_distance(yaw_prev, yaw_curr)); + if (diff > max_yaw_delta) { + max_yaw_delta = diff; + } + } + + // Recompute locale on the updated path + float rotation_threshold = enforce_path_rotation_ ? minimum_rotation_angle_ : 0.0f; + inversion_locale_ = utils::removePosesAfterFirstInversion( + global_plan_up_to_inversion_, rotation_threshold); + + RCLCPP_INFO( + logger_, + "[MPPI PathHandler] setPath: rotation_threshold=%.3f, inversion_locale=%u, path_size=%zu", + rotation_threshold, + inversion_locale_, + global_plan_up_to_inversion_.poses.size()); + + } + } + else{ + // RCLCPP_INFO( + // logger_, + // "[MPPI PathHandler] step: enforce_path_inversion=%s, enforce_path_rotation=%s, inversion_locale=%u", + // enforce_path_inversion_ ? "true" : "false", + // enforce_path_rotation_ ? "true" : "false", + // inversion_locale_); + + } + + if (transformed_plan.poses.empty()) { + throw nav2_core::InvalidPath("Resulting plan has 0 poses in it."); + } + + return transformed_plan; +} + +double PathHandler::getMaxCostmapDist() +{ + const auto & costmap = costmap_->getCostmap(); + return static_cast(std::max(costmap->getSizeInCellsX(), costmap->getSizeInCellsY())) * + costmap->getResolution() * 0.50; +} + +void PathHandler::setPath(const nav_msgs::msg::Path & plan) +{ + global_plan_ = plan; + global_plan_up_to_inversion_ = plan; + if (!enforce_path_inversion_ && !enforce_path_rotation_) { + inversion_locale_ = 0u; + return; + } + + // Find and restrict to the first rotation or inversion constraint + float rotation_threshold = enforce_path_rotation_ ? minimum_rotation_angle_ : 0.0f; + inversion_locale_ = utils::removePosesAfterFirstInversion( + global_plan_up_to_inversion_, rotation_threshold); +} + +nav_msgs::msg::Path & PathHandler::getPath() {return global_plan_;} + +void PathHandler::prunePlan(nav_msgs::msg::Path & plan, const PathIterator end) +{ + plan.poses.erase(plan.poses.begin(), end); +} + +geometry_msgs::msg::PoseStamped PathHandler::getTransformedGoal( + const builtin_interfaces::msg::Time & stamp) +{ + auto goal = global_plan_.poses.back(); + goal.header.frame_id = global_plan_.header.frame_id; + goal.header.stamp = stamp; + if (goal.header.frame_id.empty()) { + throw nav2_core::ControllerTFError("Goal pose has an empty frame_id"); + } + geometry_msgs::msg::PoseStamped transformed_goal; + if (!nav2_util::transformPoseInTargetFrame(goal, transformed_goal, *tf_buffer_, + costmap_->getGlobalFrameID(), transform_tolerance_)) + { + throw nav2_core::ControllerTFError("Unable to transform goal pose into costmap frame"); + } + return transformed_goal; +} + +bool PathHandler::isWithinInversionTolerances(const geometry_msgs::msg::PoseStamped & robot_pose) +{ + // Keep full path if we are within tolerance of the inversion pose + const auto last_pose = global_plan_up_to_inversion_.poses.back(); + float distance = hypotf( + robot_pose.pose.position.x - last_pose.pose.position.x, + robot_pose.pose.position.y - last_pose.pose.position.y); + + float angle_distance = angles::shortest_angular_distance( + tf2::getYaw(robot_pose.pose.orientation), + tf2::getYaw(last_pose.pose.orientation)); + + return distance <= inversion_xy_tolerance_ && fabs(angle_distance) <= inversion_yaw_tolerance_; +} + +} // namespace mppi \ No newline at end of file diff --git a/nav2_mppi_controller/src/path_handler.cpp b/nav2_mppi_controller/src/path_handler.cpp index 55931f6bc62..b49e8991306 100644 --- a/nav2_mppi_controller/src/path_handler.cpp +++ b/nav2_mppi_controller/src/path_handler.cpp @@ -39,9 +39,12 @@ void PathHandler::initialize( getParam(prune_distance_, "prune_distance", 1.5); getParam(transform_tolerance_, "transform_tolerance", 0.1); getParam(enforce_path_inversion_, "enforce_path_inversion", false); - if (enforce_path_inversion_) { + getParam(enforce_path_rotation_, "enforce_path_rotation", false); + + if (enforce_path_inversion_ || enforce_path_rotation_) { getParam(inversion_xy_tolerance_, "inversion_xy_tolerance", 0.2); - getParam(inversion_yaw_tolerance, "inversion_yaw_tolerance", 0.4); + getParam(inversion_yaw_tolerance_, "inversion_yaw_tolerance", 0.4); + getParam(minimum_rotation_angle_, "minimum_rotation_angle", 0.785); inversion_locale_ = 0u; } } @@ -131,13 +134,46 @@ nav_msgs::msg::Path PathHandler::transformPath( prunePlan(global_plan_up_to_inversion_, lower_bound); - if (enforce_path_inversion_ && inversion_locale_ != 0u) { + if ((enforce_path_inversion_ || enforce_path_rotation_) && inversion_locale_ != 0u) { if (isWithinInversionTolerances(global_pose)) { + // Robot has reached the inversion/rotation point, unlock the rest of the path prunePlan(global_plan_, global_plan_.poses.begin() + inversion_locale_); global_plan_up_to_inversion_ = global_plan_; - inversion_locale_ = utils::removePosesAfterFirstInversion(global_plan_up_to_inversion_); + + double max_yaw_delta = 0.0; + + for (size_t i = 1; i < global_plan_.poses.size(); i++) { + double yaw_prev = tf2::getYaw(global_plan_.poses[i-1].pose.orientation); + double yaw_curr = tf2::getYaw(global_plan_.poses[i].pose.orientation); + double diff = fabs(angles::shortest_angular_distance(yaw_prev, yaw_curr)); + if (diff > max_yaw_delta) { + max_yaw_delta = diff; + } + } + + // Recompute locale on the updated path + float rotation_threshold = enforce_path_rotation_ ? minimum_rotation_angle_ : 0.0f; + inversion_locale_ = utils::removePosesAfterFirstInversion( + global_plan_up_to_inversion_, rotation_threshold); + + RCLCPP_INFO( + logger_, + "[MPPI PathHandler] setPath: rotation_threshold=%.3f, inversion_locale=%u, path_size=%zu", + rotation_threshold, + inversion_locale_, + global_plan_up_to_inversion_.poses.size()); + } } + else{ + // RCLCPP_INFO( + // logger_, + // "[MPPI PathHandler] step: enforce_path_inversion=%s, enforce_path_rotation=%s, inversion_locale=%u", + // enforce_path_inversion_ ? "true" : "false", + // enforce_path_rotation_ ? "true" : "false", + // inversion_locale_); + + } if (transformed_plan.poses.empty()) { throw nav2_core::InvalidPath("Resulting plan has 0 poses in it."); @@ -156,10 +192,16 @@ double PathHandler::getMaxCostmapDist() void PathHandler::setPath(const nav_msgs::msg::Path & plan) { global_plan_ = plan; - global_plan_up_to_inversion_ = global_plan_; - if (enforce_path_inversion_) { - inversion_locale_ = utils::removePosesAfterFirstInversion(global_plan_up_to_inversion_); + global_plan_up_to_inversion_ = plan; + if (!enforce_path_inversion_ && !enforce_path_rotation_) { + inversion_locale_ = 0u; + return; } + + // Find and restrict to the first rotation or inversion constraint + float rotation_threshold = enforce_path_rotation_ ? minimum_rotation_angle_ : 0.0f; + inversion_locale_ = utils::removePosesAfterFirstInversion( + global_plan_up_to_inversion_, rotation_threshold); } nav_msgs::msg::Path & PathHandler::getPath() {return global_plan_;} @@ -199,7 +241,7 @@ bool PathHandler::isWithinInversionTolerances(const geometry_msgs::msg::PoseStam tf2::getYaw(robot_pose.pose.orientation), tf2::getYaw(last_pose.pose.orientation)); - return distance <= inversion_xy_tolerance_ && fabs(angle_distance) <= inversion_yaw_tolerance; + return distance <= inversion_xy_tolerance_ && fabs(angle_distance) <= inversion_yaw_tolerance_; } -} // namespace mppi +} // namespace mppi \ No newline at end of file diff --git a/nav2_mppi_controller/src/path_handler_mod.cpp b/nav2_mppi_controller/src/path_handler_mod.cpp new file mode 100644 index 00000000000..b49e8991306 --- /dev/null +++ b/nav2_mppi_controller/src/path_handler_mod.cpp @@ -0,0 +1,247 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// Copyright (c) 2023 Dexory +// Copyright (c) 2023 Open Navigation LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "nav2_mppi_controller/tools/path_handler.hpp" +#include "nav2_mppi_controller/tools/utils.hpp" +#include "nav2_costmap_2d/costmap_2d_ros.hpp" +#include "nav2_util/robot_utils.hpp" + +namespace mppi +{ + +void PathHandler::initialize( + nav2::LifecycleNode::WeakPtr parent, const std::string & name, + std::shared_ptr costmap, + std::shared_ptr buffer, ParametersHandler * param_handler) +{ + name_ = name; + costmap_ = costmap; + tf_buffer_ = buffer; + auto node = parent.lock(); + logger_ = node->get_logger(); + parameters_handler_ = param_handler; + + auto getParam = parameters_handler_->getParamGetter(name_); + getParam(max_robot_pose_search_dist_, "max_robot_pose_search_dist", getMaxCostmapDist()); + getParam(prune_distance_, "prune_distance", 1.5); + getParam(transform_tolerance_, "transform_tolerance", 0.1); + getParam(enforce_path_inversion_, "enforce_path_inversion", false); + getParam(enforce_path_rotation_, "enforce_path_rotation", false); + + if (enforce_path_inversion_ || enforce_path_rotation_) { + getParam(inversion_xy_tolerance_, "inversion_xy_tolerance", 0.2); + getParam(inversion_yaw_tolerance_, "inversion_yaw_tolerance", 0.4); + getParam(minimum_rotation_angle_, "minimum_rotation_angle", 0.785); + inversion_locale_ = 0u; + } +} + +std::pair +PathHandler::getGlobalPlanConsideringBoundsInCostmapFrame( + const geometry_msgs::msg::PoseStamped & global_pose) +{ + using nav2_util::geometry_utils::euclidean_distance; + + auto begin = global_plan_up_to_inversion_.poses.begin(); + + // Limit the search for the closest pose up to max_robot_pose_search_dist on the path + auto closest_pose_upper_bound = + nav2_util::geometry_utils::first_after_integrated_distance( + global_plan_up_to_inversion_.poses.begin(), global_plan_up_to_inversion_.poses.end(), + max_robot_pose_search_dist_); + + // Find closest point to the robot + auto closest_point = nav2_util::geometry_utils::min_by( + begin, closest_pose_upper_bound, + [&global_pose](const geometry_msgs::msg::PoseStamped & ps) { + return euclidean_distance(global_pose, ps); + }); + + nav_msgs::msg::Path transformed_plan; + transformed_plan.header.frame_id = costmap_->getGlobalFrameID(); + transformed_plan.header.stamp = global_pose.header.stamp; + + auto pruned_plan_end = + nav2_util::geometry_utils::first_after_integrated_distance( + closest_point, global_plan_up_to_inversion_.poses.end(), prune_distance_); + + unsigned int mx, my; + // Find the furthest relevant pose on the path to consider within costmap + // bounds + // Transforming it to the costmap frame in the same loop + for (auto global_plan_pose = closest_point; global_plan_pose != pruned_plan_end; + ++global_plan_pose) + { + // Transform from global plan frame to costmap frame + geometry_msgs::msg::PoseStamped costmap_plan_pose; + global_plan_pose->header.stamp = global_pose.header.stamp; + global_plan_pose->header.frame_id = global_plan_.header.frame_id; + nav2_util::transformPoseInTargetFrame(*global_plan_pose, costmap_plan_pose, *tf_buffer_, + costmap_->getGlobalFrameID(), transform_tolerance_); + + // Check if pose is inside the costmap + if (!costmap_->getCostmap()->worldToMap( + costmap_plan_pose.pose.position.x, costmap_plan_pose.pose.position.y, mx, my)) + { + return {transformed_plan, closest_point}; + } + + // Filling the transformed plan to return with the transformed pose + transformed_plan.poses.push_back(costmap_plan_pose); + } + + return {transformed_plan, closest_point}; +} + +geometry_msgs::msg::PoseStamped PathHandler::transformToGlobalPlanFrame( + const geometry_msgs::msg::PoseStamped & pose) +{ + if (global_plan_up_to_inversion_.poses.empty()) { + throw nav2_core::InvalidPath("Received plan with zero length"); + } + + geometry_msgs::msg::PoseStamped robot_pose; + if (!nav2_util::transformPoseInTargetFrame(pose, robot_pose, *tf_buffer_, + global_plan_up_to_inversion_.header.frame_id, transform_tolerance_)) + { + throw nav2_core::ControllerTFError( + "Unable to transform robot pose into global plan's frame"); + } + + return robot_pose; +} + +nav_msgs::msg::Path PathHandler::transformPath( + const geometry_msgs::msg::PoseStamped & robot_pose) +{ + // Find relevant bounds of path to use + geometry_msgs::msg::PoseStamped global_pose = + transformToGlobalPlanFrame(robot_pose); + auto [transformed_plan, lower_bound] = getGlobalPlanConsideringBoundsInCostmapFrame(global_pose); + + prunePlan(global_plan_up_to_inversion_, lower_bound); + + if ((enforce_path_inversion_ || enforce_path_rotation_) && inversion_locale_ != 0u) { + if (isWithinInversionTolerances(global_pose)) { + // Robot has reached the inversion/rotation point, unlock the rest of the path + prunePlan(global_plan_, global_plan_.poses.begin() + inversion_locale_); + global_plan_up_to_inversion_ = global_plan_; + + double max_yaw_delta = 0.0; + + for (size_t i = 1; i < global_plan_.poses.size(); i++) { + double yaw_prev = tf2::getYaw(global_plan_.poses[i-1].pose.orientation); + double yaw_curr = tf2::getYaw(global_plan_.poses[i].pose.orientation); + double diff = fabs(angles::shortest_angular_distance(yaw_prev, yaw_curr)); + if (diff > max_yaw_delta) { + max_yaw_delta = diff; + } + } + + // Recompute locale on the updated path + float rotation_threshold = enforce_path_rotation_ ? minimum_rotation_angle_ : 0.0f; + inversion_locale_ = utils::removePosesAfterFirstInversion( + global_plan_up_to_inversion_, rotation_threshold); + + RCLCPP_INFO( + logger_, + "[MPPI PathHandler] setPath: rotation_threshold=%.3f, inversion_locale=%u, path_size=%zu", + rotation_threshold, + inversion_locale_, + global_plan_up_to_inversion_.poses.size()); + + } + } + else{ + // RCLCPP_INFO( + // logger_, + // "[MPPI PathHandler] step: enforce_path_inversion=%s, enforce_path_rotation=%s, inversion_locale=%u", + // enforce_path_inversion_ ? "true" : "false", + // enforce_path_rotation_ ? "true" : "false", + // inversion_locale_); + + } + + if (transformed_plan.poses.empty()) { + throw nav2_core::InvalidPath("Resulting plan has 0 poses in it."); + } + + return transformed_plan; +} + +double PathHandler::getMaxCostmapDist() +{ + const auto & costmap = costmap_->getCostmap(); + return static_cast(std::max(costmap->getSizeInCellsX(), costmap->getSizeInCellsY())) * + costmap->getResolution() * 0.50; +} + +void PathHandler::setPath(const nav_msgs::msg::Path & plan) +{ + global_plan_ = plan; + global_plan_up_to_inversion_ = plan; + if (!enforce_path_inversion_ && !enforce_path_rotation_) { + inversion_locale_ = 0u; + return; + } + + // Find and restrict to the first rotation or inversion constraint + float rotation_threshold = enforce_path_rotation_ ? minimum_rotation_angle_ : 0.0f; + inversion_locale_ = utils::removePosesAfterFirstInversion( + global_plan_up_to_inversion_, rotation_threshold); +} + +nav_msgs::msg::Path & PathHandler::getPath() {return global_plan_;} + +void PathHandler::prunePlan(nav_msgs::msg::Path & plan, const PathIterator end) +{ + plan.poses.erase(plan.poses.begin(), end); +} + +geometry_msgs::msg::PoseStamped PathHandler::getTransformedGoal( + const builtin_interfaces::msg::Time & stamp) +{ + auto goal = global_plan_.poses.back(); + goal.header.frame_id = global_plan_.header.frame_id; + goal.header.stamp = stamp; + if (goal.header.frame_id.empty()) { + throw nav2_core::ControllerTFError("Goal pose has an empty frame_id"); + } + geometry_msgs::msg::PoseStamped transformed_goal; + if (!nav2_util::transformPoseInTargetFrame(goal, transformed_goal, *tf_buffer_, + costmap_->getGlobalFrameID(), transform_tolerance_)) + { + throw nav2_core::ControllerTFError("Unable to transform goal pose into costmap frame"); + } + return transformed_goal; +} + +bool PathHandler::isWithinInversionTolerances(const geometry_msgs::msg::PoseStamped & robot_pose) +{ + // Keep full path if we are within tolerance of the inversion pose + const auto last_pose = global_plan_up_to_inversion_.poses.back(); + float distance = hypotf( + robot_pose.pose.position.x - last_pose.pose.position.x, + robot_pose.pose.position.y - last_pose.pose.position.y); + + float angle_distance = angles::shortest_angular_distance( + tf2::getYaw(robot_pose.pose.orientation), + tf2::getYaw(last_pose.pose.orientation)); + + return distance <= inversion_xy_tolerance_ && fabs(angle_distance) <= inversion_yaw_tolerance_; +} + +} // namespace mppi \ No newline at end of file diff --git a/nav2_mppi_controller/src/path_handler_original.cpp b/nav2_mppi_controller/src/path_handler_original.cpp new file mode 100644 index 00000000000..55931f6bc62 --- /dev/null +++ b/nav2_mppi_controller/src/path_handler_original.cpp @@ -0,0 +1,205 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// Copyright (c) 2023 Dexory +// Copyright (c) 2023 Open Navigation LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "nav2_mppi_controller/tools/path_handler.hpp" +#include "nav2_mppi_controller/tools/utils.hpp" +#include "nav2_costmap_2d/costmap_2d_ros.hpp" +#include "nav2_util/robot_utils.hpp" + +namespace mppi +{ + +void PathHandler::initialize( + nav2::LifecycleNode::WeakPtr parent, const std::string & name, + std::shared_ptr costmap, + std::shared_ptr buffer, ParametersHandler * param_handler) +{ + name_ = name; + costmap_ = costmap; + tf_buffer_ = buffer; + auto node = parent.lock(); + logger_ = node->get_logger(); + parameters_handler_ = param_handler; + + auto getParam = parameters_handler_->getParamGetter(name_); + getParam(max_robot_pose_search_dist_, "max_robot_pose_search_dist", getMaxCostmapDist()); + getParam(prune_distance_, "prune_distance", 1.5); + getParam(transform_tolerance_, "transform_tolerance", 0.1); + getParam(enforce_path_inversion_, "enforce_path_inversion", false); + if (enforce_path_inversion_) { + getParam(inversion_xy_tolerance_, "inversion_xy_tolerance", 0.2); + getParam(inversion_yaw_tolerance, "inversion_yaw_tolerance", 0.4); + inversion_locale_ = 0u; + } +} + +std::pair +PathHandler::getGlobalPlanConsideringBoundsInCostmapFrame( + const geometry_msgs::msg::PoseStamped & global_pose) +{ + using nav2_util::geometry_utils::euclidean_distance; + + auto begin = global_plan_up_to_inversion_.poses.begin(); + + // Limit the search for the closest pose up to max_robot_pose_search_dist on the path + auto closest_pose_upper_bound = + nav2_util::geometry_utils::first_after_integrated_distance( + global_plan_up_to_inversion_.poses.begin(), global_plan_up_to_inversion_.poses.end(), + max_robot_pose_search_dist_); + + // Find closest point to the robot + auto closest_point = nav2_util::geometry_utils::min_by( + begin, closest_pose_upper_bound, + [&global_pose](const geometry_msgs::msg::PoseStamped & ps) { + return euclidean_distance(global_pose, ps); + }); + + nav_msgs::msg::Path transformed_plan; + transformed_plan.header.frame_id = costmap_->getGlobalFrameID(); + transformed_plan.header.stamp = global_pose.header.stamp; + + auto pruned_plan_end = + nav2_util::geometry_utils::first_after_integrated_distance( + closest_point, global_plan_up_to_inversion_.poses.end(), prune_distance_); + + unsigned int mx, my; + // Find the furthest relevant pose on the path to consider within costmap + // bounds + // Transforming it to the costmap frame in the same loop + for (auto global_plan_pose = closest_point; global_plan_pose != pruned_plan_end; + ++global_plan_pose) + { + // Transform from global plan frame to costmap frame + geometry_msgs::msg::PoseStamped costmap_plan_pose; + global_plan_pose->header.stamp = global_pose.header.stamp; + global_plan_pose->header.frame_id = global_plan_.header.frame_id; + nav2_util::transformPoseInTargetFrame(*global_plan_pose, costmap_plan_pose, *tf_buffer_, + costmap_->getGlobalFrameID(), transform_tolerance_); + + // Check if pose is inside the costmap + if (!costmap_->getCostmap()->worldToMap( + costmap_plan_pose.pose.position.x, costmap_plan_pose.pose.position.y, mx, my)) + { + return {transformed_plan, closest_point}; + } + + // Filling the transformed plan to return with the transformed pose + transformed_plan.poses.push_back(costmap_plan_pose); + } + + return {transformed_plan, closest_point}; +} + +geometry_msgs::msg::PoseStamped PathHandler::transformToGlobalPlanFrame( + const geometry_msgs::msg::PoseStamped & pose) +{ + if (global_plan_up_to_inversion_.poses.empty()) { + throw nav2_core::InvalidPath("Received plan with zero length"); + } + + geometry_msgs::msg::PoseStamped robot_pose; + if (!nav2_util::transformPoseInTargetFrame(pose, robot_pose, *tf_buffer_, + global_plan_up_to_inversion_.header.frame_id, transform_tolerance_)) + { + throw nav2_core::ControllerTFError( + "Unable to transform robot pose into global plan's frame"); + } + + return robot_pose; +} + +nav_msgs::msg::Path PathHandler::transformPath( + const geometry_msgs::msg::PoseStamped & robot_pose) +{ + // Find relevant bounds of path to use + geometry_msgs::msg::PoseStamped global_pose = + transformToGlobalPlanFrame(robot_pose); + auto [transformed_plan, lower_bound] = getGlobalPlanConsideringBoundsInCostmapFrame(global_pose); + + prunePlan(global_plan_up_to_inversion_, lower_bound); + + if (enforce_path_inversion_ && inversion_locale_ != 0u) { + if (isWithinInversionTolerances(global_pose)) { + prunePlan(global_plan_, global_plan_.poses.begin() + inversion_locale_); + global_plan_up_to_inversion_ = global_plan_; + inversion_locale_ = utils::removePosesAfterFirstInversion(global_plan_up_to_inversion_); + } + } + + if (transformed_plan.poses.empty()) { + throw nav2_core::InvalidPath("Resulting plan has 0 poses in it."); + } + + return transformed_plan; +} + +double PathHandler::getMaxCostmapDist() +{ + const auto & costmap = costmap_->getCostmap(); + return static_cast(std::max(costmap->getSizeInCellsX(), costmap->getSizeInCellsY())) * + costmap->getResolution() * 0.50; +} + +void PathHandler::setPath(const nav_msgs::msg::Path & plan) +{ + global_plan_ = plan; + global_plan_up_to_inversion_ = global_plan_; + if (enforce_path_inversion_) { + inversion_locale_ = utils::removePosesAfterFirstInversion(global_plan_up_to_inversion_); + } +} + +nav_msgs::msg::Path & PathHandler::getPath() {return global_plan_;} + +void PathHandler::prunePlan(nav_msgs::msg::Path & plan, const PathIterator end) +{ + plan.poses.erase(plan.poses.begin(), end); +} + +geometry_msgs::msg::PoseStamped PathHandler::getTransformedGoal( + const builtin_interfaces::msg::Time & stamp) +{ + auto goal = global_plan_.poses.back(); + goal.header.frame_id = global_plan_.header.frame_id; + goal.header.stamp = stamp; + if (goal.header.frame_id.empty()) { + throw nav2_core::ControllerTFError("Goal pose has an empty frame_id"); + } + geometry_msgs::msg::PoseStamped transformed_goal; + if (!nav2_util::transformPoseInTargetFrame(goal, transformed_goal, *tf_buffer_, + costmap_->getGlobalFrameID(), transform_tolerance_)) + { + throw nav2_core::ControllerTFError("Unable to transform goal pose into costmap frame"); + } + return transformed_goal; +} + +bool PathHandler::isWithinInversionTolerances(const geometry_msgs::msg::PoseStamped & robot_pose) +{ + // Keep full path if we are within tolerance of the inversion pose + const auto last_pose = global_plan_up_to_inversion_.poses.back(); + float distance = hypotf( + robot_pose.pose.position.x - last_pose.pose.position.x, + robot_pose.pose.position.y - last_pose.pose.position.y); + + float angle_distance = angles::shortest_angular_distance( + tf2::getYaw(robot_pose.pose.orientation), + tf2::getYaw(last_pose.pose.orientation)); + + return distance <= inversion_xy_tolerance_ && fabs(angle_distance) <= inversion_yaw_tolerance; +} + +} // namespace mppi diff --git a/nav2_mppi_controller/src/trajectory_validators/fixing_trajectory_validator.cpp b/nav2_mppi_controller/src/trajectory_validators/fixing_trajectory_validator.cpp new file mode 100644 index 00000000000..7c17b4e5a3e --- /dev/null +++ b/nav2_mppi_controller/src/trajectory_validators/fixing_trajectory_validator.cpp @@ -0,0 +1,407 @@ +// src/validators/auto_fixing_trajectory_validator.cpp +#include "nav2_mppi_controller/fixing_trajectory_validator.hpp" +#include +#include + +namespace mppi { + +void AutoFixingTrajectoryValidator::initialize( + const nav2::LifecycleNode::WeakPtr & parent, + const std::string & name, + const std::shared_ptr costmap, + ParametersHandler * param_handler, + const std::shared_ptr tf_buffer, + const models::OptimizerSettings & settings) +{ + OptimalTrajectoryValidator::initialize(parent, name, costmap, param_handler, tf_buffer, settings); + auto node = node_.lock(); + auto get = param_handler_->getParamGetter(name_); + + model_dt_ = settings.model_dt; + get(auto_fix_ttc_window_, "auto_fix_ttc_window", 0.8f); + + costmap_ = costmap_ros_->getCostmap(); + + get(consider_footprint_, "consider_footprint", true); + get(safe_clearance_, "auto_fix_safe_clearance", 0.12); + get(step_gain_, "auto_fix_step_gain", 0.6); + get(max_step_, "auto_fix_max_step", 0.05); + get(max_total_disp_, "auto_fix_max_total_displacement", 0.25); + get(max_iters_, "auto_fix_max_iters", 6); + get(smooth_iters_, "auto_fix_smooth_iters", 2); + + // Clamping parametrĂłw względem rozdzielczoƛci costmapy – odpornoƛć na zƂe YAML + const double r = costmap_->getResolution(); + auto clamp = [&](double &v, double lo, double hi) { v = std::clamp(v, lo, hi); }; + clamp(safe_clearance_, 0.5 * r, 1.5 * r); + clamp(max_step_, 0.25 * r, 1.5 * r); + clamp(max_total_disp_, 1.5 * r, 10.0 * r); + step_gain_ = std::clamp(step_gain_, 0.2, 0.9); + + if (consider_footprint_) { + fp_checker_ = std::make_unique>(costmap_); + } + + RCLCPP_INFO(node->get_logger(), + "[%s] AutoFixingTrajectoryValidator initialized: safe=%.2f step_gain=%.2f " + "max_step=%.2f max_disp=%.2f iters=%d smooth=%d lookahead=%.2fs (samples=%u)", + name_.c_str(), safe_clearance_, step_gain_, max_step_, max_total_disp_, + max_iters_, smooth_iters_, collision_lookahead_time_, traj_samples_to_evaluate_); +} + +bool AutoFixingTrajectoryValidator::isCollisionAt(double x, double y, double theta) const +{ + if (consider_footprint_) { + const double c = fp_checker_->footprintCostAtPose( + x, y, theta, costmap_ros_->getRobotFootprint()); + // W tym Nav2 checkerze kolizja/OOB ➜ LETHAL (254). Traktuj teĆŒ INSCRIBED (253) jako nieakceptowalne. + return (c >= static_cast(nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE)); + } + + unsigned int mx, my; + if (!costmap_->worldToMap(x, y, mx, my)) { + return true; // poza mapą = niebezpieczne + } + const unsigned char c = costmap_->getCost(mx, my); + return (c >= nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE); +} + +double AutoFixingTrajectoryValidator::clearanceAt(double x, double y) const +{ + // Prosty promieniowy skan (jeƛli masz EDT/SDF – podmieƄ) + static const int K = 12; + static const double MAX_R = 0.6; + double best = MAX_R; + for (int k = 0; k < K; ++k) { + const double ang = 2.0 * M_PI * k / K; + const double cs = std::cos(ang), sn = std::sin(ang); + double r = 0.0; + while (r < MAX_R) { + const double px = x + r * cs; + const double py = y + r * sn; + unsigned int mx, my; + if (!costmap_->worldToMap(px, py, mx, my)) break; + const unsigned char c = costmap_->getCost(mx, my); + if (c == nav2_costmap_2d::LETHAL_OBSTACLE || + c == nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE) { + best = std::min(best, r); + break; + } + r += costmap_->getResolution(); + } + } + return best; +} + +bool AutoFixingTrajectoryValidator::isSafe(double x, double y, double theta) const +{ + if (isCollisionAt(x, y, theta)) return false; + return clearanceAt(x, y) >= safe_clearance_; +} + +std::optional AutoFixingTrajectoryValidator::gradientAt(double x, double y) const +{ + unsigned int mx, my; + if (!costmap_->worldToMap(x, y, mx, my)) return std::nullopt; + + auto sample = [&](int dx, int dy) -> double { + const int ix = static_cast(mx) + dx; + const int iy = static_cast(my) + dy; + if (ix < 0 || iy < 0 || + ix >= static_cast(costmap_->getSizeInCellsX()) || + iy >= static_cast(costmap_->getSizeInCellsY())) { + return 0.0; + } + return static_cast(costmap_->getCost(ix, iy)); + }; + + const double cx1 = sample(+1, 0), cx0 = sample(-1, 0); + const double cy1 = sample(0, +1), cy0 = sample(0, -1); + const double dx_cost = (cx1 - cx0) / (2.0 * costmap_->getResolution()); + const double dy_cost = (cy1 - cy0) / (2.0 * costmap_->getResolution()); + + Eigen::Vector2d g(dx_cost, dy_cost); + if (g.norm() < 1e-6) return std::nullopt; + return g; // kierunek wzrostu kosztu ⇒ „odpychamy” w +grad +} + +void AutoFixingTrajectoryValidator::laplacianSmoothXY( + Eigen::ArrayXXf & traj, const std::vector &locked, int iters) const +{ + const int N = traj.rows(); + if (N < 3 || iters <= 0) return; + for (int it = 0; it < iters; ++it) { + for (int i = 1; i < N - 1; ++i) { + if (locked[i]) continue; + const Eigen::Vector2f prev = traj.row(i - 1).segment<2>(0); + const Eigen::Vector2f curr = traj.row(i).segment<2>(0); + const Eigen::Vector2f next = traj.row(i + 1).segment<2>(0); + const Eigen::Vector2f lap = (prev - 2.f * curr + next); + traj(i, 0) += 0.25f * lap.x(); + traj(i, 1) += 0.25f * lap.y(); + } + } +} + +void AutoFixingTrajectoryValidator::smoothYaw(Eigen::ArrayXXf & traj) const +{ + const int N = traj.rows(); + if (N < 3) return; + for (int i = 1; i < N - 1; ++i) { + const float t_prev = traj(i - 1, 2), t_cur = traj(i, 2), t_next = traj(i + 1, 2); + traj(i, 2) = 0.25f * t_prev + 0.5f * t_cur + 0.25f * t_next; + } +} + +bool AutoFixingTrajectoryValidator::tryRepair(Eigen::ArrayXXf & traj, int start_idx, int end_idx) const +{ + const int Ncap = static_cast(std::min(traj.rows(), traj_samples_to_evaluate_)); + const int s = std::clamp(start_idx, 0, Ncap); + const int e = std::clamp(end_idx, 0, Ncap); + if (e - s <= 0) return true; + + std::vector accum(e - s, 0.0); + std::vector locked(e - s, false); + + auto safe_idx = [&](int i) { return isSafe(traj(i, 0), traj(i, 1), traj(i, 2)); }; + + for (int i = s; i < e; ++i) { + locked[i - s] = safe_idx(i); + } + + for (int it = 0; it < max_iters_; ++it) { + bool improved = false; + + for (int i = s; i < e; ++i) { + if (locked[i - s]) continue; + + const double x = traj(i, 0), y = traj(i, 1), th = traj(i, 2); + double clr = clearanceAt(x, y); + + // kierunek: gradient kosztu lub fallback + Eigen::Vector2d dir(0.0, 0.0); + if (auto g = gradientAt(x, y)) { + dir = -(*g); + } else { + const int K = 16; + double best_gain = -1e9; + for (int k = 0; k < K; ++k) { + const double ang = 2.0 * M_PI * k / K; + const Eigen::Vector2d cand(std::cos(ang), std::sin(ang)); + const double nx = x + 0.02 * cand.x(); + const double ny = y + 0.02 * cand.y(); + const double gain = clearanceAt(nx, ny) - clr; + if (gain > best_gain) { best_gain = gain; dir = cand; } + } + } + + const double nrm = dir.norm(); + if (nrm < 1e-6) continue; + dir /= nrm; + + const bool in_collision = isCollisionAt(x, y, th); + double step = 0.0; + if (in_collision) { + // Silniejszy krok startowy, ĆŒeby wyjƛć poza INSCRIBED + const double min_step = std::max({ 0.5 * costmap_->getResolution(), + 0.25 * safe_clearance_, + safe_clearance_ + 0.5 * costmap_->getResolution() }); + step = std::min(std::max(min_step, step_gain_ * (safe_clearance_ + costmap_->getResolution())), + max_step_); + } else { + clr = clearanceAt(x, y); + const double deficit = std::max(0.0, safe_clearance_ - clr); + step = std::min(max_step_, step_gain_ * deficit); + } + double room = std::max(0.0, max_total_disp_ - accum[i - s]); + if (room <= 1e-6) continue; + if (step > room) step = room; + + + // limit przemieszczenia per-punkt (względem zakresu [s,e)) + if (accum[i - s] + step > max_total_disp_) { + step = std::max(0.0, max_total_disp_ - accum[i - s]); + } + if (step <= 1e-6) continue; + + double nx = x + step * dir.x(); + double ny = y + step * dir.y(); + + // krĂłtki line-search: nie wchodĆș w kolizję + int ls = 0; + while (ls < 4 && isCollisionAt(nx, ny, th)) { + step *= 0.5; nx = x + step * dir.x(); ny = y + step * dir.y(); ++ls; + } + + // jeƛli footprint nadal „zbyt blisko”, wysuƄ delikatnie w gĂłrę + if (consider_footprint_) { + int ls_up = 0; + while (ls_up < 4) { + const double c = fp_checker_->footprintCostAtPose(nx, ny, th, costmap_ros_->getRobotFootprint()); + if (c < static_cast(nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE)) break; + double extra = 0.5 * step; + + // nie przekraczaj max_total_disp_ + room = std::max(0.0, max_total_disp_ - accum[i - s] - step); + if (room <= 1e-6) break; + if (extra > room) extra = room; + + nx += extra * dir.x(); + ny += extra * dir.y(); + step += extra; + ++ls_up; + } + } + + if (step <= 1e-6) continue; // nic nie zrobiliƛmy + + traj(i, 0) = static_cast(nx); + traj(i, 1) = static_cast(ny); + accum[i - s] += step; + improved = true; + } + + // smoothing z peƂnym wektorem lockĂłw w przestrzeni [0..rows) + std::vector locked_full(traj.rows(), false); + for (int i = s; i < e; ++i) locked_full[i] = locked[i - s]; + laplacianSmoothXY(traj, locked_full, smooth_iters_); + smoothYaw(traj); + + // zaktualizuj maskę bezpiecznych w zakresie + bool all_safe = true; + for (int i = s; i < e; ++i) { + if (!safe_idx(i)) { all_safe = false; break; } + locked[i - s] = true; + } + if (all_safe) return true; + if (!improved) break; + } + + // częƛciowo lub wcale się nie udaƂo + return false; +} + +ValidationResult AutoFixingTrajectoryValidator::validateTrajectory( + const Eigen::ArrayXXf & optimal_trajectory, + const models::ControlSequence & control_sequence, + 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) +{ + (void)control_sequence; + (void)robot_pose; + (void)robot_speed; + (void)plan; + (void)goal; + + auto node = node_.lock(); + RCLCPP_DEBUG_THROTTLE(node->get_logger(), *node->get_clock(), 1000, + "[%s] validateTrajectory called. samples=%u", name_.c_str(), traj_samples_to_evaluate_); + + has_fixed_ = false; + fixed_traj_ = optimal_trajectory; // kopia robocza + + // 1) szybka weryfikacja + int first_coll_idx = -1; + const size_t limit = std::min(traj_samples_to_evaluate_, fixed_traj_.rows()); + for (size_t i = 0; i < limit; ++i) { + const double x = fixed_traj_(i, 0), y = fixed_traj_(i, 1), th = fixed_traj_(i, 2); + if (!isSafe(x, y, th)) { first_coll_idx = static_cast(i); break; } + } + + if (first_coll_idx == 0) { + const double old_disp = max_total_disp_; + const int old_iters = max_iters_; + const double old_step = max_step_; + + max_total_disp_ = std::max(max_total_disp_, 0.8); // byƂo 0.5 + max_iters_ = std::max(max_iters_, 14); // byƂo 10 + max_step_ = std::max(max_step_, 0.08); // ≄ r + + const int window_samples = std::max(1, + static_cast(std::ceil(auto_fix_ttc_window_ / std::max(1e-6f, model_dt_)))); + const bool ok0 = tryRepair(fixed_traj_, 0, std::min(limit, window_samples)); + + // Przywróć + max_total_disp_ = old_disp; + max_iters_ = old_iters; + max_step_ = old_step; + + if (!ok0) { + RCLCPP_WARN_THROTTLE(node->get_logger(), *node->get_clock(), 1000, + "[%s] Collision at sample 0: requesting soft reset for re-sampling.", + name_.c_str()); + return ValidationResult::SOFT_RESET; + } + + // Drugi pass: potwierdĆș brak kolizji po naprawie TTC=0 na caƂym oknie + bool clean = true; + for (size_t j = 0; j < limit; ++j) { + if (!isSafe(fixed_traj_(j,0), fixed_traj_(j,1), fixed_traj_(j,2))) { clean = false; break; } + } + if (clean) { + has_fixed_ = true; + RCLCPP_INFO_THROTTLE(node->get_logger(), *node->get_clock(), 1000, + "[%s] Auto-fix SUCCESS at TTC=0: using repaired trajectory.", name_.c_str()); + return ValidationResult::SUCCESS; // waĆŒne: NIE resetuj, pozwĂłl kontrolerowi uĆŒyć fixed_traj_ + } + + // Inaczej wpadnij do standardowej ƛcieĆŒki naprawy dalej w kodzie +} + + + + if (first_coll_idx > 0) { + const double ttc = static_cast(first_coll_idx) * static_cast(model_dt_); + RCLCPP_WARN_THROTTLE( + node->get_logger(), *node->get_clock(), 1000, + "[%s] Collision predicted in %.2f s at sample %d (%.2f, %.2f). Attempting auto-fix...", + name_.c_str(), ttc, first_coll_idx, fixed_traj_(first_coll_idx, 0), fixed_traj_(first_coll_idx, 1)); + + // zakres naprawy: [first - pre, first + window] + const int window_samples = std::max(1, static_cast(std::ceil( + auto_fix_ttc_window_ / std::max(1e-6f, model_dt_)))); + const int pre = std::min(6, first_coll_idx); // ~ 6 prĂłbek wstecz + const int start_idx = first_coll_idx - pre; + const int end_idx = std::min(static_cast(limit), first_coll_idx + window_samples); + + // 2) prĂłba naprawy + const bool ok = tryRepair(fixed_traj_, start_idx, end_idx); + if (!ok) { + RCLCPP_ERROR_THROTTLE( + node->get_logger(), *node->get_clock(), 1000, + "[%s] Auto-fix FAILED: trajectory still in collision after repair attempts.", + name_.c_str()); + return ValidationResult::SOFT_RESET; + } + + // 3) potwierdĆș brak kolizji po naprawie + for (size_t j = 0; j < limit; ++j) { + if (!isSafe(fixed_traj_(j, 0), fixed_traj_(j, 1), fixed_traj_(j, 2))) { + RCLCPP_ERROR_THROTTLE( + node->get_logger(), *node->get_clock(), 1000, + "[%s] Auto-fix incomplete: remaining collision at sample %zu.", + name_.c_str(), j); + return ValidationResult::SOFT_RESET; + } + } + + has_fixed_ = true; + RCLCPP_INFO_THROTTLE( + node->get_logger(), *node->get_clock(), 1000, + "[%s] Auto-fix SUCCESS: repaired up to %.2f s window; TTC was %.2f s.", + name_.c_str(), (first_coll_idx + window_samples) * model_dt_, ttc); + + return ValidationResult::SUCCESS; + } + + // juĆŒ byƂo dobrze + return ValidationResult::SUCCESS; +} + +} // namespace mppi + +#include "pluginlib/class_list_macros.hpp" +PLUGINLIB_EXPORT_CLASS(mppi::AutoFixingTrajectoryValidator, mppi::OptimalTrajectoryValidator) diff --git a/nav2_mppi_controller/src/trajectory_visualizer copy 2.cpp b/nav2_mppi_controller/src/trajectory_visualizer copy 2.cpp new file mode 100644 index 00000000000..6960b5676c6 --- /dev/null +++ b/nav2_mppi_controller/src/trajectory_visualizer copy 2.cpp @@ -0,0 +1,156 @@ +// 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 +#include "nav2_mppi_controller/tools/trajectory_visualizer.hpp" + +namespace mppi +{ + +void TrajectoryVisualizer::on_configure( + nav2::LifecycleNode::WeakPtr parent, const std::string & name, + const std::string & frame_id, ParametersHandler * parameters_handler) +{ + auto node = parent.lock(); + logger_ = node->get_logger(); + frame_id_ = frame_id; + trajectories_publisher_ = + node->create_publisher("~/candidate_trajectories"); + transformed_path_pub_ = node->create_publisher( + "~/transformed_global_plan"); + optimal_path_pub_ = node->create_publisher("~/optimal_path"); + parameters_handler_ = parameters_handler; + + auto getParam = parameters_handler->getParamGetter(name + ".TrajectoryVisualizer"); + + getParam(trajectory_step_, "trajectory_step", 5); + getParam(time_step_, "time_step", 3); + + reset(); +} + +void TrajectoryVisualizer::on_cleanup() +{ + trajectories_publisher_.reset(); + transformed_path_pub_.reset(); + optimal_path_pub_.reset(); +} + +void TrajectoryVisualizer::on_activate() +{ + trajectories_publisher_->on_activate(); + transformed_path_pub_->on_activate(); + optimal_path_pub_->on_activate(); +} + +void TrajectoryVisualizer::on_deactivate() +{ + trajectories_publisher_->on_deactivate(); + transformed_path_pub_->on_deactivate(); + optimal_path_pub_->on_deactivate(); +} + +void TrajectoryVisualizer::add( + const Eigen::ArrayXXf & trajectory, + const std::string & marker_namespace, + const builtin_interfaces::msg::Time & cmd_stamp) +{ + size_t size = trajectory.rows(); + if (!size) { + return; + } + + auto add_marker = [&](auto i) { + float component = static_cast(i) / static_cast(size); + + auto pose = utils::createPose(trajectory(i, 0), trajectory(i, 1), 0.06); + auto scale = + i != size - 1 ? + utils::createScale(0.03, 0.03, 0.07) : + utils::createScale(0.07, 0.07, 0.09); + auto color = utils::createColor(0, component, component, 1); + auto marker = utils::createMarker( + marker_id_++, pose, scale, color, frame_id_, marker_namespace); + points_->markers.push_back(marker); + + // populate optimal path + geometry_msgs::msg::PoseStamped pose_stamped; + pose_stamped.header.frame_id = frame_id_; + pose_stamped.pose = pose; + + tf2::Quaternion quaternion_tf2; + quaternion_tf2.setRPY(0., 0., trajectory(i, 2)); + pose_stamped.pose.orientation = tf2::toMsg(quaternion_tf2); + + optimal_path_->poses.push_back(pose_stamped); + }; + + optimal_path_->header.stamp = cmd_stamp; + optimal_path_->header.frame_id = frame_id_; + for (size_t i = 0; i < size; i++) { + add_marker(i); + } +} + +void TrajectoryVisualizer::add( + const models::Trajectories & trajectories, const std::string & marker_namespace) +{ + size_t n_rows = trajectories.x.rows(); + size_t n_cols = trajectories.x.cols(); + const float shape_1 = static_cast(n_cols); + points_->markers.reserve(floor(n_rows / trajectory_step_) * floor(n_cols * time_step_)); + + for (size_t i = 0; i < n_rows; i += trajectory_step_) { + for (size_t j = 0; j < n_cols; j += time_step_) { + const float j_flt = static_cast(j); + float blue_component = 1.0f - j_flt / shape_1; + float green_component = j_flt / shape_1; + + auto pose = utils::createPose(trajectories.x(i, j), trajectories.y(i, j), 0.03); + auto scale = utils::createScale(0.03, 0.03, 0.03); + auto color = utils::createColor(0, green_component, blue_component, 1); + auto marker = utils::createMarker( + marker_id_++, pose, scale, color, frame_id_, marker_namespace); + + points_->markers.push_back(marker); + } + } +} + +void TrajectoryVisualizer::reset() +{ + marker_id_ = 0; + points_ = std::make_unique(); + optimal_path_ = std::make_unique(); +} + +void TrajectoryVisualizer::visualize(const nav_msgs::msg::Path & plan) +{ + if (trajectories_publisher_->get_subscription_count() > 0) { + trajectories_publisher_->publish(std::move(points_)); + } + + if (optimal_path_pub_->get_subscription_count() > 0) { + optimal_path_pub_->publish(std::move(optimal_path_)); + } + + reset(); + + if (transformed_path_pub_->get_subscription_count() > 0) { + auto plan_ptr = std::make_unique(plan); + transformed_path_pub_->publish(std::move(plan_ptr)); + } +} + +} // namespace mppi diff --git a/nav2_mppi_controller/src/trajectory_visualizer.cpp b/nav2_mppi_controller/src/trajectory_visualizer.cpp index 6960b5676c6..28141994791 100644 --- a/nav2_mppi_controller/src/trajectory_visualizer.cpp +++ b/nav2_mppi_controller/src/trajectory_visualizer.cpp @@ -13,6 +13,8 @@ // limitations under the License. #include +#include +#include #include "nav2_mppi_controller/tools/trajectory_visualizer.hpp" namespace mppi @@ -25,17 +27,38 @@ void TrajectoryVisualizer::on_configure( auto node = parent.lock(); logger_ = node->get_logger(); frame_id_ = frame_id; - trajectories_publisher_ = - node->create_publisher("~/candidate_trajectories"); - transformed_path_pub_ = node->create_publisher( - "~/transformed_global_plan"); - optimal_path_pub_ = node->create_publisher("~/optimal_path"); parameters_handler_ = parameters_handler; - - auto getParam = parameters_handler->getParamGetter(name + ".TrajectoryVisualizer"); - + auto getParam = parameters_handler->getParamGetter(name + ".Visualization"); getParam(trajectory_step_, "trajectory_step", 5); getParam(time_step_, "time_step", 3); + getParam(publish_trajectories_with_total_cost_, "publish_trajectories_with_total_cost", false); + getParam(publish_trajectories_with_individual_cost_, "publish_trajectories_with_individual_cost", + false); + getParam(publish_optimal_footprints_, "publish_optimal_footprints", false); + getParam(publish_optimal_trajectory_msg_, "publish_optimal_trajectory_msg", false); + getParam(publish_transformed_path_, "publish_transformed_path", false); + getParam(publish_optimal_path_, "publish_optimal_path", false); + getParam(footprint_downsample_factor_, "footprint_downsample_factor", 3); + + if (publish_trajectories_with_total_cost_ || publish_trajectories_with_individual_cost_) { + trajectories_publisher_ = + node->create_publisher("~/candidate_trajectories"); + } + if (publish_transformed_path_) { + transformed_path_pub_ = node->create_publisher( + "~/transformed_global_plan"); + } + if (publish_optimal_path_) { + optimal_path_pub_ = node->create_publisher("~/optimal_path"); + } + if (publish_optimal_footprints_) { + optimal_footprints_pub_ = node->create_publisher( + "~/optimal_footprints"); + } + if (publish_optimal_trajectory_msg_) { + optimal_trajectory_msg_pub_ = node->create_publisher( + "~/optimal_trajectory"); + } reset(); } @@ -45,20 +68,46 @@ void TrajectoryVisualizer::on_cleanup() trajectories_publisher_.reset(); transformed_path_pub_.reset(); optimal_path_pub_.reset(); + optimal_footprints_pub_.reset(); + optimal_trajectory_msg_pub_.reset(); } void TrajectoryVisualizer::on_activate() { - trajectories_publisher_->on_activate(); - transformed_path_pub_->on_activate(); - optimal_path_pub_->on_activate(); + if (trajectories_publisher_) { + trajectories_publisher_->on_activate(); + } + if (transformed_path_pub_) { + transformed_path_pub_->on_activate(); + } + if (optimal_path_pub_) { + optimal_path_pub_->on_activate(); + } + if (optimal_footprints_pub_) { + optimal_footprints_pub_->on_activate(); + } + if (optimal_trajectory_msg_pub_) { + optimal_trajectory_msg_pub_->on_activate(); + } } void TrajectoryVisualizer::on_deactivate() { - trajectories_publisher_->on_deactivate(); - transformed_path_pub_->on_deactivate(); - optimal_path_pub_->on_deactivate(); + if (trajectories_publisher_) { + trajectories_publisher_->on_deactivate(); + } + if (transformed_path_pub_) { + transformed_path_pub_->on_deactivate(); + } + if (optimal_path_pub_) { + optimal_path_pub_->on_deactivate(); + } + if (optimal_footprints_pub_) { + optimal_footprints_pub_->on_deactivate(); + } + if (optimal_trajectory_msg_pub_) { + optimal_trajectory_msg_pub_->on_deactivate(); + } } void TrajectoryVisualizer::add( @@ -104,27 +153,132 @@ void TrajectoryVisualizer::add( } void TrajectoryVisualizer::add( - const models::Trajectories & trajectories, const std::string & marker_namespace) + const models::Trajectories & trajectories, + const Eigen::ArrayXf & total_costs, + const std::vector> & individual_critics_cost, + const builtin_interfaces::msg::Time & cmd_stamp, + const std::optional> & trajectories_in_collision) +{ + // Check if we should visualize per-critic costs + bool visualize_per_critic = !individual_critics_cost.empty() && + publish_trajectories_with_individual_cost_ && + trajectories_publisher_ && trajectories_publisher_->get_subscription_count() > 0; + + size_t n_rows = trajectories.x.rows(); + points_->markers.reserve(n_rows / trajectory_step_); + + // Visualize total costs if requested + if (publish_trajectories_with_total_cost_) { + createTrajectoryMarkers(trajectories, total_costs, "Total Costs", cmd_stamp, trajectories_in_collision); + } + + // Visualize each critic's contribution if requested + if (visualize_per_critic) { + for (const auto & [critic_name, costs] : individual_critics_cost) { + createTrajectoryMarkers(trajectories, costs, critic_name, cmd_stamp, trajectories_in_collision); + } + } +} + +void TrajectoryVisualizer::createTrajectoryMarkers( + const models::Trajectories & trajectories, + const Eigen::ArrayXf & costs, + const std::string & ns, + const builtin_interfaces::msg::Time & cmd_stamp, + const std::optional> & trajectories_in_collision) { size_t n_rows = trajectories.x.rows(); size_t n_cols = trajectories.x.cols(); - const float shape_1 = static_cast(n_cols); - points_->markers.reserve(floor(n_rows / trajectory_step_) * floor(n_cols * time_step_)); + + // Find min/max cost for normalization, excluding trajectories in collision + float min_cost = std::numeric_limits::max(); + float max_cost = std::numeric_limits::lowest(); + + for (Eigen::Index i = 0; i < costs.size(); ++i) { + // Skip trajectories in collision when computing min/max for normalization + bool in_collision = trajectories_in_collision && + i < static_cast(trajectories_in_collision->size()) && + (*trajectories_in_collision)[i]; + + if (!in_collision) { + min_cost = std::min(min_cost, costs(i)); + max_cost = std::max(max_cost, costs(i)); + } + } + + float cost_range = max_cost - min_cost; + + // Avoid division by zero + if (cost_range < 1e-6f) { + cost_range = 1.0f; + } for (size_t i = 0; i < n_rows; i += trajectory_step_) { - for (size_t j = 0; j < n_cols; j += time_step_) { - const float j_flt = static_cast(j); - float blue_component = 1.0f - j_flt / shape_1; - float green_component = j_flt / shape_1; + float red_component, green_component, blue_component; - auto pose = utils::createPose(trajectories.x(i, j), trajectories.y(i, j), 0.03); - auto scale = utils::createScale(0.03, 0.03, 0.03); - auto color = utils::createColor(0, green_component, blue_component, 1); - auto marker = utils::createMarker( - marker_id_++, pose, scale, color, frame_id_, marker_namespace); + // Check if trajectory is in collision + bool in_collision = trajectories_in_collision && + i < trajectories_in_collision->size() && + (*trajectories_in_collision)[i]; - points_->markers.push_back(marker); + if (in_collision) { + // Fuschia color for trajectories in collision (magenta/pink) + red_component = 1.0f; + green_component = 0.0f; + blue_component = 1.0f; + } else { + // Check if cost is zero (no contribution from this critic) + bool zero_cost = std::abs(costs(i)) < 1e-6f; + + if (zero_cost) { + // Gray color for zero cost (no contribution) + red_component = 0.5f; + green_component = 0.5f; + blue_component = 0.5f; + } else { + // Normal gradient for trajectories + float normalized_cost = (costs(i) - min_cost) / cost_range; + normalized_cost = std::clamp(normalized_cost, 0.0f, 1.0f); + + // Color scheme: Green (low cost) -> Yellow -> Red (high cost) + blue_component = 0.0f; + if (normalized_cost < 0.5f) { + // Green to Yellow (0.0 - 0.5) + red_component = normalized_cost * 2.0f; + green_component = 1.0f; + } else { + // Yellow to Red (0.5 - 1.0) + red_component = 1.0f; + green_component = 2.0f * (1.0f - normalized_cost); + } + } } + + // Create line strip marker for this trajectory + visualization_msgs::msg::Marker marker; + marker.header.frame_id = frame_id_; + marker.header.stamp = cmd_stamp; + marker.ns = ns; + marker.id = marker_id_++; + marker.type = visualization_msgs::msg::Marker::LINE_STRIP; + marker.action = visualization_msgs::msg::Marker::ADD; + marker.pose.orientation.w = 1.0; + marker.scale.x = 0.01; // Line width + marker.color.r = red_component; + marker.color.g = green_component; + marker.color.b = blue_component; + marker.color.a = 1.0; + + // Add all points in this trajectory to the line strip + for (size_t j = 0; j < n_cols; j += time_step_) { + geometry_msgs::msg::Point point; + point.x = trajectories.x(i, j); + point.y = trajectories.y(i, j); + point.z = 0.0f; + marker.points.push_back(point); + } + + points_->markers.push_back(marker); } } @@ -135,22 +289,168 @@ void TrajectoryVisualizer::reset() optimal_path_ = std::make_unique(); } -void TrajectoryVisualizer::visualize(const nav_msgs::msg::Path & plan) +void TrajectoryVisualizer::visualize( + nav_msgs::msg::Path plan, + const Eigen::ArrayXXf & optimal_trajectory, + const models::ControlSequence & control_sequence, + float model_dt, + const builtin_interfaces::msg::Time & stamp, + std::shared_ptr costmap_ros, + const models::Trajectories & candidate_trajectories, + const Eigen::ArrayXf & costs, + const std::vector> & critic_costs, + const std::optional> & trajectories_in_collision) { - if (trajectories_publisher_->get_subscription_count() > 0) { + // Create header with frame from costmap + std_msgs::msg::Header header; + header.stamp = stamp; + header.frame_id = costmap_ros->getGlobalFrameID(); + + // Visualize trajectories with total costs + if (publish_trajectories_with_total_cost_ || + (!publish_trajectories_with_individual_cost_ || critic_costs.empty())) + { + add(candidate_trajectories, costs, {}, stamp, trajectories_in_collision); + } + + // Visualize trajectories with individual critic costs + if (publish_trajectories_with_individual_cost_ && !critic_costs.empty()) { + add(candidate_trajectories, costs, critic_costs, stamp, trajectories_in_collision); + } + + // Add optimal trajectory to populate optimal_path_ + if (publish_optimal_path_ && optimal_trajectory.rows() > 0) { + add(optimal_trajectory, "Optimal Trajectory", stamp); + } + + // Publish trajectories + if (trajectories_publisher_ && trajectories_publisher_->get_subscription_count() > 0) { trajectories_publisher_->publish(std::move(points_)); } - if (optimal_path_pub_->get_subscription_count() > 0) { + // Publish optimal path if enabled + if (publish_optimal_path_ && optimal_path_pub_ && + optimal_path_pub_->get_subscription_count() > 0) + { optimal_path_pub_->publish(std::move(optimal_path_)); } + // Publish optimal footprints if enabled + if (publish_optimal_footprints_ && optimal_footprints_pub_ && + optimal_footprints_pub_->get_subscription_count() > 0 && + costmap_ros != nullptr && optimal_trajectory.rows() > 0) + { + auto footprints_msg = createFootprintMarkers(optimal_trajectory, header, costmap_ros); + optimal_footprints_pub_->publish(std::move(footprints_msg)); + } + + // Publish optimal trajectory message if enabled + if (publish_optimal_trajectory_msg_ && optimal_trajectory_msg_pub_ && + optimal_trajectory_msg_pub_->get_subscription_count() > 0) + { + auto trajectory_msg = utils::toTrajectoryMsg( + optimal_trajectory, + control_sequence, + model_dt, + header); + optimal_trajectory_msg_pub_->publish(std::move(trajectory_msg)); + } + reset(); - if (transformed_path_pub_->get_subscription_count() > 0) { + // Publish transformed path + if (transformed_path_pub_ && transformed_path_pub_->get_subscription_count() > 0) { auto plan_ptr = std::make_unique(plan); transformed_path_pub_->publish(std::move(plan_ptr)); } } -} // namespace mppi +void TrajectoryVisualizer::visualize(nav_msgs::msg::Path plan) +{ + // Simplified version for testing that only publishes what's been added + if (trajectories_publisher_ && trajectories_publisher_->get_subscription_count() > 0) { + trajectories_publisher_->publish(std::move(points_)); + } + + if (publish_optimal_path_ && optimal_path_pub_ && + optimal_path_pub_->get_subscription_count() > 0) + { + optimal_path_pub_->publish(std::move(optimal_path_)); + } + + reset(); + + if (transformed_path_pub_ && transformed_path_pub_->get_subscription_count() > 0) { + auto plan_ptr = std::make_unique(plan); + transformed_path_pub_->publish(std::move(plan_ptr)); + } +} + +visualization_msgs::msg::MarkerArray TrajectoryVisualizer::createFootprintMarkers( + const Eigen::ArrayXXf & trajectory, + const std_msgs::msg::Header & header, + std::shared_ptr costmap_ros) +{ + visualization_msgs::msg::MarkerArray marker_array; + if (trajectory.rows() == 0) { + return marker_array; + } + + // Get robot footprint + std::vector robot_footprint = + costmap_ros->getRobotFootprint(); + + // Skip if footprint is empty or very small + if (robot_footprint.size() < 3) { + return marker_array; + } + + // Create header for markers + std_msgs::msg::Header costmap_header; + costmap_header.frame_id = costmap_ros->getGlobalFrameID(); + costmap_header.stamp = header.stamp; + + int marker_id = 0; + for (int i = 0; i < trajectory.rows(); i += footprint_downsample_factor_) { + double x = trajectory(i, 0); + double y = trajectory(i, 1); + double theta = trajectory(i, 2); + + // Create oriented footprint + geometry_msgs::msg::PolygonStamped oriented_footprint; + oriented_footprint.header = costmap_header; + nav2_costmap_2d::transformFootprint(x, y, theta, robot_footprint, oriented_footprint); + // Create marker for this footprint + visualization_msgs::msg::Marker marker; + marker.header = costmap_header; + marker.ns = "optimal_footprints"; + marker.id = marker_id++; + marker.type = visualization_msgs::msg::Marker::LINE_STRIP; + marker.action = visualization_msgs::msg::Marker::ADD; + marker.pose.orientation.w = 1.0; + // Set marker scale and color + marker.scale.x = 0.02; // Line width + marker.color.r = 0.0; + marker.color.g = 1.0; + marker.color.b = 0.0; + marker.color.a = 0.8; + // Add footprint points to marker + for (const auto & point : oriented_footprint.polygon.points) { + geometry_msgs::msg::Point p; + p.x = point.x; + p.y = point.y; + p.z = 0.0; + marker.points.push_back(p); + } + // Close the polygon by adding the first point again + if (!marker.points.empty()) { + marker.points.push_back(marker.points[0]); + } + + marker_array.markers.push_back(marker); + } + + return marker_array; +} + +} // namespace mppi \ No newline at end of file diff --git a/nav2_mppi_controller/test/critic_manager_test copy 2.cpp b/nav2_mppi_controller/test/critic_manager_test copy 2.cpp new file mode 100644 index 00000000000..ff0116ed7de --- /dev/null +++ b/nav2_mppi_controller/test/critic_manager_test copy 2.cpp @@ -0,0 +1,160 @@ +// 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 +#include + +#include "gtest/gtest.h" +#include "rclcpp/rclcpp.hpp" +#include "nav2_mppi_controller/critic_manager.hpp" + +// Tests critic manager + +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 ~CriticManagerWrapper() = default; + + virtual void loadCritics() + { + critics_.clear(); + auto instance = std::unique_ptr(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() + { + const auto critic = critics_[0].get(); + if (critic == nullptr) { + return false; + } + const auto dummy_critic = dynamic_cast(critic); + return dummy_critic == nullptr ? false : dummy_critic->initialized_; + } + + bool getDummyCriticScored() + { + const auto critic = critics_[0].get(); + if (critic == nullptr) { + return false; + } + const auto dummy_critic = dynamic_cast(critic); + return dummy_critic == nullptr ? false : dummy_critic->scored_; + } +}; + +class CriticManagerWrapperEnum : public CriticManager +{ +public: + CriticManagerWrapperEnum() + : CriticManager() {} + + virtual ~CriticManagerWrapperEnum() = default; + + unsigned int getCriticNum() + { + return critics_.size(); + } +}; + +TEST(CriticManagerTests, BasicCriticOperations) +{ + auto node = std::make_shared("my_node"); + auto costmap_ros = std::make_shared( + "dummy_costmap", "", true); + std::string name = "test"; + ParametersHandler param_handler(node, name); + 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, ¶m_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; + Eigen::ArrayXf 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("my_node"); + node->declare_parameter( + "critic_manager.critics", + rclcpp::ParameterValue(std::vector{"ConstraintCritic", "PreferForwardCritic"})); + auto costmap_ros = std::make_shared( + "dummy_costmap", "", true); + std::string name = "test"; + ParametersHandler param_handler(node, name); + 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, ¶m_handler); + EXPECT_EQ(critic_manager.getCriticNum(), 2u); +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + + rclcpp::init(0, nullptr); + + int result = RUN_ALL_TESTS(); + + rclcpp::shutdown(); + + return result; +} diff --git a/nav2_mppi_controller/test/critic_manager_test.cpp b/nav2_mppi_controller/test/critic_manager_test.cpp index ff0116ed7de..f29b0ac2d41 100644 --- a/nav2_mppi_controller/test/critic_manager_test.cpp +++ b/nav2_mppi_controller/test/critic_manager_test.cpp @@ -114,7 +114,8 @@ TEST(CriticManagerTests, BasicCriticOperations) Eigen::ArrayXf costs; float model_dt = 0.1; CriticData data = - {state, generated_trajectories, path, goal, costs, model_dt, false, nullptr, nullptr, + {state, generated_trajectories, path, goal, costs, std::nullopt, model_dt, false, std::nullopt, + nullptr, nullptr, std::nullopt, std::nullopt}; data.fail_flag = true; @@ -157,4 +158,4 @@ int main(int argc, char **argv) rclcpp::shutdown(); return result; -} +} \ No newline at end of file diff --git a/nav2_mppi_controller/test/critics_tests copy 2.cpp b/nav2_mppi_controller/test/critics_tests copy 2.cpp new file mode 100644 index 00000000000..88e823019a7 --- /dev/null +++ b/nav2_mppi_controller/test/critics_tests copy 2.cpp @@ -0,0 +1,794 @@ +// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov +// Copyright (c) 2023 Open Navigation LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +#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_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 "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 + +class PathAngleCriticWrapper : public PathAngleCritic +{ +public: + PathAngleCriticWrapper() + : PathAngleCritic() + { + } + + void setMode(int mode) + { + mode_ = static_cast(mode); + } +}; + +TEST(CriticTests, ConstraintsCritic) +{ + // Standard preamble + auto node = std::make_shared("my_node"); + auto costmap_ros = std::make_shared( + "dummy_costmap", "", true); + std::string name = "test"; + ParametersHandler param_handler(node, name); + rclcpp_lifecycle::State lstate; + costmap_ros->on_configure(lstate); + + models::State state; + // provide velocities in constraints, should not have any costs + state.vx = 0.40 * Eigen::ArrayXXf::Ones(1000, 30); + state.vy = Eigen::ArrayXXf::Zero(1000, 30); + state.wz = Eigen::ArrayXXf::Ones(1000, 30); + models::ControlSequence control_sequence; + models::Trajectories generated_trajectories; + models::Path path; + geometry_msgs::msg::Pose goal; + Eigen::ArrayXf costs = Eigen::ArrayXf::Zero(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(); + + // Initialization testing + + // Make sure initializes correctly and that defaults are reasonable + ConstraintCritic critic; + critic.on_configure(node, "mppi", "critic", costmap_ros, ¶m_handler); + EXPECT_EQ(critic.getName(), "critic"); + EXPECT_TRUE(critic.getMaxVelConstraint() > 0.0); + EXPECT_TRUE(critic.getMinVelConstraint() < 0.0); + + // Scoring testing + critic.score(data); + EXPECT_NEAR(costs.sum(), 0, 1e-6); + + // provide out of maximum velocity constraint + state.vx.row(999).setConstant(0.60f); + critic.score(data); + EXPECT_GT(costs.sum(), 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.setZero(); + + // provide out of minimum velocity constraint + state.vx.row(1).setConstant(-0.45f); + critic.score(data); + EXPECT_GT(costs.sum(), 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.setZero(); + + // Now with ackermann, all in constraint so no costs to score + state.vx.setConstant(0.40f); + state.wz.setConstant(1.5f); + data.motion_model = std::make_shared(¶m_handler, node->get_name()); + critic.score(data); + EXPECT_NEAR(costs.sum(), 0, 1e-6); + + // Now violating the ackermann constraints + state.wz.setConstant(2.5f); + critic.score(data); + EXPECT_GT(costs.sum(), 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("my_node"); + auto costmap_ros = std::make_shared( + "dummy_costmap", "", true); + std::string name = "test"; + ParametersHandler param_handler(node, name); + 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, ¶m_handler), + nav2_core::ControllerException + ); +} + +TEST(CriticTests, ObstacleCriticAlignedParams) { + auto node = std::make_shared("my_node"); + auto costmap_ros = std::make_shared( + "dummy_costmap", "", true); + std::string name = "test"; + ParametersHandler param_handler(node, name); + 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, ¶m_handler); + EXPECT_EQ(critic.getName(), "critic"); +} + + +TEST(CriticTests, CostCriticMisAlignedParams) { + // Standard preamble + auto node = std::make_shared("my_node"); + auto costmap_ros = std::make_shared( + "dummy_costmap", "", true); + std::string name = "test"; + ParametersHandler param_handler(node, name); + 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, ¶m_handler), + nav2_core::ControllerException + ); +} + +TEST(CriticTests, CostCriticAlignedParams) { + // Standard preamble + auto node = std::make_shared("my_node"); + auto costmap_ros = std::make_shared( + "dummy_costmap", "", true); + std::string name = "test"; + ParametersHandler param_handler(node, name); + 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, ¶m_handler); + EXPECT_EQ(critic.getName(), "critic"); +} + +TEST(CriticTests, GoalAngleCritic) +{ + // Standard preamble + auto node = std::make_shared("my_node"); + auto costmap_ros = std::make_shared( + "dummy_costmap", "", true); + std::string name = "test"; + ParametersHandler param_handler(node, name); + 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; + path.reset(10); + Eigen::ArrayXf costs = Eigen::ArrayXf::Zero(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(); + + // Initialization testing + + // Make sure initializes correctly + GoalAngleCritic critic; + critic.on_configure(node, "mppi", "critic", costmap_ros, ¶m_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.x(9) = 10.0; + path.y(9) = 0.0; + path.yaws(9) = 3.14; + goal.position.x = 10.0; + goal.position.y = 0.0; + goal.orientation.x = 0.0; + goal.orientation.y = 0.0; + goal.orientation.z = 1.0; + goal.orientation.w = 0.0; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); + critic.score(data); + EXPECT_NEAR(costs.sum(), 0, 1e-6); + + // Let's move it even closer, just to be sure it still doesn't trigger + state.pose.pose.position.x = 9.2; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); + critic.score(data); + EXPECT_NEAR(costs.sum(), 0, 1e-6); + + // provide state pose and path below `threshold_to_consider` to consider + state.pose.pose.position.x = 9.7; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); + critic.score(data); + EXPECT_GT(costs.sum(), 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("my_node"); + auto costmap_ros = std::make_shared( + "dummy_costmap", "", true); + std::string name = "test"; + ParametersHandler param_handler(node, name); + 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; + path.reset(10); + Eigen::ArrayXf costs = Eigen::ArrayXf::Zero(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(); + + // Initialization testing + + // Make sure initializes correctly + GoalCritic critic; + critic.on_configure(node, "mppi", "critic", costmap_ros, ¶m_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.x(9) = 10.0; + path.y(9) = 0.0; + goal.position.x = 10.0; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); + critic.score(data); + EXPECT_NEAR(costs(2), 0.0, 1e-6); // (0 * 5.0 weight + EXPECT_NEAR(costs.sum(), 0.0, 1e-6); // Should all be 0 * 1000 + costs.setZero(); + + // provide state pose and path close + path.x(9) = 0.5; + path.y(9) = 0.0; + goal.position.x = 0.5; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); + critic.score(data); + EXPECT_NEAR(costs(2), 2.5, 1e-6); // (sqrt(10.0 * 10.0) * 5.0 weight + EXPECT_NEAR(costs.sum(), 2500.0, 1e-3); // should be 2.5 * 1000 +} + +TEST(CriticTests, PathAngleCritic) +{ + // Standard preamble + auto node = std::make_shared("my_node"); + auto costmap_ros = std::make_shared( + "dummy_costmap", "", true); + std::string name = "test"; + ParametersHandler param_handler(node, name); + 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; + path.reset(10); + Eigen::ArrayXf costs = Eigen::ArrayXf::Zero(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(); + TestGoalChecker goal_checker; // from utils_tests tolerance of 0.25 positionally + + // Initialization testing + + // Make sure initializes correctly + PathAngleCriticWrapper critic; + critic.on_configure(node, "mppi", "critic", costmap_ros, ¶m_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.x(9) = 0.15; + goal.position.x = 0.15; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); + critic.score(data); + EXPECT_NEAR(costs.sum(), 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; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); + 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(costs.sum(), 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(costs.sum(), 0.0); + EXPECT_NEAR(costs(0), 3.9947, 1e-2); // atan2(4,-1) [1.81] * 2.2 weight + + // Set mode to no directional preferences + reset costs + critic.setMode(1); + costs.setZero(); + + // 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) = 0.0; + critic.score(data); + EXPECT_NEAR(costs.sum(), 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 pt and pose < max_angle_to_furthest_ IF non-directional + path.y(6) = 0.0; + critic.score(data); + EXPECT_NEAR(costs.sum(), 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(costs.sum(), 0.0); + // should use reverse orientation as the closer angle in no dir preference mode + EXPECT_NEAR(costs(0), 2.9167, 1e-2); + + // Set mode to consider path directionality + reset costs + critic.setMode(2); + costs.setZero(); + + // 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) = 0.0; + critic.score(data); + EXPECT_NEAR(costs.sum(), 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 pt and pose < max_angle_to_furthest_ IF non-directional + path.y(6) = 0.0; + critic.score(data); + EXPECT_NEAR(costs.sum(), 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(costs.sum(), 0.0); + // should use reverse orientation as the closer angle in no dir preference mode + EXPECT_NEAR(costs(0), 2.9167, 1e-2); + + PathAngleMode mode; + mode = PathAngleMode::FORWARD_PREFERENCE; + EXPECT_EQ(modeToStr(mode), std::string("Forward Preference")); + mode = PathAngleMode::CONSIDER_FEASIBLE_PATH_ORIENTATIONS; + EXPECT_EQ(modeToStr(mode), std::string("Consider Feasible Path Orientations")); + mode = PathAngleMode::NO_DIRECTIONAL_PREFERENCE; + EXPECT_EQ(modeToStr(mode), std::string("No Directional Preference")); + mode = static_cast(4); + EXPECT_EQ(modeToStr(mode), std::string("Invalid mode!")); +} + +TEST(CriticTests, PreferForwardCritic) +{ + // Standard preamble + auto node = std::make_shared("my_node"); + auto costmap_ros = std::make_shared( + "dummy_costmap", "", true); + std::string name = "test"; + ParametersHandler param_handler(node, name); + 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; + path.reset(10); + Eigen::ArrayXf costs = Eigen::ArrayXf::Zero(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(); + 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, ¶m_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.x(9) = 10.0; + goal.position.x = 10.0; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); + critic.score(data); + EXPECT_NEAR(costs.sum(), 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.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); + state.vx.setOnes(); + critic.score(data); + EXPECT_NEAR(costs.sum(), 0.0f, 1e-6f); + + // provide state pose and path close to trigger behavior but with all reverse motion + state.vx.setConstant(-1.0f); + critic.score(data); + EXPECT_GT(costs.sum(), 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("my_node"); + auto costmap_ros = std::make_shared( + "dummy_costmap", "", true); + std::string name = "test"; + ParametersHandler param_handler(node, name); + 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; + path.reset(10); + Eigen::ArrayXf costs = Eigen::ArrayXf::Zero(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(); + 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, ¶m_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.x(9) = 10.0; + goal.position.x = 10.0; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); + critic.score(data); + EXPECT_NEAR(costs.sum(), 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.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); + state.wz.setZero(); + critic.score(data); + EXPECT_NEAR(costs.sum(), 0.0, 1e-6); + + // Provide nearby with some motion + state.wz.row(0).setConstant(10.0f); + critic.score(data); + EXPECT_NEAR(costs(0), 100.0, 1e-6); // (mean(10.0) * 10.0 weight + costs.setZero(); + + // Now try again with some wiggling noise + std::mt19937 engine; + std::normal_distribution normal_dist = std::normal_distribution(0.0f, 0.5f); + state.wz.row(0) = Eigen::ArrayXf::NullaryExpr(30, [&] () {return normal_dist(engine);}); + critic.score(data); + EXPECT_NEAR(costs(0), 2.581, 4e-1); // (mean of noise with mu=0, sigma=0.5 * 10.0 weight +} + +TEST(CriticTests, PathFollowCritic) +{ + // Standard preamble + auto node = std::make_shared("my_node"); + auto costmap_ros = std::make_shared( + "dummy_costmap", "", true); + std::string name = "test"; + ParametersHandler param_handler(node, name); + 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; + path.reset(6); + Eigen::ArrayXf costs = Eigen::ArrayXf::Zero(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(); + 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, ¶m_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.x(5) = 1.8; + goal.position.x = 1.8; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); + critic.score(data); + EXPECT_NEAR(costs.sum(), 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; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); + critic.score(data); + EXPECT_NEAR(costs.sum(), 750.0, 1e-2); // 0.15 * 5 weight * 1000 +} + +TEST(CriticTests, PathAlignCritic) +{ + // Standard preamble + auto node = std::make_shared("my_node"); + auto costmap_ros = std::make_shared( + "dummy_costmap", "", true); + std::string name = "test"; + ParametersHandler param_handler(node, name); + 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; + path.reset(10); + Eigen::ArrayXf costs = Eigen::ArrayXf::Zero(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(); + 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, ¶m_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.x(9) = 0.85; + goal.position.x = 0.85; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); + critic.score(data); + EXPECT_NEAR(costs.sum(), 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; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); + critic.score(data); + EXPECT_NEAR(costs.sum(), 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; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); + critic.score(data); + EXPECT_NEAR(costs.sum(), 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; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); + generated_trajectories.x.setConstant(0.66f); + critic.score(data); + // 0.66 * 1000 * 10 weight * 6 num pts eval / 6 normalization term + EXPECT_NEAR(costs.sum(), 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.setZero(); + path.x.setConstant(1.5f); + path.y.setConstant(1.5f); + goal.position.x = 1.5; + state.local_path_length = std::abs(state.pose.pose.position.x - goal.position.x); + critic.score(data); + EXPECT_NEAR(costs.sum(), 0.0, 1e-6); +} + +TEST(CriticTests, VelocityDeadbandCritic) +{ + // Standard preamble + auto node = std::make_shared("my_node"); + auto costmap_ros = std::make_shared( + "dummy_costmap", "", true); + std::string name = "test"; + ParametersHandler param_handler(node, name); + auto getParam = param_handler.getParamGetter("critic"); + std::vector deadband_velocities_; + getParam(deadband_velocities_, "deadband_velocities", std::vector{0.08, 0.08, 0.08}); + rclcpp_lifecycle::State lstate; + costmap_ros->on_configure(lstate); + + models::State state; + state.reset(1000, 30); + models::ControlSequence control_sequence; + models::Trajectories generated_trajectories; + models::Path path; + geometry_msgs::msg::Pose goal; + Eigen::ArrayXf costs = Eigen::ArrayXf::Zero(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(); + + // Initialization testing + + // Make sure initializes correctly and that defaults are reasonable + VelocityDeadbandCritic critic; + critic.on_configure(node, "mppi", "critic", costmap_ros, ¶m_handler); + EXPECT_EQ(critic.getName(), "critic"); + + // Scoring testing + + // provide velocities out of deadband bounds, should not have any costs + state.vx.setConstant(0.80f); + state.vy.setConstant(0.60f); + state.wz.setConstant(0.80f); + critic.score(data); + EXPECT_NEAR(costs.sum(), 0, 1e-6); + + // Test cost value + state.vx.setConstant(0.01f); + state.vy.setConstant(0.02f); + state.wz.setConstant(0.021f); + 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); +} diff --git a/nav2_mppi_controller/test/critics_tests.cpp b/nav2_mppi_controller/test/critics_tests.cpp index 88e823019a7..7c54114b24b 100644 --- a/nav2_mppi_controller/test/critics_tests.cpp +++ b/nav2_mppi_controller/test/critics_tests.cpp @@ -79,8 +79,8 @@ TEST(CriticTests, ConstraintsCritic) Eigen::ArrayXf costs = Eigen::ArrayXf::Zero(1000); float model_dt = 0.1; CriticData data = - {state, generated_trajectories, path, goal, costs, model_dt, - false, nullptr, nullptr, std::nullopt, std::nullopt}; + {state, generated_trajectories, path, goal, costs, std::nullopt, model_dt, + false, std::nullopt, nullptr, nullptr, std::nullopt, std::nullopt}; data.motion_model = std::make_shared(); // Initialization testing @@ -227,8 +227,8 @@ TEST(CriticTests, GoalAngleCritic) Eigen::ArrayXf costs = Eigen::ArrayXf::Zero(1000); float model_dt = 0.1; CriticData data = - {state, generated_trajectories, path, goal, costs, model_dt, - false, nullptr, nullptr, std::nullopt, std::nullopt}; + {state, generated_trajectories, path, goal, costs, std::nullopt, model_dt, + false, std::nullopt, nullptr, nullptr, std::nullopt, std::nullopt}; data.motion_model = std::make_shared(); // Initialization testing @@ -290,8 +290,8 @@ TEST(CriticTests, GoalCritic) Eigen::ArrayXf costs = Eigen::ArrayXf::Zero(1000); float model_dt = 0.1; CriticData data = - {state, generated_trajectories, path, goal, costs, model_dt, - false, nullptr, nullptr, std::nullopt, std::nullopt}; + {state, generated_trajectories, path, goal, costs, std::nullopt, model_dt, + false, std::nullopt, nullptr, nullptr, std::nullopt, std::nullopt}; data.motion_model = std::make_shared(); // Initialization testing @@ -346,8 +346,8 @@ TEST(CriticTests, PathAngleCritic) Eigen::ArrayXf costs = Eigen::ArrayXf::Zero(1000); float model_dt = 0.1; CriticData data = - {state, generated_trajectories, path, goal, costs, model_dt, - false, nullptr, nullptr, std::nullopt, std::nullopt}; + {state, generated_trajectories, path, goal, costs, std::nullopt, model_dt, + false, std::nullopt, nullptr, nullptr, std::nullopt, std::nullopt}; data.motion_model = std::make_shared(); TestGoalChecker goal_checker; // from utils_tests tolerance of 0.25 positionally @@ -467,8 +467,8 @@ TEST(CriticTests, PreferForwardCritic) Eigen::ArrayXf costs = Eigen::ArrayXf::Zero(1000); float model_dt = 0.1; CriticData data = - {state, generated_trajectories, path, goal, costs, model_dt, - false, nullptr, nullptr, std::nullopt, std::nullopt}; + {state, generated_trajectories, path, goal, costs, std::nullopt, model_dt, + false, std::nullopt, nullptr, nullptr, std::nullopt, std::nullopt}; data.motion_model = std::make_shared(); TestGoalChecker goal_checker; // from utils_tests tolerance of 0.25 positionally @@ -526,8 +526,8 @@ TEST(CriticTests, TwirlingCritic) Eigen::ArrayXf costs = Eigen::ArrayXf::Zero(1000); float model_dt = 0.1; CriticData data = - {state, generated_trajectories, path, goal, costs, model_dt, - false, nullptr, nullptr, std::nullopt, std::nullopt}; + {state, generated_trajectories, path, goal, costs, std::nullopt, model_dt, + false, std::nullopt, nullptr, nullptr, std::nullopt, std::nullopt}; data.motion_model = std::make_shared(); TestGoalChecker goal_checker; // from utils_tests tolerance of 0.25 positionally data.goal_checker = &goal_checker; @@ -593,8 +593,8 @@ TEST(CriticTests, PathFollowCritic) Eigen::ArrayXf costs = Eigen::ArrayXf::Zero(1000); float model_dt = 0.1; CriticData data = - {state, generated_trajectories, path, goal, costs, model_dt, - false, nullptr, nullptr, std::nullopt, std::nullopt}; + {state, generated_trajectories, path, goal, costs, std::nullopt, model_dt, + false, std::nullopt, nullptr, nullptr, std::nullopt, std::nullopt}; data.motion_model = std::make_shared(); TestGoalChecker goal_checker; // from utils_tests tolerance of 0.25 positionally data.goal_checker = &goal_checker; @@ -647,8 +647,8 @@ TEST(CriticTests, PathAlignCritic) Eigen::ArrayXf costs = Eigen::ArrayXf::Zero(1000); float model_dt = 0.1; CriticData data = - {state, generated_trajectories, path, goal, costs, model_dt, - false, nullptr, nullptr, std::nullopt, std::nullopt}; + {state, generated_trajectories, path, goal, costs, std::nullopt, model_dt, + false, std::nullopt, nullptr, nullptr, std::nullopt, std::nullopt}; data.motion_model = std::make_shared(); TestGoalChecker goal_checker; // from utils_tests tolerance of 0.25 positionally data.goal_checker = &goal_checker; @@ -764,8 +764,8 @@ TEST(CriticTests, VelocityDeadbandCritic) Eigen::ArrayXf costs = Eigen::ArrayXf::Zero(1000); float model_dt = 0.1; CriticData data = - {state, generated_trajectories, path, goal, costs, model_dt, - false, nullptr, nullptr, std::nullopt, std::nullopt}; + {state, generated_trajectories, path, goal, costs, std::nullopt, model_dt, + false, std::nullopt, nullptr, nullptr, std::nullopt, std::nullopt}; data.motion_model = std::make_shared(); // Initialization testing @@ -791,4 +791,4 @@ TEST(CriticTests, VelocityDeadbandCritic) 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); -} +} \ No newline at end of file diff --git a/nav2_mppi_controller/test/trajectory_visualizer_tests copy 2.cpp b/nav2_mppi_controller/test/trajectory_visualizer_tests copy 2.cpp new file mode 100644 index 00000000000..7e710fc7375 --- /dev/null +++ b/nav2_mppi_controller/test/trajectory_visualizer_tests copy 2.cpp @@ -0,0 +1,229 @@ +// 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 +#include + +#include "gtest/gtest.h" +#include "rclcpp/rclcpp.hpp" +#include "nav2_mppi_controller/tools/trajectory_visualizer.hpp" + +// Tests trajectory visualization + +using namespace mppi; // NOLINT + +TEST(TrajectoryVisualizerTests, StateTransition) +{ + auto node = std::make_shared("my_node"); + std::string name = "test"; + auto parameters_handler = std::make_unique(node, name); + + 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("my_node"); + std::string name = "test"; + auto parameters_handler = std::make_unique(node, name); + nav_msgs::msg::Path received_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( + "~/transformed_global_plan", + [&](const nav_msgs::msg::Path msg) {received_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(received_path.poses.size(), 5u); + EXPECT_EQ(received_path.header.frame_id, "fake_frame"); +} + +TEST(TrajectoryVisualizerTests, VisOptimalTrajectory) +{ + auto node = std::make_shared("my_node"); + std::string name = "test"; + auto parameters_handler = std::make_unique(node, name); + + visualization_msgs::msg::MarkerArray received_msg; + auto my_sub = node->create_subscription( + "~/candidate_trajectories", + [&](const visualization_msgs::msg::MarkerArray msg) {received_msg = msg;}); + + // optimal_trajectory empty, should fail to publish + Eigen::ArrayXXf optimal_trajectory; + TrajectoryVisualizer vis; + vis.on_configure(node, "my_name", "fkmap", parameters_handler.get()); + vis.on_activate(); + builtin_interfaces::msg::Time bogus_stamp; + vis.add(optimal_trajectory, "Optimal Trajectory", bogus_stamp); + nav_msgs::msg::Path bogus_path; + vis.visualize(bogus_path); + + rclcpp::spin_some(node->get_node_base_interface()); + EXPECT_EQ(received_msg.markers.size(), 0u); + + // Now populated with content, should publish + optimal_trajectory = Eigen::ArrayXXf::Ones(20, 3); + vis.add(optimal_trajectory, "Optimal Trajectory", bogus_stamp); + vis.visualize(bogus_path); + + rclcpp::spin_some(node->get_node_base_interface()); + + // Should have 20 trajectory points in the map frame + EXPECT_EQ(received_msg.markers.size(), 20u); + EXPECT_EQ(received_msg.markers[0].header.frame_id, "fkmap"); + + // Check IDs are properly populated + EXPECT_EQ(received_msg.markers[0].id, 0); + EXPECT_EQ(received_msg.markers[1].id, 1); + EXPECT_EQ(received_msg.markers[10].id, 10); + + // Check poses are correct + EXPECT_EQ(received_msg.markers[0].pose.position.x, 1); + EXPECT_EQ(received_msg.markers[0].pose.position.y, 1); + EXPECT_EQ(received_msg.markers[0].pose.position.z, 0.06); + + // Check that scales are rational + EXPECT_EQ(received_msg.markers[0].scale.x, 0.03); + EXPECT_EQ(received_msg.markers[0].scale.y, 0.03); + EXPECT_EQ(received_msg.markers[0].scale.z, 0.07); + + EXPECT_EQ(received_msg.markers[19].scale.x, 0.07); + EXPECT_EQ(received_msg.markers[19].scale.y, 0.07); + EXPECT_EQ(received_msg.markers[19].scale.z, 0.09); + + // Check that the colors are rational + for (unsigned int i = 0; i != received_msg.markers.size() - 1; i++) { + EXPECT_LT(received_msg.markers[i].color.g, received_msg.markers[i + 1].color.g); + EXPECT_LT(received_msg.markers[i].color.b, received_msg.markers[i + 1].color.b); + EXPECT_EQ(received_msg.markers[i].color.r, received_msg.markers[i + 1].color.r); + EXPECT_EQ(received_msg.markers[i].color.a, received_msg.markers[i + 1].color.a); + } +} + +TEST(TrajectoryVisualizerTests, VisCandidateTrajectories) +{ + auto node = std::make_shared("my_node"); + std::string name = "test"; + auto parameters_handler = std::make_unique(node, name); + + visualization_msgs::msg::MarkerArray received_msg; + auto my_sub = node->create_subscription( + "~/candidate_trajectories", + [&](const visualization_msgs::msg::MarkerArray msg) {received_msg = msg;}); + + models::Trajectories candidate_trajectories; + candidate_trajectories.x = Eigen::ArrayXXf::Ones(200, 12); + candidate_trajectories.y = Eigen::ArrayXXf::Ones(200, 12); + candidate_trajectories.yaws = Eigen::ArrayXXf::Ones(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(received_msg.markers.size(), 160u); +} + +TEST(TrajectoryVisualizerTests, VisOptimalPath) +{ + auto node = std::make_shared("my_node"); + std::string name = "test"; + auto parameters_handler = std::make_unique(node, name); + builtin_interfaces::msg::Time cmd_stamp; + cmd_stamp.sec = 5; + cmd_stamp.nanosec = 10; + + nav_msgs::msg::Path received_path; + auto my_sub = node->create_subscription( + "~/optimal_path", + [&](const nav_msgs::msg::Path msg) {received_path = msg;}); + + // optimal_trajectory empty, should fail to publish + Eigen::ArrayXXf optimal_trajectory; + TrajectoryVisualizer vis; + vis.on_configure(node, "my_name", "fkmap", parameters_handler.get()); + vis.on_activate(); + vis.add(optimal_trajectory, "Optimal Trajectory", cmd_stamp); + nav_msgs::msg::Path bogus_path; + vis.visualize(bogus_path); + + rclcpp::spin_some(node->get_node_base_interface()); + EXPECT_EQ(received_path.poses.size(), 0u); + + // Now populated with content, should publish + optimal_trajectory.resize(20, 3); + for (unsigned int i = 0; i != optimal_trajectory.rows() - 1; i++) { + optimal_trajectory(i, 0) = static_cast(i); + optimal_trajectory(i, 1) = static_cast(i); + optimal_trajectory(i, 2) = static_cast(i); + } + vis.add(optimal_trajectory, "Optimal Trajectory", cmd_stamp); + vis.visualize(bogus_path); + + rclcpp::spin_some(node->get_node_base_interface()); + + // Should have a 20 points path in the map frame and with same stamp than velocity command + EXPECT_EQ(received_path.poses.size(), 20u); + EXPECT_EQ(received_path.header.frame_id, "fkmap"); + EXPECT_EQ(received_path.header.stamp.sec, cmd_stamp.sec); + EXPECT_EQ(received_path.header.stamp.nanosec, cmd_stamp.nanosec); + + tf2::Quaternion quat; + for (unsigned int i = 0; i != received_path.poses.size() - 1; i++) { + // Poses should be in map frame too + EXPECT_EQ(received_path.poses[i].header.frame_id, "fkmap"); + + // Check positions are correct + EXPECT_EQ(received_path.poses[i].pose.position.x, static_cast(i)); + EXPECT_EQ(received_path.poses[i].pose.position.y, static_cast(i)); + EXPECT_EQ(received_path.poses[i].pose.position.z, 0.06); + + // Check orientations are correct + quat.setRPY(0., 0., optimal_trajectory(i, 2)); + auto expected_orientation = tf2::toMsg(quat); + EXPECT_EQ(received_path.poses[i].pose.orientation.x, expected_orientation.x); + EXPECT_EQ(received_path.poses[i].pose.orientation.y, expected_orientation.y); + EXPECT_EQ(received_path.poses[i].pose.orientation.z, expected_orientation.z); + EXPECT_EQ(received_path.poses[i].pose.orientation.w, expected_orientation.w); + } +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + + rclcpp::init(0, nullptr); + + int result = RUN_ALL_TESTS(); + + rclcpp::shutdown(); + + return result; +} diff --git a/nav2_mppi_controller/test/trajectory_visualizer_tests.cpp b/nav2_mppi_controller/test/trajectory_visualizer_tests.cpp index 7e710fc7375..64aefddd874 100644 --- a/nav2_mppi_controller/test/trajectory_visualizer_tests.cpp +++ b/nav2_mppi_controller/test/trajectory_visualizer_tests.cpp @@ -50,12 +50,15 @@ TEST(TrajectoryVisualizerTests, VisPathRepub) "~/transformed_global_plan", [&](const nav_msgs::msg::Path msg) {received_path = msg;}); + rclcpp::executors::SingleThreadedExecutor executor; + executor.add_node(node->get_node_base_interface()); + 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()); + executor.spin_some(); EXPECT_EQ(received_path.poses.size(), 5u); EXPECT_EQ(received_path.header.frame_id, "fake_frame"); } @@ -70,6 +73,8 @@ TEST(TrajectoryVisualizerTests, VisOptimalTrajectory) auto my_sub = node->create_subscription( "~/candidate_trajectories", [&](const visualization_msgs::msg::MarkerArray msg) {received_msg = msg;}); + rclcpp::executors::SingleThreadedExecutor executor; + executor.add_node(node->get_node_base_interface()); // optimal_trajectory empty, should fail to publish Eigen::ArrayXXf optimal_trajectory; @@ -81,7 +86,7 @@ TEST(TrajectoryVisualizerTests, VisOptimalTrajectory) nav_msgs::msg::Path bogus_path; vis.visualize(bogus_path); - rclcpp::spin_some(node->get_node_base_interface()); + executor.spin_some(); EXPECT_EQ(received_msg.markers.size(), 0u); // Now populated with content, should publish @@ -89,7 +94,7 @@ TEST(TrajectoryVisualizerTests, VisOptimalTrajectory) vis.add(optimal_trajectory, "Optimal Trajectory", bogus_stamp); vis.visualize(bogus_path); - rclcpp::spin_some(node->get_node_base_interface()); + executor.spin_some(); // Should have 20 trajectory points in the map frame EXPECT_EQ(received_msg.markers.size(), 20u); @@ -128,25 +133,33 @@ TEST(TrajectoryVisualizerTests, VisCandidateTrajectories) auto node = std::make_shared("my_node"); std::string name = "test"; auto parameters_handler = std::make_unique(node, name); + builtin_interfaces::msg::Time cmd_stamp; + cmd_stamp.sec = 5; + cmd_stamp.nanosec = 10; visualization_msgs::msg::MarkerArray received_msg; auto my_sub = node->create_subscription( "~/candidate_trajectories", [&](const visualization_msgs::msg::MarkerArray msg) {received_msg = msg;}); + rclcpp::executors::SingleThreadedExecutor executor; + executor.add_node(node->get_node_base_interface()); + models::Trajectories candidate_trajectories; candidate_trajectories.x = Eigen::ArrayXXf::Ones(200, 12); candidate_trajectories.y = Eigen::ArrayXXf::Ones(200, 12); candidate_trajectories.yaws = Eigen::ArrayXXf::Ones(200, 12); + Eigen::ArrayXf costs = Eigen::ArrayXf::Random(200); + TrajectoryVisualizer vis; vis.on_configure(node, "my_name", "fkmap", parameters_handler.get()); vis.on_activate(); - vis.add(candidate_trajectories, "Candidate Trajectories"); + vis.add(candidate_trajectories, costs, {}, cmd_stamp); nav_msgs::msg::Path bogus_path; vis.visualize(bogus_path); - rclcpp::spin_some(node->get_node_base_interface()); + executor.spin_some(); // 40 * 4, for 5 trajectory steps + 3 point steps EXPECT_EQ(received_msg.markers.size(), 160u); } @@ -165,6 +178,9 @@ TEST(TrajectoryVisualizerTests, VisOptimalPath) "~/optimal_path", [&](const nav_msgs::msg::Path msg) {received_path = msg;}); + rclcpp::executors::SingleThreadedExecutor executor; + executor.add_node(node->get_node_base_interface()); + // optimal_trajectory empty, should fail to publish Eigen::ArrayXXf optimal_trajectory; TrajectoryVisualizer vis; @@ -174,7 +190,7 @@ TEST(TrajectoryVisualizerTests, VisOptimalPath) nav_msgs::msg::Path bogus_path; vis.visualize(bogus_path); - rclcpp::spin_some(node->get_node_base_interface()); + executor.spin_some(); EXPECT_EQ(received_path.poses.size(), 0u); // Now populated with content, should publish @@ -187,7 +203,7 @@ TEST(TrajectoryVisualizerTests, VisOptimalPath) vis.add(optimal_trajectory, "Optimal Trajectory", cmd_stamp); vis.visualize(bogus_path); - rclcpp::spin_some(node->get_node_base_interface()); + executor.spin_some(); // Should have a 20 points path in the map frame and with same stamp than velocity command EXPECT_EQ(received_path.poses.size(), 20u); @@ -226,4 +242,4 @@ int main(int argc, char **argv) rclcpp::shutdown(); return result; -} +} \ No newline at end of file diff --git a/nav2_mppi_controller/test/utils_test copy 2.cpp b/nav2_mppi_controller/test/utils_test copy 2.cpp new file mode 100644 index 00000000000..70733e37db4 --- /dev/null +++ b/nav2_mppi_controller/test/utils_test copy 2.cpp @@ -0,0 +1,614 @@ +// 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 +#include +#include + +#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 + +using namespace mppi::utils; // NOLINT +using namespace mppi; // NOLINT + +class TestGoalChecker : public nav2_core::GoalChecker +{ +public: + TestGoalChecker() {} + + virtual void initialize( + const nav2::LifecycleNode::WeakPtr & /*parent*/, + const std::string & /*plugin_name*/, + const std::shared_ptr/*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.rows(), 5u); + EXPECT_EQ(path_t.y.rows(), 5u); + EXPECT_EQ(path_t.yaws.rows(), 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, AnglesTests) +{ + // Test angle normalization by creating insane angles + Eigen::ArrayXf angles(100); + angles.setConstant(1.0f); + + for (unsigned int i = 0; i != angles.size(); i++) { + angles(i) = i * i; + if (i % 2 == 0) { + angles(i) *= -1; + } + } + + auto norm_ang = normalize_angles(angles).eval(); + for (unsigned int i = 0; i != norm_ang.size(); i++) { + EXPECT_TRUE((norm_ang(i) >= -M_PIF) && (norm_ang(i) <= M_PIF)); + } + + // Test shortest angular distance + Eigen::ArrayXf zero_angles(100); + zero_angles.setZero(); + auto ang_dist = shortest_angular_distance(angles, zero_angles).eval(); + for (unsigned int i = 0; i != ang_dist.size(); i++) { + EXPECT_TRUE((ang_dist(i) >= -M_PIF) && (ang_dist(i) <= M_PIF)); + } + + // 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 point-pose angle with goal yaws + point_x = 1.0; + double point_yaw = 0.0; + EXPECT_NEAR(posePointAngle(pose, point_x, point_y, point_yaw), 0.0, 1e-6); + point_yaw = M_PI; + EXPECT_NEAR(posePointAngle(pose, point_x, point_y, point_yaw), M_PI, 1e-6); + point_yaw = 0.1; + EXPECT_NEAR(posePointAngle(pose, point_x, point_y, point_yaw), 0.0, 1e-3); + point_yaw = 3.04159; + EXPECT_NEAR(posePointAngle(pose, point_x, point_y, point_yaw), M_PI, 1e-3); +} + +TEST(UtilsTests, FurthestAndClosestReachedPoint) +{ + models::State state; + models::Trajectories generated_trajectories; + generated_trajectories.reset(100, 2); + models::Path path; + geometry_msgs::msg::Pose goal; + path.reset(10); + Eigen::ArrayXf 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 = Eigen::ArrayXXf::Ones(100, 2); + generated_trajectories.y = Eigen::ArrayXXf::Zero(100, 2); + generated_trajectories.yaws = Eigen::ArrayXXf::Zero(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), 5); +} + +TEST(UtilsTests, findPathCosts) +{ + models::State state; + models::Trajectories generated_trajectories; + models::Path path; + geometry_msgs::msg::Pose goal; + path.reset(50); + Eigen::ArrayXf 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(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( + "dummy_costmap", "", true); + 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.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.size() - 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 * Eigen::ArrayXf::Ones(30); + noisey_sequence.vy = 0.0 * Eigen::ArrayXf::Ones(30); + noisey_sequence.wz = 0.3 * Eigen::ArrayXf::Ones(30); + + // Make the sequence noisy + std::mt19937 engine; + std::normal_distribution normal_dist = std::normal_distribution(0.0f, 0.2f); + auto noises = Eigen::ArrayXf::NullaryExpr( + 30, [&] () {return normal_dist(engine);}); + noisey_sequence.vx += noises; + noisey_sequence.vy += noises; + noisey_sequence.wz += noises; + sequence_init = noisey_sequence; + + std::array 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 propagated 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 = 1; i != noisey_sequence.vx.size() - 1; 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); +} + +TEST(UtilsTests, ShiftColumnsByOnePlaceTest) +{ + // Try with scalar value + Eigen::ArrayXf scalar_val(1); + scalar_val(0) = 5; + utils::shiftColumnsByOnePlace(scalar_val, 1); + EXPECT_EQ(scalar_val.size(), 1); + EXPECT_EQ(scalar_val(0), 5); + + // Try with one dimensional array, shift right + Eigen::ArrayXf array_1d(4); + array_1d << 1, 2, 3, 4; + utils::shiftColumnsByOnePlace(array_1d, 1); + EXPECT_EQ(array_1d.size(), 4); + EXPECT_EQ(array_1d(0), 1); + EXPECT_EQ(array_1d(1), 1); + EXPECT_EQ(array_1d(2), 2); + EXPECT_EQ(array_1d(3), 3); + + // Try with one dimensional array, shift left + array_1d(1) = 5; + utils::shiftColumnsByOnePlace(array_1d, -1); + EXPECT_EQ(array_1d.size(), 4); + EXPECT_EQ(array_1d(0), 5); + EXPECT_EQ(array_1d(1), 2); + EXPECT_EQ(array_1d(2), 3); + EXPECT_EQ(array_1d(2), 3); + + // Try with two dimensional array, shift right + // 1 2 3 4 1 1 2 3 + // 5 6 7 8 -> 5 5 6 7 + // 9 10 11 12 9 9 10 11 + Eigen::ArrayXXf array_2d(3, 4); + array_2d << 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12; + utils::shiftColumnsByOnePlace(array_2d, 1); + EXPECT_EQ(array_2d.rows(), 3); + EXPECT_EQ(array_2d.cols(), 4); + EXPECT_EQ(array_2d(0, 0), 1); + EXPECT_EQ(array_2d(1, 0), 5); + EXPECT_EQ(array_2d(2, 0), 9); + EXPECT_EQ(array_2d(0, 1), 1); + EXPECT_EQ(array_2d(1, 1), 5); + EXPECT_EQ(array_2d(2, 1), 9); + EXPECT_EQ(array_2d(0, 2), 2); + EXPECT_EQ(array_2d(1, 2), 6); + EXPECT_EQ(array_2d(2, 2), 10); + EXPECT_EQ(array_2d(0, 3), 3); + EXPECT_EQ(array_2d(1, 3), 7); + EXPECT_EQ(array_2d(2, 3), 11); + + array_2d.col(0).setZero(); + + // Try with two dimensional array, shift left + // 0 1 2 3 1 2 3 3 + // 0 5 6 7 -> 5 6 7 7 + // 0 9 10 11 9 10 11 11 + utils::shiftColumnsByOnePlace(array_2d, -1); + EXPECT_EQ(array_2d.rows(), 3); + EXPECT_EQ(array_2d.cols(), 4); + EXPECT_EQ(array_2d(0, 0), 1); + EXPECT_EQ(array_2d(1, 0), 5); + EXPECT_EQ(array_2d(2, 0), 9); + EXPECT_EQ(array_2d(0, 1), 2); + EXPECT_EQ(array_2d(1, 1), 6); + EXPECT_EQ(array_2d(2, 1), 10); + EXPECT_EQ(array_2d(0, 2), 3); + EXPECT_EQ(array_2d(1, 2), 7); + EXPECT_EQ(array_2d(2, 2), 11); + EXPECT_EQ(array_2d(0, 3), 3); + EXPECT_EQ(array_2d(1, 3), 7); + EXPECT_EQ(array_2d(2, 3), 11); + + // Try with invalid direction value. + EXPECT_THROW(utils::shiftColumnsByOnePlace(array_2d, -2), std::logic_error); +} + +TEST(UtilsTests, NormalizeYawsBetweenPointsTest) +{ + Eigen::ArrayXf last_yaws(10); + last_yaws.setZero(); + + Eigen::ArrayXf yaw_between_points(10); + yaw_between_points.setZero(10); + + // Try with both angles 0 + Eigen::ArrayXf yaws_between_points_corrected = utils::normalize_yaws_between_points(last_yaws, + yaw_between_points); + EXPECT_TRUE(yaws_between_points_corrected.isApprox(yaw_between_points)); + + // Try with yaw between points as pi/4 + yaw_between_points.setConstant(M_PIF_2 / 2); + yaws_between_points_corrected = utils::normalize_yaws_between_points(last_yaws, + yaw_between_points); + EXPECT_TRUE(yaws_between_points_corrected.isApprox(yaw_between_points)); + + // Try with yaw between points as pi/2 + yaw_between_points.setConstant(M_PIF_2); + yaws_between_points_corrected = utils::normalize_yaws_between_points(last_yaws, + yaw_between_points); + EXPECT_TRUE(yaws_between_points_corrected.isApprox(yaw_between_points)); + + // Try with a few yaw between points more than pi/2 + yaw_between_points[1] = 1.2 * M_PIF_2; + yaws_between_points_corrected = utils::normalize_yaws_between_points(last_yaws, + yaw_between_points); + EXPECT_NEAR(yaws_between_points_corrected[1], -0.8 * M_PIF_2, 1e-3); + EXPECT_NEAR(yaws_between_points_corrected[0], yaw_between_points[0], 1e-3); + EXPECT_NEAR(yaws_between_points_corrected[9], yaw_between_points[9], 1e-3); + + // Try with goal angle 0 + float goal_angle = 0; + yaws_between_points_corrected = utils::normalize_yaws_between_points(goal_angle, + yaw_between_points); + EXPECT_NEAR(yaws_between_points_corrected[1], -0.8 * M_PIF_2, 1e-3); +} + +TEST(UtilsTests, toTrajectoryMsgTest) +{ + Eigen::ArrayXXf trajectory(5, 3); + trajectory << + 0.0, 0.0, 0.0, + 1.0, 1.0, 1.0, + 2.0, 2.0, 2.0, + 3.0, 3.0, 3.0, + 4.0, 4.0, 4.0; + + models::ControlSequence control_sequence; + control_sequence.vx = Eigen::ArrayXf::Ones(5); + control_sequence.wz = Eigen::ArrayXf::Ones(5); + control_sequence.vy = Eigen::ArrayXf::Zero(5); + + std_msgs::msg::Header header; + header.frame_id = "map"; + header.stamp = rclcpp::Time(100, 0, RCL_ROS_TIME); + + auto trajectory_msg = utils::toTrajectoryMsg( + trajectory, control_sequence, 1.0, header); + + EXPECT_EQ(trajectory_msg->header.frame_id, "map"); + EXPECT_EQ(trajectory_msg->header.stamp, header.stamp); + EXPECT_EQ(trajectory_msg->points.size(), 5u); + EXPECT_EQ(trajectory_msg->points[0].pose.position.x, 0.0); + EXPECT_EQ(trajectory_msg->points[0].pose.position.y, 0.0); + EXPECT_EQ(trajectory_msg->points[1].pose.position.x, 1.0); + EXPECT_EQ(trajectory_msg->points[1].pose.position.y, 1.0); + EXPECT_EQ(trajectory_msg->points[2].pose.position.x, 2.0); + EXPECT_EQ(trajectory_msg->points[2].pose.position.y, 2.0); + EXPECT_EQ(trajectory_msg->points[3].pose.position.x, 3.0); + EXPECT_EQ(trajectory_msg->points[3].pose.position.y, 3.0); + EXPECT_EQ(trajectory_msg->points[4].pose.position.x, 4.0); + EXPECT_EQ(trajectory_msg->points[4].pose.position.y, 4.0); + + EXPECT_EQ(trajectory_msg->points[0].velocity.linear.x, 1.0); + EXPECT_EQ(trajectory_msg->points[0].velocity.linear.y, 0.0); + EXPECT_EQ(trajectory_msg->points[0].velocity.angular.z, 1.0); + EXPECT_EQ(trajectory_msg->points[1].velocity.linear.x, 1.0); + EXPECT_EQ(trajectory_msg->points[1].velocity.linear.y, 0.0); + EXPECT_EQ(trajectory_msg->points[1].velocity.angular.z, 1.0); + EXPECT_EQ(trajectory_msg->points[2].velocity.linear.x, 1.0); + EXPECT_EQ(trajectory_msg->points[2].velocity.linear.y, 0.0); + EXPECT_EQ(trajectory_msg->points[2].velocity.angular.z, 1.0); + EXPECT_EQ(trajectory_msg->points[3].velocity.linear.x, 1.0); + EXPECT_EQ(trajectory_msg->points[3].velocity.linear.y, 0.0); + EXPECT_EQ(trajectory_msg->points[3].velocity.angular.z, 1.0); + EXPECT_EQ(trajectory_msg->points[4].velocity.linear.x, 1.0); + EXPECT_EQ(trajectory_msg->points[4].velocity.linear.y, 0.0); + EXPECT_EQ(trajectory_msg->points[4].velocity.angular.z, 1.0); + + EXPECT_EQ(trajectory_msg->points[0].time_from_start, rclcpp::Duration(0, 0)); + EXPECT_EQ(trajectory_msg->points[1].time_from_start, rclcpp::Duration(1, 0)); + EXPECT_EQ(trajectory_msg->points[2].time_from_start, rclcpp::Duration(2, 0)); + EXPECT_EQ(trajectory_msg->points[3].time_from_start, rclcpp::Duration(3, 0)); + EXPECT_EQ(trajectory_msg->points[4].time_from_start, rclcpp::Duration(4, 0)); +} + +TEST(UtilsTests, getLastPathPoseTest) +{ + nav_msgs::msg::Path path; + path.poses.resize(10); + path.poses[9].pose.position.x = 5.0; + path.poses[9].pose.position.y = 50.0; + path.poses[9].pose.orientation.x = 0.0; + path.poses[9].pose.orientation.y = 0.0; + path.poses[9].pose.orientation.z = 1.0; + path.poses[9].pose.orientation.w = 0.0; + + models::Path path_t = toTensor(path); + geometry_msgs::msg::Pose last_path_pose = utils::getLastPathPose(path_t); + + EXPECT_EQ(last_path_pose.position.x, 5); + EXPECT_EQ(last_path_pose.position.y, 50); + EXPECT_NEAR(last_path_pose.orientation.x, 0.0, 1e-3); + EXPECT_NEAR(last_path_pose.orientation.y, 0.0, 1e-3); + EXPECT_NEAR(last_path_pose.orientation.z, 1.0, 1e-3); + EXPECT_NEAR(last_path_pose.orientation.w, 0.0, 1e-3); +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + + rclcpp::init(0, nullptr); + + int result = RUN_ALL_TESTS(); + + rclcpp::shutdown(); + + return result; +} diff --git a/nav2_mppi_controller/test/utils_test.cpp b/nav2_mppi_controller/test/utils_test.cpp index 70733e37db4..792ae839b22 100644 --- a/nav2_mppi_controller/test/utils_test.cpp +++ b/nav2_mppi_controller/test/utils_test.cpp @@ -181,7 +181,8 @@ TEST(UtilsTests, FurthestAndClosestReachedPoint) float model_dt = 0.1; CriticData data = - {state, generated_trajectories, path, goal, costs, model_dt, false, nullptr, nullptr, + {state, generated_trajectories, path, goal, costs, std::nullopt, model_dt, false, std::nullopt, nullptr, + nullptr, std::nullopt, std::nullopt}; /// Caution, keep references // Attempt to set furthest point if notionally set, should not change @@ -191,7 +192,8 @@ TEST(UtilsTests, FurthestAndClosestReachedPoint) // 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, + {state, generated_trajectories, path, goal, costs, std::nullopt, model_dt, false, std::nullopt, nullptr, + nullptr, std::nullopt, std::nullopt}; /// Caution, keep references setPathFurthestPointIfNotSet(data2); EXPECT_EQ(data2.furthest_reached_path_point, 0); @@ -210,7 +212,8 @@ TEST(UtilsTests, FurthestAndClosestReachedPoint) path = toTensor(plan); CriticData data3 = - {state, generated_trajectories, path, goal, costs, model_dt, false, nullptr, nullptr, + {state, generated_trajectories, path, goal, costs, std::nullopt, model_dt, false, std::nullopt, nullptr, + nullptr, std::nullopt, std::nullopt}; /// Caution, keep references EXPECT_EQ(findPathFurthestReachedPoint(data3), 5); } @@ -226,7 +229,8 @@ TEST(UtilsTests, findPathCosts) float model_dt = 0.1; CriticData data = - {state, generated_trajectories, path, goal, costs, model_dt, false, nullptr, nullptr, + {state, generated_trajectories, path, goal, costs, std::nullopt, model_dt, false, std::nullopt, nullptr, + nullptr, std::nullopt, std::nullopt}; /// Caution, keep references // Test not set if already set, should not change @@ -239,7 +243,8 @@ TEST(UtilsTests, findPathCosts) EXPECT_EQ(data.path_pts_valid->size(), 10u); CriticData data3 = - {state, generated_trajectories, path, goal, costs, model_dt, false, nullptr, nullptr, + {state, generated_trajectories, path, goal, costs, std::nullopt, model_dt, false, std::nullopt, nullptr, + nullptr, std::nullopt, std::nullopt}; /// Caution, keep references auto costmap_ros = std::make_shared( @@ -611,4 +616,4 @@ int main(int argc, char **argv) rclcpp::shutdown(); return result; -} +} \ No newline at end of file diff --git a/nav2_mppi_controller/trajectory_validators.xml b/nav2_mppi_controller/trajectory_validators.xml index 5a4da246123..fb02d0dac9a 100644 --- a/nav2_mppi_controller/trajectory_validators.xml +++ b/nav2_mppi_controller/trajectory_validators.xml @@ -3,5 +3,12 @@ Default trajectory validator for MPPI controller + + + Trajectory validator with automatic collision repair before rejection. + + diff --git a/nav2_simple_commander/nav2_simple_commander/robot_navigator.py b/nav2_simple_commander/nav2_simple_commander/robot_navigator.py index ea93fc61711..f25c7774293 100644 --- a/nav2_simple_commander/nav2_simple_commander/robot_navigator.py +++ b/nav2_simple_commander/nav2_simple_commander/robot_navigator.py @@ -41,7 +41,7 @@ from rclpy.node import Node from rclpy.qos import QoSDurabilityPolicy, QoSHistoryPolicy, QoSProfile, QoSReliabilityPolicy from rclpy.task import Future -from rclpy.type_support import GetResultServiceResponse +#from rclpy.type_support import GetResultServiceResponse # Task Result enum for the result of the task being executed @@ -291,7 +291,7 @@ def goThroughPoses(self, poses: Goals, behavior_tree: str = '') -> Optional[Runn goal_msg.poses = poses goal_msg.behavior_tree = behavior_tree - self.info(f'Navigating with {len(goal_msg.poses)} goals....') + self.info(f'Navigating with {len(goal_msg.poses.goals)} goals....') send_goal_future = self.nav_through_poses_client.send_goal_async( goal_msg, self._feedbackCallback ) diff --git a/nav2_smac_planner/lattice_primitives/config.json b/nav2_smac_planner/lattice_primitives/config.json index 12a8470bdb3..531bbdccad1 100644 --- a/nav2_smac_planner/lattice_primitives/config.json +++ b/nav2_smac_planner/lattice_primitives/config.json @@ -1,6 +1,6 @@ { - "motion_model": "ackermann", - "turning_radius": 0.5, + "motion_model": "diff", + "turning_radius": 0.00, "grid_resolution": 0.05, "stopping_threshold": 5, "num_of_headings": 16 diff --git a/nav2_smac_planner/lattice_primitives/generate_motion_primitives.py b/nav2_smac_planner/lattice_primitives/generate_motion_primitives.py index bf9e64559ab..7e278a9313b 100644 --- a/nav2_smac_planner/lattice_primitives/generate_motion_primitives.py +++ b/nav2_smac_planner/lattice_primitives/generate_motion_primitives.py @@ -21,9 +21,9 @@ from typing import Any, cast, Dict, List, TypedDict import matplotlib.pyplot as plt -from nav2_smac_planner.lattice_primitives import constants -from nav2_smac_planner.lattice_primitives.lattice_generator import ConfigDict, LatticeGenerator -from nav2_smac_planner.lattice_primitives.trajectory import Trajectory +from lattice_primitives import constants +from lattice_primitives.lattice_generator import ConfigDict, LatticeGenerator +from lattice_primitives.trajectory import Trajectory import numpy as np logging.basicConfig(level=logging.INFO) diff --git a/nav2_smac_planner/lattice_primitives/lattice_generator.py b/nav2_smac_planner/lattice_primitives/lattice_generator.py index 7aacd36e156..1d909366f95 100644 --- a/nav2_smac_planner/lattice_primitives/lattice_generator.py +++ b/nav2_smac_planner/lattice_primitives/lattice_generator.py @@ -16,10 +16,10 @@ from enum import Enum from typing import Any, Dict, List, Tuple, TypedDict -from nav2_smac_planner.lattice_primitives.helper import angle_difference, interpolate_yaws -from nav2_smac_planner.lattice_primitives.trajectory import (AnyFloat, FloatNDArray, Path, +from lattice_primitives.helper import angle_difference, interpolate_yaws +from lattice_primitives.trajectory import (AnyFloat, FloatNDArray, Path, Trajectory, TrajectoryParameters) -from nav2_smac_planner.lattice_primitives.trajectory_generator import TrajectoryGenerator +from lattice_primitives.trajectory_generator import TrajectoryGenerator import numpy as np from rtree import index diff --git a/nav2_smac_planner/lattice_primitives/output.json b/nav2_smac_planner/lattice_primitives/output.json new file mode 100644 index 00000000000..b5766a0c2df --- /dev/null +++ b/nav2_smac_planner/lattice_primitives/output.json @@ -0,0 +1,3864 @@ +{ + "version": 1.0, + "date_generated": "2025-12-03", + "lattice_metadata": { + "motion_model": "diff", + "turning_radius": 0.08, + "grid_resolution": 0.05, + "stopping_threshold": 5, + "num_of_headings": 16, + "heading_angles": [ + 0.0, + 0.4636476090008061, + 0.7853981633974483, + 1.1071487177940904, + 1.5707963267948966, + 2.0344439357957027, + 2.356194490192345, + 2.677945044588987, + 3.141592653589793, + 3.6052402625905993, + 3.9269908169872414, + 4.2487413713838835, + 4.71238898038469, + 5.176036589385496, + 5.497787143782138, + 5.81953769817878 + ], + "number_of_trajectories": 136 + }, + "primitives": [ + { + "trajectory_id": 0, + "start_angle_index": 0, + "end_angle_index": 13, + "left_turn": false, + "trajectory_radius": 0.0809, + "trajectory_length": 0.15137, + "arc_length": 0.08957, + "straight_length": 0.0618, + "poses": [ + [ + 0.04725, + -0.01523, + 5.659486377865489 + ], + [ + 0.07743, + -0.05487, + 5.176036589385496 + ], + [ + 0.1, + -0.1, + 5.176036589385496 + ] + ] + }, + { + "trajectory_id": 1, + "start_angle_index": 0, + "end_angle_index": 14, + "left_turn": false, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + 0.05558, + -0.01356, + 5.80469930592716 + ], + [ + 0.1, + -0.05, + 5.497787143782138 + ] + ] + }, + { + "trajectory_id": 2, + "start_angle_index": 0, + "end_angle_index": 15, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 6.128636104179318 + ], + [ + 0.0, + 0.0, + 5.9740869011790485 + ], + [ + 0.0, + 0.0, + 5.81953769817878 + ] + ] + }, + { + "trajectory_id": 3, + "start_angle_index": 0, + "end_angle_index": 0, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.05, + "arc_length": 0.0, + "straight_length": 0.05, + "poses": [ + [ + 0.05, + 0.0, + 0.0 + ] + ] + }, + { + "trajectory_id": 4, + "start_angle_index": 0, + "end_angle_index": 1, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.1545492030002687 + ], + [ + 0.0, + 0.0, + 0.3090984060005374 + ], + [ + 0.0, + 0.0, + 0.4636476090008061 + ] + ] + }, + { + "trajectory_id": 5, + "start_angle_index": 0, + "end_angle_index": 2, + "left_turn": true, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + 0.05558, + 0.01356, + 0.47848600125242624 + ], + [ + 0.1, + 0.05, + 0.7853981633974483 + ] + ] + }, + { + "trajectory_id": 6, + "start_angle_index": 0, + "end_angle_index": 3, + "left_turn": true, + "trajectory_radius": 0.0809, + "trajectory_length": 0.15137, + "arc_length": 0.08957, + "straight_length": 0.0618, + "poses": [ + [ + 0.04725, + 0.01523, + 0.6236989293140972 + ], + [ + 0.07743, + 0.05487, + 1.1071487177940904 + ], + [ + 0.1, + 0.1, + 1.1071487177940904 + ] + ] + }, + { + "trajectory_id": 7, + "start_angle_index": 1, + "end_angle_index": 14, + "left_turn": false, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + 0.04916, + 0.02356, + 0.32653252396015897 + ], + [ + 0.10294, + 0.02617, + 6.053752487460941 + ], + [ + 0.15, + 0.0, + 5.497787143782138 + ] + ] + }, + { + "trajectory_id": 8, + "start_angle_index": 1, + "end_angle_index": 15, + "left_turn": false, + "trajectory_radius": 0.1118, + "trajectory_length": 0.10367, + "arc_length": 0.10367, + "straight_length": 0.0, + "poses": [ + [ + 0.05, + 0.0118, + 0.0 + ], + [ + 0.1, + 0.0, + 5.81953769817878 + ] + ] + }, + { + "trajectory_id": 9, + "start_angle_index": 1, + "end_angle_index": 15, + "left_turn": false, + "trajectory_radius": 0.22361, + "trajectory_length": 0.20735, + "arc_length": 0.20735, + "straight_length": 0.0, + "poses": [ + [ + 0.04862, + 0.01763, + 0.23182380450040305 + ], + [ + 0.1, + 0.02361, + 0.0 + ], + [ + 0.15138, + 0.01763, + 6.051361502679184 + ], + [ + 0.2, + 0.0, + 5.81953769817878 + ] + ] + }, + { + "trajectory_id": 10, + "start_angle_index": 1, + "end_angle_index": 15, + "left_turn": false, + "trajectory_radius": 0.33541, + "trajectory_length": 0.31102, + "arc_length": 0.31102, + "straight_length": 0.0, + "poses": [ + [ + 0.04797, + 0.01951, + 0.3090984060005374 + ], + [ + 0.09837, + 0.03141, + 0.1545492030002687 + ], + [ + 0.15, + 0.03541, + 0.0 + ], + [ + 0.20163, + 0.03141, + 6.128636104179318 + ], + [ + 0.25203, + 0.01951, + 5.9740869011790485 + ], + [ + 0.3, + 0.0, + 5.81953769817878 + ] + ] + }, + { + "trajectory_id": 11, + "start_angle_index": 1, + "end_angle_index": 0, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 0.4636476090008061 + ], + [ + 0.0, + 0.0, + 0.23182380450040305 + ], + [ + 0.0, + 0.0, + 0.0 + ] + ] + }, + { + "trajectory_id": 12, + "start_angle_index": 1, + "end_angle_index": 1, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.1118, + "arc_length": 0.0, + "straight_length": 0.1118, + "poses": [ + [ + 0.05, + 0.025, + 0.4636476090008061 + ], + [ + 0.1, + 0.05, + 0.4636476090008061 + ] + ] + }, + { + "trajectory_id": 13, + "start_angle_index": 1, + "end_angle_index": 2, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 0.4636476090008061 + ], + [ + 0.0, + 0.0, + 0.6245228861991272 + ], + [ + 0.0, + 0.0, + 0.7853981633974483 + ] + ] + }, + { + "trajectory_id": 14, + "start_angle_index": 1, + "end_angle_index": 3, + "left_turn": true, + "trajectory_radius": 0.1118, + "trajectory_length": 0.07194, + "arc_length": 0.07194, + "straight_length": 0.0, + "poses": [ + [ + 0.05, + 0.05, + 1.1071487177940904 + ] + ] + }, + { + "trajectory_id": 15, + "start_angle_index": 1, + "end_angle_index": 3, + "left_turn": true, + "trajectory_radius": 0.22361, + "trajectory_length": 0.14389, + "arc_length": 0.14389, + "straight_length": 0.0, + "poses": [ + [ + 0.04028, + 0.02587, + 0.6781479785985676 + ], + [ + 0.07413, + 0.05972, + 0.892648348196329 + ], + [ + 0.1, + 0.1, + 1.1071487177940904 + ] + ] + }, + { + "trajectory_id": 16, + "start_angle_index": 1, + "end_angle_index": 4, + "left_turn": true, + "trajectory_radius": 0.3618, + "trajectory_length": 0.42696, + "arc_length": 0.40057, + "straight_length": 0.02639, + "poses": [ + [ + 0.04092, + 0.02394, + 0.5947686722333784 + ], + [ + 0.07836, + 0.05302, + 0.7258897354659507 + ], + [ + 0.11168, + 0.08674, + 0.8570107986985229 + ], + [ + 0.1403, + 0.12453, + 0.9881318619310953 + ], + [ + 0.16374, + 0.16574, + 1.1192529251636676 + ], + [ + 0.18159, + 0.20965, + 1.25037398839624 + ], + [ + 0.19354, + 0.25553, + 1.3814950516288125 + ], + [ + 0.19939, + 0.30257, + 1.512616114861385 + ], + [ + 0.2, + 0.35, + 1.5707963267948966 + ] + ] + }, + { + "trajectory_id": 17, + "start_angle_index": 2, + "end_angle_index": 15, + "left_turn": false, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + 0.04706, + 0.02617, + 0.2294328197186447 + ], + [ + 0.10084, + 0.02356, + 5.956652783219427 + ], + [ + 0.15, + 0.0, + 5.81953769817878 + ] + ] + }, + { + "trajectory_id": 18, + "start_angle_index": 2, + "end_angle_index": 0, + "left_turn": false, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + 0.04442, + 0.03644, + 0.4784860012524262 + ], + [ + 0.1, + 0.05, + 0.0 + ] + ] + }, + { + "trajectory_id": 19, + "start_angle_index": 2, + "end_angle_index": 1, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 0.7853981633974483 + ], + [ + 0.0, + 0.0, + 0.6245228861991272 + ], + [ + 0.0, + 0.0, + 0.4636476090008061 + ] + ] + }, + { + "trajectory_id": 20, + "start_angle_index": 2, + "end_angle_index": 2, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.07071, + "arc_length": 0.0, + "straight_length": 0.07071, + "poses": [ + [ + 0.05, + 0.05, + 0.7853981633974483 + ] + ] + }, + { + "trajectory_id": 21, + "start_angle_index": 2, + "end_angle_index": 3, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 0.7853981633974483 + ], + [ + 0.0, + 0.0, + 0.9462734405957693 + ], + [ + 0.0, + 0.0, + 1.1071487177940904 + ] + ] + }, + { + "trajectory_id": 22, + "start_angle_index": 2, + "end_angle_index": 4, + "left_turn": true, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + 0.03644, + 0.04442, + 1.0923103255424704 + ], + [ + 0.05, + 0.1, + 1.5707963267948966 + ] + ] + }, + { + "trajectory_id": 23, + "start_angle_index": 2, + "end_angle_index": 5, + "left_turn": true, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + 0.02617, + 0.04706, + 1.341363507076252 + ], + [ + 0.02356, + 0.10084, + 1.8973288507550554 + ], + [ + 0.0, + 0.15, + 2.0344439357957027 + ] + ] + }, + { + "trajectory_id": 24, + "start_angle_index": 3, + "end_angle_index": 0, + "left_turn": false, + "trajectory_radius": 0.36181, + "trajectory_length": 0.42697, + "arc_length": 0.40058, + "straight_length": 0.02639, + "poses": [ + [ + 0.02393, + 0.04093, + 0.9760278785613808 + ], + [ + 0.05301, + 0.07837, + 0.8449070393286713 + ], + [ + 0.08673, + 0.11169, + 0.7137862000959616 + ], + [ + 0.12452, + 0.14031, + 0.582665360863252 + ], + [ + 0.16573, + 0.16375, + 0.4515445216305424 + ], + [ + 0.20965, + 0.18159, + 0.3204236823978327 + ], + [ + 0.25553, + 0.19355, + 0.18930284316512325 + ], + [ + 0.30257, + 0.1994, + 0.05818200393241346 + ], + [ + 0.35, + 0.2, + 0.0 + ] + ] + }, + { + "trajectory_id": 25, + "start_angle_index": 3, + "end_angle_index": 1, + "left_turn": false, + "trajectory_radius": 0.1118, + "trajectory_length": 0.07194, + "arc_length": 0.07194, + "straight_length": 0.0, + "poses": [ + [ + 0.05, + 0.05, + 0.4636476090008061 + ] + ] + }, + { + "trajectory_id": 26, + "start_angle_index": 3, + "end_angle_index": 1, + "left_turn": false, + "trajectory_radius": 0.22361, + "trajectory_length": 0.14389, + "arc_length": 0.14389, + "straight_length": 0.0, + "poses": [ + [ + 0.02587, + 0.04028, + 0.892648348196329 + ], + [ + 0.05972, + 0.07413, + 0.6781479785985676 + ], + [ + 0.1, + 0.1, + 0.4636476090008061 + ] + ] + }, + { + "trajectory_id": 27, + "start_angle_index": 3, + "end_angle_index": 2, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 1.1071487177940904 + ], + [ + 0.0, + 0.0, + 0.9998985329952097 + ], + [ + 0.0, + 0.0, + 0.892648348196329 + ], + [ + 0.0, + 0.0, + 0.7853981633974483 + ] + ] + }, + { + "trajectory_id": 28, + "start_angle_index": 3, + "end_angle_index": 3, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.1118, + "arc_length": 0.0, + "straight_length": 0.1118, + "poses": [ + [ + 0.025, + 0.05, + 1.1071487177940904 + ], + [ + 0.05, + 0.1, + 1.1071487177940904 + ] + ] + }, + { + "trajectory_id": 29, + "start_angle_index": 3, + "end_angle_index": 4, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 1.1071487177940904 + ], + [ + 0.0, + 0.0, + 1.261697920794359 + ], + [ + 0.0, + 0.0, + 1.416247123794628 + ], + [ + 0.0, + 0.0, + 1.5707963267948966 + ] + ] + }, + { + "trajectory_id": 30, + "start_angle_index": 3, + "end_angle_index": 5, + "left_turn": true, + "trajectory_radius": 0.1118, + "trajectory_length": 0.10367, + "arc_length": 0.10367, + "straight_length": 0.0, + "poses": [ + [ + 0.0118, + 0.05, + 1.5707963267948966 + ], + [ + 0.0, + 0.1, + 2.0344439357957027 + ] + ] + }, + { + "trajectory_id": 31, + "start_angle_index": 3, + "end_angle_index": 5, + "left_turn": true, + "trajectory_radius": 0.22361, + "trajectory_length": 0.20735, + "arc_length": 0.20735, + "straight_length": 0.0, + "poses": [ + [ + 0.01763, + 0.04862, + 1.3389725222944935 + ], + [ + 0.02361, + 0.1, + 1.5707963267948966 + ], + [ + 0.01763, + 0.15138, + 1.8026201312952996 + ], + [ + 0.0, + 0.2, + 2.0344439357957027 + ] + ] + }, + { + "trajectory_id": 32, + "start_angle_index": 3, + "end_angle_index": 5, + "left_turn": true, + "trajectory_radius": 0.33541, + "trajectory_length": 0.31102, + "arc_length": 0.31102, + "straight_length": 0.0, + "poses": [ + [ + 0.01951, + 0.04797, + 1.261697920794359 + ], + [ + 0.03141, + 0.09837, + 1.416247123794628 + ], + [ + 0.03541, + 0.15, + 1.5707963267948966 + ], + [ + 0.03141, + 0.20163, + 1.7253455297951654 + ], + [ + 0.01951, + 0.25203, + 1.879894732795434 + ], + [ + 0.0, + 0.3, + 2.0344439357957027 + ] + ] + }, + { + "trajectory_id": 33, + "start_angle_index": 3, + "end_angle_index": 6, + "left_turn": true, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + 0.02356, + 0.04916, + 1.2442638028347375 + ], + [ + 0.02617, + 0.10294, + 1.8002291465135412 + ], + [ + 0.0, + 0.15, + 2.356194490192345 + ] + ] + }, + { + "trajectory_id": 34, + "start_angle_index": 4, + "end_angle_index": 1, + "left_turn": false, + "trajectory_radius": 0.0809, + "trajectory_length": 0.15137, + "arc_length": 0.08957, + "straight_length": 0.0618, + "poses": [ + [ + 0.01523, + 0.04725, + 0.9470973974807994 + ], + [ + 0.05487, + 0.07743, + 0.4636476090008061 + ], + [ + 0.1, + 0.1, + 0.4636476090008061 + ] + ] + }, + { + "trajectory_id": 35, + "start_angle_index": 4, + "end_angle_index": 2, + "left_turn": false, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + 0.01356, + 0.05558, + 1.0923103255424704 + ], + [ + 0.05, + 0.1, + 0.7853981633974483 + ] + ] + }, + { + "trajectory_id": 36, + "start_angle_index": 4, + "end_angle_index": 3, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 1.5707963267948966 + ], + [ + 0.0, + 0.0, + 1.416247123794628 + ], + [ + 0.0, + 0.0, + 1.261697920794359 + ], + [ + 0.0, + 0.0, + 1.1071487177940904 + ] + ] + }, + { + "trajectory_id": 37, + "start_angle_index": 4, + "end_angle_index": 4, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.05, + "arc_length": 0.0, + "straight_length": 0.05, + "poses": [ + [ + 0.0, + 0.05, + 1.5707963267948966 + ] + ] + }, + { + "trajectory_id": 38, + "start_angle_index": 4, + "end_angle_index": 5, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 1.5707963267948966 + ], + [ + 0.0, + 0.0, + 1.7253455297951652 + ], + [ + 0.0, + 0.0, + 1.879894732795434 + ], + [ + 0.0, + 0.0, + 2.0344439357957027 + ] + ] + }, + { + "trajectory_id": 39, + "start_angle_index": 4, + "end_angle_index": 6, + "left_turn": true, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + -0.01356, + 0.05558, + 2.049282328047323 + ], + [ + -0.05, + 0.1, + 2.356194490192345 + ] + ] + }, + { + "trajectory_id": 40, + "start_angle_index": 4, + "end_angle_index": 7, + "left_turn": true, + "trajectory_radius": 0.0809, + "trajectory_length": 0.15137, + "arc_length": 0.08957, + "straight_length": 0.0618, + "poses": [ + [ + -0.01523, + 0.04725, + 2.194495256108994 + ], + [ + -0.05487, + 0.07743, + 2.677945044588987 + ], + [ + -0.1, + 0.1, + 2.677945044588987 + ] + ] + }, + { + "trajectory_id": 41, + "start_angle_index": 5, + "end_angle_index": 2, + "left_turn": false, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + -0.02356, + 0.04916, + 1.8973288507550556 + ], + [ + -0.02617, + 0.10294, + 1.341363507076252 + ], + [ + 0.0, + 0.15, + 0.7853981633974483 + ] + ] + }, + { + "trajectory_id": 42, + "start_angle_index": 5, + "end_angle_index": 3, + "left_turn": false, + "trajectory_radius": 0.1118, + "trajectory_length": 0.10367, + "arc_length": 0.10367, + "straight_length": 0.0, + "poses": [ + [ + -0.0118, + 0.05, + 1.5707963267948966 + ], + [ + 0.0, + 0.1, + 1.1071487177940904 + ] + ] + }, + { + "trajectory_id": 43, + "start_angle_index": 5, + "end_angle_index": 3, + "left_turn": false, + "trajectory_radius": 0.22361, + "trajectory_length": 0.20735, + "arc_length": 0.20735, + "straight_length": 0.0, + "poses": [ + [ + -0.01763, + 0.04862, + 1.8026201312952996 + ], + [ + -0.02361, + 0.1, + 1.5707963267948966 + ], + [ + -0.01763, + 0.15138, + 1.3389725222944935 + ], + [ + 0.0, + 0.2, + 1.1071487177940904 + ] + ] + }, + { + "trajectory_id": 44, + "start_angle_index": 5, + "end_angle_index": 3, + "left_turn": false, + "trajectory_radius": 0.33541, + "trajectory_length": 0.31102, + "arc_length": 0.31102, + "straight_length": 0.0, + "poses": [ + [ + -0.01951, + 0.04797, + 1.879894732795434 + ], + [ + -0.03141, + 0.09837, + 1.7253455297951652 + ], + [ + -0.03541, + 0.15, + 1.5707963267948966 + ], + [ + -0.03141, + 0.20163, + 1.4162471237946277 + ], + [ + -0.01951, + 0.25203, + 1.261697920794359 + ], + [ + 0.0, + 0.3, + 1.1071487177940904 + ] + ] + }, + { + "trajectory_id": 45, + "start_angle_index": 5, + "end_angle_index": 4, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 2.0344439357957027 + ], + [ + 0.0, + 0.0, + 1.8026201312952996 + ], + [ + 0.0, + 0.0, + 1.5707963267948966 + ] + ] + }, + { + "trajectory_id": 46, + "start_angle_index": 5, + "end_angle_index": 5, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.1118, + "arc_length": 0.0, + "straight_length": 0.1118, + "poses": [ + [ + -0.025, + 0.05, + 2.0344439357957027 + ], + [ + -0.05, + 0.1, + 2.0344439357957027 + ] + ] + }, + { + "trajectory_id": 47, + "start_angle_index": 5, + "end_angle_index": 6, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 2.0344439357957027 + ], + [ + 0.0, + 0.0, + 2.1953192129940238 + ], + [ + 0.0, + 0.0, + 2.356194490192345 + ] + ] + }, + { + "trajectory_id": 48, + "start_angle_index": 5, + "end_angle_index": 7, + "left_turn": true, + "trajectory_radius": 0.1118, + "trajectory_length": 0.07194, + "arc_length": 0.07194, + "straight_length": 0.0, + "poses": [ + [ + -0.05, + 0.05, + 2.677945044588987 + ] + ] + }, + { + "trajectory_id": 49, + "start_angle_index": 5, + "end_angle_index": 7, + "left_turn": true, + "trajectory_radius": 0.22361, + "trajectory_length": 0.14389, + "arc_length": 0.14389, + "straight_length": 0.0, + "poses": [ + [ + -0.02587, + 0.04028, + 2.248944305393464 + ], + [ + -0.05972, + 0.07413, + 2.4634446749912255 + ], + [ + -0.1, + 0.1, + 2.677945044588987 + ] + ] + }, + { + "trajectory_id": 50, + "start_angle_index": 5, + "end_angle_index": 8, + "left_turn": true, + "trajectory_radius": 0.36181, + "trajectory_length": 0.42697, + "arc_length": 0.40058, + "straight_length": 0.02639, + "poses": [ + [ + -0.02393, + 0.04093, + 2.1655647750284124 + ], + [ + -0.05301, + 0.07837, + 2.296685614261122 + ], + [ + -0.08673, + 0.11169, + 2.4278064534938313 + ], + [ + -0.12452, + 0.14031, + 2.558927292726541 + ], + [ + -0.16573, + 0.16375, + 2.6900481319592506 + ], + [ + -0.20965, + 0.18159, + 2.8211689711919603 + ], + [ + -0.25553, + 0.19355, + 2.95228981042467 + ], + [ + -0.30257, + 0.1994, + 3.0834106496573797 + ], + [ + -0.35, + 0.2, + 3.141592653589793 + ] + ] + }, + { + "trajectory_id": 51, + "start_angle_index": 6, + "end_angle_index": 9, + "left_turn": true, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + -0.04706, + 0.02617, + 2.9121598338711485 + ], + [ + -0.10084, + 0.02356, + 3.468125177549952 + ], + [ + -0.15, + 0.0, + 3.6052402625905993 + ] + ] + }, + { + "trajectory_id": 52, + "start_angle_index": 6, + "end_angle_index": 3, + "left_turn": false, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + -0.02617, + 0.04706, + 1.8002291465135412 + ], + [ + -0.02356, + 0.10084, + 1.2442638028347377 + ], + [ + 0.0, + 0.15, + 1.1071487177940904 + ] + ] + }, + { + "trajectory_id": 53, + "start_angle_index": 6, + "end_angle_index": 4, + "left_turn": false, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + -0.03644, + 0.04442, + 2.049282328047323 + ], + [ + -0.05, + 0.1, + 1.5707963267948966 + ] + ] + }, + { + "trajectory_id": 54, + "start_angle_index": 6, + "end_angle_index": 5, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 2.356194490192345 + ], + [ + 0.0, + 0.0, + 2.1953192129940238 + ], + [ + 0.0, + 0.0, + 2.0344439357957027 + ] + ] + }, + { + "trajectory_id": 55, + "start_angle_index": 6, + "end_angle_index": 6, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.07071, + "arc_length": 0.0, + "straight_length": 0.07071, + "poses": [ + [ + -0.05, + 0.05, + 2.356194490192345 + ] + ] + }, + { + "trajectory_id": 56, + "start_angle_index": 6, + "end_angle_index": 7, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 2.356194490192345 + ], + [ + 0.0, + 0.0, + 2.517069767390666 + ], + [ + 0.0, + 0.0, + 2.677945044588987 + ] + ] + }, + { + "trajectory_id": 57, + "start_angle_index": 6, + "end_angle_index": 8, + "left_turn": true, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + -0.04442, + 0.03644, + 2.6631066523373668 + ], + [ + -0.1, + 0.05, + 3.141592653589793 + ] + ] + }, + { + "trajectory_id": 58, + "start_angle_index": 7, + "end_angle_index": 9, + "left_turn": true, + "trajectory_radius": 0.1118, + "trajectory_length": 0.10367, + "arc_length": 0.10367, + "straight_length": 0.0, + "poses": [ + [ + -0.05, + 0.0118, + 3.141592653589793 + ], + [ + -0.1, + 0.0, + 3.6052402625905993 + ] + ] + }, + { + "trajectory_id": 59, + "start_angle_index": 7, + "end_angle_index": 9, + "left_turn": true, + "trajectory_radius": 0.22361, + "trajectory_length": 0.20735, + "arc_length": 0.20735, + "straight_length": 0.0, + "poses": [ + [ + -0.04862, + 0.01763, + 2.90976884908939 + ], + [ + -0.1, + 0.02361, + 3.141592653589793 + ], + [ + -0.15138, + 0.01763, + 3.373416458090196 + ], + [ + -0.2, + 0.0, + 3.6052402625905993 + ] + ] + }, + { + "trajectory_id": 60, + "start_angle_index": 7, + "end_angle_index": 9, + "left_turn": true, + "trajectory_radius": 0.33541, + "trajectory_length": 0.31102, + "arc_length": 0.31102, + "straight_length": 0.0, + "poses": [ + [ + -0.04797, + 0.01951, + 2.832494247589256 + ], + [ + -0.09837, + 0.03141, + 2.9870434505895243 + ], + [ + -0.15, + 0.03541, + 3.141592653589793 + ], + [ + -0.20163, + 0.03141, + 3.296141856590062 + ], + [ + -0.25203, + 0.01951, + 3.450691059590331 + ], + [ + -0.3, + 0.0, + 3.6052402625905993 + ] + ] + }, + { + "trajectory_id": 61, + "start_angle_index": 7, + "end_angle_index": 10, + "left_turn": true, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + -0.04916, + 0.02356, + 2.815060129629634 + ], + [ + -0.10294, + 0.02617, + 3.3710254733084377 + ], + [ + -0.15, + 0.0, + 3.9269908169872414 + ] + ] + }, + { + "trajectory_id": 62, + "start_angle_index": 7, + "end_angle_index": 4, + "left_turn": false, + "trajectory_radius": 0.36181, + "trajectory_length": 0.42697, + "arc_length": 0.40058, + "straight_length": 0.02639, + "poses": [ + [ + -0.04093, + 0.02393, + 2.5468242053562773 + ], + [ + -0.07837, + 0.05301, + 2.4157033661235676 + ], + [ + -0.11169, + 0.08673, + 2.2845825268908584 + ], + [ + -0.14031, + 0.12452, + 2.1534616876581487 + ], + [ + -0.16375, + 0.16573, + 2.022340848425439 + ], + [ + -0.18159, + 0.20965, + 1.8912200091927294 + ], + [ + -0.19355, + 0.25553, + 1.7600991699600197 + ], + [ + -0.1994, + 0.30257, + 1.62897833072731 + ], + [ + -0.2, + 0.35, + 1.5707963267948966 + ] + ] + }, + { + "trajectory_id": 63, + "start_angle_index": 7, + "end_angle_index": 5, + "left_turn": false, + "trajectory_radius": 0.1118, + "trajectory_length": 0.07194, + "arc_length": 0.07194, + "straight_length": 0.0, + "poses": [ + [ + -0.05, + 0.05, + 2.0344439357957027 + ] + ] + }, + { + "trajectory_id": 64, + "start_angle_index": 7, + "end_angle_index": 5, + "left_turn": false, + "trajectory_radius": 0.22361, + "trajectory_length": 0.14389, + "arc_length": 0.14389, + "straight_length": 0.0, + "poses": [ + [ + -0.04028, + 0.02587, + 2.4634446749912255 + ], + [ + -0.07413, + 0.05972, + 2.248944305393464 + ], + [ + -0.1, + 0.1, + 2.0344439357957027 + ] + ] + }, + { + "trajectory_id": 65, + "start_angle_index": 7, + "end_angle_index": 6, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 2.677945044588987 + ], + [ + 0.0, + 0.0, + 2.5706948597901063 + ], + [ + 0.0, + 0.0, + 2.4634446749912255 + ], + [ + 0.0, + 0.0, + 2.356194490192345 + ] + ] + }, + { + "trajectory_id": 66, + "start_angle_index": 7, + "end_angle_index": 7, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.1118, + "arc_length": 0.0, + "straight_length": 0.1118, + "poses": [ + [ + -0.05, + 0.025, + 2.677945044588987 + ], + [ + -0.1, + 0.05, + 2.677945044588987 + ] + ] + }, + { + "trajectory_id": 67, + "start_angle_index": 7, + "end_angle_index": 8, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 2.677945044588987 + ], + [ + 0.0, + 0.0, + 2.832494247589256 + ], + [ + 0.0, + 0.0, + 2.9870434505895243 + ], + [ + 0.0, + 0.0, + 3.141592653589793 + ] + ] + }, + { + "trajectory_id": 68, + "start_angle_index": 8, + "end_angle_index": 9, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 3.141592653589793 + ], + [ + 0.0, + 0.0, + 3.296141856590062 + ], + [ + 0.0, + 0.0, + 3.4506910595903304 + ], + [ + 0.0, + 0.0, + 3.6052402625905993 + ] + ] + }, + { + "trajectory_id": 69, + "start_angle_index": 8, + "end_angle_index": 10, + "left_turn": true, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + -0.05558, + -0.01356, + 3.6200786548422195 + ], + [ + -0.1, + -0.05, + 3.9269908169872414 + ] + ] + }, + { + "trajectory_id": 70, + "start_angle_index": 8, + "end_angle_index": 11, + "left_turn": true, + "trajectory_radius": 0.0809, + "trajectory_length": 0.15137, + "arc_length": 0.08957, + "straight_length": 0.0618, + "poses": [ + [ + -0.04725, + -0.01523, + 3.7652915829038904 + ], + [ + -0.07743, + -0.05487, + 4.2487413713838835 + ], + [ + -0.1, + -0.1, + 4.2487413713838835 + ] + ] + }, + { + "trajectory_id": 71, + "start_angle_index": 8, + "end_angle_index": 5, + "left_turn": false, + "trajectory_radius": 0.0809, + "trajectory_length": 0.15137, + "arc_length": 0.08957, + "straight_length": 0.0618, + "poses": [ + [ + -0.04725, + 0.01523, + 2.517893724275696 + ], + [ + -0.07743, + 0.05487, + 2.0344439357957027 + ], + [ + -0.1, + 0.1, + 2.0344439357957027 + ] + ] + }, + { + "trajectory_id": 72, + "start_angle_index": 8, + "end_angle_index": 6, + "left_turn": false, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + -0.05558, + 0.01356, + 2.6631066523373668 + ], + [ + -0.1, + 0.05, + 2.356194490192345 + ] + ] + }, + { + "trajectory_id": 73, + "start_angle_index": 8, + "end_angle_index": 7, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 3.141592653589793 + ], + [ + 0.0, + 0.0, + 2.9870434505895243 + ], + [ + 0.0, + 0.0, + 2.832494247589256 + ], + [ + 0.0, + 0.0, + 2.677945044588987 + ] + ] + }, + { + "trajectory_id": 74, + "start_angle_index": 8, + "end_angle_index": 8, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.05, + "arc_length": 0.0, + "straight_length": 0.05, + "poses": [ + [ + -0.05, + 0.0, + 3.141592653589793 + ] + ] + }, + { + "trajectory_id": 75, + "start_angle_index": 9, + "end_angle_index": 9, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.1118, + "arc_length": 0.0, + "straight_length": 0.1118, + "poses": [ + [ + -0.05, + -0.025, + 3.6052402625905993 + ], + [ + -0.1, + -0.05, + 3.6052402625905993 + ] + ] + }, + { + "trajectory_id": 76, + "start_angle_index": 9, + "end_angle_index": 10, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 3.6052402625905993 + ], + [ + 0.0, + 0.0, + 3.7661155397889203 + ], + [ + 0.0, + 0.0, + 3.9269908169872414 + ] + ] + }, + { + "trajectory_id": 77, + "start_angle_index": 9, + "end_angle_index": 11, + "left_turn": true, + "trajectory_radius": 0.1118, + "trajectory_length": 0.07194, + "arc_length": 0.07194, + "straight_length": 0.0, + "poses": [ + [ + -0.05, + -0.05, + 4.2487413713838835 + ] + ] + }, + { + "trajectory_id": 78, + "start_angle_index": 9, + "end_angle_index": 11, + "left_turn": true, + "trajectory_radius": 0.22361, + "trajectory_length": 0.14389, + "arc_length": 0.14389, + "straight_length": 0.0, + "poses": [ + [ + -0.04028, + -0.02587, + 3.8197406321883607 + ], + [ + -0.07413, + -0.05972, + 4.034241001786122 + ], + [ + -0.1, + -0.1, + 4.2487413713838835 + ] + ] + }, + { + "trajectory_id": 79, + "start_angle_index": 9, + "end_angle_index": 12, + "left_turn": true, + "trajectory_radius": 0.36181, + "trajectory_length": 0.42697, + "arc_length": 0.40058, + "straight_length": 0.02639, + "poses": [ + [ + -0.04093, + -0.02393, + 3.736361101823309 + ], + [ + -0.07837, + -0.05301, + 3.8674819410560186 + ], + [ + -0.11169, + -0.08673, + 3.998602780288728 + ], + [ + -0.14031, + -0.12452, + 4.129723619521437 + ], + [ + -0.16375, + -0.16573, + 4.260844458754147 + ], + [ + -0.18159, + -0.20965, + 4.391965297986857 + ], + [ + -0.19355, + -0.25553, + 4.5230861372195665 + ], + [ + -0.1994, + -0.30257, + 4.654206976452276 + ], + [ + -0.2, + -0.35, + 4.71238898038469 + ] + ] + }, + { + "trajectory_id": 80, + "start_angle_index": 9, + "end_angle_index": 6, + "left_turn": false, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + -0.04916, + -0.02356, + 3.468125177549952 + ], + [ + -0.10294, + -0.02617, + 2.9121598338711485 + ], + [ + -0.15, + 0.0, + 2.356194490192345 + ] + ] + }, + { + "trajectory_id": 81, + "start_angle_index": 9, + "end_angle_index": 7, + "left_turn": false, + "trajectory_radius": 0.1118, + "trajectory_length": 0.10367, + "arc_length": 0.10367, + "straight_length": 0.0, + "poses": [ + [ + -0.05, + -0.0118, + 3.141592653589793 + ], + [ + -0.1, + 0.0, + 2.677945044588987 + ] + ] + }, + { + "trajectory_id": 82, + "start_angle_index": 9, + "end_angle_index": 7, + "left_turn": false, + "trajectory_radius": 0.22361, + "trajectory_length": 0.20735, + "arc_length": 0.20735, + "straight_length": 0.0, + "poses": [ + [ + -0.04862, + -0.01763, + 3.373416458090196 + ], + [ + -0.1, + -0.02361, + 3.141592653589793 + ], + [ + -0.15138, + -0.01763, + 2.90976884908939 + ], + [ + -0.2, + 0.0, + 2.677945044588987 + ] + ] + }, + { + "trajectory_id": 83, + "start_angle_index": 9, + "end_angle_index": 7, + "left_turn": false, + "trajectory_radius": 0.33541, + "trajectory_length": 0.31102, + "arc_length": 0.31102, + "straight_length": 0.0, + "poses": [ + [ + -0.04797, + -0.01951, + 3.4506910595903304 + ], + [ + -0.09837, + -0.03141, + 3.296141856590062 + ], + [ + -0.15, + -0.03541, + 3.141592653589793 + ], + [ + -0.20163, + -0.03141, + 2.9870434505895243 + ], + [ + -0.25203, + -0.01951, + 2.8324942475892554 + ], + [ + -0.3, + 0.0, + 2.677945044588987 + ] + ] + }, + { + "trajectory_id": 84, + "start_angle_index": 9, + "end_angle_index": 8, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 3.6052402625905993 + ], + [ + 0.0, + 0.0, + 3.373416458090196 + ], + [ + 0.0, + 0.0, + 3.141592653589793 + ] + ] + }, + { + "trajectory_id": 85, + "start_angle_index": 10, + "end_angle_index": 9, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 3.9269908169872414 + ], + [ + 0.0, + 0.0, + 3.7661155397889203 + ], + [ + 0.0, + 0.0, + 3.6052402625905993 + ] + ] + }, + { + "trajectory_id": 86, + "start_angle_index": 10, + "end_angle_index": 10, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.07071, + "arc_length": 0.0, + "straight_length": 0.07071, + "poses": [ + [ + -0.05, + -0.05, + 3.9269908169872414 + ] + ] + }, + { + "trajectory_id": 87, + "start_angle_index": 10, + "end_angle_index": 11, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 3.9269908169872414 + ], + [ + 0.0, + 0.0, + 4.0878660941855625 + ], + [ + 0.0, + 0.0, + 4.2487413713838835 + ] + ] + }, + { + "trajectory_id": 88, + "start_angle_index": 10, + "end_angle_index": 12, + "left_turn": true, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + -0.03644, + -0.04442, + 4.233902979132264 + ], + [ + -0.05, + -0.1, + 4.71238898038469 + ] + ] + }, + { + "trajectory_id": 89, + "start_angle_index": 10, + "end_angle_index": 13, + "left_turn": true, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + -0.02617, + -0.04706, + 4.4829561606660455 + ], + [ + -0.02356, + -0.10084, + 5.038921504344849 + ], + [ + 0.0, + -0.15, + 5.176036589385496 + ] + ] + }, + { + "trajectory_id": 90, + "start_angle_index": 10, + "end_angle_index": 7, + "left_turn": false, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + -0.04706, + -0.02617, + 3.3710254733084377 + ], + [ + -0.10084, + -0.02356, + 2.815060129629634 + ], + [ + -0.15, + 0.0, + 2.677945044588987 + ] + ] + }, + { + "trajectory_id": 91, + "start_angle_index": 10, + "end_angle_index": 8, + "left_turn": false, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + -0.04442, + -0.03644, + 3.6200786548422195 + ], + [ + -0.1, + -0.05, + 3.141592653589793 + ] + ] + }, + { + "trajectory_id": 92, + "start_angle_index": 11, + "end_angle_index": 9, + "left_turn": false, + "trajectory_radius": 0.1118, + "trajectory_length": 0.07194, + "arc_length": 0.07194, + "straight_length": 0.0, + "poses": [ + [ + -0.05, + -0.05, + 3.6052402625905993 + ] + ] + }, + { + "trajectory_id": 93, + "start_angle_index": 11, + "end_angle_index": 9, + "left_turn": false, + "trajectory_radius": 0.22361, + "trajectory_length": 0.14389, + "arc_length": 0.14389, + "straight_length": 0.0, + "poses": [ + [ + -0.02587, + -0.04028, + 4.034241001786122 + ], + [ + -0.05972, + -0.07413, + 3.8197406321883607 + ], + [ + -0.1, + -0.1, + 3.6052402625905993 + ] + ] + }, + { + "trajectory_id": 94, + "start_angle_index": 11, + "end_angle_index": 10, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 4.2487413713838835 + ], + [ + 0.0, + 0.0, + 4.141491186585003 + ], + [ + 0.0, + 0.0, + 4.034241001786122 + ], + [ + 0.0, + 0.0, + 3.9269908169872414 + ] + ] + }, + { + "trajectory_id": 95, + "start_angle_index": 11, + "end_angle_index": 11, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.1118, + "arc_length": 0.0, + "straight_length": 0.1118, + "poses": [ + [ + -0.025, + -0.05, + 4.2487413713838835 + ], + [ + -0.05, + -0.1, + 4.2487413713838835 + ] + ] + }, + { + "trajectory_id": 96, + "start_angle_index": 11, + "end_angle_index": 12, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 4.2487413713838835 + ], + [ + 0.0, + 0.0, + 4.403290574384152 + ], + [ + 0.0, + 0.0, + 4.557839777384421 + ], + [ + 0.0, + 0.0, + 4.71238898038469 + ] + ] + }, + { + "trajectory_id": 97, + "start_angle_index": 11, + "end_angle_index": 13, + "left_turn": true, + "trajectory_radius": 0.1118, + "trajectory_length": 0.10367, + "arc_length": 0.10367, + "straight_length": 0.0, + "poses": [ + [ + -0.0118, + -0.05, + 4.71238898038469 + ], + [ + 0.0, + -0.1, + 5.176036589385496 + ] + ] + }, + { + "trajectory_id": 98, + "start_angle_index": 11, + "end_angle_index": 13, + "left_turn": true, + "trajectory_radius": 0.22361, + "trajectory_length": 0.20735, + "arc_length": 0.20735, + "straight_length": 0.0, + "poses": [ + [ + -0.01763, + -0.04862, + 4.480565175884287 + ], + [ + -0.02361, + -0.1, + 4.71238898038469 + ], + [ + -0.01763, + -0.15138, + 4.944212784885092 + ], + [ + 0.0, + -0.2, + 5.176036589385496 + ] + ] + }, + { + "trajectory_id": 99, + "start_angle_index": 11, + "end_angle_index": 13, + "left_turn": true, + "trajectory_radius": 0.33541, + "trajectory_length": 0.31102, + "arc_length": 0.31102, + "straight_length": 0.0, + "poses": [ + [ + -0.01951, + -0.04797, + 4.403290574384152 + ], + [ + -0.03141, + -0.09837, + 4.557839777384421 + ], + [ + -0.03541, + -0.15, + 4.71238898038469 + ], + [ + -0.03141, + -0.20163, + 4.866938183384958 + ], + [ + -0.01951, + -0.25203, + 5.021487386385227 + ], + [ + 0.0, + -0.3, + 5.176036589385496 + ] + ] + }, + { + "trajectory_id": 100, + "start_angle_index": 11, + "end_angle_index": 14, + "left_turn": true, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + -0.02356, + -0.04916, + 4.385856456424531 + ], + [ + -0.02617, + -0.10294, + 4.941821800103334 + ], + [ + 0.0, + -0.15, + 5.497787143782138 + ] + ] + }, + { + "trajectory_id": 101, + "start_angle_index": 11, + "end_angle_index": 8, + "left_turn": false, + "trajectory_radius": 0.36181, + "trajectory_length": 0.42697, + "arc_length": 0.40058, + "straight_length": 0.02639, + "poses": [ + [ + -0.02393, + -0.04093, + 4.117620532151173 + ], + [ + -0.05301, + -0.07837, + 3.986499692918464 + ], + [ + -0.08673, + -0.11169, + 3.855378853685755 + ], + [ + -0.12452, + -0.14031, + 3.7242580144530453 + ], + [ + -0.16573, + -0.16375, + 3.5931371752203356 + ], + [ + -0.20965, + -0.18159, + 3.462016335987626 + ], + [ + -0.25553, + -0.19355, + 3.3308954967549163 + ], + [ + -0.30257, + -0.1994, + 3.1997746575222066 + ], + [ + -0.35, + -0.2, + 3.141592653589793 + ] + ] + }, + { + "trajectory_id": 102, + "start_angle_index": 12, + "end_angle_index": 9, + "left_turn": false, + "trajectory_radius": 0.0809, + "trajectory_length": 0.15137, + "arc_length": 0.08957, + "straight_length": 0.0618, + "poses": [ + [ + -0.01523, + -0.04725, + 4.088690051070593 + ], + [ + -0.05487, + -0.07743, + 3.6052402625905993 + ], + [ + -0.1, + -0.1, + 3.6052402625905993 + ] + ] + }, + { + "trajectory_id": 103, + "start_angle_index": 12, + "end_angle_index": 10, + "left_turn": false, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + -0.01356, + -0.05558, + 4.233902979132264 + ], + [ + -0.05, + -0.1, + 3.9269908169872414 + ] + ] + }, + { + "trajectory_id": 104, + "start_angle_index": 12, + "end_angle_index": 11, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 4.71238898038469 + ], + [ + 0.0, + 0.0, + 4.557839777384421 + ], + [ + 0.0, + 0.0, + 4.403290574384152 + ], + [ + 0.0, + 0.0, + 4.2487413713838835 + ] + ] + }, + { + "trajectory_id": 105, + "start_angle_index": 12, + "end_angle_index": 12, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.05, + "arc_length": 0.0, + "straight_length": 0.05, + "poses": [ + [ + 0.0, + -0.05, + 4.71238898038469 + ] + ] + }, + { + "trajectory_id": 106, + "start_angle_index": 12, + "end_angle_index": 13, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 4.71238898038469 + ], + [ + 0.0, + 0.0, + 4.866938183384958 + ], + [ + 0.0, + 0.0, + 5.021487386385227 + ], + [ + 0.0, + 0.0, + 5.176036589385496 + ] + ] + }, + { + "trajectory_id": 107, + "start_angle_index": 12, + "end_angle_index": 14, + "left_turn": true, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + 0.01356, + -0.05558, + 5.190874981637116 + ], + [ + 0.05, + -0.1, + 5.497787143782138 + ] + ] + }, + { + "trajectory_id": 108, + "start_angle_index": 12, + "end_angle_index": 15, + "left_turn": true, + "trajectory_radius": 0.0809, + "trajectory_length": 0.15137, + "arc_length": 0.08957, + "straight_length": 0.0618, + "poses": [ + [ + 0.01523, + -0.04725, + 5.3360879096987865 + ], + [ + 0.05487, + -0.07743, + 5.81953769817878 + ], + [ + 0.1, + -0.1, + 5.81953769817878 + ] + ] + }, + { + "trajectory_id": 109, + "start_angle_index": 13, + "end_angle_index": 10, + "left_turn": false, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + 0.02356, + -0.04916, + 5.038921504344849 + ], + [ + 0.02617, + -0.10294, + 4.4829561606660455 + ], + [ + 0.0, + -0.15, + 3.9269908169872414 + ] + ] + }, + { + "trajectory_id": 110, + "start_angle_index": 13, + "end_angle_index": 11, + "left_turn": false, + "trajectory_radius": 0.1118, + "trajectory_length": 0.10367, + "arc_length": 0.10367, + "straight_length": 0.0, + "poses": [ + [ + 0.0118, + -0.05, + 4.71238898038469 + ], + [ + 0.0, + -0.1, + 4.2487413713838835 + ] + ] + }, + { + "trajectory_id": 111, + "start_angle_index": 13, + "end_angle_index": 11, + "left_turn": false, + "trajectory_radius": 0.22361, + "trajectory_length": 0.20735, + "arc_length": 0.20735, + "straight_length": 0.0, + "poses": [ + [ + 0.01763, + -0.04862, + 4.944212784885092 + ], + [ + 0.02361, + -0.1, + 4.71238898038469 + ], + [ + 0.01763, + -0.15138, + 4.480565175884287 + ], + [ + 0.0, + -0.2, + 4.2487413713838835 + ] + ] + }, + { + "trajectory_id": 112, + "start_angle_index": 13, + "end_angle_index": 11, + "left_turn": false, + "trajectory_radius": 0.33541, + "trajectory_length": 0.31102, + "arc_length": 0.31102, + "straight_length": 0.0, + "poses": [ + [ + 0.01951, + -0.04797, + 5.021487386385227 + ], + [ + 0.03141, + -0.09837, + 4.866938183384958 + ], + [ + 0.03541, + -0.15, + 4.71238898038469 + ], + [ + 0.03141, + -0.20163, + 4.557839777384421 + ], + [ + 0.01951, + -0.25203, + 4.403290574384152 + ], + [ + 0.0, + -0.3, + 4.2487413713838835 + ] + ] + }, + { + "trajectory_id": 113, + "start_angle_index": 13, + "end_angle_index": 12, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 5.176036589385496 + ], + [ + 0.0, + 0.0, + 4.944212784885092 + ], + [ + 0.0, + 0.0, + 4.71238898038469 + ] + ] + }, + { + "trajectory_id": 114, + "start_angle_index": 13, + "end_angle_index": 13, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.1118, + "arc_length": 0.0, + "straight_length": 0.1118, + "poses": [ + [ + 0.025, + -0.05, + 5.176036589385496 + ], + [ + 0.05, + -0.1, + 5.176036589385496 + ] + ] + }, + { + "trajectory_id": 115, + "start_angle_index": 13, + "end_angle_index": 14, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 5.176036589385496 + ], + [ + 0.0, + 0.0, + 5.336911866583817 + ], + [ + 0.0, + 0.0, + 5.497787143782138 + ] + ] + }, + { + "trajectory_id": 116, + "start_angle_index": 13, + "end_angle_index": 15, + "left_turn": true, + "trajectory_radius": 0.1118, + "trajectory_length": 0.07194, + "arc_length": 0.07194, + "straight_length": 0.0, + "poses": [ + [ + 0.05, + -0.05, + 5.81953769817878 + ] + ] + }, + { + "trajectory_id": 117, + "start_angle_index": 13, + "end_angle_index": 15, + "left_turn": true, + "trajectory_radius": 0.22361, + "trajectory_length": 0.14389, + "arc_length": 0.14389, + "straight_length": 0.0, + "poses": [ + [ + 0.02587, + -0.04028, + 5.390536958983257 + ], + [ + 0.05972, + -0.07413, + 5.605037328581019 + ], + [ + 0.1, + -0.1, + 5.81953769817878 + ] + ] + }, + { + "trajectory_id": 118, + "start_angle_index": 13, + "end_angle_index": 0, + "left_turn": true, + "trajectory_radius": 0.36181, + "trajectory_length": 0.42697, + "arc_length": 0.40058, + "straight_length": 0.02639, + "poses": [ + [ + 0.02393, + -0.04093, + 5.307157428618205 + ], + [ + 0.05301, + -0.07837, + 5.438278267850915 + ], + [ + 0.08673, + -0.11169, + 5.569399107083624 + ], + [ + 0.12452, + -0.14031, + 5.7005199463163345 + ], + [ + 0.16573, + -0.16375, + 5.831640785549044 + ], + [ + 0.20965, + -0.18159, + 5.962761624781754 + ], + [ + 0.25553, + -0.19355, + 6.093882464014463 + ], + [ + 0.30257, + -0.1994, + 6.225003303247172 + ], + [ + 0.35, + -0.2, + 0.0 + ] + ] + }, + { + "trajectory_id": 119, + "start_angle_index": 14, + "end_angle_index": 11, + "left_turn": false, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + 0.02617, + -0.04706, + 4.941821800103334 + ], + [ + 0.02356, + -0.10084, + 4.385856456424531 + ], + [ + 0.0, + -0.15, + 4.2487413713838835 + ] + ] + }, + { + "trajectory_id": 120, + "start_angle_index": 14, + "end_angle_index": 12, + "left_turn": false, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + 0.03644, + -0.04442, + 5.190874981637116 + ], + [ + 0.05, + -0.1, + 4.71238898038469 + ] + ] + }, + { + "trajectory_id": 121, + "start_angle_index": 14, + "end_angle_index": 13, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 5.497787143782138 + ], + [ + 0.0, + 0.0, + 5.336911866583817 + ], + [ + 0.0, + 0.0, + 5.176036589385496 + ] + ] + }, + { + "trajectory_id": 122, + "start_angle_index": 14, + "end_angle_index": 14, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.07071, + "arc_length": 0.0, + "straight_length": 0.07071, + "poses": [ + [ + 0.05, + -0.05, + 5.497787143782138 + ] + ] + }, + { + "trajectory_id": 123, + "start_angle_index": 14, + "end_angle_index": 15, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 5.497787143782138 + ], + [ + 0.0, + 0.0, + 5.658662420980459 + ], + [ + 0.0, + 0.0, + 5.81953769817878 + ] + ] + }, + { + "trajectory_id": 124, + "start_angle_index": 14, + "end_angle_index": 0, + "left_turn": true, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + 0.04442, + -0.03644, + 5.80469930592716 + ], + [ + 0.1, + -0.05, + 0.0 + ] + ] + }, + { + "trajectory_id": 125, + "start_angle_index": 14, + "end_angle_index": 1, + "left_turn": true, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + 0.04706, + -0.02617, + 6.053752487460941 + ], + [ + 0.10084, + -0.02356, + 0.32653252396015886 + ], + [ + 0.15, + 0.0, + 0.4636476090008061 + ] + ] + }, + { + "trajectory_id": 126, + "start_angle_index": 15, + "end_angle_index": 12, + "left_turn": false, + "trajectory_radius": 0.3618, + "trajectory_length": 0.42696, + "arc_length": 0.40057, + "straight_length": 0.02639, + "poses": [ + [ + 0.04092, + -0.02394, + 5.688416634946208 + ], + [ + 0.07836, + -0.05302, + 5.557295571713635 + ], + [ + 0.11168, + -0.08674, + 5.426174508481063 + ], + [ + 0.1403, + -0.12453, + 5.295053445248491 + ], + [ + 0.16374, + -0.16574, + 5.163932382015918 + ], + [ + 0.18159, + -0.20965, + 5.032811318783346 + ], + [ + 0.19354, + -0.25553, + 4.901690255550774 + ], + [ + 0.19939, + -0.30257, + 4.770569192318201 + ], + [ + 0.2, + -0.35, + 4.71238898038469 + ] + ] + }, + { + "trajectory_id": 127, + "start_angle_index": 15, + "end_angle_index": 13, + "left_turn": false, + "trajectory_radius": 0.1118, + "trajectory_length": 0.07194, + "arc_length": 0.07194, + "straight_length": 0.0, + "poses": [ + [ + 0.05, + -0.05, + 5.176036589385496 + ] + ] + }, + { + "trajectory_id": 128, + "start_angle_index": 15, + "end_angle_index": 13, + "left_turn": false, + "trajectory_radius": 0.22361, + "trajectory_length": 0.14389, + "arc_length": 0.14389, + "straight_length": 0.0, + "poses": [ + [ + 0.04028, + -0.02587, + 5.605037328581019 + ], + [ + 0.07413, + -0.05972, + 5.390536958983257 + ], + [ + 0.1, + -0.1, + 5.176036589385496 + ] + ] + }, + { + "trajectory_id": 129, + "start_angle_index": 15, + "end_angle_index": 14, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 5.81953769817878 + ], + [ + 0.0, + 0.0, + 5.712287513379899 + ], + [ + 0.0, + 0.0, + 5.605037328581019 + ], + [ + 0.0, + 0.0, + 5.497787143782138 + ] + ] + }, + { + "trajectory_id": 130, + "start_angle_index": 15, + "end_angle_index": 15, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.1118, + "arc_length": 0.0, + "straight_length": 0.1118, + "poses": [ + [ + 0.05, + -0.025, + 5.81953769817878 + ], + [ + 0.1, + -0.05, + 5.81953769817878 + ] + ] + }, + { + "trajectory_id": 131, + "start_angle_index": 15, + "end_angle_index": 0, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 5.81953769817878 + ], + [ + 0.0, + 0.0, + 5.9740869011790485 + ], + [ + 0.0, + 0.0, + 6.128636104179318 + ], + [ + 0.0, + 0.0, + 0.0 + ] + ] + }, + { + "trajectory_id": 132, + "start_angle_index": 15, + "end_angle_index": 1, + "left_turn": true, + "trajectory_radius": 0.1118, + "trajectory_length": 0.10367, + "arc_length": 0.10367, + "straight_length": 0.0, + "poses": [ + [ + 0.05, + -0.0118, + 0.0 + ], + [ + 0.1, + 0.0, + 0.4636476090008061 + ] + ] + }, + { + "trajectory_id": 133, + "start_angle_index": 15, + "end_angle_index": 1, + "left_turn": true, + "trajectory_radius": 0.22361, + "trajectory_length": 0.20735, + "arc_length": 0.20735, + "straight_length": 0.0, + "poses": [ + [ + 0.04862, + -0.01763, + 6.051361502679184 + ], + [ + 0.1, + -0.02361, + 0.0 + ], + [ + 0.15138, + -0.01763, + 0.23182380450040302 + ], + [ + 0.2, + 0.0, + 0.4636476090008061 + ] + ] + }, + { + "trajectory_id": 134, + "start_angle_index": 15, + "end_angle_index": 1, + "left_turn": true, + "trajectory_radius": 0.33541, + "trajectory_length": 0.31102, + "arc_length": 0.31102, + "straight_length": 0.0, + "poses": [ + [ + 0.04797, + -0.01951, + 5.9740869011790485 + ], + [ + 0.09837, + -0.03141, + 6.128636104179318 + ], + [ + 0.15, + -0.03541, + 0.0 + ], + [ + 0.20163, + -0.03141, + 0.1545492030002687 + ], + [ + 0.25203, + -0.01951, + 0.30909840600053734 + ], + [ + 0.3, + 0.0, + 0.4636476090008061 + ] + ] + }, + { + "trajectory_id": 135, + "start_angle_index": 15, + "end_angle_index": 2, + "left_turn": true, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + 0.04916, + -0.02356, + 5.956652783219427 + ], + [ + 0.10294, + -0.02617, + 0.22943281971864465 + ], + [ + 0.15, + 0.0, + 0.7853981633974483 + ] + ] + } + ] +} \ No newline at end of file diff --git a/nav2_smac_planner/lattice_primitives/sample_primitives/5cm_resolution/0.5m_turning_radius/diff/output1.json b/nav2_smac_planner/lattice_primitives/sample_primitives/5cm_resolution/0.5m_turning_radius/diff/output1.json new file mode 100644 index 00000000000..dfff481543d --- /dev/null +++ b/nav2_smac_planner/lattice_primitives/sample_primitives/5cm_resolution/0.5m_turning_radius/diff/output1.json @@ -0,0 +1,3864 @@ +{ + "version": 1.0, + "date_generated": "2025-10-30", + "lattice_metadata": { + "motion_model": "diff", + "turning_radius": 0.08, + "grid_resolution": 0.05, + "stopping_threshold": 5, + "num_of_headings": 16, + "heading_angles": [ + 0.0, + 0.4636476090008061, + 0.7853981633974483, + 1.1071487177940904, + 1.5707963267948966, + 2.0344439357957027, + 2.356194490192345, + 2.677945044588987, + 3.141592653589793, + 3.6052402625905993, + 3.9269908169872414, + 4.2487413713838835, + 4.71238898038469, + 5.176036589385496, + 5.497787143782138, + 5.81953769817878 + ], + "number_of_trajectories": 136 + }, + "primitives": [ + { + "trajectory_id": 0, + "start_angle_index": 0, + "end_angle_index": 13, + "left_turn": false, + "trajectory_radius": 0.0809, + "trajectory_length": 0.15137, + "arc_length": 0.08957, + "straight_length": 0.0618, + "poses": [ + [ + 0.04725, + -0.01523, + 5.659486377865489 + ], + [ + 0.07743, + -0.05487, + 5.176036589385496 + ], + [ + 0.1, + -0.1, + 5.176036589385496 + ] + ] + }, + { + "trajectory_id": 1, + "start_angle_index": 0, + "end_angle_index": 14, + "left_turn": false, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + 0.05558, + -0.01356, + 5.80469930592716 + ], + [ + 0.1, + -0.05, + 5.497787143782138 + ] + ] + }, + { + "trajectory_id": 2, + "start_angle_index": 0, + "end_angle_index": 15, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 6.128636104179318 + ], + [ + 0.0, + 0.0, + 5.9740869011790485 + ], + [ + 0.0, + 0.0, + 5.81953769817878 + ] + ] + }, + { + "trajectory_id": 3, + "start_angle_index": 0, + "end_angle_index": 0, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.05, + "arc_length": 0.0, + "straight_length": 0.05, + "poses": [ + [ + 0.05, + 0.0, + 0.0 + ] + ] + }, + { + "trajectory_id": 4, + "start_angle_index": 0, + "end_angle_index": 1, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.1545492030002687 + ], + [ + 0.0, + 0.0, + 0.3090984060005374 + ], + [ + 0.0, + 0.0, + 0.4636476090008061 + ] + ] + }, + { + "trajectory_id": 5, + "start_angle_index": 0, + "end_angle_index": 2, + "left_turn": true, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + 0.05558, + 0.01356, + 0.47848600125242624 + ], + [ + 0.1, + 0.05, + 0.7853981633974483 + ] + ] + }, + { + "trajectory_id": 6, + "start_angle_index": 0, + "end_angle_index": 3, + "left_turn": true, + "trajectory_radius": 0.0809, + "trajectory_length": 0.15137, + "arc_length": 0.08957, + "straight_length": 0.0618, + "poses": [ + [ + 0.04725, + 0.01523, + 0.6236989293140972 + ], + [ + 0.07743, + 0.05487, + 1.1071487177940904 + ], + [ + 0.1, + 0.1, + 1.1071487177940904 + ] + ] + }, + { + "trajectory_id": 7, + "start_angle_index": 1, + "end_angle_index": 14, + "left_turn": false, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + 0.04916, + 0.02356, + 0.32653252396015897 + ], + [ + 0.10294, + 0.02617, + 6.053752487460941 + ], + [ + 0.15, + 0.0, + 5.497787143782138 + ] + ] + }, + { + "trajectory_id": 8, + "start_angle_index": 1, + "end_angle_index": 15, + "left_turn": false, + "trajectory_radius": 0.1118, + "trajectory_length": 0.10367, + "arc_length": 0.10367, + "straight_length": 0.0, + "poses": [ + [ + 0.05, + 0.0118, + 0.0 + ], + [ + 0.1, + 0.0, + 5.81953769817878 + ] + ] + }, + { + "trajectory_id": 9, + "start_angle_index": 1, + "end_angle_index": 15, + "left_turn": false, + "trajectory_radius": 0.22361, + "trajectory_length": 0.20735, + "arc_length": 0.20735, + "straight_length": 0.0, + "poses": [ + [ + 0.04862, + 0.01763, + 0.23182380450040305 + ], + [ + 0.1, + 0.02361, + 0.0 + ], + [ + 0.15138, + 0.01763, + 6.051361502679184 + ], + [ + 0.2, + 0.0, + 5.81953769817878 + ] + ] + }, + { + "trajectory_id": 10, + "start_angle_index": 1, + "end_angle_index": 15, + "left_turn": false, + "trajectory_radius": 0.33541, + "trajectory_length": 0.31102, + "arc_length": 0.31102, + "straight_length": 0.0, + "poses": [ + [ + 0.04797, + 0.01951, + 0.3090984060005374 + ], + [ + 0.09837, + 0.03141, + 0.1545492030002687 + ], + [ + 0.15, + 0.03541, + 0.0 + ], + [ + 0.20163, + 0.03141, + 6.128636104179318 + ], + [ + 0.25203, + 0.01951, + 5.9740869011790485 + ], + [ + 0.3, + 0.0, + 5.81953769817878 + ] + ] + }, + { + "trajectory_id": 11, + "start_angle_index": 1, + "end_angle_index": 0, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 0.4636476090008061 + ], + [ + 0.0, + 0.0, + 0.23182380450040305 + ], + [ + 0.0, + 0.0, + 0.0 + ] + ] + }, + { + "trajectory_id": 12, + "start_angle_index": 1, + "end_angle_index": 1, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.1118, + "arc_length": 0.0, + "straight_length": 0.1118, + "poses": [ + [ + 0.05, + 0.025, + 0.4636476090008061 + ], + [ + 0.1, + 0.05, + 0.4636476090008061 + ] + ] + }, + { + "trajectory_id": 13, + "start_angle_index": 1, + "end_angle_index": 2, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 0.4636476090008061 + ], + [ + 0.0, + 0.0, + 0.6245228861991272 + ], + [ + 0.0, + 0.0, + 0.7853981633974483 + ] + ] + }, + { + "trajectory_id": 14, + "start_angle_index": 1, + "end_angle_index": 3, + "left_turn": true, + "trajectory_radius": 0.1118, + "trajectory_length": 0.07194, + "arc_length": 0.07194, + "straight_length": 0.0, + "poses": [ + [ + 0.05, + 0.05, + 1.1071487177940904 + ] + ] + }, + { + "trajectory_id": 15, + "start_angle_index": 1, + "end_angle_index": 3, + "left_turn": true, + "trajectory_radius": 0.22361, + "trajectory_length": 0.14389, + "arc_length": 0.14389, + "straight_length": 0.0, + "poses": [ + [ + 0.04028, + 0.02587, + 0.6781479785985676 + ], + [ + 0.07413, + 0.05972, + 0.892648348196329 + ], + [ + 0.1, + 0.1, + 1.1071487177940904 + ] + ] + }, + { + "trajectory_id": 16, + "start_angle_index": 1, + "end_angle_index": 4, + "left_turn": true, + "trajectory_radius": 0.3618, + "trajectory_length": 0.42696, + "arc_length": 0.40057, + "straight_length": 0.02639, + "poses": [ + [ + 0.04092, + 0.02394, + 0.5947686722333784 + ], + [ + 0.07836, + 0.05302, + 0.7258897354659507 + ], + [ + 0.11168, + 0.08674, + 0.8570107986985229 + ], + [ + 0.1403, + 0.12453, + 0.9881318619310953 + ], + [ + 0.16374, + 0.16574, + 1.1192529251636676 + ], + [ + 0.18159, + 0.20965, + 1.25037398839624 + ], + [ + 0.19354, + 0.25553, + 1.3814950516288125 + ], + [ + 0.19939, + 0.30257, + 1.512616114861385 + ], + [ + 0.2, + 0.35, + 1.5707963267948966 + ] + ] + }, + { + "trajectory_id": 17, + "start_angle_index": 2, + "end_angle_index": 15, + "left_turn": false, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + 0.04706, + 0.02617, + 0.2294328197186447 + ], + [ + 0.10084, + 0.02356, + 5.956652783219427 + ], + [ + 0.15, + 0.0, + 5.81953769817878 + ] + ] + }, + { + "trajectory_id": 18, + "start_angle_index": 2, + "end_angle_index": 0, + "left_turn": false, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + 0.04442, + 0.03644, + 0.4784860012524262 + ], + [ + 0.1, + 0.05, + 0.0 + ] + ] + }, + { + "trajectory_id": 19, + "start_angle_index": 2, + "end_angle_index": 1, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 0.7853981633974483 + ], + [ + 0.0, + 0.0, + 0.6245228861991272 + ], + [ + 0.0, + 0.0, + 0.4636476090008061 + ] + ] + }, + { + "trajectory_id": 20, + "start_angle_index": 2, + "end_angle_index": 2, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.07071, + "arc_length": 0.0, + "straight_length": 0.07071, + "poses": [ + [ + 0.05, + 0.05, + 0.7853981633974483 + ] + ] + }, + { + "trajectory_id": 21, + "start_angle_index": 2, + "end_angle_index": 3, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 0.7853981633974483 + ], + [ + 0.0, + 0.0, + 0.9462734405957693 + ], + [ + 0.0, + 0.0, + 1.1071487177940904 + ] + ] + }, + { + "trajectory_id": 22, + "start_angle_index": 2, + "end_angle_index": 4, + "left_turn": true, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + 0.03644, + 0.04442, + 1.0923103255424704 + ], + [ + 0.05, + 0.1, + 1.5707963267948966 + ] + ] + }, + { + "trajectory_id": 23, + "start_angle_index": 2, + "end_angle_index": 5, + "left_turn": true, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + 0.02617, + 0.04706, + 1.341363507076252 + ], + [ + 0.02356, + 0.10084, + 1.8973288507550554 + ], + [ + 0.0, + 0.15, + 2.0344439357957027 + ] + ] + }, + { + "trajectory_id": 24, + "start_angle_index": 3, + "end_angle_index": 0, + "left_turn": false, + "trajectory_radius": 0.36181, + "trajectory_length": 0.42697, + "arc_length": 0.40058, + "straight_length": 0.02639, + "poses": [ + [ + 0.02393, + 0.04093, + 0.9760278785613808 + ], + [ + 0.05301, + 0.07837, + 0.8449070393286713 + ], + [ + 0.08673, + 0.11169, + 0.7137862000959616 + ], + [ + 0.12452, + 0.14031, + 0.582665360863252 + ], + [ + 0.16573, + 0.16375, + 0.4515445216305424 + ], + [ + 0.20965, + 0.18159, + 0.3204236823978327 + ], + [ + 0.25553, + 0.19355, + 0.18930284316512325 + ], + [ + 0.30257, + 0.1994, + 0.05818200393241346 + ], + [ + 0.35, + 0.2, + 0.0 + ] + ] + }, + { + "trajectory_id": 25, + "start_angle_index": 3, + "end_angle_index": 1, + "left_turn": false, + "trajectory_radius": 0.1118, + "trajectory_length": 0.07194, + "arc_length": 0.07194, + "straight_length": 0.0, + "poses": [ + [ + 0.05, + 0.05, + 0.4636476090008061 + ] + ] + }, + { + "trajectory_id": 26, + "start_angle_index": 3, + "end_angle_index": 1, + "left_turn": false, + "trajectory_radius": 0.22361, + "trajectory_length": 0.14389, + "arc_length": 0.14389, + "straight_length": 0.0, + "poses": [ + [ + 0.02587, + 0.04028, + 0.892648348196329 + ], + [ + 0.05972, + 0.07413, + 0.6781479785985676 + ], + [ + 0.1, + 0.1, + 0.4636476090008061 + ] + ] + }, + { + "trajectory_id": 27, + "start_angle_index": 3, + "end_angle_index": 2, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 1.1071487177940904 + ], + [ + 0.0, + 0.0, + 0.9998985329952097 + ], + [ + 0.0, + 0.0, + 0.892648348196329 + ], + [ + 0.0, + 0.0, + 0.7853981633974483 + ] + ] + }, + { + "trajectory_id": 28, + "start_angle_index": 3, + "end_angle_index": 3, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.1118, + "arc_length": 0.0, + "straight_length": 0.1118, + "poses": [ + [ + 0.025, + 0.05, + 1.1071487177940904 + ], + [ + 0.05, + 0.1, + 1.1071487177940904 + ] + ] + }, + { + "trajectory_id": 29, + "start_angle_index": 3, + "end_angle_index": 4, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 1.1071487177940904 + ], + [ + 0.0, + 0.0, + 1.261697920794359 + ], + [ + 0.0, + 0.0, + 1.416247123794628 + ], + [ + 0.0, + 0.0, + 1.5707963267948966 + ] + ] + }, + { + "trajectory_id": 30, + "start_angle_index": 3, + "end_angle_index": 5, + "left_turn": true, + "trajectory_radius": 0.1118, + "trajectory_length": 0.10367, + "arc_length": 0.10367, + "straight_length": 0.0, + "poses": [ + [ + 0.0118, + 0.05, + 1.5707963267948966 + ], + [ + 0.0, + 0.1, + 2.0344439357957027 + ] + ] + }, + { + "trajectory_id": 31, + "start_angle_index": 3, + "end_angle_index": 5, + "left_turn": true, + "trajectory_radius": 0.22361, + "trajectory_length": 0.20735, + "arc_length": 0.20735, + "straight_length": 0.0, + "poses": [ + [ + 0.01763, + 0.04862, + 1.3389725222944935 + ], + [ + 0.02361, + 0.1, + 1.5707963267948966 + ], + [ + 0.01763, + 0.15138, + 1.8026201312952996 + ], + [ + 0.0, + 0.2, + 2.0344439357957027 + ] + ] + }, + { + "trajectory_id": 32, + "start_angle_index": 3, + "end_angle_index": 5, + "left_turn": true, + "trajectory_radius": 0.33541, + "trajectory_length": 0.31102, + "arc_length": 0.31102, + "straight_length": 0.0, + "poses": [ + [ + 0.01951, + 0.04797, + 1.261697920794359 + ], + [ + 0.03141, + 0.09837, + 1.416247123794628 + ], + [ + 0.03541, + 0.15, + 1.5707963267948966 + ], + [ + 0.03141, + 0.20163, + 1.7253455297951654 + ], + [ + 0.01951, + 0.25203, + 1.879894732795434 + ], + [ + 0.0, + 0.3, + 2.0344439357957027 + ] + ] + }, + { + "trajectory_id": 33, + "start_angle_index": 3, + "end_angle_index": 6, + "left_turn": true, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + 0.02356, + 0.04916, + 1.2442638028347375 + ], + [ + 0.02617, + 0.10294, + 1.8002291465135412 + ], + [ + 0.0, + 0.15, + 2.356194490192345 + ] + ] + }, + { + "trajectory_id": 34, + "start_angle_index": 4, + "end_angle_index": 1, + "left_turn": false, + "trajectory_radius": 0.0809, + "trajectory_length": 0.15137, + "arc_length": 0.08957, + "straight_length": 0.0618, + "poses": [ + [ + 0.01523, + 0.04725, + 0.9470973974807994 + ], + [ + 0.05487, + 0.07743, + 0.4636476090008061 + ], + [ + 0.1, + 0.1, + 0.4636476090008061 + ] + ] + }, + { + "trajectory_id": 35, + "start_angle_index": 4, + "end_angle_index": 2, + "left_turn": false, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + 0.01356, + 0.05558, + 1.0923103255424704 + ], + [ + 0.05, + 0.1, + 0.7853981633974483 + ] + ] + }, + { + "trajectory_id": 36, + "start_angle_index": 4, + "end_angle_index": 3, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 1.5707963267948966 + ], + [ + 0.0, + 0.0, + 1.416247123794628 + ], + [ + 0.0, + 0.0, + 1.261697920794359 + ], + [ + 0.0, + 0.0, + 1.1071487177940904 + ] + ] + }, + { + "trajectory_id": 37, + "start_angle_index": 4, + "end_angle_index": 4, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.05, + "arc_length": 0.0, + "straight_length": 0.05, + "poses": [ + [ + 0.0, + 0.05, + 1.5707963267948966 + ] + ] + }, + { + "trajectory_id": 38, + "start_angle_index": 4, + "end_angle_index": 5, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 1.5707963267948966 + ], + [ + 0.0, + 0.0, + 1.7253455297951652 + ], + [ + 0.0, + 0.0, + 1.879894732795434 + ], + [ + 0.0, + 0.0, + 2.0344439357957027 + ] + ] + }, + { + "trajectory_id": 39, + "start_angle_index": 4, + "end_angle_index": 6, + "left_turn": true, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + -0.01356, + 0.05558, + 2.049282328047323 + ], + [ + -0.05, + 0.1, + 2.356194490192345 + ] + ] + }, + { + "trajectory_id": 40, + "start_angle_index": 4, + "end_angle_index": 7, + "left_turn": true, + "trajectory_radius": 0.0809, + "trajectory_length": 0.15137, + "arc_length": 0.08957, + "straight_length": 0.0618, + "poses": [ + [ + -0.01523, + 0.04725, + 2.194495256108994 + ], + [ + -0.05487, + 0.07743, + 2.677945044588987 + ], + [ + -0.1, + 0.1, + 2.677945044588987 + ] + ] + }, + { + "trajectory_id": 41, + "start_angle_index": 5, + "end_angle_index": 2, + "left_turn": false, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + -0.02356, + 0.04916, + 1.8973288507550556 + ], + [ + -0.02617, + 0.10294, + 1.341363507076252 + ], + [ + 0.0, + 0.15, + 0.7853981633974483 + ] + ] + }, + { + "trajectory_id": 42, + "start_angle_index": 5, + "end_angle_index": 3, + "left_turn": false, + "trajectory_radius": 0.1118, + "trajectory_length": 0.10367, + "arc_length": 0.10367, + "straight_length": 0.0, + "poses": [ + [ + -0.0118, + 0.05, + 1.5707963267948966 + ], + [ + 0.0, + 0.1, + 1.1071487177940904 + ] + ] + }, + { + "trajectory_id": 43, + "start_angle_index": 5, + "end_angle_index": 3, + "left_turn": false, + "trajectory_radius": 0.22361, + "trajectory_length": 0.20735, + "arc_length": 0.20735, + "straight_length": 0.0, + "poses": [ + [ + -0.01763, + 0.04862, + 1.8026201312952996 + ], + [ + -0.02361, + 0.1, + 1.5707963267948966 + ], + [ + -0.01763, + 0.15138, + 1.3389725222944935 + ], + [ + 0.0, + 0.2, + 1.1071487177940904 + ] + ] + }, + { + "trajectory_id": 44, + "start_angle_index": 5, + "end_angle_index": 3, + "left_turn": false, + "trajectory_radius": 0.33541, + "trajectory_length": 0.31102, + "arc_length": 0.31102, + "straight_length": 0.0, + "poses": [ + [ + -0.01951, + 0.04797, + 1.879894732795434 + ], + [ + -0.03141, + 0.09837, + 1.7253455297951652 + ], + [ + -0.03541, + 0.15, + 1.5707963267948966 + ], + [ + -0.03141, + 0.20163, + 1.4162471237946277 + ], + [ + -0.01951, + 0.25203, + 1.261697920794359 + ], + [ + 0.0, + 0.3, + 1.1071487177940904 + ] + ] + }, + { + "trajectory_id": 45, + "start_angle_index": 5, + "end_angle_index": 4, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 2.0344439357957027 + ], + [ + 0.0, + 0.0, + 1.8026201312952996 + ], + [ + 0.0, + 0.0, + 1.5707963267948966 + ] + ] + }, + { + "trajectory_id": 46, + "start_angle_index": 5, + "end_angle_index": 5, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.1118, + "arc_length": 0.0, + "straight_length": 0.1118, + "poses": [ + [ + -0.025, + 0.05, + 2.0344439357957027 + ], + [ + -0.05, + 0.1, + 2.0344439357957027 + ] + ] + }, + { + "trajectory_id": 47, + "start_angle_index": 5, + "end_angle_index": 6, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 2.0344439357957027 + ], + [ + 0.0, + 0.0, + 2.1953192129940238 + ], + [ + 0.0, + 0.0, + 2.356194490192345 + ] + ] + }, + { + "trajectory_id": 48, + "start_angle_index": 5, + "end_angle_index": 7, + "left_turn": true, + "trajectory_radius": 0.1118, + "trajectory_length": 0.07194, + "arc_length": 0.07194, + "straight_length": 0.0, + "poses": [ + [ + -0.05, + 0.05, + 2.677945044588987 + ] + ] + }, + { + "trajectory_id": 49, + "start_angle_index": 5, + "end_angle_index": 7, + "left_turn": true, + "trajectory_radius": 0.22361, + "trajectory_length": 0.14389, + "arc_length": 0.14389, + "straight_length": 0.0, + "poses": [ + [ + -0.02587, + 0.04028, + 2.248944305393464 + ], + [ + -0.05972, + 0.07413, + 2.4634446749912255 + ], + [ + -0.1, + 0.1, + 2.677945044588987 + ] + ] + }, + { + "trajectory_id": 50, + "start_angle_index": 5, + "end_angle_index": 8, + "left_turn": true, + "trajectory_radius": 0.36181, + "trajectory_length": 0.42697, + "arc_length": 0.40058, + "straight_length": 0.02639, + "poses": [ + [ + -0.02393, + 0.04093, + 2.1655647750284124 + ], + [ + -0.05301, + 0.07837, + 2.296685614261122 + ], + [ + -0.08673, + 0.11169, + 2.4278064534938313 + ], + [ + -0.12452, + 0.14031, + 2.558927292726541 + ], + [ + -0.16573, + 0.16375, + 2.6900481319592506 + ], + [ + -0.20965, + 0.18159, + 2.8211689711919603 + ], + [ + -0.25553, + 0.19355, + 2.95228981042467 + ], + [ + -0.30257, + 0.1994, + 3.0834106496573797 + ], + [ + -0.35, + 0.2, + 3.141592653589793 + ] + ] + }, + { + "trajectory_id": 51, + "start_angle_index": 6, + "end_angle_index": 9, + "left_turn": true, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + -0.04706, + 0.02617, + 2.9121598338711485 + ], + [ + -0.10084, + 0.02356, + 3.468125177549952 + ], + [ + -0.15, + 0.0, + 3.6052402625905993 + ] + ] + }, + { + "trajectory_id": 52, + "start_angle_index": 6, + "end_angle_index": 3, + "left_turn": false, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + -0.02617, + 0.04706, + 1.8002291465135412 + ], + [ + -0.02356, + 0.10084, + 1.2442638028347377 + ], + [ + 0.0, + 0.15, + 1.1071487177940904 + ] + ] + }, + { + "trajectory_id": 53, + "start_angle_index": 6, + "end_angle_index": 4, + "left_turn": false, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + -0.03644, + 0.04442, + 2.049282328047323 + ], + [ + -0.05, + 0.1, + 1.5707963267948966 + ] + ] + }, + { + "trajectory_id": 54, + "start_angle_index": 6, + "end_angle_index": 5, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 2.356194490192345 + ], + [ + 0.0, + 0.0, + 2.1953192129940238 + ], + [ + 0.0, + 0.0, + 2.0344439357957027 + ] + ] + }, + { + "trajectory_id": 55, + "start_angle_index": 6, + "end_angle_index": 6, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.07071, + "arc_length": 0.0, + "straight_length": 0.07071, + "poses": [ + [ + -0.05, + 0.05, + 2.356194490192345 + ] + ] + }, + { + "trajectory_id": 56, + "start_angle_index": 6, + "end_angle_index": 7, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 2.356194490192345 + ], + [ + 0.0, + 0.0, + 2.517069767390666 + ], + [ + 0.0, + 0.0, + 2.677945044588987 + ] + ] + }, + { + "trajectory_id": 57, + "start_angle_index": 6, + "end_angle_index": 8, + "left_turn": true, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + -0.04442, + 0.03644, + 2.6631066523373668 + ], + [ + -0.1, + 0.05, + 3.141592653589793 + ] + ] + }, + { + "trajectory_id": 58, + "start_angle_index": 7, + "end_angle_index": 9, + "left_turn": true, + "trajectory_radius": 0.1118, + "trajectory_length": 0.10367, + "arc_length": 0.10367, + "straight_length": 0.0, + "poses": [ + [ + -0.05, + 0.0118, + 3.141592653589793 + ], + [ + -0.1, + 0.0, + 3.6052402625905993 + ] + ] + }, + { + "trajectory_id": 59, + "start_angle_index": 7, + "end_angle_index": 9, + "left_turn": true, + "trajectory_radius": 0.22361, + "trajectory_length": 0.20735, + "arc_length": 0.20735, + "straight_length": 0.0, + "poses": [ + [ + -0.04862, + 0.01763, + 2.90976884908939 + ], + [ + -0.1, + 0.02361, + 3.141592653589793 + ], + [ + -0.15138, + 0.01763, + 3.373416458090196 + ], + [ + -0.2, + 0.0, + 3.6052402625905993 + ] + ] + }, + { + "trajectory_id": 60, + "start_angle_index": 7, + "end_angle_index": 9, + "left_turn": true, + "trajectory_radius": 0.33541, + "trajectory_length": 0.31102, + "arc_length": 0.31102, + "straight_length": 0.0, + "poses": [ + [ + -0.04797, + 0.01951, + 2.832494247589256 + ], + [ + -0.09837, + 0.03141, + 2.9870434505895243 + ], + [ + -0.15, + 0.03541, + 3.141592653589793 + ], + [ + -0.20163, + 0.03141, + 3.296141856590062 + ], + [ + -0.25203, + 0.01951, + 3.450691059590331 + ], + [ + -0.3, + 0.0, + 3.6052402625905993 + ] + ] + }, + { + "trajectory_id": 61, + "start_angle_index": 7, + "end_angle_index": 10, + "left_turn": true, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + -0.04916, + 0.02356, + 2.815060129629634 + ], + [ + -0.10294, + 0.02617, + 3.3710254733084377 + ], + [ + -0.15, + 0.0, + 3.9269908169872414 + ] + ] + }, + { + "trajectory_id": 62, + "start_angle_index": 7, + "end_angle_index": 4, + "left_turn": false, + "trajectory_radius": 0.36181, + "trajectory_length": 0.42697, + "arc_length": 0.40058, + "straight_length": 0.02639, + "poses": [ + [ + -0.04093, + 0.02393, + 2.5468242053562773 + ], + [ + -0.07837, + 0.05301, + 2.4157033661235676 + ], + [ + -0.11169, + 0.08673, + 2.2845825268908584 + ], + [ + -0.14031, + 0.12452, + 2.1534616876581487 + ], + [ + -0.16375, + 0.16573, + 2.022340848425439 + ], + [ + -0.18159, + 0.20965, + 1.8912200091927294 + ], + [ + -0.19355, + 0.25553, + 1.7600991699600197 + ], + [ + -0.1994, + 0.30257, + 1.62897833072731 + ], + [ + -0.2, + 0.35, + 1.5707963267948966 + ] + ] + }, + { + "trajectory_id": 63, + "start_angle_index": 7, + "end_angle_index": 5, + "left_turn": false, + "trajectory_radius": 0.1118, + "trajectory_length": 0.07194, + "arc_length": 0.07194, + "straight_length": 0.0, + "poses": [ + [ + -0.05, + 0.05, + 2.0344439357957027 + ] + ] + }, + { + "trajectory_id": 64, + "start_angle_index": 7, + "end_angle_index": 5, + "left_turn": false, + "trajectory_radius": 0.22361, + "trajectory_length": 0.14389, + "arc_length": 0.14389, + "straight_length": 0.0, + "poses": [ + [ + -0.04028, + 0.02587, + 2.4634446749912255 + ], + [ + -0.07413, + 0.05972, + 2.248944305393464 + ], + [ + -0.1, + 0.1, + 2.0344439357957027 + ] + ] + }, + { + "trajectory_id": 65, + "start_angle_index": 7, + "end_angle_index": 6, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 2.677945044588987 + ], + [ + 0.0, + 0.0, + 2.5706948597901063 + ], + [ + 0.0, + 0.0, + 2.4634446749912255 + ], + [ + 0.0, + 0.0, + 2.356194490192345 + ] + ] + }, + { + "trajectory_id": 66, + "start_angle_index": 7, + "end_angle_index": 7, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.1118, + "arc_length": 0.0, + "straight_length": 0.1118, + "poses": [ + [ + -0.05, + 0.025, + 2.677945044588987 + ], + [ + -0.1, + 0.05, + 2.677945044588987 + ] + ] + }, + { + "trajectory_id": 67, + "start_angle_index": 7, + "end_angle_index": 8, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 2.677945044588987 + ], + [ + 0.0, + 0.0, + 2.832494247589256 + ], + [ + 0.0, + 0.0, + 2.9870434505895243 + ], + [ + 0.0, + 0.0, + 3.141592653589793 + ] + ] + }, + { + "trajectory_id": 68, + "start_angle_index": 8, + "end_angle_index": 9, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 3.141592653589793 + ], + [ + 0.0, + 0.0, + 3.296141856590062 + ], + [ + 0.0, + 0.0, + 3.4506910595903304 + ], + [ + 0.0, + 0.0, + 3.6052402625905993 + ] + ] + }, + { + "trajectory_id": 69, + "start_angle_index": 8, + "end_angle_index": 10, + "left_turn": true, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + -0.05558, + -0.01356, + 3.6200786548422195 + ], + [ + -0.1, + -0.05, + 3.9269908169872414 + ] + ] + }, + { + "trajectory_id": 70, + "start_angle_index": 8, + "end_angle_index": 11, + "left_turn": true, + "trajectory_radius": 0.0809, + "trajectory_length": 0.15137, + "arc_length": 0.08957, + "straight_length": 0.0618, + "poses": [ + [ + -0.04725, + -0.01523, + 3.7652915829038904 + ], + [ + -0.07743, + -0.05487, + 4.2487413713838835 + ], + [ + -0.1, + -0.1, + 4.2487413713838835 + ] + ] + }, + { + "trajectory_id": 71, + "start_angle_index": 8, + "end_angle_index": 5, + "left_turn": false, + "trajectory_radius": 0.0809, + "trajectory_length": 0.15137, + "arc_length": 0.08957, + "straight_length": 0.0618, + "poses": [ + [ + -0.04725, + 0.01523, + 2.517893724275696 + ], + [ + -0.07743, + 0.05487, + 2.0344439357957027 + ], + [ + -0.1, + 0.1, + 2.0344439357957027 + ] + ] + }, + { + "trajectory_id": 72, + "start_angle_index": 8, + "end_angle_index": 6, + "left_turn": false, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + -0.05558, + 0.01356, + 2.6631066523373668 + ], + [ + -0.1, + 0.05, + 2.356194490192345 + ] + ] + }, + { + "trajectory_id": 73, + "start_angle_index": 8, + "end_angle_index": 7, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 3.141592653589793 + ], + [ + 0.0, + 0.0, + 2.9870434505895243 + ], + [ + 0.0, + 0.0, + 2.832494247589256 + ], + [ + 0.0, + 0.0, + 2.677945044588987 + ] + ] + }, + { + "trajectory_id": 74, + "start_angle_index": 8, + "end_angle_index": 8, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.05, + "arc_length": 0.0, + "straight_length": 0.05, + "poses": [ + [ + -0.05, + 0.0, + 3.141592653589793 + ] + ] + }, + { + "trajectory_id": 75, + "start_angle_index": 9, + "end_angle_index": 9, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.1118, + "arc_length": 0.0, + "straight_length": 0.1118, + "poses": [ + [ + -0.05, + -0.025, + 3.6052402625905993 + ], + [ + -0.1, + -0.05, + 3.6052402625905993 + ] + ] + }, + { + "trajectory_id": 76, + "start_angle_index": 9, + "end_angle_index": 10, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 3.6052402625905993 + ], + [ + 0.0, + 0.0, + 3.7661155397889203 + ], + [ + 0.0, + 0.0, + 3.9269908169872414 + ] + ] + }, + { + "trajectory_id": 77, + "start_angle_index": 9, + "end_angle_index": 11, + "left_turn": true, + "trajectory_radius": 0.1118, + "trajectory_length": 0.07194, + "arc_length": 0.07194, + "straight_length": 0.0, + "poses": [ + [ + -0.05, + -0.05, + 4.2487413713838835 + ] + ] + }, + { + "trajectory_id": 78, + "start_angle_index": 9, + "end_angle_index": 11, + "left_turn": true, + "trajectory_radius": 0.22361, + "trajectory_length": 0.14389, + "arc_length": 0.14389, + "straight_length": 0.0, + "poses": [ + [ + -0.04028, + -0.02587, + 3.8197406321883607 + ], + [ + -0.07413, + -0.05972, + 4.034241001786122 + ], + [ + -0.1, + -0.1, + 4.2487413713838835 + ] + ] + }, + { + "trajectory_id": 79, + "start_angle_index": 9, + "end_angle_index": 12, + "left_turn": true, + "trajectory_radius": 0.36181, + "trajectory_length": 0.42697, + "arc_length": 0.40058, + "straight_length": 0.02639, + "poses": [ + [ + -0.04093, + -0.02393, + 3.736361101823309 + ], + [ + -0.07837, + -0.05301, + 3.8674819410560186 + ], + [ + -0.11169, + -0.08673, + 3.998602780288728 + ], + [ + -0.14031, + -0.12452, + 4.129723619521437 + ], + [ + -0.16375, + -0.16573, + 4.260844458754147 + ], + [ + -0.18159, + -0.20965, + 4.391965297986857 + ], + [ + -0.19355, + -0.25553, + 4.5230861372195665 + ], + [ + -0.1994, + -0.30257, + 4.654206976452276 + ], + [ + -0.2, + -0.35, + 4.71238898038469 + ] + ] + }, + { + "trajectory_id": 80, + "start_angle_index": 9, + "end_angle_index": 6, + "left_turn": false, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + -0.04916, + -0.02356, + 3.468125177549952 + ], + [ + -0.10294, + -0.02617, + 2.9121598338711485 + ], + [ + -0.15, + 0.0, + 2.356194490192345 + ] + ] + }, + { + "trajectory_id": 81, + "start_angle_index": 9, + "end_angle_index": 7, + "left_turn": false, + "trajectory_radius": 0.1118, + "trajectory_length": 0.10367, + "arc_length": 0.10367, + "straight_length": 0.0, + "poses": [ + [ + -0.05, + -0.0118, + 3.141592653589793 + ], + [ + -0.1, + 0.0, + 2.677945044588987 + ] + ] + }, + { + "trajectory_id": 82, + "start_angle_index": 9, + "end_angle_index": 7, + "left_turn": false, + "trajectory_radius": 0.22361, + "trajectory_length": 0.20735, + "arc_length": 0.20735, + "straight_length": 0.0, + "poses": [ + [ + -0.04862, + -0.01763, + 3.373416458090196 + ], + [ + -0.1, + -0.02361, + 3.141592653589793 + ], + [ + -0.15138, + -0.01763, + 2.90976884908939 + ], + [ + -0.2, + 0.0, + 2.677945044588987 + ] + ] + }, + { + "trajectory_id": 83, + "start_angle_index": 9, + "end_angle_index": 7, + "left_turn": false, + "trajectory_radius": 0.33541, + "trajectory_length": 0.31102, + "arc_length": 0.31102, + "straight_length": 0.0, + "poses": [ + [ + -0.04797, + -0.01951, + 3.4506910595903304 + ], + [ + -0.09837, + -0.03141, + 3.296141856590062 + ], + [ + -0.15, + -0.03541, + 3.141592653589793 + ], + [ + -0.20163, + -0.03141, + 2.9870434505895243 + ], + [ + -0.25203, + -0.01951, + 2.8324942475892554 + ], + [ + -0.3, + 0.0, + 2.677945044588987 + ] + ] + }, + { + "trajectory_id": 84, + "start_angle_index": 9, + "end_angle_index": 8, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 3.6052402625905993 + ], + [ + 0.0, + 0.0, + 3.373416458090196 + ], + [ + 0.0, + 0.0, + 3.141592653589793 + ] + ] + }, + { + "trajectory_id": 85, + "start_angle_index": 10, + "end_angle_index": 9, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 3.9269908169872414 + ], + [ + 0.0, + 0.0, + 3.7661155397889203 + ], + [ + 0.0, + 0.0, + 3.6052402625905993 + ] + ] + }, + { + "trajectory_id": 86, + "start_angle_index": 10, + "end_angle_index": 10, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.07071, + "arc_length": 0.0, + "straight_length": 0.07071, + "poses": [ + [ + -0.05, + -0.05, + 3.9269908169872414 + ] + ] + }, + { + "trajectory_id": 87, + "start_angle_index": 10, + "end_angle_index": 11, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 3.9269908169872414 + ], + [ + 0.0, + 0.0, + 4.0878660941855625 + ], + [ + 0.0, + 0.0, + 4.2487413713838835 + ] + ] + }, + { + "trajectory_id": 88, + "start_angle_index": 10, + "end_angle_index": 12, + "left_turn": true, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + -0.03644, + -0.04442, + 4.233902979132264 + ], + [ + -0.05, + -0.1, + 4.71238898038469 + ] + ] + }, + { + "trajectory_id": 89, + "start_angle_index": 10, + "end_angle_index": 13, + "left_turn": true, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + -0.02617, + -0.04706, + 4.4829561606660455 + ], + [ + -0.02356, + -0.10084, + 5.038921504344849 + ], + [ + 0.0, + -0.15, + 5.176036589385496 + ] + ] + }, + { + "trajectory_id": 90, + "start_angle_index": 10, + "end_angle_index": 7, + "left_turn": false, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + -0.04706, + -0.02617, + 3.3710254733084377 + ], + [ + -0.10084, + -0.02356, + 2.815060129629634 + ], + [ + -0.15, + 0.0, + 2.677945044588987 + ] + ] + }, + { + "trajectory_id": 91, + "start_angle_index": 10, + "end_angle_index": 8, + "left_turn": false, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + -0.04442, + -0.03644, + 3.6200786548422195 + ], + [ + -0.1, + -0.05, + 3.141592653589793 + ] + ] + }, + { + "trajectory_id": 92, + "start_angle_index": 11, + "end_angle_index": 9, + "left_turn": false, + "trajectory_radius": 0.1118, + "trajectory_length": 0.07194, + "arc_length": 0.07194, + "straight_length": 0.0, + "poses": [ + [ + -0.05, + -0.05, + 3.6052402625905993 + ] + ] + }, + { + "trajectory_id": 93, + "start_angle_index": 11, + "end_angle_index": 9, + "left_turn": false, + "trajectory_radius": 0.22361, + "trajectory_length": 0.14389, + "arc_length": 0.14389, + "straight_length": 0.0, + "poses": [ + [ + -0.02587, + -0.04028, + 4.034241001786122 + ], + [ + -0.05972, + -0.07413, + 3.8197406321883607 + ], + [ + -0.1, + -0.1, + 3.6052402625905993 + ] + ] + }, + { + "trajectory_id": 94, + "start_angle_index": 11, + "end_angle_index": 10, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 4.2487413713838835 + ], + [ + 0.0, + 0.0, + 4.141491186585003 + ], + [ + 0.0, + 0.0, + 4.034241001786122 + ], + [ + 0.0, + 0.0, + 3.9269908169872414 + ] + ] + }, + { + "trajectory_id": 95, + "start_angle_index": 11, + "end_angle_index": 11, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.1118, + "arc_length": 0.0, + "straight_length": 0.1118, + "poses": [ + [ + -0.025, + -0.05, + 4.2487413713838835 + ], + [ + -0.05, + -0.1, + 4.2487413713838835 + ] + ] + }, + { + "trajectory_id": 96, + "start_angle_index": 11, + "end_angle_index": 12, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 4.2487413713838835 + ], + [ + 0.0, + 0.0, + 4.403290574384152 + ], + [ + 0.0, + 0.0, + 4.557839777384421 + ], + [ + 0.0, + 0.0, + 4.71238898038469 + ] + ] + }, + { + "trajectory_id": 97, + "start_angle_index": 11, + "end_angle_index": 13, + "left_turn": true, + "trajectory_radius": 0.1118, + "trajectory_length": 0.10367, + "arc_length": 0.10367, + "straight_length": 0.0, + "poses": [ + [ + -0.0118, + -0.05, + 4.71238898038469 + ], + [ + 0.0, + -0.1, + 5.176036589385496 + ] + ] + }, + { + "trajectory_id": 98, + "start_angle_index": 11, + "end_angle_index": 13, + "left_turn": true, + "trajectory_radius": 0.22361, + "trajectory_length": 0.20735, + "arc_length": 0.20735, + "straight_length": 0.0, + "poses": [ + [ + -0.01763, + -0.04862, + 4.480565175884287 + ], + [ + -0.02361, + -0.1, + 4.71238898038469 + ], + [ + -0.01763, + -0.15138, + 4.944212784885092 + ], + [ + 0.0, + -0.2, + 5.176036589385496 + ] + ] + }, + { + "trajectory_id": 99, + "start_angle_index": 11, + "end_angle_index": 13, + "left_turn": true, + "trajectory_radius": 0.33541, + "trajectory_length": 0.31102, + "arc_length": 0.31102, + "straight_length": 0.0, + "poses": [ + [ + -0.01951, + -0.04797, + 4.403290574384152 + ], + [ + -0.03141, + -0.09837, + 4.557839777384421 + ], + [ + -0.03541, + -0.15, + 4.71238898038469 + ], + [ + -0.03141, + -0.20163, + 4.866938183384958 + ], + [ + -0.01951, + -0.25203, + 5.021487386385227 + ], + [ + 0.0, + -0.3, + 5.176036589385496 + ] + ] + }, + { + "trajectory_id": 100, + "start_angle_index": 11, + "end_angle_index": 14, + "left_turn": true, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + -0.02356, + -0.04916, + 4.385856456424531 + ], + [ + -0.02617, + -0.10294, + 4.941821800103334 + ], + [ + 0.0, + -0.15, + 5.497787143782138 + ] + ] + }, + { + "trajectory_id": 101, + "start_angle_index": 11, + "end_angle_index": 8, + "left_turn": false, + "trajectory_radius": 0.36181, + "trajectory_length": 0.42697, + "arc_length": 0.40058, + "straight_length": 0.02639, + "poses": [ + [ + -0.02393, + -0.04093, + 4.117620532151173 + ], + [ + -0.05301, + -0.07837, + 3.986499692918464 + ], + [ + -0.08673, + -0.11169, + 3.855378853685755 + ], + [ + -0.12452, + -0.14031, + 3.7242580144530453 + ], + [ + -0.16573, + -0.16375, + 3.5931371752203356 + ], + [ + -0.20965, + -0.18159, + 3.462016335987626 + ], + [ + -0.25553, + -0.19355, + 3.3308954967549163 + ], + [ + -0.30257, + -0.1994, + 3.1997746575222066 + ], + [ + -0.35, + -0.2, + 3.141592653589793 + ] + ] + }, + { + "trajectory_id": 102, + "start_angle_index": 12, + "end_angle_index": 9, + "left_turn": false, + "trajectory_radius": 0.0809, + "trajectory_length": 0.15137, + "arc_length": 0.08957, + "straight_length": 0.0618, + "poses": [ + [ + -0.01523, + -0.04725, + 4.088690051070593 + ], + [ + -0.05487, + -0.07743, + 3.6052402625905993 + ], + [ + -0.1, + -0.1, + 3.6052402625905993 + ] + ] + }, + { + "trajectory_id": 103, + "start_angle_index": 12, + "end_angle_index": 10, + "left_turn": false, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + -0.01356, + -0.05558, + 4.233902979132264 + ], + [ + -0.05, + -0.1, + 3.9269908169872414 + ] + ] + }, + { + "trajectory_id": 104, + "start_angle_index": 12, + "end_angle_index": 11, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 4.71238898038469 + ], + [ + 0.0, + 0.0, + 4.557839777384421 + ], + [ + 0.0, + 0.0, + 4.403290574384152 + ], + [ + 0.0, + 0.0, + 4.2487413713838835 + ] + ] + }, + { + "trajectory_id": 105, + "start_angle_index": 12, + "end_angle_index": 12, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.05, + "arc_length": 0.0, + "straight_length": 0.05, + "poses": [ + [ + 0.0, + -0.05, + 4.71238898038469 + ] + ] + }, + { + "trajectory_id": 106, + "start_angle_index": 12, + "end_angle_index": 13, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 4.71238898038469 + ], + [ + 0.0, + 0.0, + 4.866938183384958 + ], + [ + 0.0, + 0.0, + 5.021487386385227 + ], + [ + 0.0, + 0.0, + 5.176036589385496 + ] + ] + }, + { + "trajectory_id": 107, + "start_angle_index": 12, + "end_angle_index": 14, + "left_turn": true, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + 0.01356, + -0.05558, + 5.190874981637116 + ], + [ + 0.05, + -0.1, + 5.497787143782138 + ] + ] + }, + { + "trajectory_id": 108, + "start_angle_index": 12, + "end_angle_index": 15, + "left_turn": true, + "trajectory_radius": 0.0809, + "trajectory_length": 0.15137, + "arc_length": 0.08957, + "straight_length": 0.0618, + "poses": [ + [ + 0.01523, + -0.04725, + 5.3360879096987865 + ], + [ + 0.05487, + -0.07743, + 5.81953769817878 + ], + [ + 0.1, + -0.1, + 5.81953769817878 + ] + ] + }, + { + "trajectory_id": 109, + "start_angle_index": 13, + "end_angle_index": 10, + "left_turn": false, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + 0.02356, + -0.04916, + 5.038921504344849 + ], + [ + 0.02617, + -0.10294, + 4.4829561606660455 + ], + [ + 0.0, + -0.15, + 3.9269908169872414 + ] + ] + }, + { + "trajectory_id": 110, + "start_angle_index": 13, + "end_angle_index": 11, + "left_turn": false, + "trajectory_radius": 0.1118, + "trajectory_length": 0.10367, + "arc_length": 0.10367, + "straight_length": 0.0, + "poses": [ + [ + 0.0118, + -0.05, + 4.71238898038469 + ], + [ + 0.0, + -0.1, + 4.2487413713838835 + ] + ] + }, + { + "trajectory_id": 111, + "start_angle_index": 13, + "end_angle_index": 11, + "left_turn": false, + "trajectory_radius": 0.22361, + "trajectory_length": 0.20735, + "arc_length": 0.20735, + "straight_length": 0.0, + "poses": [ + [ + 0.01763, + -0.04862, + 4.944212784885092 + ], + [ + 0.02361, + -0.1, + 4.71238898038469 + ], + [ + 0.01763, + -0.15138, + 4.480565175884287 + ], + [ + 0.0, + -0.2, + 4.2487413713838835 + ] + ] + }, + { + "trajectory_id": 112, + "start_angle_index": 13, + "end_angle_index": 11, + "left_turn": false, + "trajectory_radius": 0.33541, + "trajectory_length": 0.31102, + "arc_length": 0.31102, + "straight_length": 0.0, + "poses": [ + [ + 0.01951, + -0.04797, + 5.021487386385227 + ], + [ + 0.03141, + -0.09837, + 4.866938183384958 + ], + [ + 0.03541, + -0.15, + 4.71238898038469 + ], + [ + 0.03141, + -0.20163, + 4.557839777384421 + ], + [ + 0.01951, + -0.25203, + 4.403290574384152 + ], + [ + 0.0, + -0.3, + 4.2487413713838835 + ] + ] + }, + { + "trajectory_id": 113, + "start_angle_index": 13, + "end_angle_index": 12, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 5.176036589385496 + ], + [ + 0.0, + 0.0, + 4.944212784885092 + ], + [ + 0.0, + 0.0, + 4.71238898038469 + ] + ] + }, + { + "trajectory_id": 114, + "start_angle_index": 13, + "end_angle_index": 13, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.1118, + "arc_length": 0.0, + "straight_length": 0.1118, + "poses": [ + [ + 0.025, + -0.05, + 5.176036589385496 + ], + [ + 0.05, + -0.1, + 5.176036589385496 + ] + ] + }, + { + "trajectory_id": 115, + "start_angle_index": 13, + "end_angle_index": 14, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 5.176036589385496 + ], + [ + 0.0, + 0.0, + 5.336911866583817 + ], + [ + 0.0, + 0.0, + 5.497787143782138 + ] + ] + }, + { + "trajectory_id": 116, + "start_angle_index": 13, + "end_angle_index": 15, + "left_turn": true, + "trajectory_radius": 0.1118, + "trajectory_length": 0.07194, + "arc_length": 0.07194, + "straight_length": 0.0, + "poses": [ + [ + 0.05, + -0.05, + 5.81953769817878 + ] + ] + }, + { + "trajectory_id": 117, + "start_angle_index": 13, + "end_angle_index": 15, + "left_turn": true, + "trajectory_radius": 0.22361, + "trajectory_length": 0.14389, + "arc_length": 0.14389, + "straight_length": 0.0, + "poses": [ + [ + 0.02587, + -0.04028, + 5.390536958983257 + ], + [ + 0.05972, + -0.07413, + 5.605037328581019 + ], + [ + 0.1, + -0.1, + 5.81953769817878 + ] + ] + }, + { + "trajectory_id": 118, + "start_angle_index": 13, + "end_angle_index": 0, + "left_turn": true, + "trajectory_radius": 0.36181, + "trajectory_length": 0.42697, + "arc_length": 0.40058, + "straight_length": 0.02639, + "poses": [ + [ + 0.02393, + -0.04093, + 5.307157428618205 + ], + [ + 0.05301, + -0.07837, + 5.438278267850915 + ], + [ + 0.08673, + -0.11169, + 5.569399107083624 + ], + [ + 0.12452, + -0.14031, + 5.7005199463163345 + ], + [ + 0.16573, + -0.16375, + 5.831640785549044 + ], + [ + 0.20965, + -0.18159, + 5.962761624781754 + ], + [ + 0.25553, + -0.19355, + 6.093882464014463 + ], + [ + 0.30257, + -0.1994, + 6.225003303247172 + ], + [ + 0.35, + -0.2, + 0.0 + ] + ] + }, + { + "trajectory_id": 119, + "start_angle_index": 14, + "end_angle_index": 11, + "left_turn": false, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + 0.02617, + -0.04706, + 4.941821800103334 + ], + [ + 0.02356, + -0.10084, + 4.385856456424531 + ], + [ + 0.0, + -0.15, + 4.2487413713838835 + ] + ] + }, + { + "trajectory_id": 120, + "start_angle_index": 14, + "end_angle_index": 12, + "left_turn": false, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + 0.03644, + -0.04442, + 5.190874981637116 + ], + [ + 0.05, + -0.1, + 4.71238898038469 + ] + ] + }, + { + "trajectory_id": 121, + "start_angle_index": 14, + "end_angle_index": 13, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 5.497787143782138 + ], + [ + 0.0, + 0.0, + 5.336911866583817 + ], + [ + 0.0, + 0.0, + 5.176036589385496 + ] + ] + }, + { + "trajectory_id": 122, + "start_angle_index": 14, + "end_angle_index": 14, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.07071, + "arc_length": 0.0, + "straight_length": 0.07071, + "poses": [ + [ + 0.05, + -0.05, + 5.497787143782138 + ] + ] + }, + { + "trajectory_id": 123, + "start_angle_index": 14, + "end_angle_index": 15, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 5.497787143782138 + ], + [ + 0.0, + 0.0, + 5.658662420980459 + ], + [ + 0.0, + 0.0, + 5.81953769817878 + ] + ] + }, + { + "trajectory_id": 124, + "start_angle_index": 14, + "end_angle_index": 0, + "left_turn": true, + "trajectory_radius": 0.12071, + "trajectory_length": 0.11552, + "arc_length": 0.09481, + "straight_length": 0.02071, + "poses": [ + [ + 0.04442, + -0.03644, + 5.80469930592716 + ], + [ + 0.1, + -0.05, + 0.0 + ] + ] + }, + { + "trajectory_id": 125, + "start_angle_index": 14, + "end_angle_index": 1, + "left_turn": true, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + 0.04706, + -0.02617, + 6.053752487460941 + ], + [ + 0.10084, + -0.02356, + 0.32653252396015886 + ], + [ + 0.15, + 0.0, + 0.4636476090008061 + ] + ] + }, + { + "trajectory_id": 126, + "start_angle_index": 15, + "end_angle_index": 12, + "left_turn": false, + "trajectory_radius": 0.3618, + "trajectory_length": 0.42696, + "arc_length": 0.40057, + "straight_length": 0.02639, + "poses": [ + [ + 0.04092, + -0.02394, + 5.688416634946208 + ], + [ + 0.07836, + -0.05302, + 5.557295571713635 + ], + [ + 0.11168, + -0.08674, + 5.426174508481063 + ], + [ + 0.1403, + -0.12453, + 5.295053445248491 + ], + [ + 0.16374, + -0.16574, + 5.163932382015918 + ], + [ + 0.18159, + -0.20965, + 5.032811318783346 + ], + [ + 0.19354, + -0.25553, + 4.901690255550774 + ], + [ + 0.19939, + -0.30257, + 4.770569192318201 + ], + [ + 0.2, + -0.35, + 4.71238898038469 + ] + ] + }, + { + "trajectory_id": 127, + "start_angle_index": 15, + "end_angle_index": 13, + "left_turn": false, + "trajectory_radius": 0.1118, + "trajectory_length": 0.07194, + "arc_length": 0.07194, + "straight_length": 0.0, + "poses": [ + [ + 0.05, + -0.05, + 5.176036589385496 + ] + ] + }, + { + "trajectory_id": 128, + "start_angle_index": 15, + "end_angle_index": 13, + "left_turn": false, + "trajectory_radius": 0.22361, + "trajectory_length": 0.14389, + "arc_length": 0.14389, + "straight_length": 0.0, + "poses": [ + [ + 0.04028, + -0.02587, + 5.605037328581019 + ], + [ + 0.07413, + -0.05972, + 5.390536958983257 + ], + [ + 0.1, + -0.1, + 5.176036589385496 + ] + ] + }, + { + "trajectory_id": 129, + "start_angle_index": 15, + "end_angle_index": 14, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 5.81953769817878 + ], + [ + 0.0, + 0.0, + 5.712287513379899 + ], + [ + 0.0, + 0.0, + 5.605037328581019 + ], + [ + 0.0, + 0.0, + 5.497787143782138 + ] + ] + }, + { + "trajectory_id": 130, + "start_angle_index": 15, + "end_angle_index": 15, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.1118, + "arc_length": 0.0, + "straight_length": 0.1118, + "poses": [ + [ + 0.05, + -0.025, + 5.81953769817878 + ], + [ + 0.1, + -0.05, + 5.81953769817878 + ] + ] + }, + { + "trajectory_id": 131, + "start_angle_index": 15, + "end_angle_index": 0, + "left_turn": true, + "trajectory_radius": 0.0, + "trajectory_length": 0.0, + "arc_length": 0.0, + "straight_length": 0.0, + "poses": [ + [ + 0.0, + 0.0, + 5.81953769817878 + ], + [ + 0.0, + 0.0, + 5.9740869011790485 + ], + [ + 0.0, + 0.0, + 6.128636104179318 + ], + [ + 0.0, + 0.0, + 0.0 + ] + ] + }, + { + "trajectory_id": 132, + "start_angle_index": 15, + "end_angle_index": 1, + "left_turn": true, + "trajectory_radius": 0.1118, + "trajectory_length": 0.10367, + "arc_length": 0.10367, + "straight_length": 0.0, + "poses": [ + [ + 0.05, + -0.0118, + 0.0 + ], + [ + 0.1, + 0.0, + 0.4636476090008061 + ] + ] + }, + { + "trajectory_id": 133, + "start_angle_index": 15, + "end_angle_index": 1, + "left_turn": true, + "trajectory_radius": 0.22361, + "trajectory_length": 0.20735, + "arc_length": 0.20735, + "straight_length": 0.0, + "poses": [ + [ + 0.04862, + -0.01763, + 6.051361502679184 + ], + [ + 0.1, + -0.02361, + 0.0 + ], + [ + 0.15138, + -0.01763, + 0.23182380450040302 + ], + [ + 0.2, + 0.0, + 0.4636476090008061 + ] + ] + }, + { + "trajectory_id": 134, + "start_angle_index": 15, + "end_angle_index": 1, + "left_turn": true, + "trajectory_radius": 0.33541, + "trajectory_length": 0.31102, + "arc_length": 0.31102, + "straight_length": 0.0, + "poses": [ + [ + 0.04797, + -0.01951, + 5.9740869011790485 + ], + [ + 0.09837, + -0.03141, + 6.128636104179318 + ], + [ + 0.15, + -0.03541, + 0.0 + ], + [ + 0.20163, + -0.03141, + 0.1545492030002687 + ], + [ + 0.25203, + -0.01951, + 0.30909840600053734 + ], + [ + 0.3, + 0.0, + 0.4636476090008061 + ] + ] + }, + { + "trajectory_id": 135, + "start_angle_index": 15, + "end_angle_index": 2, + "left_turn": true, + "trajectory_radius": 0.09811, + "trajectory_length": 0.16364, + "arc_length": 0.12254, + "straight_length": 0.04109, + "poses": [ + [ + 0.04916, + -0.02356, + 5.956652783219427 + ], + [ + 0.10294, + -0.02617, + 0.22943281971864465 + ], + [ + 0.15, + 0.0, + 0.7853981633974483 + ] + ] + } + ] +} \ No newline at end of file diff --git a/nav2_smac_planner/lattice_primitives/trajectory.py b/nav2_smac_planner/lattice_primitives/trajectory.py index 0ba9141411c..637e2d993e1 100644 --- a/nav2_smac_planner/lattice_primitives/trajectory.py +++ b/nav2_smac_planner/lattice_primitives/trajectory.py @@ -15,7 +15,7 @@ from dataclasses import dataclass from typing import Any, Union -from nav2_smac_planner.lattice_primitives.helper import angle_difference, normalize_angle +from lattice_primitives.helper import angle_difference, normalize_angle import numpy as np from numpy.typing import NDArray diff --git a/nav2_smac_planner/lattice_primitives/trajectory_generator.py b/nav2_smac_planner/lattice_primitives/trajectory_generator.py index f576b14c188..5bf36a7efe2 100644 --- a/nav2_smac_planner/lattice_primitives/trajectory_generator.py +++ b/nav2_smac_planner/lattice_primitives/trajectory_generator.py @@ -15,7 +15,7 @@ import logging from typing import Any, List, Tuple, TypedDict, Union -from nav2_smac_planner.lattice_primitives.trajectory import (FloatNDArray, Path, Trajectory, +from lattice_primitives.trajectory import (FloatNDArray, Path, Trajectory, TrajectoryFloat, TrajectoryParameters) import numpy as np diff --git a/nav2_smac_planner/lattice_primitives/visualizations/0.0.png b/nav2_smac_planner/lattice_primitives/visualizations/0.0.png new file mode 100644 index 0000000000000000000000000000000000000000..a0cb1b53329d71809cd2df83ea1aba4c2beb54b0 GIT binary patch literal 12662 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 BdkO#m literal 0 HcmV?d00001 diff --git a/nav2_smac_planner/lattice_primitives/visualizations/26.56505117707799.png b/nav2_smac_planner/lattice_primitives/visualizations/26.56505117707799.png new file mode 100644 index 0000000000000000000000000000000000000000..f66c187cb6a6fb9630605d42726fd530936247e5 GIT binary patch literal 15826 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 E54LcP%m4rY literal 0 HcmV?d00001 diff --git a/nav2_smac_planner/lattice_primitives/visualizations/45.0.png b/nav2_smac_planner/lattice_primitives/visualizations/45.0.png new file mode 100644 index 0000000000000000000000000000000000000000..2d6cc75e11094ca9c1fcf3fa6b319725c135baba GIT binary patch literal 11922 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 E0`;BYC;$Ke literal 0 HcmV?d00001 diff --git a/nav2_smac_planner/lattice_primitives/visualizations/63.43494882292201.png b/nav2_smac_planner/lattice_primitives/visualizations/63.43494882292201.png new file mode 100644 index 0000000000000000000000000000000000000000..a8a7428be7739e0d04f90ee625df48802144592b GIT binary patch literal 16013 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 W(341+wEqo(P*>HtioSB|{(l4BCutf0 literal 0 HcmV?d00001 diff --git a/nav2_smac_planner/lattice_primitives/visualizations/all_trajectories.png b/nav2_smac_planner/lattice_primitives/visualizations/all_trajectories.png new file mode 100644 index 0000000000000000000000000000000000000000..6c356ad7a524f74771753c1b3195ab88d3825a14 GIT binary patch literal 42112 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 literal 0 HcmV?d00001 diff --git a/nav2_util/include/nav2_util/controller_utils.hpp b/nav2_util/include/nav2_util/controller_utils.hpp index dbe04b1f2f3..02afd8ea575 100644 --- a/nav2_util/include/nav2_util/controller_utils.hpp +++ b/nav2_util/include/nav2_util/controller_utils.hpp @@ -49,6 +49,21 @@ geometry_msgs::msg::PoseStamped getLookAheadPoint( double &, const nav_msgs::msg::Path &, const bool interpolate_after_goal = false); + +/** +* @brief Find the linear interpolation between two points +* at a given distance starting from first endpoint. +* @param p1 first endpoint of line segment +* @param p2 second endpoint of line segment +* @param target_dist interpolation distance from first endpoint of line segment +* @return point of intersection +*/ +geometry_msgs::msg::Point linearInterpolation( + const geometry_msgs::msg::Point & p1, + const geometry_msgs::msg::Point & p2, + const double target_dist); + + } // namespace nav2_util #endif // NAV2_UTIL__CONTROLLER_UTILS_HPP_ diff --git a/nav2_util/src/controller_utils.cpp b/nav2_util/src/controller_utils.cpp index 18f2ae42dd3..52b484d1d91 100644 --- a/nav2_util/src/controller_utils.cpp +++ b/nav2_util/src/controller_utils.cpp @@ -54,6 +54,25 @@ geometry_msgs::msg::Point circleSegmentIntersection( return p; } +geometry_msgs::msg::Point linearInterpolation( + const geometry_msgs::msg::Point & p1, + const geometry_msgs::msg::Point & p2, + const double target_dist) +{ + geometry_msgs::msg::Point result; + + double dx = p2.x - p1.x; + double dy = p2.y - p1.y; + double d_dist = std::hypot(dx, dy); + + double target_ratio = target_dist / d_dist; + + result.x = p1.x + target_ratio * dx; + result.y = p1.y + target_ratio * dy; + return result; +} + + geometry_msgs::msg::PoseStamped getLookAheadPoint( double & lookahead_dist, const nav_msgs::msg::Path & transformed_plan, @@ -63,21 +82,25 @@ geometry_msgs::msg::PoseStamped getLookAheadPoint( // Using distance along the path const auto & poses = transformed_plan.poses; auto goal_pose_it = poses.begin(); - double d = 0.0; + double path_dist = 0.0; + double interpolation_dist = 0.0; bool pose_found = false; for (size_t i = 1; i < poses.size(); i++) { const auto & prev_pose = poses[i - 1].pose.position; const auto & curr_pose = poses[i].pose.position; - d += std::hypot(curr_pose.x - prev_pose.x, curr_pose.y - prev_pose.y); - if (d >= lookahead_dist) { + const double d = std::hypot(curr_pose.x - prev_pose.x, curr_pose.y - prev_pose.y); + if (path_dist + d >= lookahead_dist) { goal_pose_it = poses.begin() + i; pose_found = true; break; } + + path_dist += d; } + interpolation_dist = lookahead_dist - path_dist; if (!pose_found) { goal_pose_it = poses.end(); } @@ -98,10 +121,10 @@ geometry_msgs::msg::PoseStamped getLookAheadPoint( projected_position.x += cos(end_path_orientation) * lookahead_dist; projected_position.y += sin(end_path_orientation) * lookahead_dist; - // Use the circle intersection to find the position at the correct look + // Use the linear interpolation to find the position at the correct look // ahead distance - const auto interpolated_position = circleSegmentIntersection( - last_pose_it->pose.position, projected_position, lookahead_dist); + const auto interpolated_position = linearInterpolation( + last_pose_it->pose.position, projected_position, interpolation_dist); geometry_msgs::msg::PoseStamped interpolated_pose; interpolated_pose.header = last_pose_it->header; @@ -109,19 +132,19 @@ geometry_msgs::msg::PoseStamped getLookAheadPoint( return interpolated_pose; } else { - lookahead_dist = d; // Updating lookahead distance since using the final point + lookahead_dist = path_dist; // Updating lookahead distance since using the final point goal_pose_it = std::prev(transformed_plan.poses.end()); } } else if (goal_pose_it != transformed_plan.poses.begin()) { - // Find the point on the line segment between the two poses + // Find the point on the robot path // that is exactly the lookahead distance away from the robot pose (the origin) - // This can be found with a closed form for the intersection of a segment and a circle - // Because of the way we did the std::find_if, prev_pose is guaranteed to be inside the circle, - // and goal_pose is guaranteed to be outside the circle. + // This can be found with a linear interpolation between the prev_pose and + // the goal_pose, moving interpolation_dist starting from prev_pose in the + // direction of goal_pose. auto prev_pose_it = std::prev(goal_pose_it); - auto point = circleSegmentIntersection( + auto point = linearInterpolation( prev_pose_it->pose.position, - goal_pose_it->pose.position, lookahead_dist); + goal_pose_it->pose.position, interpolation_dist); // Calculate orientation towards interpolated position // Convert yaw to quaternion