add humble-navigation2
This commit is contained in:
Executable
+197
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/python3
|
||||
# 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.
|
||||
|
||||
# This tool converts a behavior tree XML file to a PNG image. Run bt2img.py -h
|
||||
# for instructions
|
||||
|
||||
import argparse
|
||||
import xml.etree.ElementTree
|
||||
import graphviz # pip3 install graphviz
|
||||
|
||||
control_nodes = [
|
||||
"Fallback",
|
||||
"Parallel",
|
||||
"ReactiveFallback",
|
||||
"ReactiveSequence",
|
||||
"Sequence",
|
||||
"SequenceStar",
|
||||
"BlackboardCheckInt",
|
||||
"BlackboardCheckDouble",
|
||||
"BlackboardCheckString",
|
||||
"ForceFailure",
|
||||
"ForceSuccess",
|
||||
"Inverter",
|
||||
"Repeat",
|
||||
"Subtree",
|
||||
"Timeout",
|
||||
"RecoveryNode",
|
||||
"PipelineSequence",
|
||||
"RoundRobin",
|
||||
"Control",
|
||||
]
|
||||
action_nodes = [
|
||||
"AlwaysFailure",
|
||||
"AlwaysSuccess",
|
||||
"SetBlackboard",
|
||||
"ComputePathToPose",
|
||||
"FollowPath",
|
||||
"BackUp",
|
||||
"Spin",
|
||||
"Wait",
|
||||
"ClearEntireCostmap",
|
||||
"ReinitializeGlobalLocalization",
|
||||
"Action",
|
||||
]
|
||||
condition_nodes = [
|
||||
"IsStuck",
|
||||
"GoalReached",
|
||||
"initialPoseReceived",
|
||||
"GoalUpdated",
|
||||
"DistanceTraveled",
|
||||
"TimeExpired",
|
||||
"TransformAvailable",
|
||||
"Condition",
|
||||
]
|
||||
decorator_nodes = [
|
||||
"Decorator",
|
||||
"RateController",
|
||||
"DistanceController",
|
||||
"SpeedController",
|
||||
]
|
||||
subtree_nodes = [
|
||||
"SubTree",
|
||||
]
|
||||
|
||||
global xml_tree
|
||||
|
||||
def main():
|
||||
global xml_tree
|
||||
args = parse_command_line()
|
||||
xml_tree = xml.etree.ElementTree.parse(args.behavior_tree)
|
||||
root_tree_name = find_root_tree_name(xml_tree)
|
||||
behavior_tree = find_behavior_tree(xml_tree, root_tree_name)
|
||||
dot = convert2dot(behavior_tree)
|
||||
if args.legend:
|
||||
legend = make_legend()
|
||||
legend.format = 'png'
|
||||
legend.render(args.legend)
|
||||
dot.format = 'png'
|
||||
if args.save_dot:
|
||||
print(f'Saving dot to {args.save_dot}')
|
||||
args.save_dot.write(dot.source)
|
||||
dot.render(args.image_out, view=args.display)
|
||||
|
||||
def parse_command_line():
|
||||
parser = argparse.ArgumentParser(description='Convert a behavior tree XML file to an image')
|
||||
parser.add_argument('--behavior_tree', required=True,
|
||||
help='the behavior tree XML file to convert to an image')
|
||||
parser.add_argument('--image_out', required=True,
|
||||
help='The name of the output image file. Leave off the .png extension')
|
||||
parser.add_argument('--display', action="store_true",
|
||||
help='If specified, opens the image in the default viewer')
|
||||
parser.add_argument('--save_dot', type=argparse.FileType('w'),
|
||||
help='Saves the intermediate dot source to the specified file')
|
||||
parser.add_argument('--legend',
|
||||
help='Generate a legend image as well')
|
||||
return parser.parse_args()
|
||||
|
||||
def find_root_tree_name(xml_tree):
|
||||
return xml_tree.getroot().get('main_tree_to_execute')
|
||||
|
||||
def find_behavior_tree(xml_tree, tree_name):
|
||||
trees = xml_tree.findall('BehaviorTree')
|
||||
if len(trees) == 0:
|
||||
raise RuntimeError("No behavior trees were found in the XML file")
|
||||
|
||||
for tree in trees:
|
||||
if tree_name == tree.get('ID'):
|
||||
return tree
|
||||
|
||||
raise RuntimeError(f'No behavior tree for name {tree_name} found in the XML file')
|
||||
|
||||
# Generate a dot description of the root of the behavior tree.
|
||||
def convert2dot(behavior_tree):
|
||||
dot = graphviz.Digraph()
|
||||
root = behavior_tree
|
||||
parent_dot_name = str(hash(root))
|
||||
dot.node(parent_dot_name, root.get('ID'), shape='box')
|
||||
convert_subtree(dot, root, parent_dot_name)
|
||||
return dot
|
||||
|
||||
# Recursive function. We add the children to the dot file, and then recursively
|
||||
# call this function on the children. Nodes are given an ID that is the hash
|
||||
# of the node to ensure each is unique.
|
||||
def convert_subtree(dot, parent_node, parent_dot_name):
|
||||
if parent_node.tag == "SubTree":
|
||||
add_sub_tree(dot, parent_dot_name, parent_node)
|
||||
else:
|
||||
add_nodes(dot, parent_dot_name, parent_node)
|
||||
|
||||
def add_sub_tree(dot, parent_dot_name, parent_node):
|
||||
root_tree_name = parent_node.get('ID')
|
||||
dot.node(parent_dot_name, root_tree_name, shape='box')
|
||||
behavior_tree = find_behavior_tree(xml_tree, root_tree_name)
|
||||
convert_subtree(dot, behavior_tree, parent_dot_name)
|
||||
|
||||
def add_nodes(dot, parent_dot_name, parent_node):
|
||||
for node in list(parent_node):
|
||||
label = make_label(node)
|
||||
dot.node(str(hash(node)), label, color=node_color(node.tag), style='filled', shape='box')
|
||||
dot_name = str(hash(node))
|
||||
dot.edge(parent_dot_name, dot_name)
|
||||
convert_subtree(dot, node, dot_name)
|
||||
|
||||
# The node label contains the:
|
||||
# type, the name if provided, and the parameters.
|
||||
def make_label(node):
|
||||
label = '< <table border="0" cellspacing="0" cellpadding="0">'
|
||||
label += '<tr><td align="text"><i>' + node.tag + '</i></td></tr>'
|
||||
name = node.get('name')
|
||||
if name:
|
||||
label += '<tr><td align="text"><b>' + name + '</b></td></tr>'
|
||||
|
||||
for (param_name, value) in node.items():
|
||||
label += '<tr><td align="left"><sub>' + param_name + '=' + value + '</sub></td></tr>'
|
||||
label += '</table> >'
|
||||
return label
|
||||
|
||||
def node_color(type):
|
||||
if type in control_nodes:
|
||||
return "chartreuse4"
|
||||
if type in action_nodes:
|
||||
return "cornflowerblue"
|
||||
if type in condition_nodes:
|
||||
return "yellow2"
|
||||
if type in decorator_nodes:
|
||||
return "darkorange1"
|
||||
if type in subtree_nodes:
|
||||
return "darkorchid1"
|
||||
#else it's unknown
|
||||
return "grey"
|
||||
|
||||
# creates a legend which can be provided with the other images.
|
||||
def make_legend():
|
||||
legend = graphviz.Digraph(graph_attr={'rankdir': 'LR'})
|
||||
legend.attr(label='Legend')
|
||||
legend.node('Unknown', shape='box', style='filled', color="grey")
|
||||
legend.node('Action', 'Action Node', shape='box', style='filled', color="cornflowerblue")
|
||||
legend.node('Condition', 'Condition Node', shape='box', style='filled', color="yellow2")
|
||||
legend.node('Control', 'Control Node', shape='box', style='filled', color="chartreuse4")
|
||||
|
||||
return legend
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ ! -d build ]; then
|
||||
echo "Please run this script from the root of your workspace."
|
||||
echo "Expected directory hierarchy is:"
|
||||
echo "example_ws"
|
||||
echo " - build"
|
||||
echo " - - package_a"
|
||||
echo " - - package_b"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
LCOVDIR=lcov
|
||||
PWD=`pwd`
|
||||
|
||||
COVERAGE_REPORT_VIEW="genhtml"
|
||||
|
||||
for opt in "$@" ; do
|
||||
case "$opt" in
|
||||
clean)
|
||||
rm -rf install build log $LCOVDIR
|
||||
exit 0
|
||||
;;
|
||||
codecovio)
|
||||
COVERAGE_REPORT_VIEW=codecovio
|
||||
;;
|
||||
genhtml)
|
||||
COVERAGE_REPORT_VIEW=genhtml
|
||||
;;
|
||||
ci)
|
||||
COVERAGE_REPORT_VIEW=ci
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
set -o xtrace
|
||||
mkdir -p ${LCOVDIR}
|
||||
|
||||
# Ignore certain packages:
|
||||
# - messages, which are auto generated files
|
||||
# - system tests, which are themselves all test artifacts
|
||||
# - rviz plugins, which are not used for real navigation
|
||||
EXCLUDE_PACKAGES=$(
|
||||
colcon list \
|
||||
--names-only \
|
||||
--packages-select-regex \
|
||||
".*_msgs" \
|
||||
".*_tests" \
|
||||
".*_rviz.*" \
|
||||
| xargs)
|
||||
INCLUDE_PACKAGES=$(
|
||||
colcon list \
|
||||
--names-only \
|
||||
--packages-ignore \
|
||||
$EXCLUDE_PACKAGES \
|
||||
| xargs)
|
||||
|
||||
# Capture executed code data.
|
||||
fastcov --lcov \
|
||||
-d build \
|
||||
--exclude test/ $EXCLUDE_PACKAGES thirdparty/ \
|
||||
--include $INCLUDE_PACKAGES \
|
||||
--process-gcno \
|
||||
--validate-sources \
|
||||
--dump-statistic \
|
||||
--output ${LCOVDIR}/total_coverage.info
|
||||
|
||||
if [ $COVERAGE_REPORT_VIEW = codecovio ]; then
|
||||
curl -s https://codecov.io/bash > codecov
|
||||
codecov_version=$(grep -o 'VERSION=\"[0-9\.]*\"' codecov | cut -d'"' -f2)
|
||||
shasum -a 512 -c <(curl -s "https://raw.githubusercontent.com/codecov/codecov-bash/${codecov_version}/SHA512SUM" | grep -w "codecov")
|
||||
bash codecov \
|
||||
-f ${LCOVDIR}/total_coverage.info \
|
||||
-R src/navigation2
|
||||
elif [ $COVERAGE_REPORT_VIEW = genhtml ]; then
|
||||
genhtml ${LCOVDIR}/total_coverage.info \
|
||||
--output-directory ${LCOVDIR}/html
|
||||
fi
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/bin/bash
|
||||
|
||||
usage() {
|
||||
echo "ctest_retry.bash [options]"
|
||||
echo "Reruns ctest until the test passes or it has run 3 times (by default)."
|
||||
echo " -r <number> Retry the test up to <number> times [Default: 3]"
|
||||
echo " -d <path> Execute ctest from the given path [Default: Current working directory]"
|
||||
echo " -t <testname> Execute only <testname> [Default: execute all tests available in <path> ]"
|
||||
echo " -h displays this usage summary"
|
||||
exit 0
|
||||
}
|
||||
|
||||
RETRIES=3
|
||||
TESTDIR=.
|
||||
SPECIFIC_TEST=""
|
||||
|
||||
# Check Options
|
||||
while getopts ":hr:d:t:" opt; do
|
||||
case "$opt" in
|
||||
h)
|
||||
usage
|
||||
;;
|
||||
r)
|
||||
RETRIES=$OPTARG
|
||||
;;
|
||||
d)
|
||||
TESTDIR=$OPTARG
|
||||
;;
|
||||
t)
|
||||
SPECIFIC_TEST="-R $OPTARG"
|
||||
;;
|
||||
\?)
|
||||
echo "Invalid option: -$OPTARG" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
:)
|
||||
echo "Option -$OPTARG requires an argument." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
total=$RETRIES
|
||||
cd $TESTDIR
|
||||
export RCUTILS_LOGGING_BUFFERED_STREAM=1
|
||||
export RCUTILS_LOGGING_USE_STDOUT=1
|
||||
|
||||
echo "Retrying Ctest up to " $total " times."
|
||||
for ((i=1;i<=total;i++))
|
||||
do
|
||||
ctest -V $SPECIFIC_TEST
|
||||
result=$?
|
||||
if [ "$result" == "0" ] # if ctest succeeded, then exit the retry loop
|
||||
then
|
||||
echo "Test succeeded on try " $i
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
echo "Test failed " $total " times."
|
||||
exit $result
|
||||
@@ -0,0 +1,101 @@
|
||||
# syntax=docker/dockerfile:experimental
|
||||
|
||||
# Use experimental buildkit for faster builds
|
||||
# https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/experimental.md
|
||||
# Use `--progress=plain` to use plane stdout for docker build
|
||||
#
|
||||
# Example build command:
|
||||
# export DOCKER_BUILDKIT=1
|
||||
# export FROM_IMAGE="ros:humble"
|
||||
# export OVERLAY_MIXINS="release ccache lld"
|
||||
# docker build -t nav2:humble \
|
||||
# --build-arg FROM_IMAGE \
|
||||
# --build-arg OVERLAY_MIXINS \
|
||||
# -f distro.Dockerfile ../
|
||||
|
||||
ARG FROM_IMAGE=ros:humble
|
||||
ARG OVERLAY_WS=/opt/overlay_ws
|
||||
|
||||
# multi-stage for caching
|
||||
FROM $FROM_IMAGE AS cacher
|
||||
|
||||
# copy overlay source
|
||||
ARG OVERLAY_WS
|
||||
WORKDIR $OVERLAY_WS/src
|
||||
RUN echo "\
|
||||
repositories: \n\
|
||||
ros-planning/navigation2: \n\
|
||||
type: git \n\
|
||||
url: https://github.com/ros-planning/navigation2.git \n\
|
||||
version: ${ROS_DISTRO}-devel \n\
|
||||
" > ../overlay.repos
|
||||
RUN vcs import ./ < ../overlay.repos && \
|
||||
find ./ -name ".git" | xargs rm -rf
|
||||
# COPY ./ ./ros-planning/navigation2
|
||||
|
||||
# copy manifests for caching
|
||||
WORKDIR /opt
|
||||
RUN mkdir -p /tmp/opt && \
|
||||
find ./ -name "package.xml" | \
|
||||
xargs cp --parents -t /tmp/opt && \
|
||||
find ./ -name "COLCON_IGNORE" | \
|
||||
xargs cp --parents -t /tmp/opt || true
|
||||
|
||||
# multi-stage for building
|
||||
FROM $FROM_IMAGE AS builder
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# edit apt for caching
|
||||
RUN cp /etc/apt/apt.conf.d/docker-clean /etc/apt/ && \
|
||||
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' \
|
||||
> /etc/apt/apt.conf.d/docker-clean
|
||||
|
||||
# install CI dependencies
|
||||
RUN --mount=type=cache,target=/var/cache/apt \
|
||||
--mount=type=cache,target=/var/lib/apt \
|
||||
apt-get update && apt-get install -q -y \
|
||||
ccache \
|
||||
lcov \
|
||||
lld \
|
||||
&& rosdep update
|
||||
|
||||
# install overlay dependencies
|
||||
ARG OVERLAY_WS
|
||||
WORKDIR $OVERLAY_WS
|
||||
COPY --from=cacher /tmp/$OVERLAY_WS/src ./src
|
||||
RUN --mount=type=cache,target=/var/cache/apt \
|
||||
--mount=type=cache,target=/var/lib/apt \
|
||||
. /opt/ros/$ROS_DISTRO/setup.sh && \
|
||||
apt-get update && rosdep install -q -y \
|
||||
--from-paths src \
|
||||
--ignore-src \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# build overlay source
|
||||
COPY --from=cacher $OVERLAY_WS/src ./src
|
||||
ARG OVERLAY_MIXINS="release ccache lld"
|
||||
RUN --mount=type=cache,target=/root/.ccache \
|
||||
. /opt/ros/$ROS_DISTRO/setup.sh && \
|
||||
colcon build \
|
||||
--symlink-install \
|
||||
--mixin $OVERLAY_MIXINS
|
||||
|
||||
# restore apt for docker
|
||||
RUN mv /etc/apt/docker-clean /etc/apt/apt.conf.d/ && \
|
||||
rm -rf /var/lib/apt/lists/
|
||||
|
||||
# source overlay from entrypoint
|
||||
ENV OVERLAY_WS $OVERLAY_WS
|
||||
RUN sed --in-place \
|
||||
's|^source .*|source "$OVERLAY_WS/install/setup.bash"|' \
|
||||
/ros_entrypoint.sh
|
||||
|
||||
# test overlay build
|
||||
ARG RUN_TESTS
|
||||
ARG FAIL_ON_TEST_FAILURE
|
||||
RUN if [ -n "$RUN_TESTS" ]; then \
|
||||
. install/setup.sh && \
|
||||
colcon test && \
|
||||
colcon test-result \
|
||||
|| ([ -z "$FAIL_ON_TEST_FAILURE" ] || exit 1) \
|
||||
fi
|
||||
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
image: 100by100_10.pgm
|
||||
resolution: 0.05
|
||||
origin: [0.0, 0.000000, 0.000000]
|
||||
negate: 1
|
||||
occupied_thresh: 0.65
|
||||
free_thresh: 0.196
|
||||
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
image: 100by100_15.pgm
|
||||
resolution: 0.05
|
||||
origin: [0.0, 0.000000, 0.000000]
|
||||
negate: 1
|
||||
occupied_thresh: 0.65
|
||||
free_thresh: 0.196
|
||||
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
image: 100by100_20.pgm
|
||||
resolution: 0.05
|
||||
origin: [0.0, 0.000000, 0.000000]
|
||||
negate: 1
|
||||
occupied_thresh: 0.65
|
||||
free_thresh: 0.196
|
||||
@@ -0,0 +1,30 @@
|
||||
# Planning Benchmark
|
||||
|
||||
This experiment runs a set of planners over randomly generated maps, with randomly generated goals for objective benchmarking.
|
||||
|
||||
To use, modify the Nav2 bringup parameters to include the planners of interest:
|
||||
|
||||
```
|
||||
planner_server:
|
||||
ros__parameters:
|
||||
expected_planner_frequency: 20.0
|
||||
use_sim_time: True
|
||||
planner_plugins: ["SmacHybrid", "Smac2d", "SmacLattice", "Navfn", "ThetaStar"]
|
||||
SmacHybrid:
|
||||
plugin: "nav2_smac_planner/SmacPlannerHybrid"
|
||||
Smac2d:
|
||||
plugin: "nav2_smac_planner/SmacPlanner2D"
|
||||
SmacLattice:
|
||||
plugin: "nav2_smac_planner/SmacPlannerLattice"
|
||||
Navfn:
|
||||
plugin: "nav2_navfn_planner/NavfnPlanner"
|
||||
ThetaStar:
|
||||
plugin: "nav2_theta_star_planner/ThetaStarPlanner"
|
||||
```
|
||||
|
||||
Set global costmap settings to those desired for benchmarking. The global map will be automatically set in the script. Inside of `metrics.py`, you can modify the map or set of planners to use.
|
||||
|
||||
Launch the benchmark via `ros2 launch ./planning_benchmark_bringup.py` to launch the planner and map servers, then run each script in this directory:
|
||||
|
||||
- `metrics.py` to capture data in `.pickle` files.
|
||||
- `process_data.py` to take the metric files and process them into key results (and plots)
|
||||
@@ -0,0 +1,150 @@
|
||||
#! /usr/bin/env python3
|
||||
# Copyright 2022 Joshua Wallace
|
||||
#
|
||||
# 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.
|
||||
|
||||
from geometry_msgs.msg import PoseStamped
|
||||
from nav2_simple_commander.robot_navigator import BasicNavigator
|
||||
import rclpy
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
|
||||
import math
|
||||
import os
|
||||
import pickle
|
||||
import glob
|
||||
import time
|
||||
import numpy as np
|
||||
|
||||
from random import seed
|
||||
from random import randint
|
||||
from random import uniform
|
||||
|
||||
from transforms3d.euler import euler2quat
|
||||
|
||||
|
||||
def getPlannerResults(navigator, initial_pose, goal_pose, planners):
|
||||
results = []
|
||||
for planner in planners:
|
||||
path = navigator._getPathImpl(initial_pose, goal_pose, planner, use_start=True)
|
||||
if path is not None:
|
||||
results.append(path)
|
||||
else:
|
||||
return results
|
||||
return results
|
||||
|
||||
|
||||
def getRandomStart(costmap, max_cost, side_buffer, time_stamp, res):
|
||||
start = PoseStamped()
|
||||
start.header.frame_id = 'map'
|
||||
start.header.stamp = time_stamp
|
||||
while True:
|
||||
row = randint(side_buffer, costmap.shape[0]-side_buffer)
|
||||
col = randint(side_buffer, costmap.shape[1]-side_buffer)
|
||||
|
||||
if costmap[row, col] < max_cost:
|
||||
start.pose.position.x = col*res
|
||||
start.pose.position.y = row*res
|
||||
|
||||
yaw = uniform(0, 1) * 2*math.pi
|
||||
quad = euler2quat(0.0, 0.0, yaw)
|
||||
start.pose.orientation.w = quad[0]
|
||||
start.pose.orientation.x = quad[1]
|
||||
start.pose.orientation.y = quad[2]
|
||||
start.pose.orientation.z = quad[3]
|
||||
break
|
||||
return start
|
||||
|
||||
|
||||
def getRandomGoal(costmap, start, max_cost, side_buffer, time_stamp, res):
|
||||
goal = PoseStamped()
|
||||
goal.header.frame_id = 'map'
|
||||
goal.header.stamp = time_stamp
|
||||
while True:
|
||||
row = randint(side_buffer, costmap.shape[0]-side_buffer)
|
||||
col = randint(side_buffer, costmap.shape[1]-side_buffer)
|
||||
|
||||
start_x = start.pose.position.x
|
||||
start_y = start.pose.position.y
|
||||
goal_x = col*res
|
||||
goal_y = row*res
|
||||
x_diff = goal_x - start_x
|
||||
y_diff = goal_y - start_y
|
||||
dist = math.sqrt(x_diff ** 2 + y_diff ** 2)
|
||||
|
||||
if costmap[row, col] < max_cost and dist > 3.0:
|
||||
goal.pose.position.x = goal_x
|
||||
goal.pose.position.y = goal_y
|
||||
|
||||
yaw = uniform(0, 1) * 2*math.pi
|
||||
quad = euler2quat(0.0, 0.0, yaw)
|
||||
goal.pose.orientation.w = quad[0]
|
||||
goal.pose.orientation.x = quad[1]
|
||||
goal.pose.orientation.y = quad[2]
|
||||
goal.pose.orientation.z = quad[3]
|
||||
break
|
||||
return goal
|
||||
|
||||
|
||||
def main():
|
||||
rclpy.init()
|
||||
|
||||
navigator = BasicNavigator()
|
||||
|
||||
# Set map to use, other options: 100by100_15, 100by100_10
|
||||
map_path = os.getcwd() + '/' + glob.glob('**/100by100_20.yaml', recursive=True)[0]
|
||||
navigator.changeMap(map_path)
|
||||
time.sleep(2)
|
||||
|
||||
# Get the costmap for start/goal validation
|
||||
costmap_msg = navigator.getGlobalCostmap()
|
||||
costmap = np.asarray(costmap_msg.data)
|
||||
costmap.resize(costmap_msg.metadata.size_y, costmap_msg.metadata.size_x)
|
||||
|
||||
planners = ['Navfn', 'ThetaStar', 'SmacHybrid', 'Smac2d', 'SmacLattice']
|
||||
max_cost = 210
|
||||
side_buffer = 100
|
||||
time_stamp = navigator.get_clock().now().to_msg()
|
||||
results = []
|
||||
seed(33)
|
||||
|
||||
random_pairs = 100
|
||||
res = costmap_msg.metadata.resolution
|
||||
i = 0
|
||||
while len(results) != random_pairs:
|
||||
print("Cycle: ", i, "out of: ", random_pairs)
|
||||
start = getRandomStart(costmap, max_cost, side_buffer, time_stamp, res)
|
||||
goal = getRandomGoal(costmap, start, max_cost, side_buffer, time_stamp, res)
|
||||
print("Start", start)
|
||||
print("Goal", goal)
|
||||
result = getPlannerResults(navigator, start, goal, planners)
|
||||
if len(result) == len(planners):
|
||||
results.append(result)
|
||||
i = i + 1
|
||||
else:
|
||||
print("One of the planners was invalid")
|
||||
|
||||
print("Write Results...")
|
||||
with open(os.getcwd() + '/results.pickle', 'wb+') as f:
|
||||
pickle.dump(results, f, pickle.HIGHEST_PROTOCOL)
|
||||
|
||||
with open(os.getcwd() + '/costmap.pickle', 'wb+') as f:
|
||||
pickle.dump(costmap_msg, f, pickle.HIGHEST_PROTOCOL)
|
||||
|
||||
with open(os.getcwd() + '/planners.pickle', 'wb+') as f:
|
||||
pickle.dump(planners, f, pickle.HIGHEST_PROTOCOL)
|
||||
print("Write Complete")
|
||||
exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,73 @@
|
||||
# Copyright (c) 2022 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.
|
||||
|
||||
import os
|
||||
|
||||
from launch import LaunchDescription
|
||||
from launch.actions import IncludeLaunchDescription
|
||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||
from launch_ros.actions import Node
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
|
||||
def generate_launch_description():
|
||||
nav2_bringup_dir = get_package_share_directory('nav2_bringup')
|
||||
config = os.path.join(get_package_share_directory('nav2_bringup'), 'params', 'nav2_params.yaml')
|
||||
map_file = os.path.join(nav2_bringup_dir, 'maps', 'turtlebot3_world.yaml')
|
||||
lifecycle_nodes = ['map_server', 'planner_server']
|
||||
|
||||
return LaunchDescription([
|
||||
Node(
|
||||
package='nav2_map_server',
|
||||
executable='map_server',
|
||||
name='map_server',
|
||||
output='screen',
|
||||
parameters=[{'use_sim_time': True},
|
||||
{'yaml_filename': map_file},
|
||||
{'topic_name': "map"}]),
|
||||
|
||||
Node(
|
||||
package='nav2_planner',
|
||||
executable='planner_server',
|
||||
name='planner_server',
|
||||
output='screen',
|
||||
parameters=[config]),
|
||||
|
||||
Node(
|
||||
package = 'tf2_ros',
|
||||
executable = 'static_transform_publisher',
|
||||
output = 'screen',
|
||||
arguments = ["0", "0", "0", "0", "0", "0", "base_link", "map"]),
|
||||
|
||||
Node(
|
||||
package = 'tf2_ros',
|
||||
executable = 'static_transform_publisher',
|
||||
output = 'screen',
|
||||
arguments = ["0", "0", "0", "0", "0", "0", "base_link", "odom"]),
|
||||
|
||||
Node(
|
||||
package='nav2_lifecycle_manager',
|
||||
executable='lifecycle_manager',
|
||||
name='lifecycle_manager',
|
||||
output='screen',
|
||||
parameters=[{'use_sim_time': True},
|
||||
{'autostart': True},
|
||||
{'node_names': lifecycle_nodes}]),
|
||||
|
||||
IncludeLaunchDescription(
|
||||
PythonLaunchDescriptionSource(
|
||||
os.path.join(nav2_bringup_dir, 'launch', 'rviz_launch.py')),
|
||||
launch_arguments={'namespace': '',
|
||||
'use_namespace': 'False'}.items())
|
||||
|
||||
])
|
||||
@@ -0,0 +1,173 @@
|
||||
#! /usr/bin/env python3
|
||||
# Copyright 2022 Joshua Wallace
|
||||
#
|
||||
# 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 numpy as np
|
||||
import math
|
||||
|
||||
import os
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
import pickle
|
||||
|
||||
import seaborn as sns
|
||||
import matplotlib.pylab as plt
|
||||
from tabulate import tabulate
|
||||
|
||||
|
||||
def getPaths(results):
|
||||
paths = []
|
||||
for result in results:
|
||||
for path in result:
|
||||
paths.append(path.path)
|
||||
return paths
|
||||
|
||||
|
||||
def getTimes(results):
|
||||
times = []
|
||||
for result in results:
|
||||
for time in result:
|
||||
times.append(time.planning_time.nanosec/1e09 + time.planning_time.sec)
|
||||
return times
|
||||
|
||||
|
||||
def getMapCoordsFromPaths(paths, resolution):
|
||||
coords = []
|
||||
for path in paths:
|
||||
x = []
|
||||
y = []
|
||||
for pose in path.poses:
|
||||
x.append(pose.pose.position.x/resolution)
|
||||
y.append(pose.pose.position.y/resolution)
|
||||
coords.append(x)
|
||||
coords.append(y)
|
||||
return coords
|
||||
|
||||
|
||||
def getPathLength(path):
|
||||
path_length = 0
|
||||
x_prev = path.poses[0].pose.position.x
|
||||
y_prev = path.poses[0].pose.position.y
|
||||
for i in range(1, len(path.poses)):
|
||||
x_curr = path.poses[i].pose.position.x
|
||||
y_curr = path.poses[i].pose.position.y
|
||||
path_length = path_length + math.sqrt((x_curr-x_prev)**2 + (y_curr-y_prev)**2)
|
||||
x_prev = x_curr
|
||||
y_prev = y_curr
|
||||
return path_length
|
||||
|
||||
|
||||
def plotResults(costmap, paths):
|
||||
coords = getMapCoordsFromPaths(paths, costmap.metadata.resolution)
|
||||
data = np.asarray(costmap.data)
|
||||
data.resize(costmap.metadata.size_y, costmap.metadata.size_x)
|
||||
data = np.where(data <= 253, 0, data)
|
||||
|
||||
plt.figure(3)
|
||||
ax = sns.heatmap(data, cmap='Greys', cbar=False)
|
||||
for i in range(0, len(coords), 2):
|
||||
ax.plot(coords[i], coords[i+1], linewidth=0.7)
|
||||
plt.axis('off')
|
||||
ax.set_aspect('equal', 'box')
|
||||
plt.show()
|
||||
|
||||
|
||||
def averagePathCost(paths, costmap, num_of_planners):
|
||||
coords = getMapCoordsFromPaths(paths, costmap.metadata.resolution)
|
||||
data = np.asarray(costmap.data)
|
||||
data.resize(costmap.metadata.size_y, costmap.metadata.size_x)
|
||||
|
||||
average_path_costs = []
|
||||
for i in range(num_of_planners):
|
||||
average_path_costs.append([])
|
||||
|
||||
k = 0
|
||||
for i in range(0, len(coords), 2):
|
||||
costs = []
|
||||
for j in range(len(coords[i])):
|
||||
costs.append(data[math.floor(coords[i+1][j])][math.floor(coords[i][j])])
|
||||
average_path_costs[k % num_of_planners].append(sum(costs)/len(costs))
|
||||
k += 1
|
||||
|
||||
return average_path_costs
|
||||
|
||||
|
||||
def maxPathCost(paths, costmap, num_of_planners):
|
||||
coords = getMapCoordsFromPaths(paths, costmap.metadata.resolution)
|
||||
data = np.asarray(costmap.data)
|
||||
data.resize(costmap.metadata.size_y, costmap.metadata.size_x)
|
||||
|
||||
max_path_costs = []
|
||||
for i in range(num_of_planners):
|
||||
max_path_costs.append([])
|
||||
|
||||
k = 0
|
||||
for i in range(0, len(coords), 2):
|
||||
max_cost = 0
|
||||
for j in range(len(coords[i])):
|
||||
cost = data[math.floor(coords[i+1][j])][math.floor(coords[i][j])]
|
||||
if max_cost < cost:
|
||||
max_cost = cost
|
||||
max_path_costs[k % num_of_planners].append(max_cost)
|
||||
k += 1
|
||||
|
||||
return max_path_costs
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
print("Read data")
|
||||
with open(os.getcwd() + '/results.pickle', 'rb') as f:
|
||||
results = pickle.load(f)
|
||||
|
||||
with open(os.getcwd() + '/planners.pickle', 'rb') as f:
|
||||
planners = pickle.load(f)
|
||||
|
||||
with open(os.getcwd() + '/costmap.pickle', 'rb') as f:
|
||||
costmap = pickle.load(f)
|
||||
|
||||
paths = getPaths(results)
|
||||
path_lengths = []
|
||||
|
||||
for path in paths:
|
||||
path_lengths.append(getPathLength(path))
|
||||
path_lengths = np.asarray(path_lengths)
|
||||
total_paths = len(paths)
|
||||
|
||||
path_lengths.resize((int(total_paths/len(planners)), len(planners)))
|
||||
path_lengths = path_lengths.transpose()
|
||||
|
||||
times = getTimes(results)
|
||||
times = np.asarray(times)
|
||||
times.resize((int(total_paths/len(planners)), len(planners)))
|
||||
times = np.transpose(times)
|
||||
|
||||
# Costs
|
||||
average_path_costs = np.asarray(averagePathCost(paths, costmap, len(planners)))
|
||||
max_path_costs = np.asarray(maxPathCost(paths, costmap, len(planners)))
|
||||
|
||||
# Generate table
|
||||
planner_table = [['Planner', 'Average path length (m)', 'Average Time (s)',
|
||||
'Average cost', 'Max cost']]
|
||||
|
||||
for i in range(0, len(planners)):
|
||||
planner_table.append([planners[i], np.average(path_lengths[i]), np.average(times[i]),
|
||||
np.average(average_path_costs[i]), np.average(max_path_costs[i])])
|
||||
|
||||
# Visualize results
|
||||
print(tabulate(planner_table))
|
||||
plotResults(costmap, paths)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
|
||||
# colcon sanitizer plugin instructions at
|
||||
# https://github.com/colcon/colcon-sanitizer-reports/blob/master/README.rst
|
||||
|
||||
# To use this script, make sure you have the colcon plugins installed according
|
||||
# to the instructions above. Then build ros2_ws and navstack_dependencies_ws.
|
||||
# Source the navstack_dependencies_ws and then cd to the nav2_ws.
|
||||
#
|
||||
# Run this script by invoking navigation2/tools/run_sanitizers
|
||||
# Afterwards, there should be two files in the root of the workspace:
|
||||
# - sanitizer_report-asan.csv
|
||||
# - sanitizer_report-tsan.csv
|
||||
|
||||
clean_workspace() {
|
||||
rm -rf build install log
|
||||
}
|
||||
|
||||
build_with_sanitizer() {
|
||||
colcon build --build-base=build-$1 --install-base=install-$1 \
|
||||
--cmake-args -DOSRF_TESTING_TOOLS_CPP_DISABLE_MEMORY_TOOLS=ON \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
--mixin $1 \
|
||||
--symlink-install
|
||||
}
|
||||
|
||||
test_with_sanitizer() {
|
||||
colcon test --build-base=build-$1 --install-base=install-$1 --retest-until-pass 3 \
|
||||
--event-handlers sanitizer_report+
|
||||
}
|
||||
|
||||
|
||||
clean_workspace
|
||||
|
||||
build_with_sanitizer asan-gcc
|
||||
build_with_sanitizer tsan
|
||||
|
||||
test_with_sanitizer asan-gcc
|
||||
mv sanitizer_report.csv sanitizer_report-asan.csv
|
||||
test_with_sanitizer tsan
|
||||
mv sanitizer_report.csv sanitizer_report-tsan.csv
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # gets the directory of this script
|
||||
|
||||
# Skip flaky tests. Nav2 system tests will be run later.
|
||||
colcon test --packages-skip nav2_system_tests nav2_behaviors
|
||||
|
||||
# run the stable tests in nav2_behaviors
|
||||
colcon test --packages-select nav2_behaviors --ctest-args --exclude-regex "test_recoveries"
|
||||
|
||||
# run the linters in nav2_system_tests. They only need to be run once.
|
||||
colcon test --packages-select nav2_system_tests --ctest-args --exclude-regex "test_.*" # run the linters
|
||||
|
||||
# Each of the `colcon test` lines above runs tests on independent sets of packages.
|
||||
# As a result the test logs of each line won't overwrite the others. The single
|
||||
# call to `colcon test-result` will look through all packages and report any errors
|
||||
# that happened in any of the `colcon test` lines above.
|
||||
colcon test-result --verbose
|
||||
|
||||
# $SCRIPT_DIR/ctest_retry.bash -r 3 -d build/nav2_system_tests -t test_localization$
|
||||
# $SCRIPT_DIR/ctest_retry.bash -r 3 -d build/nav2_system_tests -t test_planner_costmaps$
|
||||
# $SCRIPT_DIR/ctest_retry.bash -r 3 -d build/nav2_system_tests -t test_planner_random$
|
||||
# $SCRIPT_DIR/ctest_retry.bash -r 3 -d build/nav2_system_tests -t test_bt_navigator$
|
||||
# $SCRIPT_DIR/ctest_retry.bash -r 3 -d build/nav2_system_tests -t test_bt_navigator_with_dijkstra$
|
||||
$SCRIPT_DIR/ctest_retry.bash -r 3 -d build/nav2_system_tests -t test_dynamic_obstacle$
|
||||
# $SCRIPT_DIR/ctest_retry.bash -r 3 -d build/nav2_system_tests -t test_multi_robot$
|
||||
@@ -0,0 +1,7 @@
|
||||
console_bridge
|
||||
fastcdr
|
||||
fastrtps
|
||||
libopensplice69
|
||||
rti-connext-dds-5.3.1
|
||||
slam_toolbox
|
||||
urdfdom_headers
|
||||
@@ -0,0 +1,98 @@
|
||||
# Planners Smoothing Benchmark
|
||||
|
||||
This experiment runs a set with randomly generated goals for objective benchmarking.
|
||||
|
||||
Bechmarking scripts require the following python packages to be installed:
|
||||
|
||||
```
|
||||
pip install transforms3d
|
||||
pip install seaborn
|
||||
pip install tabulate
|
||||
```
|
||||
|
||||
To use the suite, modify the Nav2 bringup parameters `nav2_params.yaml` to include selected path planner:
|
||||
|
||||
```
|
||||
planner_server:
|
||||
ros__parameters:
|
||||
expected_planner_frequency: 20.0
|
||||
planner_plugins: ["SmacHybrid"]
|
||||
SmacHybrid:
|
||||
plugin: "nav2_smac_planner/SmacPlannerHybrid"
|
||||
tolerance: 0.5
|
||||
motion_model_for_search: "DUBIN" # default, non-reverse motion
|
||||
smooth_path: false # should be disabled for experiment
|
||||
analytic_expansion_max_length: 0.3 # decreased to avoid robot jerking
|
||||
```
|
||||
|
||||
... and path smoothers for benchmark:
|
||||
|
||||
```
|
||||
smoother_server:
|
||||
ros__parameters:
|
||||
smoother_plugins: ["simple_smoother", "constrained_smoother"]
|
||||
simple_smoother:
|
||||
plugin: "nav2_smoother::SimpleSmoother"
|
||||
constrained_smoother:
|
||||
plugin: "nav2_constrained_smoother/ConstrainedSmoother"
|
||||
w_smooth: 100000.0 # tuned
|
||||
```
|
||||
|
||||
Set global costmap, path planner and smoothers parameters to those desired in `nav2_params.yaml`.
|
||||
Inside of `metrics.py`, you can change reference path planner / path smoothers to use.
|
||||
|
||||
For the benchmarking purposes, the clarification of execution time may be made for planner and smoother servers, to reduce impacts caused by other system actions outside of the planning / smoothing algorithm (optional):
|
||||
|
||||
```
|
||||
diff --git a/nav2_planner/src/planner_server.cpp b/nav2_planner/src/planner_server.cpp
|
||||
index c7a90bcb..6f93edbf 100644
|
||||
--- a/nav2_planner/src/planner_server.cpp
|
||||
+++ b/nav2_planner/src/planner_server.cpp
|
||||
@@ -381,7 +381,10 @@ void PlannerServer::computePlanThroughPoses()
|
||||
}
|
||||
|
||||
// Get plan from start -> goal
|
||||
+ auto planning_start = steady_clock_.now();
|
||||
nav_msgs::msg::Path curr_path = getPlan(curr_start, curr_goal, goal->planner_id);
|
||||
+ auto planning_duration = steady_clock_.now() - planning_start;
|
||||
+ result->planning_time = planning_duration;
|
||||
|
||||
if (!validatePath<ActionThroughPoses>(curr_goal, curr_path, goal->planner_id)) {
|
||||
throw nav2_core::NoValidPathCouldBeFound(goal->planner_id + "generated a empty path");
|
||||
@@ -398,7 +401,7 @@ void PlannerServer::computePlanThroughPoses()
|
||||
publishPlan(result->path);
|
||||
|
||||
auto cycle_duration = steady_clock_.now() - start_time;
|
||||
- result->planning_time = cycle_duration;
|
||||
+ // result->planning_time = cycle_duration;
|
||||
|
||||
if (max_planner_duration_ && cycle_duration.seconds() > max_planner_duration_) {
|
||||
RCLCPP_WARN(
|
||||
diff --git a/nav2_smoother/src/nav2_smoother.cpp b/nav2_smoother/src/nav2_smoother.cpp
|
||||
index ada1f664..610e9512 100644
|
||||
--- a/nav2_smoother/src/nav2_smoother.cpp
|
||||
+++ b/nav2_smoother/src/nav2_smoother.cpp
|
||||
@@ -253,8 +253,6 @@ bool SmootherServer::findSmootherId(
|
||||
|
||||
void SmootherServer::smoothPlan()
|
||||
{
|
||||
- auto start_time = steady_clock_.now();
|
||||
-
|
||||
RCLCPP_INFO(get_logger(), "Received a path to smooth.");
|
||||
|
||||
auto result = std::make_shared<Action::Result>();
|
||||
@@ -271,6 +269,8 @@ void SmootherServer::smoothPlan()
|
||||
// Perform smoothing
|
||||
auto goal = action_server_->get_current_goal();
|
||||
result->path = goal->path;
|
||||
+
|
||||
+ auto start_time = steady_clock_.now();
|
||||
result->was_completed = smoothers_[current_smoother_]->smooth(
|
||||
result->path, goal->max_smoothing_duration);
|
||||
result->smoothing_duration = steady_clock_.now() - start_time;
|
||||
```
|
||||
|
||||
Then execute the benchmarking:
|
||||
|
||||
- `ros2 launch ./smoother_benchmark_bringup.py` to launch the nav2 stack and path smoothers benchmarking
|
||||
- `python3 ./process_data.py` to take the metric files and process them into key results (and plots)
|
||||
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
image: smoothers_world.pgm
|
||||
resolution: 0.050000
|
||||
origin: [0.0, 0.0, 0.0]
|
||||
negate: 0
|
||||
occupied_thresh: 0.65
|
||||
free_thresh: 0.196
|
||||
@@ -0,0 +1,157 @@
|
||||
#! /usr/bin/env python3
|
||||
# Copyright (c) 2022 Samsung R&D Institute Russia
|
||||
# Copyright (c) 2022 Joshua Wallace
|
||||
#
|
||||
# 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.
|
||||
|
||||
from geometry_msgs.msg import PoseStamped
|
||||
from nav2_simple_commander.robot_navigator import BasicNavigator
|
||||
import rclpy
|
||||
|
||||
import math
|
||||
import os
|
||||
import pickle
|
||||
import numpy as np
|
||||
|
||||
from random import seed
|
||||
from random import randint
|
||||
from random import uniform
|
||||
|
||||
from transforms3d.euler import euler2quat
|
||||
|
||||
|
||||
# Note: Map origin is assumed to be (0,0)
|
||||
|
||||
def getPlannerResults(navigator, initial_pose, goal_pose, planner):
|
||||
return navigator._getPathImpl(initial_pose, goal_pose, planner, use_start=True)
|
||||
|
||||
def getSmootherResults(navigator, path, smoothers):
|
||||
smoothed_results = []
|
||||
for smoother in smoothers:
|
||||
smoothed_result = navigator._smoothPathImpl(path, smoother)
|
||||
if smoothed_result is not None:
|
||||
smoothed_results.append(smoothed_result)
|
||||
else:
|
||||
print(smoother, " failed to smooth the path")
|
||||
return None
|
||||
return smoothed_results
|
||||
|
||||
def getRandomStart(costmap, max_cost, side_buffer, time_stamp, res):
|
||||
start = PoseStamped()
|
||||
start.header.frame_id = 'map'
|
||||
start.header.stamp = time_stamp
|
||||
while True:
|
||||
row = randint(side_buffer, costmap.shape[0]-side_buffer)
|
||||
col = randint(side_buffer, costmap.shape[1]-side_buffer)
|
||||
|
||||
if costmap[row, col] < max_cost:
|
||||
start.pose.position.x = col*res
|
||||
start.pose.position.y = row*res
|
||||
|
||||
yaw = uniform(0, 1) * 2*math.pi
|
||||
quad = euler2quat(0.0, 0.0, yaw)
|
||||
start.pose.orientation.w = quad[0]
|
||||
start.pose.orientation.x = quad[1]
|
||||
start.pose.orientation.y = quad[2]
|
||||
start.pose.orientation.z = quad[3]
|
||||
break
|
||||
return start
|
||||
|
||||
def getRandomGoal(costmap, start, max_cost, side_buffer, time_stamp, res):
|
||||
goal = PoseStamped()
|
||||
goal.header.frame_id = 'map'
|
||||
goal.header.stamp = time_stamp
|
||||
while True:
|
||||
row = randint(side_buffer, costmap.shape[0]-side_buffer)
|
||||
col = randint(side_buffer, costmap.shape[1]-side_buffer)
|
||||
|
||||
start_x = start.pose.position.x
|
||||
start_y = start.pose.position.y
|
||||
goal_x = col*res
|
||||
goal_y = row*res
|
||||
x_diff = goal_x - start_x
|
||||
y_diff = goal_y - start_y
|
||||
dist = math.sqrt(x_diff ** 2 + y_diff ** 2)
|
||||
|
||||
if costmap[row, col] < max_cost and dist > 3.0:
|
||||
goal.pose.position.x = goal_x
|
||||
goal.pose.position.y = goal_y
|
||||
|
||||
yaw = uniform(0, 1) * 2*math.pi
|
||||
quad = euler2quat(0.0, 0.0, yaw)
|
||||
goal.pose.orientation.w = quad[0]
|
||||
goal.pose.orientation.x = quad[1]
|
||||
goal.pose.orientation.y = quad[2]
|
||||
goal.pose.orientation.z = quad[3]
|
||||
break
|
||||
return goal
|
||||
|
||||
def main():
|
||||
rclpy.init()
|
||||
|
||||
navigator = BasicNavigator()
|
||||
|
||||
# Wait for planner and smoother to fully activate
|
||||
print("Waiting for planner and smoother servers to activate")
|
||||
navigator.waitUntilNav2Active('smoother_server', 'planner_server')
|
||||
|
||||
# Get the costmap for start/goal validation
|
||||
costmap_msg = navigator.getGlobalCostmap()
|
||||
costmap = np.asarray(costmap_msg.data)
|
||||
costmap.resize(costmap_msg.metadata.size_y, costmap_msg.metadata.size_x)
|
||||
|
||||
planner = 'SmacHybrid'
|
||||
smoothers = ['simple_smoother', 'constrained_smoother']
|
||||
max_cost = 210
|
||||
side_buffer = 10
|
||||
time_stamp = navigator.get_clock().now().to_msg()
|
||||
results = []
|
||||
seed(33)
|
||||
|
||||
random_pairs = 100
|
||||
i = 0
|
||||
res = costmap_msg.metadata.resolution
|
||||
while i < random_pairs:
|
||||
print("Cycle: ", i, "out of: ", random_pairs)
|
||||
start = getRandomStart(costmap, max_cost, side_buffer, time_stamp, res)
|
||||
goal = getRandomGoal(costmap, start, max_cost, side_buffer, time_stamp, res)
|
||||
print("Start", start)
|
||||
print("Goal", goal)
|
||||
result = getPlannerResults(navigator, start, goal, planner)
|
||||
if result is not None:
|
||||
smoothed_results = getSmootherResults(navigator, result.path, smoothers)
|
||||
if smoothed_results is not None:
|
||||
results.append(result)
|
||||
results.append(smoothed_results)
|
||||
i += 1
|
||||
else:
|
||||
print(planner, " planner failed to produce the path")
|
||||
|
||||
print("Write Results...")
|
||||
benchmark_dir = os.getcwd()
|
||||
with open(os.path.join(benchmark_dir, 'results.pickle'), 'wb') as f:
|
||||
pickle.dump(results, f, pickle.HIGHEST_PROTOCOL)
|
||||
|
||||
with open(os.path.join(benchmark_dir, 'costmap.pickle'), 'wb') as f:
|
||||
pickle.dump(costmap_msg, f, pickle.HIGHEST_PROTOCOL)
|
||||
|
||||
smoothers.insert(0, planner)
|
||||
with open(os.path.join(benchmark_dir, 'methods.pickle'), 'wb') as f:
|
||||
pickle.dump(smoothers, f, pickle.HIGHEST_PROTOCOL)
|
||||
print("Write Complete")
|
||||
|
||||
exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,302 @@
|
||||
#! /usr/bin/env python3
|
||||
# Copyright (c) 2022 Samsung R&D Institute Russia
|
||||
# Copyright (c) 2022 Joshua Wallace
|
||||
# Copyright (c) 2021 RoboTech Vision
|
||||
#
|
||||
# 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 numpy as np
|
||||
import math
|
||||
|
||||
import os
|
||||
import pickle
|
||||
|
||||
import seaborn as sns
|
||||
import matplotlib.pylab as plt
|
||||
from tabulate import tabulate
|
||||
|
||||
|
||||
def getPaths(results):
|
||||
paths = []
|
||||
for i in range(len(results)):
|
||||
if (i % 2) == 0:
|
||||
# Append non-smoothed path
|
||||
paths.append(results[i].path)
|
||||
else:
|
||||
# Append smoothed paths array
|
||||
for result in results[i]:
|
||||
paths.append(result.path)
|
||||
return paths
|
||||
|
||||
|
||||
def getTimes(results):
|
||||
times = []
|
||||
for i in range(len(results)):
|
||||
if (i % 2) == 0:
|
||||
# Append non-smoothed time
|
||||
times.append(results[i].planning_time.nanosec/1e09 + results[i].planning_time.sec)
|
||||
else:
|
||||
# Append smoothed times array
|
||||
for result in results[i]:
|
||||
times.append(result.smoothing_duration.nanosec/1e09 + result.smoothing_duration.sec)
|
||||
return times
|
||||
|
||||
|
||||
def getMapCoordsFromPaths(paths, resolution):
|
||||
coords = []
|
||||
for path in paths:
|
||||
x = []
|
||||
y = []
|
||||
for pose in path.poses:
|
||||
x.append(pose.pose.position.x/resolution)
|
||||
y.append(pose.pose.position.y/resolution)
|
||||
coords.append(x)
|
||||
coords.append(y)
|
||||
return coords
|
||||
|
||||
|
||||
def getPathLength(path):
|
||||
path_length = 0
|
||||
x_prev = path.poses[0].pose.position.x
|
||||
y_prev = path.poses[0].pose.position.y
|
||||
for i in range(1, len(path.poses)):
|
||||
x_curr = path.poses[i].pose.position.x
|
||||
y_curr = path.poses[i].pose.position.y
|
||||
path_length = path_length + math.sqrt((x_curr-x_prev)**2 + (y_curr-y_prev)**2)
|
||||
x_prev = x_curr
|
||||
y_prev = y_curr
|
||||
return path_length
|
||||
|
||||
# Path smoothness calculations
|
||||
def getSmoothness(pt_prev, pt, pt_next):
|
||||
d1 = pt - pt_prev
|
||||
d2 = pt_next - pt
|
||||
delta = d2 - d1
|
||||
return np.dot(delta, delta)
|
||||
|
||||
def getPathSmoothnesses(paths):
|
||||
smoothnesses = []
|
||||
pm0 = np.array([0.0, 0.0])
|
||||
pm1 = np.array([0.0, 0.0])
|
||||
pm2 = np.array([0.0, 0.0])
|
||||
for path in paths:
|
||||
smoothness = 0.0
|
||||
for i in range(2, len(path.poses)):
|
||||
pm0[0] = path.poses[i].pose.position.x
|
||||
pm0[1] = path.poses[i].pose.position.y
|
||||
pm1[0] = path.poses[i-1].pose.position.x
|
||||
pm1[1] = path.poses[i-1].pose.position.y
|
||||
pm2[0] = path.poses[i-2].pose.position.x
|
||||
pm2[1] = path.poses[i-2].pose.position.y
|
||||
smoothness += getSmoothness(pm2, pm1, pm0)
|
||||
smoothnesses.append(smoothness)
|
||||
return smoothnesses
|
||||
|
||||
# Curvature calculations
|
||||
def arcCenter(pt_prev, pt, pt_next):
|
||||
cusp_thresh = -0.7
|
||||
|
||||
d1 = pt - pt_prev
|
||||
d2 = pt_next - pt
|
||||
|
||||
d1_norm = d1 / np.linalg.norm(d1)
|
||||
d2_norm = d2 / np.linalg.norm(d2)
|
||||
cos_angle = np.dot(d1_norm, d2_norm)
|
||||
|
||||
if cos_angle < cusp_thresh:
|
||||
# cusp case
|
||||
d2 = -d2
|
||||
pt_next = pt + d2
|
||||
|
||||
det = d1[0] * d2[1] - d1[1] * d2[0]
|
||||
if abs(det) < 1e-4: # straight line
|
||||
return (float('inf'), float('inf'))
|
||||
|
||||
# circle center is at the intersection of mirror axes of the segments:
|
||||
# http://paulbourke.net/geometry/circlesphere/
|
||||
# line intersection:
|
||||
# https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Intersection%20of%20two%20lines
|
||||
mid1 = (pt_prev + pt) / 2
|
||||
mid2 = (pt + pt_next) / 2
|
||||
n1 = (-d1[1], d1[0])
|
||||
n2 = (-d2[1], d2[0])
|
||||
det1 = (mid1[0] + n1[0]) * mid1[1] - (mid1[1] + n1[1]) * mid1[0]
|
||||
det2 = (mid2[0] + n2[0]) * mid2[1] - (mid2[1] + n2[1]) * mid2[0]
|
||||
center = np.array([(det1 * n2[0] - det2 * n1[0]) / det, (det1 * n2[1] - det2 * n1[1]) / det])
|
||||
return center
|
||||
|
||||
def getPathCurvatures(paths):
|
||||
curvatures = []
|
||||
pm0 = np.array([0.0, 0.0])
|
||||
pm1 = np.array([0.0, 0.0])
|
||||
pm2 = np.array([0.0, 0.0])
|
||||
for path in paths:
|
||||
radiuses = []
|
||||
for i in range(2, len(path.poses)):
|
||||
pm0[0] = path.poses[i].pose.position.x
|
||||
pm0[1] = path.poses[i].pose.position.y
|
||||
pm1[0] = path.poses[i-1].pose.position.x
|
||||
pm1[1] = path.poses[i-1].pose.position.y
|
||||
pm2[0] = path.poses[i-2].pose.position.x
|
||||
pm2[1] = path.poses[i-2].pose.position.y
|
||||
center = arcCenter(pm2, pm1, pm0)
|
||||
if center[0] != float('inf'):
|
||||
turning_rad = np.linalg.norm(pm1 - center);
|
||||
radiuses.append(turning_rad)
|
||||
curvatures.append(np.average(radiuses))
|
||||
return curvatures
|
||||
|
||||
def plotResults(costmap, paths):
|
||||
coords = getMapCoordsFromPaths(paths, costmap.metadata.resolution)
|
||||
data = np.asarray(costmap.data)
|
||||
data.resize(costmap.metadata.size_y, costmap.metadata.size_x)
|
||||
data = np.where(data <= 253, 0, data)
|
||||
|
||||
plt.figure(3)
|
||||
ax = sns.heatmap(data, cmap='Greys', cbar=False)
|
||||
for i in range(0, len(coords), 2):
|
||||
ax.plot(coords[i], coords[i+1], linewidth=0.7)
|
||||
plt.axis('off')
|
||||
ax.set_aspect('equal', 'box')
|
||||
plt.show()
|
||||
|
||||
|
||||
def averagePathCost(paths, costmap, num_of_planners):
|
||||
coords = getMapCoordsFromPaths(paths, costmap.metadata.resolution)
|
||||
data = np.asarray(costmap.data)
|
||||
data.resize(costmap.metadata.size_y, costmap.metadata.size_x)
|
||||
|
||||
average_path_costs = []
|
||||
for i in range(num_of_planners):
|
||||
average_path_costs.append([])
|
||||
|
||||
k = 0
|
||||
for i in range(0, len(coords), 2):
|
||||
costs = []
|
||||
for j in range(len(coords[i])):
|
||||
costs.append(data[math.floor(coords[i+1][j])][math.floor(coords[i][j])])
|
||||
average_path_costs[k % num_of_planners].append(sum(costs)/len(costs))
|
||||
k += 1
|
||||
|
||||
return average_path_costs
|
||||
|
||||
|
||||
def maxPathCost(paths, costmap, num_of_planners):
|
||||
coords = getMapCoordsFromPaths(paths, costmap.metadata.resolution)
|
||||
data = np.asarray(costmap.data)
|
||||
data.resize(costmap.metadata.size_y, costmap.metadata.size_x)
|
||||
|
||||
max_path_costs = []
|
||||
for i in range(num_of_planners):
|
||||
max_path_costs.append([])
|
||||
|
||||
k = 0
|
||||
for i in range(0, len(coords), 2):
|
||||
max_cost = 0
|
||||
for j in range(len(coords[i])):
|
||||
cost = data[math.floor(coords[i+1][j])][math.floor(coords[i][j])]
|
||||
if max_cost < cost:
|
||||
max_cost = cost
|
||||
max_path_costs[k % num_of_planners].append(max_cost)
|
||||
k += 1
|
||||
|
||||
return max_path_costs
|
||||
|
||||
|
||||
def main():
|
||||
# Read the data
|
||||
benchmark_dir = os.getcwd()
|
||||
print("Read data")
|
||||
with open(os.path.join(benchmark_dir, 'results.pickle'), 'rb') as f:
|
||||
results = pickle.load(f)
|
||||
|
||||
with open(os.path.join(benchmark_dir, 'methods.pickle'), 'rb') as f:
|
||||
smoothers = pickle.load(f)
|
||||
planner = smoothers[0]
|
||||
del smoothers[0]
|
||||
methods_num = len(smoothers) + 1
|
||||
|
||||
with open(os.path.join(benchmark_dir, 'costmap.pickle'), 'rb') as f:
|
||||
costmap = pickle.load(f)
|
||||
|
||||
# Paths (planner and smoothers)
|
||||
paths = getPaths(results)
|
||||
path_lengths = []
|
||||
|
||||
for path in paths:
|
||||
path_lengths.append(getPathLength(path))
|
||||
path_lengths = np.asarray(path_lengths)
|
||||
total_paths = len(paths)
|
||||
|
||||
# [planner, smoothers] path lenghth in a row
|
||||
path_lengths.resize((int(total_paths/methods_num), methods_num))
|
||||
# [planner, smoothers] path length in a column
|
||||
path_lengths = path_lengths.transpose()
|
||||
|
||||
# Times
|
||||
times = getTimes(results)
|
||||
times = np.asarray(times)
|
||||
times.resize((int(total_paths/methods_num), methods_num))
|
||||
times = np.transpose(times)
|
||||
|
||||
# Costs
|
||||
average_path_costs = np.asarray(averagePathCost(paths, costmap, methods_num))
|
||||
max_path_costs = np.asarray(maxPathCost(paths, costmap, methods_num))
|
||||
|
||||
# Smoothness
|
||||
smoothnesses = getPathSmoothnesses(paths)
|
||||
smoothnesses = np.asarray(smoothnesses)
|
||||
smoothnesses.resize((int(total_paths/methods_num), methods_num))
|
||||
smoothnesses = np.transpose(smoothnesses)
|
||||
|
||||
# Curvatures
|
||||
curvatures = getPathCurvatures(paths)
|
||||
curvatures = np.asarray(curvatures)
|
||||
curvatures.resize((int(total_paths/methods_num), methods_num))
|
||||
curvatures = np.transpose(curvatures)
|
||||
|
||||
# Generate table
|
||||
planner_table = [['Planner',
|
||||
'Time (s)',
|
||||
'Path length (m)',
|
||||
'Average cost',
|
||||
'Max cost',
|
||||
'Path smoothness (x100)',
|
||||
'Average turning rad (m)']]
|
||||
# for path planner
|
||||
planner_table.append([planner,
|
||||
np.average(times[0]),
|
||||
np.average(path_lengths[0]),
|
||||
np.average(average_path_costs[0]),
|
||||
np.average(max_path_costs[0]),
|
||||
np.average(smoothnesses[0]) * 100,
|
||||
np.average(curvatures[0])])
|
||||
# for path smoothers
|
||||
for i in range(1, methods_num):
|
||||
planner_table.append([smoothers[i-1],
|
||||
np.average(times[i]),
|
||||
np.average(path_lengths[i]),
|
||||
np.average(average_path_costs[i]),
|
||||
np.average(max_path_costs[i]),
|
||||
np.average(smoothnesses[i]) * 100,
|
||||
np.average(curvatures[i])])
|
||||
|
||||
# Visualize results
|
||||
print(tabulate(planner_table))
|
||||
plotResults(costmap, paths)
|
||||
|
||||
exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,94 @@
|
||||
# Copyright (c) 2022 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.
|
||||
|
||||
import os
|
||||
|
||||
from launch import LaunchDescription
|
||||
from launch.actions import ExecuteProcess, IncludeLaunchDescription
|
||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||
from launch_ros.actions import Node
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
|
||||
def generate_launch_description():
|
||||
nav2_bringup_dir = get_package_share_directory('nav2_bringup')
|
||||
benchmark_dir = os.getcwd()
|
||||
metrics_py = os.path.join(benchmark_dir, 'metrics.py')
|
||||
config = os.path.join(get_package_share_directory('nav2_bringup'), 'params', 'nav2_params.yaml')
|
||||
map_file = os.path.join(benchmark_dir, 'maps', 'smoothers_world.yaml')
|
||||
lifecycle_nodes = ['map_server', 'planner_server', 'smoother_server']
|
||||
|
||||
static_transform_one = Node(
|
||||
package = 'tf2_ros',
|
||||
executable = 'static_transform_publisher',
|
||||
output = 'screen',
|
||||
arguments = ["0", "0", "0", "0", "0", "0", "base_link", "map"])
|
||||
|
||||
static_transform_two = Node(
|
||||
package = 'tf2_ros',
|
||||
executable = 'static_transform_publisher',
|
||||
output = 'screen',
|
||||
arguments = ["0", "0", "0", "0", "0", "0", "base_link", "odom"])
|
||||
|
||||
start_map_server_cmd = Node(
|
||||
package='nav2_map_server',
|
||||
executable='map_server',
|
||||
name='map_server',
|
||||
output='screen',
|
||||
parameters=[{'use_sim_time': True},
|
||||
{'yaml_filename': map_file},
|
||||
{'topic_name': "map"}])
|
||||
|
||||
start_planner_server_cmd = Node(
|
||||
package='nav2_planner',
|
||||
executable='planner_server',
|
||||
name='planner_server',
|
||||
output='screen',
|
||||
parameters=[config])
|
||||
|
||||
start_smoother_server_cmd = Node(
|
||||
package='nav2_smoother',
|
||||
executable='smoother_server',
|
||||
name='smoother_server',
|
||||
output='screen',
|
||||
parameters=[config])
|
||||
|
||||
start_lifecycle_manager_cmd = Node(
|
||||
package='nav2_lifecycle_manager',
|
||||
executable='lifecycle_manager',
|
||||
name='lifecycle_manager',
|
||||
output='screen',
|
||||
parameters=[{'use_sim_time': True},
|
||||
{'autostart': True},
|
||||
{'node_names': lifecycle_nodes}])
|
||||
|
||||
rviz_cmd = IncludeLaunchDescription(
|
||||
PythonLaunchDescriptionSource(
|
||||
os.path.join(nav2_bringup_dir, 'launch', 'rviz_launch.py')),
|
||||
launch_arguments={'namespace': '',
|
||||
'use_namespace': 'False'}.items())
|
||||
|
||||
metrics_cmd = ExecuteProcess(
|
||||
cmd=['python3', '-u', metrics_py],
|
||||
cwd=[benchmark_dir], output='screen')
|
||||
|
||||
ld = LaunchDescription()
|
||||
ld.add_action(static_transform_one)
|
||||
ld.add_action(static_transform_two)
|
||||
ld.add_action(start_map_server_cmd)
|
||||
ld.add_action(start_planner_server_cmd)
|
||||
ld.add_action(start_smoother_server_cmd)
|
||||
ld.add_action(start_lifecycle_manager_cmd)
|
||||
ld.add_action(rviz_cmd)
|
||||
ld.add_action(metrics_cmd)
|
||||
return ld
|
||||
@@ -0,0 +1,247 @@
|
||||
# syntax=docker/dockerfile:experimental
|
||||
|
||||
# Use experimental buildkit for faster builds
|
||||
# https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/experimental.md
|
||||
# Use `--progress=plain` to use plane stdout for docker build
|
||||
#
|
||||
# Example build command:
|
||||
# This determines which version of the ROS2 code base to pull
|
||||
# export ROS2_BRANCH=main
|
||||
# export DOCKER_BUILDKIT=1
|
||||
# docker build \
|
||||
# --tag nav2:source \
|
||||
# --file source.Dockerfile ../
|
||||
#
|
||||
# Use `--no-cache` to break the local docker build cache.
|
||||
# Use `--pull` to pull the latest parent image from the remote registry.
|
||||
# Use `--target=<stage_name>` to build stages not used for final stage.
|
||||
#
|
||||
# We're only building on top of a ros2 devel image to get the basics
|
||||
# prerequisites installed such as the apt source, rosdep, etc. We don't want to
|
||||
# actually use any of the ros release packages. Instead we are going to build
|
||||
# everything from source in one big workspace.
|
||||
|
||||
ARG FROM_IMAGE=osrf/ros2:devel
|
||||
ARG UNDERLAY_WS=/opt/underlay_ws
|
||||
ARG OVERLAY_WS=/opt/overlay_ws
|
||||
|
||||
# multi-stage for caching
|
||||
FROM $FROM_IMAGE AS cacher
|
||||
|
||||
# clone ros2 source
|
||||
ARG ROS2_BRANCH=master
|
||||
ARG ROS2_REPO=https://github.com/ros2/ros2.git
|
||||
WORKDIR $ROS2_WS/src
|
||||
RUN git clone $ROS2_REPO -b $ROS2_BRANCH && \
|
||||
vcs import ./ < ros2/ros2.repos && \
|
||||
find ./ -name ".git" | xargs rm -rf
|
||||
|
||||
# clone underlay source
|
||||
ARG UNDERLAY_WS
|
||||
WORKDIR $UNDERLAY_WS/src
|
||||
COPY ./tools/underlay.repos ../
|
||||
RUN vcs import ./ < ../underlay.repos && \
|
||||
find ./ -name ".git" | xargs rm -rf
|
||||
|
||||
# copy overlay source
|
||||
ARG OVERLAY_WS
|
||||
WORKDIR $OVERLAY_WS/src
|
||||
COPY ./ ./ros-planning/navigation2
|
||||
RUN colcon list --names-only | cat >> /opt/packages.txt
|
||||
|
||||
# remove skiped packages
|
||||
WORKDIR /opt
|
||||
RUN find ./ \
|
||||
-name "AMENT_IGNORE" -o \
|
||||
-name "CATKIN_IGNORE" -o \
|
||||
-name "COLCON_IGNORE" \
|
||||
| xargs dirname | xargs rm -rf || true && \
|
||||
colcon list --paths-only \
|
||||
--packages-skip-up-to \
|
||||
$(cat packages.txt | xargs) \
|
||||
| xargs rm -rf
|
||||
|
||||
# copy manifests for caching
|
||||
RUN mkdir -p /tmp/opt && \
|
||||
find ./ -name "package.xml" | \
|
||||
xargs cp --parents -t /tmp/opt
|
||||
|
||||
# multi-stage for ros2 dependencies
|
||||
FROM $FROM_IMAGE AS ros2_depender
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# edit apt for caching
|
||||
RUN cp /etc/apt/apt.conf.d/docker-clean /etc/apt/ && \
|
||||
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' \
|
||||
> /etc/apt/apt.conf.d/docker-clean
|
||||
|
||||
# install packages
|
||||
RUN --mount=type=cache,target=/var/cache/apt \
|
||||
--mount=type=cache,target=/var/lib/apt \
|
||||
apt-get update && apt-get install -q -y \
|
||||
ccache \
|
||||
libasio-dev \
|
||||
libtinyxml2-dev \
|
||||
lld \
|
||||
&& rosdep update
|
||||
|
||||
ENV ROS_VERSION=2 \
|
||||
ROS_PYTHON_VERSION=3
|
||||
|
||||
# install ros2 dependencies
|
||||
WORKDIR $ROS2_WS
|
||||
COPY --from=cacher /tmp/$ROS2_WS ./
|
||||
COPY ./tools/skip_keys.txt /tmp/
|
||||
RUN --mount=type=cache,target=/var/cache/apt \
|
||||
--mount=type=cache,target=/var/lib/apt \
|
||||
apt-get update && rosdep install -q -y \
|
||||
--from-paths src \
|
||||
--ignore-src \
|
||||
--skip-keys " \
|
||||
$(cat /tmp/skip_keys.txt | xargs) \
|
||||
"
|
||||
|
||||
# multi-stage for building ros2
|
||||
FROM ros2_depender AS ros2_builder
|
||||
|
||||
# build ros2 source
|
||||
COPY --from=cacher $ROS2_WS ./
|
||||
ARG ROS2_MIXINS="release ccache lld"
|
||||
RUN --mount=type=cache,target=/root/.ccache \
|
||||
colcon build \
|
||||
--symlink-install \
|
||||
--mixin $ROS2_MIXINS
|
||||
|
||||
# multi-stage for testing ros2
|
||||
FROM ros2_builder AS ros2_tester
|
||||
|
||||
# test overlay build
|
||||
ARG RUN_TESTS
|
||||
ARG FAIL_ON_TEST_FAILURE
|
||||
RUN if [ -n "$RUN_TESTS" ]; then \
|
||||
. install/setup.sh && \
|
||||
colcon test && \
|
||||
colcon test-result \
|
||||
|| ([ -z "$FAIL_ON_TEST_FAILURE" ] || exit 1) \
|
||||
fi
|
||||
|
||||
# multi-stage for underlay dependencies
|
||||
FROM ros2_depender AS underlay_depender
|
||||
|
||||
# copy manifests for caching
|
||||
COPY --from=cacher /tmp/$ROS2_WS $ROS2_WS
|
||||
|
||||
# install underlay dependencies
|
||||
ARG UNDERLAY_WS
|
||||
WORKDIR $UNDERLAY_WS
|
||||
COPY --from=cacher /tmp/$UNDERLAY_WS ./
|
||||
RUN --mount=type=cache,target=/var/cache/apt \
|
||||
--mount=type=cache,target=/var/lib/apt \
|
||||
apt-get update && rosdep install -q -y \
|
||||
--from-paths src \
|
||||
$ROS2_WS/src \
|
||||
--ignore-src \
|
||||
--skip-keys " \
|
||||
$(cat /tmp/skip_keys.txt | xargs) \
|
||||
"
|
||||
|
||||
# multi-stage for building underlay
|
||||
FROM underlay_depender AS underlay_builder
|
||||
|
||||
# copy workspace for caching
|
||||
COPY --from=ros2_builder $ROS2_WS $ROS2_WS
|
||||
|
||||
# build underlay source
|
||||
COPY --from=cacher $UNDERLAY_WS ./
|
||||
ARG UNDERLAY_MIXINS="release ccache lld"
|
||||
RUN --mount=type=cache,target=/root/.ccache \
|
||||
. $ROS2_WS/install/setup.sh && \
|
||||
colcon build \
|
||||
--symlink-install \
|
||||
--mixin $UNDERLAY_MIXINS
|
||||
|
||||
# multi-stage for testing underlay
|
||||
FROM underlay_builder AS underlay_tester
|
||||
|
||||
# test overlay build
|
||||
ARG RUN_TESTS
|
||||
ARG FAIL_ON_TEST_FAILURE
|
||||
RUN if [ -n "$RUN_TESTS" ]; then \
|
||||
. install/setup.sh && \
|
||||
colcon test && \
|
||||
colcon test-result \
|
||||
|| ([ -z "$FAIL_ON_TEST_FAILURE" ] || exit 1) \
|
||||
fi
|
||||
|
||||
# multi-stage for overlay dependencies
|
||||
FROM underlay_depender AS overlay_depender
|
||||
|
||||
# copy manifests for caching
|
||||
COPY --from=cacher /tmp/$ROS2_WS $ROS2_WS
|
||||
COPY --from=cacher /tmp/$UNDERLAY_WS $UNDERLAY_WS
|
||||
|
||||
# install overlay dependencies
|
||||
ARG OVERLAY_WS
|
||||
WORKDIR $OVERLAY_WS
|
||||
COPY --from=cacher /tmp/$OVERLAY_WS ./
|
||||
RUN --mount=type=cache,target=/var/cache/apt \
|
||||
--mount=type=cache,target=/var/lib/apt \
|
||||
apt-get update && rosdep install -q -y \
|
||||
--from-paths src \
|
||||
$ROS2_WS/src \
|
||||
$UNDERLAY_WS/src \
|
||||
--ignore-src \
|
||||
--skip-keys " \
|
||||
$(cat /tmp/skip_keys.txt | xargs) \
|
||||
"
|
||||
|
||||
# multi-stage for building overlay
|
||||
FROM overlay_depender AS overlay_builder
|
||||
|
||||
# copy workspace for caching
|
||||
COPY --from=ros2_builder $ROS2_WS $ROS2_WS
|
||||
COPY --from=underlay_builder $UNDERLAY_WS $UNDERLAY_WS
|
||||
|
||||
# build overlay source
|
||||
COPY --from=cacher $OVERLAY_WS ./
|
||||
ARG OVERLAY_MIXINS="release ccache lld"
|
||||
RUN --mount=type=cache,target=/root/.ccache \
|
||||
. $UNDERLAY_WS/install/setup.sh && \
|
||||
colcon build \
|
||||
--symlink-install \
|
||||
--mixin $OVERLAY_MIXINS
|
||||
|
||||
# multi-stage for testing overlay
|
||||
FROM overlay_builder AS overlay_tester
|
||||
|
||||
# test overlay build
|
||||
ARG RUN_TESTS
|
||||
ARG FAIL_ON_TEST_FAILURE
|
||||
RUN if [ -n "$RUN_TESTS" ]; then \
|
||||
. install/setup.sh && \
|
||||
colcon test && \
|
||||
colcon test-result \
|
||||
|| ([ -z "$FAIL_ON_TEST_FAILURE" ] || exit 1) \
|
||||
fi
|
||||
|
||||
# multi-stage for testing workspaces
|
||||
FROM overlay_builder AS workspaces_tester
|
||||
|
||||
# copy workspace test results
|
||||
COPY --from=ros2_tester $ROS2_WS/log $ROS2_WS/log
|
||||
COPY --from=underlay_tester $UNDERLAY_WS/log $UNDERLAY_WS/log
|
||||
COPY --from=overlay_tester $OVERLAY_WS/log $OVERLAY_WS/log
|
||||
|
||||
# multi-stage for shipping overlay
|
||||
FROM overlay_builder AS overlay_shipper
|
||||
|
||||
# restore apt for docker
|
||||
RUN mv /etc/apt/docker-clean /etc/apt/apt.conf.d/ && \
|
||||
rm -rf /var/lib/apt/lists/
|
||||
|
||||
# source overlay from entrypoint
|
||||
ENV UNDERLAY_WS $UNDERLAY_WS
|
||||
ENV OVERLAY_WS $OVERLAY_WS
|
||||
RUN sed --in-place \
|
||||
's|^source .*|source "$OVERLAY_WS/install/setup.bash"|' \
|
||||
/ros_entrypoint.sh
|
||||
@@ -0,0 +1,29 @@
|
||||
repositories:
|
||||
# BehaviorTree/BehaviorTree.CPP:
|
||||
# type: git
|
||||
# url: https://github.com/BehaviorTree/BehaviorTree.CPP.git
|
||||
# version: master
|
||||
# ros/angles:
|
||||
# type: git
|
||||
# url: https://github.com/ros/angles.git
|
||||
# version: ros2
|
||||
# ros-simulation/gazebo_ros_pkgs:
|
||||
# type: git
|
||||
# url: https://github.com/ros-simulation/gazebo_ros_pkgs.git
|
||||
# version: ros2
|
||||
# ros-perception/vision_opencv:
|
||||
# type: git
|
||||
# url: https://github.com/ros-perception/vision_opencv.git
|
||||
# version: ros2
|
||||
# ros/bond_core:
|
||||
# type: git
|
||||
# url: https://github.com/ros/bond_core.git
|
||||
# version: ros2
|
||||
# ompl/ompl:
|
||||
# type: git
|
||||
# url: https://github.com/ompl/ompl.git
|
||||
# version: main
|
||||
# ros-simulation/gazebo_ros_pkgs:
|
||||
# type: git
|
||||
# url: https://github.com/ros-simulation/gazebo_ros_pkgs.git
|
||||
# version: ros2
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run this from the root of the workspace to update these behavior_tree images
|
||||
# in the doc directory of the nav2_bt_navigator package
|
||||
navigation2/tools/bt2img.py \
|
||||
--behavior_tree navigation2/nav2_bt_navigator/behavior_trees/navigate_w_replanning.xml \
|
||||
--image_out navigation2/nav2_bt_navigator/doc/simple_parallel \
|
||||
--legend navigation2/nav2_bt_navigator/doc/legend
|
||||
navigation2/tools/bt2img.py \
|
||||
--behavior_tree navigation2/nav2_bt_navigator/behavior_trees/navigate_to_pose_w_replanning_and_recovery.xml \
|
||||
--image_out navigation2/nav2_bt_navigator/doc/parallel_w_recovery
|
||||
navigation2/tools/bt2img.py \
|
||||
--behavior_tree navigation2/nav2_bt_navigator/behavior_trees/navigate_through_poses_w_replanning_and_recovery.xml \
|
||||
--image_out navigation2/nav2_bt_navigator/doc/parallel_through_poses_w_recovery
|
||||
Reference in New Issue
Block a user