Files
agv_pro_ros2/navigation2/nav2_smac_planner/src/node_lattice.cpp
T
2025-05-27 19:03:40 +08:00

592 lines
22 KiB
C++

// Copyright (c) 2021, 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. Reserved.
#include <math.h>
#include <chrono>
#include <vector>
#include <memory>
#include <algorithm>
#include <queue>
#include <limits>
#include <string>
#include <fstream>
#include <cmath>
#include "ompl/base/ScopedState.h"
#include "ompl/base/spaces/DubinsStateSpace.h"
#include "ompl/base/spaces/ReedsSheppStateSpace.h"
#include "nav2_smac_planner/node_lattice.hpp"
using namespace std::chrono; // NOLINT
namespace nav2_smac_planner
{
// defining static member for all instance to share
LatticeMotionTable NodeLattice::motion_table;
float NodeLattice::size_lookup = 25;
LookupTable NodeLattice::dist_heuristic_lookup_table;
// Each of these tables are the projected motion models through
// time and space applied to the search on the current node in
// continuous map-coordinates (e.g. not meters but partial map cells)
// Currently, these are set to project *at minimum* into a neighboring
// cell. Though this could be later modified to project a certain
// amount of time or particular distance forward.
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;
// Get the metadata about this minimum control set
lattice_metadata = getLatticeMetadata(current_lattice_filepath);
std::ifstream latticeFile(current_lattice_filepath);
if (!latticeFile.is_open()) {
throw std::runtime_error("Could not open lattice file");
}
nlohmann::json json;
latticeFile >> json;
num_angle_quantization = lattice_metadata.number_of_headings;
if (!state_space) {
if (!allow_reverse_expansion) {
state_space = std::make_unique<ompl::base::DubinsStateSpace>(
lattice_metadata.min_turning_radius);
} else {
state_space = std::make_unique<ompl::base::ReedsSheppStateSpace>(
lattice_metadata.min_turning_radius);
}
}
// Populate the motion primitives at each heading angle
float prev_start_angle = 0.0;
std::vector<MotionPrimitive> primitives;
nlohmann::json json_primitives = json["primitives"];
for (unsigned int i = 0; i < json_primitives.size(); ++i) {
MotionPrimitive new_primitive;
fromJsonToMotionPrimitive(json_primitives[i], new_primitive);
if (prev_start_angle != new_primitive.start_angle) {
motion_primitives.push_back(primitives);
primitives.clear();
prev_start_angle = new_primitive.start_angle;
}
primitives.push_back(new_primitive);
}
motion_primitives.push_back(primitives);
// Populate useful precomputed values to be leveraged
trig_values.reserve(lattice_metadata.number_of_headings);
for (unsigned int i = 0; i < lattice_metadata.heading_angles.size(); ++i) {
trig_values.emplace_back(
cos(lattice_metadata.heading_angles[i]),
sin(lattice_metadata.heading_angles[i]));
}
}
MotionPrimitivePtrs LatticeMotionTable::getMotionPrimitives(const NodeLattice * node)
{
MotionPrimitives & prims_at_heading = motion_primitives[node->pose.theta];
MotionPrimitivePtrs primitive_projection_list;
for (unsigned int i = 0; i != prims_at_heading.size(); i++) {
primitive_projection_list.push_back(&prims_at_heading[i]);
}
if (allow_reverse_expansion) {
// Find normalized heading bin of the reverse expansion
double reserve_heading = node->pose.theta - (num_angle_quantization / 2);
if (reserve_heading < 0) {
reserve_heading += num_angle_quantization;
}
if (reserve_heading > num_angle_quantization) {
reserve_heading -= num_angle_quantization;
}
MotionPrimitives & prims_at_reverse_heading = motion_primitives[reserve_heading];
for (unsigned int i = 0; i != prims_at_reverse_heading.size(); i++) {
primitive_projection_list.push_back(&prims_at_reverse_heading[i]);
}
}
return primitive_projection_list;
}
LatticeMetadata LatticeMotionTable::getLatticeMetadata(const std::string & lattice_filepath)
{
std::ifstream lattice_file(lattice_filepath);
if (!lattice_file.is_open()) {
throw std::runtime_error("Could not open lattice file!");
}
nlohmann::json j;
lattice_file >> j;
LatticeMetadata metadata;
fromJsonToMetaData(j["lattice_metadata"], metadata);
return metadata;
}
unsigned int LatticeMotionTable::getClosestAngularBin(const double & theta)
{
float min_dist = std::numeric_limits<float>::max();
unsigned int closest_idx = 0;
float dist = 0.0;
for (unsigned int i = 0; i != lattice_metadata.heading_angles.size(); i++) {
dist = fabs(angles::shortest_angular_distance(theta, lattice_metadata.heading_angles[i]));
if (dist < min_dist) {
min_dist = dist;
closest_idx = i;
}
}
return closest_idx;
}
float & LatticeMotionTable::getAngleFromBin(const unsigned int & bin_idx)
{
return lattice_metadata.heading_angles[bin_idx];
}
NodeLattice::NodeLattice(const unsigned int index)
: parent(nullptr),
pose(0.0f, 0.0f, 0.0f),
_cell_cost(std::numeric_limits<float>::quiet_NaN()),
_accumulated_cost(std::numeric_limits<float>::max()),
_index(index),
_was_visited(false),
_motion_primitive(nullptr),
_backwards(false)
{
}
NodeLattice::~NodeLattice()
{
parent = nullptr;
}
void NodeLattice::reset()
{
parent = nullptr;
_cell_cost = std::numeric_limits<float>::quiet_NaN();
_accumulated_cost = std::numeric_limits<float>::max();
_was_visited = false;
pose.x = 0.0f;
pose.y = 0.0f;
pose.theta = 0.0f;
_motion_primitive = nullptr;
_backwards = false;
}
bool NodeLattice::isNodeValid(
const bool & traverse_unknown,
GridCollisionChecker * collision_checker,
MotionPrimitive * motion_primitive,
bool is_backwards)
{
// 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;
if (collision_checker->inCollision(
this->pose.x, this->pose.y, angle /*bin in collision checker*/, traverse_unknown))
{
return false;
}
// Set the cost of a node to the highest cost across the primitive
float max_cell_cost = collision_checker->getCost();
// If valid motion primitives are set, check intermediary poses > 1 cell apart
if (motion_primitive) {
const float & grid_resolution = motion_table.lattice_metadata.grid_resolution;
const float & resolution_diag_sq = 2.0 * grid_resolution * grid_resolution;
MotionPose last_pose(1e9, 1e9, 1e9), pose_dist(0.0, 0.0, 0.0);
// Back out the initial node starting point to move motion primitive relative to
MotionPose initial_pose, prim_pose;
initial_pose._x = this->pose.x - (motion_primitive->poses.back()._x / grid_resolution);
initial_pose._y = this->pose.y - (motion_primitive->poses.back()._y / grid_resolution);
initial_pose._theta = motion_table.getAngleFromBin(motion_primitive->start_angle);
for (auto it = motion_primitive->poses.begin(); it != motion_primitive->poses.end(); ++it) {
// poses are in metric coordinates from (0, 0), not grid space yet
pose_dist = *it - last_pose;
// Avoid square roots by (hypot(x, y) > res) == (x*x+y*y > diag*diag)
if (pose_dist._x * pose_dist._x + pose_dist._y * pose_dist._y > resolution_diag_sq) {
last_pose = *it;
// Convert primitive pose into grid space if it should be checked
prim_pose._x = initial_pose._x + (it->_x / grid_resolution);
prim_pose._y = initial_pose._y + (it->_y / grid_resolution);
// If reversing, invert the angle because the robot is backing into the primitive
// not driving forward with it
if (is_backwards) {
prim_pose._theta = std::fmod(it->_theta + M_PI, 2.0 * M_PI);
} else {
prim_pose._theta = it->_theta;
}
if (collision_checker->inCollision(
prim_pose._x,
prim_pose._y,
prim_pose._theta / bin_size /*bin in collision checker*/,
traverse_unknown))
{
return false;
}
max_cell_cost = std::max(max_cell_cost, collision_checker->getCost());
}
}
}
_cell_cost = max_cell_cost;
return true;
}
float NodeLattice::getTraversalCost(const NodePtr & child)
{
const float normalized_cost = child->getCost() / 252.0;
if (std::isnan(normalized_cost)) {
throw std::runtime_error(
"Node attempted to get traversal "
"cost without a known collision cost!");
}
// this is the first node
MotionPrimitive * prim = this->getMotionPrimitive();
MotionPrimitive * transition_prim = child->getMotionPrimitive();
const float prim_length =
transition_prim->trajectory_length / motion_table.lattice_metadata.grid_resolution;
if (prim == nullptr) {
return prim_length;
}
// Pure rotation in place 1 angular bin in either direction
if (transition_prim->trajectory_length < 1e-4) {
return motion_table.rotation_penalty * (1.0 + motion_table.cost_penalty * normalized_cost);
}
float travel_cost = 0.0;
float travel_cost_raw = prim_length *
(motion_table.travel_distance_reward + motion_table.cost_penalty * normalized_cost);
if (transition_prim->arc_length < 0.001) {
// New motion is a straight motion, no additional costs to be applied
travel_cost = travel_cost_raw;
} else {
if (prim->left_turn == transition_prim->left_turn) {
// Turning motion but keeps in same general direction: encourages to commit to actions
travel_cost = travel_cost_raw * motion_table.non_straight_penalty;
} else {
// Turning motion and velocity directions: penalizes wiggling.
travel_cost = travel_cost_raw *
(motion_table.non_straight_penalty + motion_table.change_penalty);
}
}
// If backwards flag is set, this primitive is moving in reverse
if (child->isBackward()) {
// reverse direction
travel_cost *= motion_table.reverse_penalty;
}
return travel_cost;
}
float NodeLattice::getHeuristicCost(
const Coordinates & node_coords,
const Coordinates & goal_coords,
const nav2_costmap_2d::Costmap2D * costmap)
{
// get obstacle heuristic value
const float obstacle_heuristic = getObstacleHeuristic(
node_coords, goal_coords, motion_table.cost_penalty);
const float distance_heuristic =
getDistanceHeuristic(node_coords, goal_coords, obstacle_heuristic);
return std::max(obstacle_heuristic, distance_heuristic);
}
void NodeLattice::initMotionModel(
const MotionModel & motion_model,
unsigned int & size_x,
unsigned int & /*size_y*/,
unsigned int & /*num_angle_quantization*/,
SearchInfo & search_info)
{
if (motion_model != MotionModel::STATE_LATTICE) {
throw std::runtime_error(
"Invalid motion model for Lattice node. Please select"
" STATE_LATTICE and provide a valid lattice file.");
}
motion_table.initMotionModel(size_x, search_info);
}
float NodeLattice::getDistanceHeuristic(
const Coordinates & node_coords,
const Coordinates & goal_coords,
const float & obstacle_heuristic)
{
// rotate and translate node_coords such that goal_coords relative is (0,0,0)
// Due to the rounding involved in exact cell increments for caching,
// this is not an exact replica of a live heuristic, but has bounded error.
// (Usually less than 1 cell length)
// This angle is negative since we are de-rotating the current node
// by the goal angle; cos(-th) = cos(th) & sin(-th) = -sin(th)
const TrigValues & trig_vals = motion_table.trig_values[goal_coords.theta];
const float cos_th = trig_vals.first;
const float sin_th = -trig_vals.second;
const float dx = node_coords.x - goal_coords.x;
const float dy = node_coords.y - goal_coords.y;
double dtheta_bin = node_coords.theta - goal_coords.theta;
if (dtheta_bin < 0) {
dtheta_bin += motion_table.num_angle_quantization;
}
if (dtheta_bin > motion_table.num_angle_quantization) {
dtheta_bin -= motion_table.num_angle_quantization;
}
Coordinates node_coords_relative(
round(dx * cos_th - dy * sin_th),
round(dx * sin_th + dy * cos_th),
round(dtheta_bin));
// Check if the relative node coordinate is within the localized window around the goal
// to apply the distance heuristic. Since the lookup table is contains only the positive
// X axis, we mirror the Y and theta values across the X axis to find the heuristic values.
float motion_heuristic = 0.0;
const int floored_size = floor(size_lookup / 2.0);
const int ceiling_size = ceil(size_lookup / 2.0);
const float mirrored_relative_y = abs(node_coords_relative.y);
if (abs(node_coords_relative.x) < floored_size && mirrored_relative_y < floored_size) {
// Need to mirror angle if Y coordinate was mirrored
int theta_pos;
if (node_coords_relative.y < 0.0) {
theta_pos = motion_table.num_angle_quantization - node_coords_relative.theta;
} else {
theta_pos = node_coords_relative.theta;
}
const int x_pos = node_coords_relative.x + floored_size;
const int y_pos = static_cast<int>(mirrored_relative_y);
const int index =
x_pos * ceiling_size * motion_table.num_angle_quantization +
y_pos * motion_table.num_angle_quantization +
theta_pos;
motion_heuristic = dist_heuristic_lookup_table[index];
} else if (obstacle_heuristic == 0.0) {
static ompl::base::ScopedState<> from(motion_table.state_space), to(motion_table.state_space);
to[0] = goal_coords.x;
to[1] = goal_coords.y;
to[2] = motion_table.getAngleFromBin(goal_coords.theta);
from[0] = node_coords.x;
from[1] = node_coords.y;
from[2] = motion_table.getAngleFromBin(node_coords.theta);
motion_heuristic = motion_table.state_space->distance(from(), to());
}
return motion_heuristic;
}
void NodeLattice::precomputeDistanceHeuristic(
const float & lookup_table_dim,
const MotionModel & motion_model,
const unsigned int & dim_3_size,
const SearchInfo & search_info)
{
// Dubin or Reeds-Shepp shortest distances
if (!search_info.allow_reverse_expansion) {
motion_table.state_space = std::make_unique<ompl::base::DubinsStateSpace>(
search_info.minimum_turning_radius);
} else {
motion_table.state_space = std::make_unique<ompl::base::ReedsSheppStateSpace>(
search_info.minimum_turning_radius);
}
motion_table.lattice_metadata =
LatticeMotionTable::getLatticeMetadata(search_info.lattice_filepath);
ompl::base::ScopedState<> from(motion_table.state_space), to(motion_table.state_space);
to[0] = 0.0;
to[1] = 0.0;
to[2] = 0.0;
size_lookup = lookup_table_dim;
float motion_heuristic = 0.0;
unsigned int index = 0;
int dim_3_size_int = static_cast<int>(dim_3_size);
// Create a lookup table of Dubin/Reeds-Shepp distances in a window around the goal
// to help drive the search towards admissible approaches. Deu to symmetries in the
// Heuristic space, we need to only store 2 of the 4 quadrants and simply mirror
// around the X axis any relative node lookup. This reduces memory overhead and increases
// the size of a window a platform can store in memory.
dist_heuristic_lookup_table.resize(size_lookup * ceil(size_lookup / 2.0) * dim_3_size_int);
for (float x = ceil(-size_lookup / 2.0); x <= floor(size_lookup / 2.0); x += 1.0) {
for (float y = 0.0; y <= floor(size_lookup / 2.0); y += 1.0) {
for (int heading = 0; heading != dim_3_size_int; heading++) {
from[0] = x;
from[1] = y;
from[2] = motion_table.getAngleFromBin(heading);
motion_heuristic = motion_table.state_space->distance(from(), to());
dist_heuristic_lookup_table[index] = motion_heuristic;
index++;
}
}
}
}
void NodeLattice::getNeighbors(
std::function<bool(const unsigned int &, nav2_smac_planner::NodeLattice * &)> & NeighborGetter,
GridCollisionChecker * collision_checker,
const bool & traverse_unknown,
NodeVector & neighbors)
{
unsigned int index = 0;
bool backwards = false;
NodePtr neighbor = nullptr;
Coordinates initial_node_coords, motion_projection;
MotionPrimitivePtrs motion_primitives = motion_table.getMotionPrimitives(this);
const float & grid_resolution = motion_table.lattice_metadata.grid_resolution;
unsigned int direction_change_idx = 1e9;
for (unsigned int i = 0; i != motion_primitives.size(); i++) {
if (motion_primitives[0]->start_angle != motion_primitives[i]->start_angle) {
direction_change_idx = i;
break;
}
}
for (unsigned int i = 0; i != motion_primitives.size(); i++) {
const MotionPose & end_pose = motion_primitives[i]->poses.back();
motion_projection.x = this->pose.x + (end_pose._x / grid_resolution);
motion_projection.y = this->pose.y + (end_pose._y / grid_resolution);
motion_projection.theta = motion_primitives[i]->end_angle /*this is the ending angular bin*/;
// if i >= idx, then we're in a reversing primitive. In that situation,
// the orientation of the robot is mirrored from what it would otherwise
// appear to be from the motion primitives file. We want to take this into
// account in case the robot base footprint is asymmetric.
backwards = false;
if (i >= direction_change_idx) {
backwards = true;
float opposite_heading_theta =
motion_projection.theta - (motion_table.num_angle_quantization / 2);
if (opposite_heading_theta < 0) {
opposite_heading_theta += motion_table.num_angle_quantization;
}
if (opposite_heading_theta > motion_table.num_angle_quantization) {
opposite_heading_theta -= motion_table.num_angle_quantization;
}
motion_projection.theta = opposite_heading_theta;
}
index = NodeLattice::getIndex(
static_cast<unsigned int>(motion_projection.x),
static_cast<unsigned int>(motion_projection.y),
static_cast<unsigned int>(motion_projection.theta));
if (NeighborGetter(index, neighbor) && !neighbor->wasVisited()) {
// Cache the initial pose in case it was visited but valid
// don't want to disrupt continuous coordinate expansion
initial_node_coords = neighbor->pose;
neighbor->setPose(
Coordinates(
motion_projection.x,
motion_projection.y,
motion_projection.theta));
// Using a special isNodeValid API here, giving the motion primitive to use to
// validity check the transition of the current node to the new node over
if (neighbor->isNodeValid(
traverse_unknown, collision_checker, motion_primitives[i], backwards))
{
neighbor->setMotionPrimitive(motion_primitives[i]);
// Marking if this search was obtained in the reverse direction
neighbor->backwards(backwards);
neighbors.push_back(neighbor);
} else {
neighbor->setPose(initial_node_coords);
}
}
}
}
bool NodeLattice::backtracePath(CoordinateVector & path)
{
if (!this->parent) {
return false;
}
NodePtr current_node = this;
while (current_node->parent) {
addNodeToPath(current_node, path);
current_node = current_node->parent;
}
// add start to path
addNodeToPath(current_node, path);
return true;
}
void NodeLattice::addNodeToPath(
NodeLattice::NodePtr current_node,
NodeLattice::CoordinateVector & path)
{
Coordinates initial_pose, prim_pose;
MotionPrimitive * prim = nullptr;
const float & grid_resolution = NodeLattice::motion_table.lattice_metadata.grid_resolution;
prim = current_node->getMotionPrimitive();
// if motion primitive is valid, then was searched (rather than analytically expanded),
// include dense path of subpoints making up the primitive at grid resolution
if (prim) {
initial_pose.x = current_node->pose.x - (prim->poses.back()._x / grid_resolution);
initial_pose.y = current_node->pose.y - (prim->poses.back()._y / grid_resolution);
initial_pose.theta = NodeLattice::motion_table.getAngleFromBin(prim->start_angle);
for (auto it = prim->poses.crbegin(); it != prim->poses.crend(); ++it) {
// Convert primitive pose into grid space if it should be checked
prim_pose.x = initial_pose.x + (it->_x / grid_resolution);
prim_pose.y = initial_pose.y + (it->_y / grid_resolution);
// If reversing, invert the angle because the robot is backing into the primitive
// not driving forward with it
if (current_node->isBackward()) {
prim_pose.theta = std::fmod(it->_theta + M_PI, 2.0 * M_PI);
} else {
prim_pose.theta = it->_theta;
}
path.push_back(prim_pose);
}
} else {
// For analytic expansion nodes where there is no valid motion primitive
path.push_back(current_node->pose);
path.back().theta = NodeLattice::motion_table.getAngleFromBin(path.back().theta);
}
}
} // namespace nav2_smac_planner