diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..9aa6b30 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,47 @@ +name: CI + +on: + push: + branches: + - main + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + # Standard drop-in approach that should work for most people. + - uses: ammaraskar/sphinx-action@master + with: + pre-build-command: "pip install sphinx-rtd-theme sphinx-autoapi numpydoc numpy==1.21 scipy matplotlib pyfastnoisesimd numba tqdm networkx" + docs-folder: "docs/" + # Great extra actions to compose with: + # Create an artifact of the html output. + - uses: actions/upload-artifact@v1 + with: + name: DocumentationHTML + path: docs/_build/html/ + # Publish built docs to gh-pages branch. + # =============================== + - name: Commit documentation changes + run: | + git clone https://github.com/ammaraskar/sphinx-action-test.git --branch gh-pages --single-branch gh-pages + cp -r docs/_build/html/* gh-pages/ + cd gh-pages + touch .nojekyll + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add . + git commit -m "Update documentation" -a || true + # The above command will fail if no changes were present, so we ignore + # that. + - name: Push changes + uses: ad-m/github-push-action@master + with: + force: true + branch: gh-pages + directory: gh-pages + github_token: ${{ secrets.GITHUB_TOKEN }} + # =============================== diff --git a/docs/Makefile b/docs/Makefile new file mode 100755 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/getting-started-plan.png b/docs/_static/getting-started-plan.png new file mode 100644 index 0000000..633561b Binary files /dev/null and b/docs/_static/getting-started-plan.png differ diff --git a/docs/conf.py b/docs/conf.py new file mode 100755 index 0000000..c10dfbe --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,86 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys + +sys.path.insert(0, os.path.abspath("..")) +try: + import rrtplanner +except ImportError: + raise ImportError("check that `rrtplanner` is available to your system path.") + +# -- Project information ----------------------------------------------------- + +project = "RRT Planner" +copyright = "2021, Mike Sutherland" +author = "Mike Sutherland" + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "autoapi.extension", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.doctest", + "sphinx.ext.mathjax", + "sphinx.ext.viewcode", + "sphinx.ext.githubpages", + "sphinx.ext.intersphinx", + "numpydoc", +] + +autoapi_type = "python" +autoapi_dirs = ["../rrtplanner/"] + + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +pygments_style = "solarized-dark" + +autodoc_mock_imports = ["scipy", "numpy", "matplotlib", "cvxpy"] + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +import sphinx_rtd_theme + +html_theme = "sphinx_rtd_theme" +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +master_doc = "index" + +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), + "numpy": ("https://numpy.org/doc/stable/", None), + "matplotlib": ("https://matplotlib.org/stable", None), + "scipy": ("https://docs.scipy.org/doc/scipy/reference", None), + "networkx": ("https://networkx.org/documentation/stable/", None), +} diff --git a/docs/getting-started.rst b/docs/getting-started.rst new file mode 100644 index 0000000..5260b0d --- /dev/null +++ b/docs/getting-started.rst @@ -0,0 +1,133 @@ +Getting Started +=============== + + +Installation +------------ + +Installation is done with ``pip`` + +.. code-block:: bash + + pip install rrtplanner + +How to Plan +----------- + +Path Planning is the process of finding a viable path from one point to another in some configuration space. This package provides some example implementations of RRT, RRT*, and RRT*Informed planners. These planners can be used to plan in a two dimensional configuration space, with no differential constraints on the motion of the robot. This means that a path between two points is simply a straight line. + +Plans are computed by these objects: + +* :class:`rrtplanner.RRTStandard` +* :class:`rrtplanner.RRTStar` +* :class:`rrtplanner.RRTStarInformed` + +Plans are computed on an OccupancyGrid, which is a MxN array of integers. This package considers a value of 0 to be free space, and anything other than 0 to be an obstacle. We can create random, boolean arrays with the built-in ``perlin_occupancygrid`` function: + +.. code-block:: python + + from rrtplanner import perlin_occupancygrid + og = perlin_occupancygrid(240, 240, 0.33) + +This will create an occupancy grid (really, just an 400x400 array) of points, with blocks of 1 where obstacles are present and blocks of 0 that are free space. + +.. important:: + + Using Perlin noise often generates good random obstacles. However, it sometimes creates an occupancygrid with free space that is split -- from a given point, there is no guarantee that all free space is reachable if you are using the ``perlin_occupancygrid`` noise generator. + +In real applications, occupancyGrids can be generated by e.g. a SLAM algorithm or pre-computed map. + +With our occupancy grid, we are ready to plan. + +First, we create the planner object. This object takes three important arguments: ``og``, which is the occupancy grid over which we want to plan, ``n`` which is the number of **attempted** sample points, and ``r_rewire``, which is the radius for which to rewire. + +As a general rule, we choose ``n`` appropriately, according to the amount of time we have to compute new plans vs the optimality of computed plans. When ``n`` is large, more samples are attempted, and so there are more opportunities to generate an optimal plan. When ``n`` is small, plan time is short, but plans are less optimal. + + We also want to choose ``r_rewire`` so that it is sized to the obstacle features. Very large values of ``r_rewire`` will result in the algorithm taking a long time to compute a plan, with little benefit, since obstacles block long straight-line paths. While very small values of ``r_rewire`` will result in the algorithm quickly finding a sub-optimal plan. + +.. code-block:: python + + from rrtplanner import RRTStar, random_point_og + n = 1200 + r_rewire = 80 + rrts = RRTStar(og, n, r_rewire) + + +Now, we can plan a path. We choose a start and a goal point randomly from the free space of the world, and call the ``RRTStar``'s ``plan`` method. + +.. code-block:: python + + xstart = random_point_og(og) + xgoal = random_point_og(og) + T, gv = rrts.plan(xstart, xgoal) + +The plan is computed, and we have two return values. The first is the tree itself, formatted as a `NetworkX DiGraph object `_ Points are stored in keys called ```pt`` and edges have the attribute ``cost``, which is the cost of the leaf node connected to that edge. The second return value is the vertex of the goal point on the tree. + +Once our tree is created, we traverse it to find a path from vertex 0 to the goal vertex, and subsequently find each point in the order of that traversal: + +.. code-block:: python + + + path = rrts.route2gv(T, gv) + path_pts = rrts.vertices_as_ndarray(T, path) + +Then, we have computed our path and it is stored in ``path_pts``. + +This package includes a number of plotting functions (which use `matplotlib `_) that can be used to visualize the 2-D world and plans in it. + +.. code-block:: python + + from rrtplanner import plot_rrt_lines, plot_path, plot_og, plot_start_goal + import matplotlib.pyplot as plt + + # create figure and ax. + fig = plt.figure() + ax = fig.add_subplot() + + # these functions alter ax in-place. + plot_og(ax, og) + plot_start_goal(ax, xstart, xgoal) + plot_rrt_lines(ax, T) + plot_path(ax, path_pts) + + plt.show() + + +We see the results of our plan. Because points and obstacles are generated randomly when you run this script, your result should bear a superficial resemblance to the figure below: + +.. image:: _static/getting-started-plan.png + +The full code used to generate this figure is shown below: + +.. code-block:: python + + from rrtplanner import perlin_occupancygrid + + og = perlin_occupancygrid(240, 240, 0.33) + from rrtplanner import RRTStar, random_point_og + n = 1200 + r_rewire = 80 # large enough for our 400x400 world + rrts = RRTStar(og, n, r_rewire) + + xstart = random_point_og(og) + xgoal = random_point_og(og) + + T, gv = rrts.plan(xstart, xgoal) + + path = rrts.route2gv(T, gv) + path_pts = rrts.vertices_as_ndarray(T, path) + + from rrtplanner import plot_rrt_lines, plot_path, plot_og, plot_start_goal + import matplotlib.pyplot as plt + + # create figure and ax. + fig = plt.figure() + ax = fig.add_subplot() + + # these functions alter ax in-place. + plot_og(ax, og) + plot_start_goal(ax, xstart, xgoal) + plot_rrt_lines(ax, T) + plot_path(ax, path_pts) + + plt.show() diff --git a/docs/index.rst b/docs/index.rst new file mode 100755 index 0000000..01085d2 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,16 @@ +RRTPlanner Documentation +######################## + +.. toctree:: + :maxdepth: 2 + :caption: Contents + + Getting Started + Git Repository + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` \ No newline at end of file diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100755 index 0000000..c2479e5 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,8 @@ +Installation +============ + +Installation is done with pip: + +.. code-block:: bash + + pip install rrtplanner \ No newline at end of file diff --git a/docs/make.bat b/docs/make.bat new file mode 100755 index 0000000..922152e --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8fe2f47 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/rrtplanner/__init__.py b/rrtplanner/__init__.py index e69de29..12e387e 100644 --- a/rrtplanner/__init__.py +++ b/rrtplanner/__init__.py @@ -0,0 +1,10 @@ +"""Top-level package for rrtplanner.""" + +__author__ = """Mike Sutherland""" +__email__ = "msutherl@uci.edu" +__version__ = "0.1.0" + +from .rrt import * +from .anim import * +from .oggen import * +from .plots import * diff --git a/rrtplanner/anim.py b/rrtplanner/anim.py index 81e97b4..f24e241 100644 --- a/rrtplanner/anim.py +++ b/rrtplanner/anim.py @@ -5,7 +5,8 @@ from matplotlib.patches import Circle from tqdm import tqdm from scipy.ndimage import binary_dilation -from rrt import random_point_og, RRT + +from .rrt import random_point_og, RRT DEFAULT_APPEARANCE = { diff --git a/rrtplanner/rrt.py b/rrtplanner/rrt.py index d8cad9f..d65c6c7 100644 --- a/rrtplanner/rrt.py +++ b/rrtplanner/rrt.py @@ -9,6 +9,18 @@ @nb.njit(fastmath=True) def r2norm(x): + """compute 2-norm of a vector + + Parameters + ---------- + x : np.ndarray + shape (2, ) array + + Returns + ------- + float + 2-norm of x + """ return sqrt(x[0] * x[0] + x[1] * x[1]) @@ -29,24 +41,19 @@ def random_point_og(og: np.ndarray) -> np.ndarray: return free[np.random.randint(0, free.shape[0])] -############# RRT BASE CLASS ################################################## +############# RRT BASE CLASS ########################################################## class RRT(object): - """base class containing common RRT methods""" - def __init__( self, og: np.ndarray, n: int, costfn: callable = None, - every: int = 10, pbar: bool = True, ): # whether to display a progress bar self.pbar = pbar - # every n tries, attempt to go to goal - self.every = every # array containing vertex points self.n = n # occupancyGrid free space @@ -71,9 +78,44 @@ def costfn( self.not_a_dist = np.inf def route2gv(self, T: nx.DiGraph, gv) -> List[int]: + """ + Returns a list of vertices on `T` that are the shortest path from the root of + `T` to the node `gv`. This is a wrapper of `nx.shortest_path`. + + see also, `vertices_as_ndarray` method + + Parameters + ---------- + T : nx.DiGraph + The RRT DiGraph object, returned by rrt.make() + gv : int + vertex of the goal on `T`. + + Returns + ------- + List[int] + ordered list of vertices on `T`. Iterating through this list will give the + vertices of the tree in order from the root to `gv`. + """ return nx.shortest_path(T, source=0, target=gv, weight="dist") - def path_points(self, T: nx.DiGraph, path: list) -> np.ndarray: + def vertices_as_ndarray(self, T: nx.DiGraph, path: list) -> np.ndarray: + """ + Helper method for obtaining an ordered list of vertices on `T` as an (M x 2) + array. + + Parameters + ---------- + T : nx.DiGraph + The RRT DiGraph object, returned by rrt.make() + path : list + List of vertices on the RRT DiGraph object + + Returns + ------- + np.ndarray + (M x 2) array of points. M is the number of vertices in `T`. + """ lines = [] for i in range(len(path) - 1): lines.append([T.nodes[path[i]]["pt"], T.nodes[path[i + 1]]["pt"]]) @@ -81,6 +123,22 @@ def path_points(self, T: nx.DiGraph, path: list) -> np.ndarray: @staticmethod def near(points: np.ndarray, x: np.ndarray) -> np.ndarray: + """ + Obtain all points in `points` that are near `x`, sorted in ascending order of + distance. + + Parameters + ---------- + points : np.ndarray + (M x 2) array of points + x : np.ndarray + (2, ) array, point to find near + + Returns + ------- + np.ndarray + (M x 2) sorted array of points) + """ # vector from x to all points p2x = points - x # norm of that vector @@ -91,6 +149,22 @@ def near(points: np.ndarray, x: np.ndarray) -> np.ndarray: @staticmethod def within(points: np.ndarray, x: np.ndarray, r: float) -> np.ndarray: + """Obtain un-ordered array of points that are within `r` of the point `x`. + + Parameters + ---------- + points : np.ndarray + (M x 2) array of points + x : np.ndarray + (2, ) array, point to find near + r : float + radius to search within + + Returns + ------- + np.ndarray + (? x 2) array of points within `r` of `x` + """ # vector from x to all points p2x = points - x # dot dist with self to get r2 @@ -102,7 +176,22 @@ def within(points: np.ndarray, x: np.ndarray, r: float) -> np.ndarray: @staticmethod @nb.njit() def collisionfree(og, a, b) -> bool: - """calculate linear collision on occupancyGrid between points a, b""" + """Check occupancyGrid for collisions between points `a` and `b`. + + Parameters + ---------- + og : np.ndarray + the occupancyGrid where 0 is free space. Anything other than 0 is treated as an obstacle. + a : np.ndarray + (2, ) array of integer coordinates point a + b : np.ndarray + (2, ) array of integer coordinates point b + + Returns + ------- + bool + whether or not there is a collision between the two points + """ x0 = a[0] y0 = a[1] x1 = b[0] @@ -133,26 +222,85 @@ def collisionfree(og, a, b) -> bool: y0 += sy def sample_all_free(self): - """sample uniformly from free space in the occupancyGrid.""" + """ + Sample uniformly from free space in this object's occupancy grid. + + Returns + ------- + np.ndarray + (2, ) array of a point in free space of occupancy grid. + """ return self.free[np.random.choice(self.free.shape[0])] - def make(self, xstart: np.ndarray, xgoal: np.ndarray): - raise NotImplementedError + def plan(self, xstart: np.ndarray, xgoal: np.ndarray): + """ + Compute a plan from `xstart` to `xgoal`. Raises an exception if called on the + base class. + + Parameters + ---------- + xstart : np.ndarray + starting point + xgoal : np.ndarray + goal point + + Raises + ------ + NotImplementedError + if called on the base class + """ + raise NotImplementedError("This method is not implemented in the base class.") def set_og(self, og_new: np.ndarray): - """update occupancyGrid""" + """ + Set a new occupancy grid. This method also updates free space in the passed + occupancy grid. + + Parameters + ---------- + og_new : np.ndarray + the new occupancy grid + """ self.og = og_new self.free = np.argwhere(og_new == 0) def set_n(self, n: int): - """update n""" - self.n = n + """Update the number of attempted sample points, `n`. - def set_every(self, every: int): - """update every""" - self.every = every + Parameters + ---------- + n : int + new number of sample points attempted when calling plan() from this object. + """ + self.n = n def go2goal(self, vcosts, points, xgoal, j, children, parents): + """ + Attempt to find a path from an existing point on the tree to the goal. This will + update `vcosts`, `points`, `children`, and `parents` if a path is found, and + return a vertex corresponding to the goal. If no path can be computed from the + tree to the goal, the nearest vertex to the goal is selected. + + Parameters + ---------- + vcosts : np.ndarray + (M x 1) array of costs to vertices) + points : np.ndarray + (Mx2) array of points + xgoal : np.ndarray + (2, ) array of goal point + j : int + counter for the "current" vertex's index + children : dict + dict of children of vertices + parents : dict + dict of parents of children + + Returns + ------- + tuple (int, dict, dict, np.ndarray, np.ndarray) + tuple containing (vgoal, children, parents, points, vcosts) + """ # cost for all existing points costs = np.empty(vcosts.shape) for i in range(points.shape[0]): @@ -177,6 +325,28 @@ def go2goal(self, vcosts, points, xgoal, j, children, parents): return vgoal, children, parents, points, vcosts def build_graph(self, vgoal, points, parents, vcosts): + """ + Build the networkx DiGraph object from the points, parents dict, costs dict. + + Parameters + ---------- + vgoal : int + index of the goal vertex + points : np.ndarray + (Mx2) array of points + parents : dict + array of parents. Each vertex has a single parent (hence, the tree), except + the root node which does not have a parent. + vcosts : np.ndarray + (M, 1) array of costs to vertices + + Returns + ------- + nx.DiGraph + Graph of the tree. Edges have attributes: `cost` (the cost of the edge's + leaf -- remember, costs are additive!), `dist` (distance between the + vertices). + """ assert points.max() < self.not_a_point[0] - 1 # build graph T = nx.DiGraph() @@ -201,12 +371,31 @@ def __init__( og: np.ndarray, n: int, costfn: callable = None, - every=100, pbar=True, ): - super().__init__(og, n, costfn=costfn, every=every, pbar=pbar) - - def make(self, xstart: np.ndarray, xgoal: np.ndarray) -> Tuple[nx.DiGraph, int]: + super().__init__(og, n, costfn=costfn, pbar=pbar) + + def plan(self, xstart: np.ndarray, xgoal: np.ndarray) -> Tuple[nx.DiGraph, int]: + """ + Compute a plan from `xstart` to `xgoal`. Using the Standard RRT algorithm. + + Compute a plan from `xstart` to `xgoal`. The plan is a tree, with the root at + `xstart` and a leaf at `xgoal`. If xgoal could not be connected to the tree, the + leaf nearest to xgoal is considered the "goal" leaf. + + Parameters + ---------- + xstart : np.ndarray + (2, ) start point + xgoal : np.ndarray + (2, ) goal point + + Returns + ------- + Tuple[nx.DiGraph, int] + DiGraph of the tree, and the vertex of the goal leaf (if goal could be + reached) or the closest tree node. + """ sampled = set() points = np.full((self.n, 2), dtype=int, fill_value=self.not_a_point) vcosts = np.full((self.n,), fill_value=self.not_a_dist) @@ -260,13 +449,32 @@ def __init__( n: int, r_rewire: float, costfn: callable = None, - every=100, pbar=True, ): - super().__init__(og, n, costfn=costfn, every=every, pbar=pbar) + super().__init__(og, n, costfn=costfn, pbar=pbar) self.r_rewire = r_rewire - def make(self, xstart: np.ndarray, xgoal: np.ndarray): + def plan(self, xstart: np.ndarray, xgoal: np.ndarray): + """ + Compute a plan from `xstart` to `xgoal`. Using the RRT* algorithm. + + The plan is a tree, with the root at + `xstart` and a leaf at `xgoal`. If xgoal could not be connected to the tree, the + leaf nearest to xgoal is considered the "goal" leaf. + + Parameters + ---------- + xstart : np.ndarray + (2, ) start point + xgoal : np.ndarray + (2, ) goal point + + Returns + ------- + Tuple[nx.DiGraph, int] + DiGraph of the tree, and the vertex of the goal leaf (if goal could be + reached) or the closest tree node. + """ sampled = set() points = np.full((self.n, 2), dtype=int, fill_value=self.not_a_point) vcosts = np.full((self.n,), fill_value=self.not_a_dist) @@ -350,10 +558,9 @@ def __init__( r_rewire: float, r_goal: float, costfn: callable = None, - every: int = 100, pbar: bool = True, ): - super().__init__(og, n, costfn=costfn, every=every, pbar=pbar) + super().__init__(og, n, costfn=costfn, pbar=pbar) self.r_rewire = r_rewire self.r_goal = r_goal # store the ellipses for plotting later @@ -434,8 +641,31 @@ def get_ellipse_for_plt( ang = self.rad2deg(np.arctan2((a)[1], (a)[0])) return xcent, majax, minax, ang - def make(self, xstart: np.ndarray, xgoal: np.ndarray): - + def plan(self, xstart: np.ndarray, xgoal: np.ndarray): + """ + Compute a plan from `xstart` to `xgoal`. Using the RRT* Informed algorithm. + + Keep in mind: if the cost function does not correspond to the Euclidean cost, + this algorithm may undersample the solution space -- therefore, for non R^2 + distance cost functions, it is recommended to use the RRT* algorithm instead. + + The plan is a tree, with the root at `xstart` and a leaf at `xgoal`. If xgoal + could not be connected to the tree, the leaf nearest to xgoal is considered the + "goal" leaf. + + Parameters + ---------- + xstart : np.ndarray + (2, ) start point + xgoal : np.ndarray + (2, ) goal point + + Returns + ------- + Tuple[nx.DiGraph, int] + DiGraph of the tree, and the vertex of the goal leaf (if goal could be + reached) or the closest tree node. + """ vsoln = [] sampled = set() points = np.full((self.n, 2), dtype=int, fill_value=self.not_a_point) diff --git a/setup.py b/setup.py index e73b849..13c46af 100644 --- a/setup.py +++ b/setup.py @@ -1,31 +1,142 @@ -from setuptools import find_packages, setup +# Always prefer setuptools over distutils +from setuptools import setup, find_packages import pathlib -HERE = pathlib.Path(__file__).parent -README = (HERE / "README.md").read_text() +VERSION = "0.1.2" + +here = pathlib.Path(__file__).parent.resolve() + +# Get the long description from the README file +long_description = (here / "README.md").read_text(encoding="utf-8") + +# Arguments marked as "Required" below must be included for upload to PyPI. +# Fields marked as "Optional" may be commented out. setup( - name="rrtplanner", - version="1.0.0", - description="A (partially) vectorized RRT, RRT*, RRT*Informed planner for 2-D occupancyGrids.", - long_description=README, - long_description_content_type="text/markdown", - url="https://github.com/rland93/rrt_pathplanner", - author="rland93", - author_email="msutherl@uci.edu", - license="MIT", - packages=find_packages(exclude=["tests"]), - include_package_data=True, - python_requires=">=3.8", + # This is the name of your project. The first time you publish this + # package, this name will be registered for you. It will determine how + # users can install this project, e.g.: + # + # $ pip install sampleproject + # + # And where it will live on PyPI: https://pypi.org/project/sampleproject/ + # + # There are some restrictions on what makes a valid project name + # specification here: + # https://packaging.python.org/specifications/core-metadata/#name + name="rrtplanner", # Required + # Versions should comply with PEP 440: + # https://www.python.org/dev/peps/pep-0440/ + # + # For a discussion on single-sourcing the version across setup.py and the + # project code, see + # https://packaging.python.org/guides/single-sourcing-package-version/ + version=VERSION, # Required + # This is a one-line description or tagline of what your project does. This + # corresponds to the "Summary" metadata field: + # https://packaging.python.org/specifications/core-metadata/#summary + description="Vectorized RRT, RRT*, and RRT*Informed planning in Python!", # Optional + # This is an optional longer description of your project that represents + # the body of text which users will see when they visit PyPI. + # + # Often, this is the same as your README, so you can just read it in from + # that file directly (as we have already done above) + # + # This field corresponds to the "Description" metadata field: + # https://packaging.python.org/specifications/core-metadata/#description-optional + long_description=long_description, # Optional + # Denotes that our long_description is in Markdown; valid values are + # text/plain, text/x-rst, and text/markdown + # + # Optional if long_description is written in reStructuredText (rst) but + # required for plain-text or Markdown; if unspecified, "applications should + # attempt to render [the long_description] as text/x-rst; charset=UTF-8 and + # fall back to text/plain if it is not valid rst" (see link below) + # + # This field corresponds to the "Description-Content-Type" metadata field: + # https://packaging.python.org/specifications/core-metadata/#description-content-type-optional + long_description_content_type="text/markdown", # Optional (see note above) + # This should be a valid link to your project's main homepage. + # + # This field corresponds to the "Home-Page" metadata field: + # https://packaging.python.org/specifications/core-metadata/#home-page-optional + url="https://github.com/rland93/rrtplanner", # Optional + # This should be your name or the name of the organization which owns the + # project. + author="Mike Sutherland", # Optional + # This should be a valid email address corresponding to the author listed + # above. + author_email="msutherl@uci.edu", # Optional + # Classifiers help users find your project by categorizing it. + # + # For a list of valid classifiers, see https://pypi.org/classifiers/ + classifiers=[ # Optional + # How mature is this project? Common values are + # 3 - Alpha + # 4 - Beta + # 5 - Production/Stable + "Development Status :: 3 - Alpha", + # Indicate who your project is intended for + "Intended Audience :: Developers", + "Topic :: Software Development :: Build Tools", + # Pick your license as you wish + "License :: OSI Approved :: MIT License", + # Specify the Python versions you support here. In particular, ensure + # that you indicate you support Python 3. These classifiers are *not* + # checked by 'pip install'. See instead 'python_requires' below. + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3 :: Only", + ], + # This field adds keywords for your project which will appear on the + # project page. What does your project relate to? + # + # Note that this is a list of additional keywords, separated + # by commas, to be used to assist searching for the distribution in a + # larger catalog. + keywords="robotics, path planning", # Optional + # When your source code is in a subdirectory under the project root, e.g. + # `src/`, it is necessary to specify the `package_dir` argument. + # You can just specify package directories manually here if your project is + # simple. Or you can use find_packages(). + # + # Alternatively, if you just want to distribute a single Python file, use + # the `py_modules` argument instead as follows, which will expect a file + # called `my_module.py` to exist: + # + # py_modules=["my_module"], + # + packages=find_packages(include=["rrtplanner"]), # Required + # Specify which Python versions you support. In contrast to the + # 'Programming Language' classifiers above, 'pip install' will check this + # and refuse to install the project if the version does not match. See + # https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires + python_requires=">=3.6, <4", + # This field lists other packages that your project depends on to run. + # Any package you put here will be installed by pip when your project is + # installed, so they must be valid existing projects. + # + # For an analysis of "install_requires" vs pip's requirements files see: + # https://packaging.python.org/discussions/install-requires-vs-requirements/ install_requires=[ - "cvxpy>=1.1", - "matplotlib>=3.5", + "numpy>=1.18", + "matplotlib>=3.4", "networkx>=2.6", - "numba==0.55", - "numpy>=1.20", - "pyfastnoisesimd==0.4", - "pytest==6.2", - "scipy==1.7", - "tqdm==4.62", - ], + "scipy>=1.7", + "tqdm>=4", + "numba>=0.50", + "pyfastnoisesimd>=0.4", + ], # Optional + # List additional URLs that are relevant to your project as a dict. + # + # This field corresponds to the "Project-URL" metadata fields: + # https://packaging.python.org/specifications/core-metadata/#project-url-multiple-use + # + # Examples listed include a pattern for specifying where the package tracks + # issues, where the source is hosted, where to say thanks to the package + # maintainers, and where to support the project financially. The key is + # what's used to render the link text on PyPI. + project_urls={ # Optional + "Bug Reports": "https://github.com/rland93/rrtplanner/issues", + "Source": "https://github.com/rland93/rrtplanner", + }, ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..ab6c4a6 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Unit test package for rrtplanner."""