diff --git a/nav2_theta_star_planner/CMakeLists.txt b/nav2_theta_star_planner/CMakeLists.txt new file mode 100644 index 0000000000..8cf8a28c35 --- /dev/null +++ b/nav2_theta_star_planner/CMakeLists.txt @@ -0,0 +1,81 @@ +cmake_minimum_required(VERSION 3.5) +project(nav2_theta_star_planner) + +find_package(ament_cmake REQUIRED) +find_package(builtin_interfaces 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(pluginlib REQUIRED) +find_package(rclcpp REQUIRED) +find_package(rclcpp_action REQUIRED) +find_package(rclcpp_lifecycle REQUIRED) +find_package(tf2_ros REQUIRED) + +nav2_package() #Calls the nav2_package.cmake file +add_compile_options(-O3) + +include_directories( + include +) + +set(library_name ${PROJECT_NAME}) + +set(dependencies ament_cmake + builtin_interfaces + nav2_common + nav2_core + nav2_costmap_2d + nav2_msgs + nav2_util + pluginlib + rclcpp + rclcpp_action + rclcpp_lifecycle + tf2_ros +) + + +add_library(${library_name} SHARED + src/theta_star.cpp + src/theta_star_planner.cpp +) + +ament_target_dependencies(${library_name} ${dependencies}) + +target_compile_definitions(${library_name} PUBLIC "PLUGINLIB_DISABLE_BOOST_FUNCTIONS") + +pluginlib_export_plugin_description_file(nav2_core theta_star_planner.xml) + +install(TARGETS ${library_name} + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION lib/${PROJECT_NAME} +) + +install(DIRECTORY include/ + DESTINATION include/ +) + +install(FILES theta_star_planner.xml + DESTINATION share/${PROJECT_NAME} +) + + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + set(gtest_disable_pthreads OFF) + ament_lint_auto_find_test_dependencies() + find_package(ament_cmake_gtest REQUIRED) + ament_add_gtest(test_theta_star test/test_theta_star.cpp) + ament_target_dependencies(test_theta_star ${dependencies}) + target_link_libraries(test_theta_star ${library_name}) +endif() + + +ament_export_include_directories(include) +ament_export_libraries(${library_name}) +ament_export_dependencies(${dependencies}) +ament_package() diff --git a/nav2_theta_star_planner/README.md b/nav2_theta_star_planner/README.md new file mode 100644 index 0000000000..ed3f810f09 --- /dev/null +++ b/nav2_theta_star_planner/README.md @@ -0,0 +1,95 @@ +# Nav2 Theta Star Planner +The Theta Star Planner is a global planning plugin meant to be used with the Nav2 Planner Server. The `nav2_theta_star_planner` implements a highly optimized version of the Theta\* Planner (specifically the [Lazy Theta\* P variant](http://idm-lab.org/bib/abstracts/papers/aaai10b.pdf)) meant to plan any-angle paths using A\*. The planner supports differential-drive and omni-directional robots. + +## Features +- The planner uses A\* search along with line of sight (LOS) checks to form any-angle paths thus avoiding zig-zag paths that may be present in the usual implementation of A\* +- As it also considers the costmap traversal cost during execution it tends to smoothen the paths automatically, thus mitigating the need to smoothen the path (The presence of sharp turns depends on the resolution of the map, and it decreases as the map resolution increases) +- Uses the costs from the costmap to penalise high cost regions +- Allows to control the path behavior to either be any angle directed or to be in the middle of the spaces +- Is well suited for smaller robots of the omni-directional and differential drive kind +- The algorithmic part of the planner has been segregated from the plugin part to allow for reusability + +## Metrics +For the below example the planner took ~46ms (averaged value) to compute the path of 87.5m - +![example.png](img/00-37.png) + +The parameters were set to - `w_euc_cost: 1.0`, `w_traversal_cost: 5.0`, `w_heuristic_cost: 1.0` and the `global_costmap`'s `inflation_layer` parameters are set as - `cost_scaling_factor:5.0`, `inflation_radius: 5.5` + +## Cost Function Details +### Symbols and their meanings +**g(a)** - cost function cost for the node 'a' + +**h(a)** - heuristic function cost for the node 'a' + +**f(a)** - total cost (g(a) + h(a)) for the node 'a' + +**LETHAL_COST** - a value of the costmap traversal cost that inscribes an obstacle with +respect to a function, value = 253 + +**curr** - represents the node whose neighbours are being added to the list + +**neigh** - one of the neighboring nodes of curr + +**par** - parent node of curr + +**euc_cost(a,b)** - euclidean distance between the node type 'a' and type 'b' + +**costmap_cost(a,b)** - the costmap traversal cost (ranges from 0 - 252, 254 = unknown value) between the node type 'a' and type 'b' + +### Cost function +``` +g(neigh) = g(curr) + w_euc_cost*euc_cost(curr, neigh) + w_traversal_cost*(costmap_cost(curr,neigh)/LETHAL_COST)^2 +h(neigh) = w_heuristic_cost * euc_cost(neigh, goal) +f(neigh) = g(neigh) + h(neigh) +``` +Because of how the program works when the 'neigh' init_rclcpp is to be expanded, depending +on the result of the LOS check, (if the LOS check returns true) the value of g(neigh) might change to `g(par) + +w1*euc_cost(par, neigh) + w2*(costmap(par,neigh)/LETHAL_COST)^2` + +## Parameters +The parameters of the planner are : +- ` .how_many_corners ` : to choose between 4-connected and 8-connected graph expansions, the accepted values are 4 and 8 +- ` .w_euc_cost ` : weight applied on the length of the path. +- ` .w_traversal_cost ` : it tunes how harshly the nodes of high cost are penalised. From the above g(neigh) equation you can see that the cost-aware component of the cost function forms a parabolic curve, thus this parameter would, on increasing its value, make that curve steeper allowing for a greater differentiation (as the delta of costs would increase, when the graph becomes steep) among the nodes of different costs. +- ` .w_heuristic_cost ` : it has been provided to have an admissible heuristic + +Below are the default values of the parameters : +``` +planner_server: + ros__parameters: + planner_plugin_types: ["nav2_theta_star_planner/ThetaStarPlanner"] + use_sim_time: True + planner_plugin_ids: ["GridBased"] + GridBased: + how_many_corners: 8 + w_euc_cost: 1.0 + w_traversal_cost: 2.0 + w_heuristic_cost: 1.0 +``` + +## Usage Notes + +### Tuning the Parameters +Before starting off, do note that the costmap_cost(curr,neigh) component after being operated (ie before being multiplied to its parameter and being substituted in g(init_rclcpp)) varies from 0 to 1. + +This planner uses the costs associated with each cell from the `global_costmap` as a measure of the point's proximity to the obstacles. Providing a gentle potential field that covers the entirety of the region (thus leading to only small pocket like regions of cost = 0) is recommended in order to achieve paths that pass through the middle of the spaces. A good starting point could be to set the `inflation_layer`'s parameters as - `cost_scaling_factor: 10.0`, `inflation_radius: 5.5` and then decrease the value of `cost_scaling_factor` to achieve the said potential field. + +Providing a gentle potential field over the region allows the planner to exchange the increase in path lengths for a decrease in the accumulated traversal cost, thus leading to an increase in the distance from the obstacles. This around a corner allows for naturally smoothing the turns and removes the requirement for an external path smoother. + +To begin with, you can set the parameters to its default values and then try increasing the value of `w_traversal_cost` to achieve those middling paths, but this would adversly make the paths less taut. So it is recommend ed that you simultaneously tune the value of `w_euc_cost`. Increasing `w_euc_cost` increases the tautness of the path, which leads to more straight line like paths (any-angled paths). Do note that the query time from the planner would increase for higher values of `w_traversal_cost` as more nodes would have to be expanded to lower the cost of the path, to counteract this you may also try setting `w_euc_cost` to a higher value and check if it reduces the query time. + +Usually set `w_heuristic_cost` at the same value as `w_euc_cost` or 1.0 (whichever is lower). Though as a last resort you may increase the value of `w_heuristic_cost` to speed up the planning process, this is not recommended as it might not always lead to shorter query times, but would definitely give you sub optimal paths + +Also note that changes to `w_traversal_cost` might cause slow downs, in case the number of node expanisions increase thus tuning it along with `w_euc_cost` is the way to go to. + +While tuning the planner's parameters you can also change the `inflation_layer`'s parameters (of the global costmap) to tune the behavior of the paths. + +### Path Smoothing +Because of how the cost function works, the output path has a natural tendency to form smooth curves around corners, though the smoothness of the path depends on how wide the turn is, and the number of cells in that turn. + +This planner is recommended to be used with local planners like DWB or TEB (or other any local planner / controllers that form a local trajectory to be traversed) as these take into account the abrupt turns which might arise due to the planner not being able to find a smoother turns owing to the aforementioned reasons. + +While smoother paths can be achieved by increasing the costmap resolution (ie using a costmap of 1cm resolution rather than a 5cm one) it is not recommended to do so because it might increase the query times from the planner. Test the planners performance on the higher cm/px costmaps before making a switch to finer costmaps. + +### Possible Applications +This planner could be used in scenarios where planning speed matters over an extremely smooth path, which could anyways be handled by using a local planner/controller as mentioned above. Because of the any-angled nature of paths you can traverse environments diagonally (wherever it is allowed, eg: in a wide open region). diff --git a/nav2_theta_star_planner/img/00-37.png b/nav2_theta_star_planner/img/00-37.png new file mode 100644 index 0000000000..fa4115bfb9 Binary files /dev/null and b/nav2_theta_star_planner/img/00-37.png differ diff --git a/nav2_theta_star_planner/include/nav2_theta_star_planner/theta_star.hpp b/nav2_theta_star_planner/include/nav2_theta_star_planner/theta_star.hpp new file mode 100644 index 0000000000..dacd8a57bc --- /dev/null +++ b/nav2_theta_star_planner/include/nav2_theta_star_planner/theta_star.hpp @@ -0,0 +1,306 @@ +// Copyright 2020 Anshumaan Singh +// +// 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_THETA_STAR_PLANNER__THETA_STAR_HPP_ +#define NAV2_THETA_STAR_PLANNER__THETA_STAR_HPP_ + +#include +#include +#include +#include +#include +#include "rclcpp/rclcpp.hpp" +#include "nav2_costmap_2d/costmap_2d_ros.hpp" + +const double INF_COST = DBL_MAX; +const int LETHAL_COST = 252; + +struct coordsM +{ + int x, y; +}; + +struct coordsW +{ + double x, y; +}; + +struct tree_node +{ + int x, y; + double g = INF_COST; + double h = INF_COST; + const tree_node * parent_id = nullptr; + bool is_in_queue = false; + double f = INF_COST; +}; + +struct comp +{ + bool operator()(const tree_node * p1, const tree_node * p2) + { + return (p1->f) > (p2->f); + } +}; + +namespace theta_star +{ +class ThetaStar +{ +public: + coordsM src_{}, dst_{}; + nav2_costmap_2d::Costmap2D * costmap_{}; + + /// weight on the costmap traversal cost + double w_traversal_cost_; + /// weight on the euclidean distance cost (used for calculations of g_cost) + double w_euc_cost_; + /// weight on the heuristic cost (used for h_cost calculations) + double w_heuristic_cost_; + /// parameter to set the number of adjacent nodes to be searched on + int how_many_corners_; + /// the x-directional and y-directional lengths of the map respectively + int size_x_, size_y_; + + ThetaStar(); + + ~ThetaStar() = default; + + /** + * @brief it iteratively searches upon the nodes in the queue (open list) until the + * current node is the goal pose or the size of queue becomes 0 + * @param raw_path is used to return the path obtained by executing the algorithm + * @return true if a path is found, false if no path is found in between the start and goal pose + */ + bool generatePath(std::vector & raw_path); + + /** + * @brief this function checks whether the cost of a point(cx, cy) on the costmap is less than the LETHAL_COST + * @return the result of the check + */ + inline bool isSafe(const int & cx, const int & cy) const + { + return costmap_->getCost(cx, cy) < LETHAL_COST; + } + + /** + * @brief initialises the values of the start and goal points + */ + void setStartAndGoal( + const geometry_msgs::msg::PoseStamped & start, + const geometry_msgs::msg::PoseStamped & goal); + + /** + * @brief checks whether the start and goal points have costmap costs greater than LETHAL_COST + * @return true if the cost of any one of the points is greater than LETHAL_COST + */ + bool isUnsafeToPlan() const + { + return !(isSafe(src_.x, src_.y)) || !(isSafe(dst_.x, dst_.y)); + } + + int nodes_opened = 0; + +protected: + /// for the coordinates (x,y), it stores at node_position_[size_x_ * y + x], + /// the pointer to the location at which the data of the node is present in nodes_data_ + /// it is initialised with size_x_ * size_y_ elements + /// and its number of elements increases to account for a change in map size + std::vector node_position_; + + /// the vector nodes_data_ stores the coordinates, costs and index of the parent node, + /// and whether or not the node is present in queue_, for all the nodes searched + /// it is initialised with no elements + /// and its size increases depending on the number of nodes searched + std::vector nodes_data_; + + /// this is the priority queue (open_list) to select the next node to be expanded + std::priority_queue, comp> queue_; + + /// it is a counter like variable used to generate consecutive indices + /// such that the data for all the nodes (in open and closed lists) could be stored + /// consecutively in nodes_data_ + int index_generated_; + + const coordsM moves[8] = {{0, 1}, + {0, -1}, + {1, 0}, + {-1, 0}, + {1, -1}, + {-1, 1}, + {1, 1}, + {-1, -1}}; + + tree_node * exp_node; + + + /** @brief it performs a line of sight (los) check between the current node and the parent node of its parent node; + * if an los is found and the new costs calculated are lesser, then the cost and parent node + * of the current node is updated + * @param data of the current node + */ + void resetParent(tree_node * curr_data); + + /** + * @brief this function expands the current node + * @param curr_data used to send the data of the current node + * @param curr_id used to send the index of the current node as stored in nodes_position_ + */ + void setNeighbors(const tree_node * curr_data); + + /** + * @brief performs the line of sight check using Bresenham's Algorithm, + * and has been modified to calculate the traversal cost incurred in a straight line path between + * the two points whose coordinates are (x0, y0) and (x1, y1) + * @param sl_cost is used to return the cost thus incurred + * @return true if a line of sight exists between the points + */ + bool losCheck( + const int & x0, const int & y0, const int & x1, const int & y1, + double & sl_cost) const; + + /** + * @brief it returns the path by backtracking from the goal to the start, by using their parent nodes + * @param raw_points used to return the path thus found + * @param curr_id sends in the index of the goal coordinate, as stored in nodes_position + */ + void backtrace(std::vector & raw_points, const tree_node * curr_n) const; + + /** + * @brief it is an overloaded function to ease the cost calculations while performing the LOS check + * @param cost denotes the total straight line traversal cost; it adds the traversal cost for the node (cx, cy) at every instance; it is also being returned + * @return false if the traversal cost is greater than / equal to the LETHAL_COST and true otherwise + */ + bool isSafe(const int & cx, const int & cy, double & cost) const + { + double curr_cost = getCost(cx, cy); + if (curr_cost < LETHAL_COST) { + cost += w_traversal_cost_ * curr_cost * curr_cost / LETHAL_COST / LETHAL_COST; + return true; + } else { + return false; + } + } + + /* + * @brief this function scales the costmap cost by shifting the origin to 25 and then multiply + * the actual costmap cost by 0.9 to keep the output in the range of [25, 255) + */ + inline double getCost(const int & cx, const int & cy) const + { + return 26 + 0.9 * costmap_->getCost(cx, cy); + } + + /** + * @brief for the point(cx, cy), its traversal cost is calculated by + * *()^2/()^2 + * @return the traversal cost thus calculated + */ + inline double getTraversalCost(const int & cx, const int & cy) + { + double curr_cost = getCost(cx, cy); + return w_traversal_cost_ * curr_cost * curr_cost / LETHAL_COST / LETHAL_COST; + } + + /** + * @brief calculates the piecewise straight line euclidean distances by + * * + * @return the distance thus calculated + */ + inline double getEuclideanCost(const int & ax, const int & ay, const int & bx, const int & by) + { + return w_euc_cost_ * std::hypot(ax - bx, ay - by); + } + + /** + * @brief for the point(cx, cy), its heuristic cost is calculated by + * * + * @return the heuristic cost + */ + inline double getHCost(const int & cx, const int & cy) + { + return w_heuristic_cost_ * std::hypot(cx - dst_.x, cy - dst_.y); + } + + /** + * @brief checks if the given coordinates(cx, cy) lies within the map + * @return the result of the check + */ + inline bool withinLimits(const int & cx, const int & cy) const + { + return cx >= 0 && cx < size_x_ && cy >= 0 && cy < size_y_; + } + + /** + * @brief checks if the coordinates of a node is the goal or not + * @return the result of the check + */ + inline bool isGoal(const tree_node & this_node) const + { + return this_node.x == dst_.x && this_node.y == dst_.y; + } + + /** + * @brief initialises the node_position_ vector by storing -1 as index for all points(x, y) within the limits of the map + * @param size_inc is used to increase the number of elements in node_position_ in case the size of the map increases + */ + void initializePosn(int size_inc = 0); + + /** + * @brief it stores id_this in node_position_ at the index [ size_x_*cy + cx ] + * @param id_this a pointer to the location at which the data of the point(cx, cy) is stored in nodes_data_ + */ + inline void addIndex(const int & cx, const int & cy, tree_node * node_this) + { + node_position_[size_x_ * cy + cx] = node_this; + } + + /** + * @brief retrieves the pointer of the location at which the data of the point(cx, cy) is stored in nodes_data + * @return id_this is the pointer to that location + */ + inline tree_node * getIndex(const int & cx, const int & cy) + { + return node_position_[size_x_ * cy + cx]; + } + + /** + * @brief this function depending on the size of the nodes_data_ vector allots space to store the data for a node(x, y) + * @param id_this is the index at which the data is stored/has to be stored for that node + */ + void addToNodesData(const int & id_this) + { + if (static_cast(nodes_data_.size()) <= id_this) { + nodes_data_.push_back({}); + } else { + nodes_data_[id_this] = {}; + } + } + + /** + * @brief initialises the values of global variables at beginning of the execution of the generatePath function + */ + void resetContainers(); + + /** + * @brief clears the priority queue after each execution of the generatePath function + */ + void clearQueue() + { + queue_ = std::priority_queue, comp>(); + } +}; +} // namespace theta_star + +#endif // NAV2_THETA_STAR_PLANNER__THETA_STAR_HPP_ diff --git a/nav2_theta_star_planner/include/nav2_theta_star_planner/theta_star_planner.hpp b/nav2_theta_star_planner/include/nav2_theta_star_planner/theta_star_planner.hpp new file mode 100644 index 0000000000..c90b809291 --- /dev/null +++ b/nav2_theta_star_planner/include/nav2_theta_star_planner/theta_star_planner.hpp @@ -0,0 +1,83 @@ +// Copyright 2020 Anshumaan Singh +// +// 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_THETA_STAR_PLANNER__THETA_STAR_PLANNER_HPP_ +#define NAV2_THETA_STAR_PLANNER__THETA_STAR_PLANNER_HPP_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "rclcpp/rclcpp.hpp" +#include "nav2_core/global_planner.hpp" +#include "nav_msgs/msg/path.hpp" +#include "nav2_util/robot_utils.hpp" +#include "nav2_util/lifecycle_node.hpp" +#include "nav2_costmap_2d/costmap_2d_ros.hpp" +#include "nav2_costmap_2d/cost_values.hpp" +#include "nav2_util/node_utils.hpp" +#include "nav2_theta_star_planner/theta_star.hpp" + +namespace nav2_theta_star_planner +{ + +class ThetaStarPlanner : public nav2_core::GlobalPlanner +{ +public: + void configure( + const rclcpp_lifecycle::LifecycleNode::WeakPtr & parent, + std::string name, std::shared_ptr tf, + std::shared_ptr costmap_ros) override; + + void cleanup() override; + + void activate() override; + + void deactivate() override; + + nav_msgs::msg::Path createPlan( + const geometry_msgs::msg::PoseStamped & start, + const geometry_msgs::msg::PoseStamped & goal) override; + +protected: + std::shared_ptr tf_; + rclcpp::Clock::SharedPtr clock_; + rclcpp::Logger logger_{rclcpp::get_logger("ThetaStarPlanner")}; + std::string global_frame_, name_; + + std::unique_ptr planner_; + + /** + * @brief the function responsible for calling the algorithm and retrieving a path from it + * @return global_path is the planned path to be taken + */ + void getPlan(nav_msgs::msg::Path & global_path); + + /** + * @brief interpolates points between the consecutive waypoints of the path + * @param raw_path is used to send in the path received from the planner + * @param dist_bw_points is used to send in the interpolation_resolution (which has been set as the costmap resolution) + * @return the final path with waypoints at a distance of the value of interpolation_resolution of each other + */ + static nav_msgs::msg::Path linearInterpolation( + const std::vector & raw_path, + const double & dist_bw_points); +}; +} // namespace nav2_theta_star_planner + +#endif // NAV2_THETA_STAR_PLANNER__THETA_STAR_PLANNER_HPP_ diff --git a/nav2_theta_star_planner/package.xml b/nav2_theta_star_planner/package.xml new file mode 100644 index 0000000000..67a4839abf --- /dev/null +++ b/nav2_theta_star_planner/package.xml @@ -0,0 +1,34 @@ + + + + nav2_theta_star_planner + 0.4.0 + Theta* Global Planning Plugin + Steve Macenski + Anshumaan Singh + Apache-2.0 + + ament_cmake + + builtin_interfaces + nav2_common + nav2_core + nav2_costmap_2d + nav2_msgs + nav2_util + pluginlib + rclcpp + rclcpp_action + rclcpp_lifecycle + tf2_ros + + ament_lint_auto + ament_lint_common + ament_cmake_gtest + + + ament_cmake + + + + diff --git a/nav2_theta_star_planner/src/theta_star.cpp b/nav2_theta_star_planner/src/theta_star.cpp new file mode 100644 index 0000000000..e0267b34c0 --- /dev/null +++ b/nav2_theta_star_planner/src/theta_star.cpp @@ -0,0 +1,262 @@ +// Copyright 2020 Anshumaan Singh +// +// 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_theta_star_planner/theta_star.hpp" + +namespace theta_star +{ + +ThetaStar::ThetaStar() +: w_traversal_cost_(1.0), + w_euc_cost_(2.0), + w_heuristic_cost_(1.0), + how_many_corners_(8), + size_x_(0), + size_y_(0), + index_generated_(0) +{ + exp_node = new tree_node; +} + +void ThetaStar::setStartAndGoal( + const geometry_msgs::msg::PoseStamped & start, + const geometry_msgs::msg::PoseStamped & goal) +{ + unsigned int s[2], d[2]; + costmap_->worldToMap(start.pose.position.x, start.pose.position.y, s[0], s[1]); + costmap_->worldToMap(goal.pose.position.x, goal.pose.position.y, d[0], d[1]); + + src_ = {static_cast(s[0]), static_cast(s[1])}; + dst_ = {static_cast(d[0]), static_cast(d[1])}; +} + +bool ThetaStar::generatePath(std::vector & raw_path) +{ + resetContainers(); + addToNodesData(index_generated_); + double src_g_cost = getTraversalCost(src_.x, src_.y), src_h_cost = getHCost(src_.x, src_.y); + nodes_data_[index_generated_] = + {src_.x, src_.y, src_g_cost, src_h_cost, &nodes_data_[index_generated_], true, + src_g_cost + src_h_cost}; + queue_.push({&nodes_data_[index_generated_]}); + addIndex(src_.x, src_.y, &nodes_data_[index_generated_]); + tree_node * curr_data = &nodes_data_[index_generated_]; + index_generated_++; + nodes_opened = 0; + + while (!queue_.empty()) { + nodes_opened++; + + if (isGoal(*curr_data)) { + break; + } + + resetParent(curr_data); + setNeighbors(curr_data); + + curr_data = queue_.top(); + queue_.pop(); + } + + if (queue_.empty()) { + raw_path.clear(); + return false; + } + + backtrace(raw_path, curr_data); + clearQueue(); + + return true; +} + +void ThetaStar::resetParent(tree_node * curr_data) +{ + double g_cost, los_cost = 0; + curr_data->is_in_queue = false; + const tree_node * curr_par = curr_data->parent_id; + const tree_node * maybe_par = curr_par->parent_id; + + if (losCheck(curr_data->x, curr_data->y, maybe_par->x, maybe_par->y, los_cost)) { + g_cost = maybe_par->g + + getEuclideanCost(curr_data->x, curr_data->y, maybe_par->x, maybe_par->y) + los_cost; + + if (g_cost < curr_data->g) { + curr_data->parent_id = maybe_par; + curr_data->g = g_cost; + curr_data->f = g_cost + curr_data->h; + } + } +} + +void ThetaStar::setNeighbors(const tree_node * curr_data) +{ + int mx, my; + tree_node * m_id = nullptr; + double g_cost, h_cost, cal_cost; + + for (int i = 0; i < how_many_corners_; i++) { + mx = curr_data->x + moves[i].x; + my = curr_data->y + moves[i].y; + + if (withinLimits(mx, my)) { + if (!isSafe(mx, my)) { + continue; + } + } else { + continue; + } + + g_cost = curr_data->g + getEuclideanCost(curr_data->x, curr_data->y, mx, my) + + getTraversalCost(mx, my); + + m_id = getIndex(mx, my); + + if (m_id == nullptr) { + addToNodesData(index_generated_); + m_id = &nodes_data_[index_generated_]; + addIndex(mx, my, m_id); + index_generated_++; + } + + exp_node = m_id; + + h_cost = getHCost(mx, my); + cal_cost = g_cost + h_cost; + if (exp_node->f > cal_cost) { + exp_node->g = g_cost; + exp_node->h = h_cost; + exp_node->f = cal_cost; + exp_node->parent_id = curr_data; + if (!exp_node->is_in_queue) { + exp_node->x = mx; + exp_node->y = my; + exp_node->is_in_queue = true; + queue_.push({m_id}); + } + } + } +} + +void ThetaStar::backtrace(std::vector & raw_points, const tree_node * curr_n) const +{ + std::vector path_rev; + coordsW world{}; + do { + costmap_->mapToWorld(curr_n->x, curr_n->y, world.x, world.y); + path_rev.push_back(world); + if (path_rev.size() > 1) { + curr_n = curr_n->parent_id; + } + } while (curr_n->parent_id != curr_n); + costmap_->mapToWorld(curr_n->x, curr_n->y, world.x, world.y); + path_rev.push_back(world); + + raw_points.reserve(path_rev.size()); + for (int i = static_cast(path_rev.size()) - 1; i >= 0; i--) { + raw_points.push_back(path_rev[i]); + } +} + +bool ThetaStar::losCheck( + const int & x0, const int & y0, const int & x1, const int & y1, + double & sl_cost) const +{ + sl_cost = 0; + + int cx, cy; + int dy = abs(y1 - y0), dx = abs(x1 - x0), f = 0; + int sx, sy; + sx = x1 > x0 ? 1 : -1; + sy = y1 > y0 ? 1 : -1; + + int u_x = (sx - 1) / 2; + int u_y = (sy - 1) / 2; + cx = x0; + cy = y0; + + if (dx >= dy) { + while (cx != x1) { + f += dy; + if (f >= dx) { + if (!isSafe(cx + u_x, cy + u_y, sl_cost)) { + return false; + } + cy += sy; + f -= dx; + } + if (f != 0 && !isSafe(cx + u_x, cy + u_y, sl_cost)) { + return false; + } + if (dy == 0 && !isSafe(cx + u_x, cy, sl_cost) && !isSafe(cx + u_x, cy - 1, sl_cost)) { + return false; + } + cx += sx; + } + } else { + while (cy != y1) { + f = f + dx; + if (f >= dy) { + if (!isSafe(cx + u_x, cy + u_y, sl_cost)) { + return false; + } + cx += sx; + f -= dy; + } + if (f != 0 && !isSafe(cx + u_x, cy + u_y, sl_cost)) { + return false; + } + if (dx == 0 && !isSafe(cx, cy + u_y, sl_cost) && !isSafe(cx - 1, cy + u_y, sl_cost)) { + return false; + } + cy += sy; + } + } + return true; +} + +void ThetaStar::resetContainers() +{ + index_generated_ = 0; + int last_size_x = size_x_; + int last_size_y = size_y_; + int curr_size_x = static_cast(costmap_->getSizeInCellsX()); + int curr_size_y = static_cast(costmap_->getSizeInCellsY()); + if (((last_size_x != curr_size_x) || (last_size_y != curr_size_y)) && + static_cast(node_position_.size()) < (curr_size_x * curr_size_y)) + { + initializePosn(curr_size_y * curr_size_x - last_size_y * last_size_x); + nodes_data_.reserve(curr_size_x * curr_size_y); + } else { + initializePosn(); + } + size_x_ = curr_size_x; + size_y_ = curr_size_y; +} + +void ThetaStar::initializePosn(int size_inc) +{ + int i = 0; + + if (!node_position_.empty()) { + for (; i < size_x_ * size_y_; i++) { + node_position_[i] = nullptr; + } + } + + for (; i < size_inc; i++) { + node_position_.push_back(nullptr); + } +} +} // namespace theta_star diff --git a/nav2_theta_star_planner/src/theta_star_planner.cpp b/nav2_theta_star_planner/src/theta_star_planner.cpp new file mode 100644 index 0000000000..437cbd4471 --- /dev/null +++ b/nav2_theta_star_planner/src/theta_star_planner.cpp @@ -0,0 +1,142 @@ +// Copyright 2020 Anshumaan Singh +// +// 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 "nav2_theta_star_planner/theta_star_planner.hpp" +#include "nav2_theta_star_planner/theta_star.hpp" + +namespace nav2_theta_star_planner +{ +void ThetaStarPlanner::configure( + const rclcpp_lifecycle::LifecycleNode::WeakPtr & parent, + std::string name, std::shared_ptr tf, + std::shared_ptr costmap_ros) +{ + planner_ = std::make_unique(); + auto node = parent.lock(); + logger_ = node->get_logger(); + clock_ = node->get_clock(); + name_ = name; + tf_ = tf; + planner_->costmap_ = costmap_ros->getCostmap(); + global_frame_ = costmap_ros->getGlobalFrameID(); + + nav2_util::declare_parameter_if_not_declared( + node, name_ + ".how_many_corners", rclcpp::ParameterValue(8)); + + node->get_parameter(name_ + ".how_many_corners", planner_->how_many_corners_); + + if (planner_->how_many_corners_ != 8 && planner_->how_many_corners_ != 4) { + planner_->how_many_corners_ = 8; + RCLCPP_WARN(logger_, "Your value for - .how_many_corners was overridden, and is now set to 8"); + } + + nav2_util::declare_parameter_if_not_declared( + node, name_ + ".w_euc_cost", rclcpp::ParameterValue(1.0)); + node->get_parameter(name_ + ".w_euc_cost", planner_->w_euc_cost_); + + nav2_util::declare_parameter_if_not_declared( + node, name_ + ".w_traversal_cost", rclcpp::ParameterValue(2.0)); + node->get_parameter(name_ + ".w_traversal_cost", planner_->w_traversal_cost_); + + nav2_util::declare_parameter_if_not_declared( + node, name_ + ".w_heuristic_cost", rclcpp::ParameterValue(1.0)); + node->get_parameter(name_ + ".w_heuristic_cost", planner_->w_heuristic_cost_); +} + +void ThetaStarPlanner::cleanup() +{ + RCLCPP_INFO(logger_, "CleaningUp plugin %s of type nav2_theta_star_planner", name_.c_str()); + planner_.reset(); +} + +void ThetaStarPlanner::activate() +{ + RCLCPP_INFO(logger_, "Activating plugin %s of type nav2_theta_star_planner", name_.c_str()); +} + +void ThetaStarPlanner::deactivate() +{ + RCLCPP_INFO(logger_, "Deactivating plugin %s of type nav2_theta_star_planner", name_.c_str()); +} + +nav_msgs::msg::Path ThetaStarPlanner::createPlan( + const geometry_msgs::msg::PoseStamped & start, + const geometry_msgs::msg::PoseStamped & goal) +{ + nav_msgs::msg::Path global_path; + auto start_time = std::chrono::steady_clock::now(); + planner_->setStartAndGoal(start, goal); + RCLCPP_DEBUG( + logger_, "Got the src and dst... (%i, %i) && (%i, %i)", + planner_->src_.x, planner_->src_.y, planner_->dst_.x, planner_->dst_.y); + getPlan(global_path); + auto stop_time = std::chrono::steady_clock::now(); + auto dur = std::chrono::duration_cast(stop_time - start_time); + RCLCPP_DEBUG(logger_, "the time taken for pointy is : %i", static_cast(dur.count())); + RCLCPP_DEBUG(logger_, "the nodes_opened are: %i", planner_->nodes_opened); + return global_path; +} + +void ThetaStarPlanner::getPlan(nav_msgs::msg::Path & global_path) +{ + std::vector path; + if (planner_->isUnsafeToPlan()) { + RCLCPP_ERROR(logger_, "Either of the start or goal pose are an obstacle! "); + global_path.poses.clear(); + } else if (planner_->generatePath(path)) { + global_path = linearInterpolation(path, planner_->costmap_->getResolution()); + } else { + RCLCPP_ERROR(logger_, "Could not generate path between the given poses"); + global_path.poses.clear(); + } + global_path.header.stamp = clock_->now(); + global_path.header.frame_id = global_frame_; +} + +nav_msgs::msg::Path ThetaStarPlanner::linearInterpolation( + const std::vector & raw_path, + const double & dist_bw_points) +{ + nav_msgs::msg::Path pa; + + for (unsigned int j = 0; j < raw_path.size() - 1; j++) { + geometry_msgs::msg::PoseStamped p; + coordsW pt1 = raw_path[j]; + p.pose.position.x = pt1.x; + p.pose.position.y = pt1.y; + pa.poses.push_back(p); + + coordsW pt2 = raw_path[j + 1]; + geometry_msgs::msg::PoseStamped p1; + double distance = std::hypot(pt2.x - pt1.x, pt2.y - pt1.y); + int loops = static_cast(distance / dist_bw_points); + double sin_alpha = (pt2.y - pt1.y) / distance; + double cos_alpha = (pt2.x - pt1.x) / distance; + for (int k = 1; k < loops; k++) { + p1.pose.position.x = pt1.x + k * dist_bw_points * cos_alpha; + p1.pose.position.y = pt1.y + k * dist_bw_points * sin_alpha; + pa.poses.push_back(p1); + } + } + return pa; +} + + +} // namespace nav2_theta_star_planner + +#include "pluginlib/class_list_macros.hpp" +PLUGINLIB_EXPORT_CLASS(nav2_theta_star_planner::ThetaStarPlanner, nav2_core::GlobalPlanner) diff --git a/nav2_theta_star_planner/test/test_theta_star.cpp b/nav2_theta_star_planner/test/test_theta_star.cpp new file mode 100644 index 0000000000..3deb02a4cc --- /dev/null +++ b/nav2_theta_star_planner/test/test_theta_star.cpp @@ -0,0 +1,176 @@ +// Copyright 2020 Anshumaan Singh +// +// 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 "rclcpp/rclcpp.hpp" +#include "nav2_theta_star_planner/theta_star.hpp" +#include "nav2_theta_star_planner/theta_star_planner.hpp" + +class init_rclcpp +{ +public: + init_rclcpp() {rclcpp::init(0, nullptr);} + ~init_rclcpp() {rclcpp::shutdown();} +}; + +/// class created to access the protected members of the ThetaStar class +/// u is used as shorthand for use +class test_theta_star : public theta_star::ThetaStar +{ +public: + int getSizeOfNodePosition() + { + return static_cast(node_position_.size()); + } + + bool ulosCheck(const int & x0, const int & y0, const int & x1, const int & y1, double & sl_cost) + { + return losCheck(x0, y0, x1, y1, sl_cost); + } + + bool uwithinLimits(const int & cx, const int & cy) {return withinLimits(cx, cy);} + + bool uisGoal(const tree_node & this_node) {return isGoal(this_node);} + + void uinitializePosn(int size_inc = 0) + { + node_position_.reserve(size_x_ * size_y_); initializePosn(size_inc); + } + + void uaddIndex(const int & cx, const int & cy) {addIndex(cx, cy, &nodes_data_[0]);} + + tree_node * ugetIndex(const int & cx, const int & cy) {return getIndex(cx, cy);} + + tree_node * test_getIndex() {return &nodes_data_[0];} + + void uaddToNodesData(const int & id) {addToNodesData(id);} + + void uresetContainers() {nodes_data_.clear(); resetContainers();} + + bool runAlgo(std::vector & path) + { + if (!isUnsafeToPlan()) { + return generatePath(path); + } + return false; + } +}; + +init_rclcpp node; + +// Tests meant to test the algorithm itself and its helper functions +TEST(ThetaStarTest, test_theta_star) { + auto planner_ = std::make_unique(); + planner_->costmap_ = new nav2_costmap_2d::Costmap2D(50, 50, 1.0, 0.0, 0.0, 0); + for (int i = 7; i <= 14; i++) { + for (int j = 7; j <= 14; j++) { + planner_->costmap_->setCost(i, j, 253); + } + } + coordsM s = {5, 5}, g = {18, 18}; + int size_x = 20, size_y = 20; + planner_->size_x_ = size_x; + planner_->size_y_ = size_y; + geometry_msgs::msg::PoseStamped start, goal; + start.pose.position.x = s.x; + start.pose.position.y = s.y; + start.pose.orientation.w = 1.0; + goal.pose.position.x = g.x; + goal.pose.position.y = g.y; + goal.pose.orientation.w = 1.0; + /// Check if the setStartAndGoal function works properly + planner_->setStartAndGoal(start, goal); + EXPECT_TRUE(planner_->src_.x == s.x && planner_->src_.y == s.y); + EXPECT_TRUE(planner_->dst_.x == g.x && planner_->dst_.y == g.y); + + /// Check if the initializePosn function works properly + planner_->uinitializePosn(size_x * size_y); + EXPECT_EQ(planner_->getSizeOfNodePosition(), (size_x * size_y)); + + /// Check if the withinLimits function works properly + EXPECT_TRUE(planner_->uwithinLimits(18, 18)); + EXPECT_FALSE(planner_->uwithinLimits(120, 140)); + + tree_node n = {g.x, g.y, 120, 0, NULL, false, 20}; + n.parent_id = &n; + /// Check if the isGoal function works properly + EXPECT_TRUE(planner_->uisGoal(n)); // both (x,y) are the goal coordinates + n.x = 25; + EXPECT_FALSE(planner_->uisGoal(n)); // only y coordinate matches with that of goal + n.x = g.x; + n.y = 20; + EXPECT_FALSE(planner_->uisGoal(n)); // only x coordinate matches with that of goal + n.x = 30; + EXPECT_FALSE(planner_->uisGoal(n)); // both (x, y) are different from the goal coordinate + + /// Check if the isSafe functions work properly + EXPECT_TRUE(planner_->isSafe(5, 5)); // cost at this point is 0 + EXPECT_FALSE(planner_->isSafe(10, 10)); // cost at this point is 253 (>LETHAL_COST) + + /// Check if the functions addIndex & getIndex work properly + coordsM c = {20, 30}; + planner_->uaddToNodesData(0); + planner_->uaddIndex(c.x, c.y); + tree_node * c_node = planner_->ugetIndex(c.x, c.y); + EXPECT_EQ(c_node, planner_->test_getIndex()); + + double sl_cost = 0.0; + /// Checking for the case where the losCheck should return the value as true + EXPECT_TRUE(planner_->ulosCheck(2, 2, 7, 20, sl_cost)); + /// and as false + EXPECT_FALSE(planner_->ulosCheck(2, 2, 18, 18, sl_cost)); + + planner_->uresetContainers(); + std::vector path; + /// Check if the planner returns a path for the case where a path exists + EXPECT_TRUE(planner_->runAlgo(path)); + EXPECT_GT(static_cast(path.size()), 0); + /// and where it doesn't exist + path.clear(); + planner_->src_ = {10, 10}; + EXPECT_FALSE(planner_->runAlgo(path)); + EXPECT_EQ(static_cast(path.size()), 0); +} + +// Smoke tests meant to detect issues arising from the plugin part rather than the algorithm +TEST(ThetaStarPlanner, test_theta_star_planner) { + rclcpp_lifecycle::LifecycleNode::SharedPtr life_node = + std::make_shared("ThetaStarPlannerTest"); + + std::shared_ptr costmap_ros = + std::make_shared("global_costmap"); + costmap_ros->on_configure(rclcpp_lifecycle::State()); + + geometry_msgs::msg::PoseStamped start, goal; + start.pose.position.x = 0.0; + start.pose.position.y = 0.0; + start.pose.orientation.w = 1.0; + goal = start; + auto planner_2d = std::make_unique(); + planner_2d->configure(life_node, "test", nullptr, costmap_ros); + planner_2d->activate(); + + nav_msgs::msg::Path path = planner_2d->createPlan(start, goal); + EXPECT_GT(static_cast(path.poses.size()), 0); + + planner_2d->deactivate(); + planner_2d->cleanup(); + + planner_2d.reset(); + costmap_ros->on_cleanup(rclcpp_lifecycle::State()); + life_node.reset(); + costmap_ros.reset(); +} diff --git a/nav2_theta_star_planner/theta_star_planner.xml b/nav2_theta_star_planner/theta_star_planner.xml new file mode 100644 index 0000000000..1890ab14c3 --- /dev/null +++ b/nav2_theta_star_planner/theta_star_planner.xml @@ -0,0 +1,5 @@ + + + An implementation of the Theta* Algorithm + +