pyastar2d 1.0.6__tar.gz → 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyastar2d-1.0.6 → pyastar2d-1.1.0}/PKG-INFO +40 -10
- pyastar2d-1.0.6/src/pyastar2d.egg-info/PKG-INFO → pyastar2d-1.1.0/README.md +26 -18
- pyastar2d-1.1.0/pyproject.toml +3 -0
- pyastar2d-1.1.0/setup.cfg +16 -0
- pyastar2d-1.1.0/setup.py +58 -0
- {pyastar2d-1.0.6 → pyastar2d-1.1.0}/src/cpp/astar.cpp +8 -5
- pyastar2d-1.1.0/src/pyastar2d/__init__.py +3 -0
- pyastar2d-1.1.0/src/pyastar2d/astar_wrapper.py +77 -0
- pyastar2d-1.0.6/README.md → pyastar2d-1.1.0/src/pyastar2d.egg-info/PKG-INFO +48 -4
- {pyastar2d-1.0.6 → pyastar2d-1.1.0}/src/pyastar2d.egg-info/SOURCES.txt +4 -1
- pyastar2d-1.1.0/tests/test_astar.py +156 -0
- pyastar2d-1.0.6/setup.cfg +0 -4
- pyastar2d-1.0.6/setup.py +0 -44
- pyastar2d-1.0.6/src/pyastar2d/__init__.py +0 -2
- pyastar2d-1.0.6/src/pyastar2d/astar_wrapper.py +0 -73
- {pyastar2d-1.0.6 → pyastar2d-1.1.0}/LICENSE +0 -0
- {pyastar2d-1.0.6 → pyastar2d-1.1.0}/MANIFEST.in +0 -0
- {pyastar2d-1.0.6 → pyastar2d-1.1.0}/requirements.txt +0 -0
- {pyastar2d-1.0.6 → pyastar2d-1.1.0}/src/cpp/experimental_heuristics.cpp +0 -0
- {pyastar2d-1.0.6 → pyastar2d-1.1.0}/src/cpp/experimental_heuristics.h +0 -0
- {pyastar2d-1.0.6 → pyastar2d-1.1.0}/src/pyastar2d.egg-info/dependency_links.txt +0 -0
- {pyastar2d-1.0.6 → pyastar2d-1.1.0}/src/pyastar2d.egg-info/requires.txt +0 -0
- {pyastar2d-1.0.6 → pyastar2d-1.1.0}/src/pyastar2d.egg-info/top_level.txt +0 -0
|
@@ -1,16 +1,24 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pyastar2d
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: A simple implementation of the A* algorithm for path-finding on a two-dimensional grid.
|
|
5
5
|
Home-page: https://github.com/hjweide/pyastar2d
|
|
6
6
|
Author: Hendrik Weideman
|
|
7
7
|
Author-email: hjweide@gmail.com
|
|
8
|
-
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Operating System :: OS Independent
|
|
11
|
-
Requires-Python: >=3.7
|
|
8
|
+
Requires-Python: >=3.8
|
|
12
9
|
Description-Content-Type: text/markdown
|
|
13
10
|
License-File: LICENSE
|
|
11
|
+
Requires-Dist: imageio
|
|
12
|
+
Requires-Dist: numpy
|
|
13
|
+
Dynamic: author
|
|
14
|
+
Dynamic: author-email
|
|
15
|
+
Dynamic: description
|
|
16
|
+
Dynamic: description-content-type
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
Dynamic: requires-dist
|
|
20
|
+
Dynamic: requires-python
|
|
21
|
+
Dynamic: summary
|
|
14
22
|
|
|
15
23
|
[](https://travis-ci.com/hjweide/pyastar2d)
|
|
16
24
|
[](https://coveralls.io/github/hjweide/pyastar2d?branch=master)
|
|
@@ -107,7 +115,7 @@ python examples/maze_solver.py --input mazes/maze_small.png --output solns/maze_
|
|
|
107
115
|
python examples/maze_solver.py --input mazes/maze_large.png --output solns/maze_large.png
|
|
108
116
|
```
|
|
109
117
|
|
|
110
|
-
### Small Maze (1802 x 1802):
|
|
118
|
+
### Small Maze (1802 x 1802):
|
|
111
119
|
```bash
|
|
112
120
|
time python examples/maze_solver.py --input mazes/maze_small.png --output solns/maze_small.png
|
|
113
121
|
Loaded maze of shape (1802, 1802) from mazes/maze_small.png
|
|
@@ -122,7 +130,7 @@ sys 0m0.606s
|
|
|
122
130
|
The solution found for the small maze is shown below:
|
|
123
131
|
<img src="https://github.com/hjweide/pyastar2d/raw/master/solns/maze_small_soln.png" alt="Maze Small Solution" style="width: 100%"/>
|
|
124
132
|
|
|
125
|
-
### Large Maze (4002 x 4002):
|
|
133
|
+
### Large Maze (4002 x 4002):
|
|
126
134
|
```bash
|
|
127
135
|
time python examples/maze_solver.py --input mazes/maze_large.png --output solns/maze_large.png
|
|
128
136
|
Loaded maze of shape (4002, 4002) from mazes/maze_large.png
|
|
@@ -162,12 +170,34 @@ pip install -r requirements-dev.txt
|
|
|
162
170
|
```
|
|
163
171
|
before running
|
|
164
172
|
```bash
|
|
165
|
-
|
|
173
|
+
pytest
|
|
166
174
|
```
|
|
167
175
|
The tests are fairly basic but cover some of the
|
|
168
176
|
more common pitfalls. Pull requests for more extensive tests are welcome.
|
|
169
177
|
|
|
178
|
+
## Code Formatting
|
|
179
|
+
|
|
180
|
+
It's recommended that you use `pre-commit` to ensure linting procedures are
|
|
181
|
+
run on any code you write. See [pre-commit.com](https://pre-commit.com/) for
|
|
182
|
+
more information.
|
|
183
|
+
|
|
184
|
+
Reference [pre-commit's installation instructions](https://pre-commit.com/#install)
|
|
185
|
+
for software installation on your OS/platform. After you have the software
|
|
186
|
+
installed, run `pre-commit install` on the command line. Now every time you commit
|
|
187
|
+
to this project's code base the linter procedures will automatically run over the
|
|
188
|
+
changed files. To run pre-commit on files preemtively from the command line use:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
pip install -r requirements-dev.txt
|
|
192
|
+
pre-commit run --all-files
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The code base has been formatted by [Black](https://black.readthedocs.io/en/stable/).
|
|
196
|
+
Furthermore, try to conform to `PEP8`. You should set up your preferred editor to
|
|
197
|
+
use `flake8` as its Python linter, but pre-commit will ensure compliance before a
|
|
198
|
+
git commit is completed. This will use the `flake8` configuration within
|
|
199
|
+
`.flake8`, which ignores several errors and stylistic considerations.
|
|
200
|
+
|
|
170
201
|
## References
|
|
171
202
|
1. [A\* search algorithm on Wikipedia](https://en.wikipedia.org/wiki/A*_search_algorithm#Pseudocode)
|
|
172
203
|
2. [Pathfinding with A* on Red Blob Games](http://www.redblobgames.com/pathfinding/a-star/introduction.html)
|
|
173
|
-
|
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: pyastar2d
|
|
3
|
-
Version: 1.0.6
|
|
4
|
-
Summary: A simple implementation of the A* algorithm for path-finding on a two-dimensional grid.
|
|
5
|
-
Home-page: https://github.com/hjweide/pyastar2d
|
|
6
|
-
Author: Hendrik Weideman
|
|
7
|
-
Author-email: hjweide@gmail.com
|
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Operating System :: OS Independent
|
|
11
|
-
Requires-Python: >=3.7
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
License-File: LICENSE
|
|
14
|
-
|
|
15
1
|
[](https://travis-ci.com/hjweide/pyastar2d)
|
|
16
2
|
[](https://coveralls.io/github/hjweide/pyastar2d?branch=master)
|
|
17
3
|
[](https://badge.fury.io/py/pyastar2d)
|
|
@@ -107,7 +93,7 @@ python examples/maze_solver.py --input mazes/maze_small.png --output solns/maze_
|
|
|
107
93
|
python examples/maze_solver.py --input mazes/maze_large.png --output solns/maze_large.png
|
|
108
94
|
```
|
|
109
95
|
|
|
110
|
-
### Small Maze (1802 x 1802):
|
|
96
|
+
### Small Maze (1802 x 1802):
|
|
111
97
|
```bash
|
|
112
98
|
time python examples/maze_solver.py --input mazes/maze_small.png --output solns/maze_small.png
|
|
113
99
|
Loaded maze of shape (1802, 1802) from mazes/maze_small.png
|
|
@@ -122,7 +108,7 @@ sys 0m0.606s
|
|
|
122
108
|
The solution found for the small maze is shown below:
|
|
123
109
|
<img src="https://github.com/hjweide/pyastar2d/raw/master/solns/maze_small_soln.png" alt="Maze Small Solution" style="width: 100%"/>
|
|
124
110
|
|
|
125
|
-
### Large Maze (4002 x 4002):
|
|
111
|
+
### Large Maze (4002 x 4002):
|
|
126
112
|
```bash
|
|
127
113
|
time python examples/maze_solver.py --input mazes/maze_large.png --output solns/maze_large.png
|
|
128
114
|
Loaded maze of shape (4002, 4002) from mazes/maze_large.png
|
|
@@ -162,12 +148,34 @@ pip install -r requirements-dev.txt
|
|
|
162
148
|
```
|
|
163
149
|
before running
|
|
164
150
|
```bash
|
|
165
|
-
|
|
151
|
+
pytest
|
|
166
152
|
```
|
|
167
153
|
The tests are fairly basic but cover some of the
|
|
168
154
|
more common pitfalls. Pull requests for more extensive tests are welcome.
|
|
169
155
|
|
|
156
|
+
## Code Formatting
|
|
157
|
+
|
|
158
|
+
It's recommended that you use `pre-commit` to ensure linting procedures are
|
|
159
|
+
run on any code you write. See [pre-commit.com](https://pre-commit.com/) for
|
|
160
|
+
more information.
|
|
161
|
+
|
|
162
|
+
Reference [pre-commit's installation instructions](https://pre-commit.com/#install)
|
|
163
|
+
for software installation on your OS/platform. After you have the software
|
|
164
|
+
installed, run `pre-commit install` on the command line. Now every time you commit
|
|
165
|
+
to this project's code base the linter procedures will automatically run over the
|
|
166
|
+
changed files. To run pre-commit on files preemtively from the command line use:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
pip install -r requirements-dev.txt
|
|
170
|
+
pre-commit run --all-files
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The code base has been formatted by [Black](https://black.readthedocs.io/en/stable/).
|
|
174
|
+
Furthermore, try to conform to `PEP8`. You should set up your preferred editor to
|
|
175
|
+
use `flake8` as its Python linter, but pre-commit will ensure compliance before a
|
|
176
|
+
git commit is completed. This will use the `flake8` configuration within
|
|
177
|
+
`.flake8`, which ignores several errors and stylistic considerations.
|
|
178
|
+
|
|
170
179
|
## References
|
|
171
180
|
1. [A\* search algorithm on Wikipedia](https://en.wikipedia.org/wiki/A*_search_algorithm#Pseudocode)
|
|
172
181
|
2. [Pathfinding with A* on Red Blob Games](http://www.redblobgames.com/pathfinding/a-star/introduction.html)
|
|
173
|
-
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[flake8]
|
|
2
|
+
exclude = .git
|
|
3
|
+
|
|
4
|
+
[tool:isort]
|
|
5
|
+
profile = black
|
|
6
|
+
line_length = 90
|
|
7
|
+
multi_line_output = 3
|
|
8
|
+
include_trailing_comma = true
|
|
9
|
+
force_grid_wrap = 0
|
|
10
|
+
use_parentheses = true
|
|
11
|
+
ensure_newline_before_comments = true
|
|
12
|
+
|
|
13
|
+
[egg_info]
|
|
14
|
+
tag_build =
|
|
15
|
+
tag_date = 0
|
|
16
|
+
|
pyastar2d-1.1.0/setup.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
3
|
+
from setuptools import Extension, find_packages, setup
|
|
4
|
+
|
|
5
|
+
# Use pathlib for paths
|
|
6
|
+
here = pathlib.Path(__file__).parent.resolve()
|
|
7
|
+
|
|
8
|
+
# Read README and requirements
|
|
9
|
+
long_description = (here / 'README.md').read_text(encoding='utf-8')
|
|
10
|
+
install_requires = (here / 'requirements.txt').read_text().splitlines()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class get_numpy_include:
|
|
14
|
+
"""Defer numpy import until it is actually installed."""
|
|
15
|
+
|
|
16
|
+
def __str__(self):
|
|
17
|
+
import numpy
|
|
18
|
+
|
|
19
|
+
return numpy.get_include()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Define the C++ extension
|
|
23
|
+
astar_module = Extension(
|
|
24
|
+
name='pyastar2d.astar',
|
|
25
|
+
sources=[
|
|
26
|
+
'src/cpp/astar.cpp',
|
|
27
|
+
'src/cpp/experimental_heuristics.cpp',
|
|
28
|
+
],
|
|
29
|
+
define_macros=[
|
|
30
|
+
('Py_LIMITED_API', '0x03080000'),
|
|
31
|
+
('NPY_NO_DEPRECATED_API', 'NPY_1_7_API_VERSION'),
|
|
32
|
+
],
|
|
33
|
+
py_limited_api=True,
|
|
34
|
+
include_dirs=[
|
|
35
|
+
'src/cpp',
|
|
36
|
+
get_numpy_include(),
|
|
37
|
+
],
|
|
38
|
+
extra_compile_args=['-O3', '-fpic', '-Wall'],
|
|
39
|
+
language='c++',
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Define package metadata
|
|
43
|
+
setup(
|
|
44
|
+
name='pyastar2d',
|
|
45
|
+
version='1.1.0',
|
|
46
|
+
author='Hendrik Weideman',
|
|
47
|
+
author_email='hjweide@gmail.com',
|
|
48
|
+
description='A simple implementation of the A* algorithm for path-finding on a two-dimensional grid.',
|
|
49
|
+
long_description=long_description,
|
|
50
|
+
long_description_content_type='text/markdown',
|
|
51
|
+
url='https://github.com/hjweide/pyastar2d',
|
|
52
|
+
packages=find_packages(where='src', exclude=('tests',)),
|
|
53
|
+
package_dir={'': 'src'},
|
|
54
|
+
install_requires=install_requires,
|
|
55
|
+
python_requires='>=3.8',
|
|
56
|
+
ext_modules=[astar_module],
|
|
57
|
+
options={'bdist_wheel': {'py_limited_api': 'cp38'}},
|
|
58
|
+
)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
#include <limits>
|
|
3
3
|
#include <cmath>
|
|
4
4
|
#include <Python.h>
|
|
5
|
+
#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
|
|
5
6
|
#include <numpy/arrayobject.h>
|
|
6
7
|
#include <iostream>
|
|
7
8
|
#include <experimental_heuristics.h>
|
|
@@ -44,7 +45,7 @@ inline float l1_norm(int i0, int j0, int i1, int j1) {
|
|
|
44
45
|
// diag_ok: if true, allows diagonal moves (8-conn.)
|
|
45
46
|
// paths (output): for each node, stores previous node in path
|
|
46
47
|
static PyObject *astar(PyObject *self, PyObject *args) {
|
|
47
|
-
|
|
48
|
+
PyArrayObject* weights_object;
|
|
48
49
|
int h;
|
|
49
50
|
int w;
|
|
50
51
|
int start;
|
|
@@ -61,7 +62,7 @@ static PyObject *astar(PyObject *self, PyObject *args) {
|
|
|
61
62
|
))
|
|
62
63
|
return NULL;
|
|
63
64
|
|
|
64
|
-
float* weights = (float*) weights_object
|
|
65
|
+
float* weights = (float*) PyArray_DATA(weights_object);
|
|
65
66
|
int* paths = new int[h * w];
|
|
66
67
|
int path_length = -1;
|
|
67
68
|
|
|
@@ -76,7 +77,7 @@ static PyObject *astar(PyObject *self, PyObject *args) {
|
|
|
76
77
|
nodes_to_visit.push(start_node);
|
|
77
78
|
|
|
78
79
|
int* nbrs = new int[8];
|
|
79
|
-
|
|
80
|
+
|
|
80
81
|
int goal_i = goal / w;
|
|
81
82
|
int goal_j = goal % w;
|
|
82
83
|
int start_i = start / w;
|
|
@@ -141,11 +142,13 @@ static PyObject *astar(PyObject *self, PyObject *args) {
|
|
|
141
142
|
if (path_length >= 0) {
|
|
142
143
|
npy_intp dims[2] = {path_length, 2};
|
|
143
144
|
PyArrayObject* path = (PyArrayObject*) PyArray_SimpleNew(2, dims, NPY_INT32);
|
|
145
|
+
char* data = (char*) PyArray_BYTES(path);
|
|
146
|
+
npy_intp* strides = PyArray_STRIDES(path);
|
|
144
147
|
npy_int32 *iptr, *jptr;
|
|
145
148
|
int idx = goal;
|
|
146
149
|
for (npy_intp i = dims[0] - 1; i >= 0; --i) {
|
|
147
|
-
iptr = (npy_int32*) (
|
|
148
|
-
jptr = (npy_int32*) (
|
|
150
|
+
iptr = (npy_int32*) (data + i * strides[0]);
|
|
151
|
+
jptr = (npy_int32*) (data + i * strides[0] + strides[1]);
|
|
149
152
|
|
|
150
153
|
*iptr = idx / w;
|
|
151
154
|
*jptr = idx % w;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
from enum import IntEnum
|
|
3
|
+
from typing import Optional, Tuple
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
import pyastar2d.astar
|
|
8
|
+
|
|
9
|
+
# Define array types
|
|
10
|
+
ndmat_f_type = np.ctypeslib.ndpointer(dtype=np.float32, ndim=1, flags='C_CONTIGUOUS')
|
|
11
|
+
ndmat_i2_type = np.ctypeslib.ndpointer(dtype=np.int32, ndim=2, flags='C_CONTIGUOUS')
|
|
12
|
+
|
|
13
|
+
# Define input/output types
|
|
14
|
+
pyastar2d.astar.restype = ndmat_i2_type # Nx2 (i, j) coordinates or None
|
|
15
|
+
pyastar2d.astar.argtypes = [
|
|
16
|
+
ndmat_f_type, # weights
|
|
17
|
+
ctypes.c_int, # height
|
|
18
|
+
ctypes.c_int, # width
|
|
19
|
+
ctypes.c_int, # start index in flattened grid
|
|
20
|
+
ctypes.c_int, # goal index in flattened grid
|
|
21
|
+
ctypes.c_bool, # allow diagonal
|
|
22
|
+
ctypes.c_int, # heuristic_override
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Heuristic(IntEnum):
|
|
27
|
+
"""The supported heuristics."""
|
|
28
|
+
|
|
29
|
+
DEFAULT = 0
|
|
30
|
+
ORTHOGONAL_X = 1
|
|
31
|
+
ORTHOGONAL_Y = 2
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def astar_path(
|
|
35
|
+
weights: np.ndarray,
|
|
36
|
+
start: Tuple[int, int],
|
|
37
|
+
goal: Tuple[int, int],
|
|
38
|
+
allow_diagonal: bool = False,
|
|
39
|
+
heuristic_override: Heuristic = Heuristic.DEFAULT,
|
|
40
|
+
) -> Optional[np.ndarray]:
|
|
41
|
+
"""
|
|
42
|
+
Run astar algorithm on 2d weights.
|
|
43
|
+
|
|
44
|
+
param np.ndarray weights: A grid of weights e.g. np.ones((10, 10), dtype=np.float32)
|
|
45
|
+
param Tuple[int, int] start: (i, j)
|
|
46
|
+
param Tuple[int, int] goal: (i, j)
|
|
47
|
+
param bool allow_diagonal: Whether to allow diagonal moves
|
|
48
|
+
param Heuristic heuristic_override: Override heuristic, see Heuristic(IntEnum)
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
assert (
|
|
52
|
+
weights.dtype == np.float32
|
|
53
|
+
), f'weights must have np.float32 data type, but has {weights.dtype}'
|
|
54
|
+
# For the heuristic to be valid, each move must cost at least 1.
|
|
55
|
+
if weights.min(axis=None) < 1.0:
|
|
56
|
+
raise ValueError('Minimum cost to move must be 1, but got %f' % (weights.min(axis=None)))
|
|
57
|
+
# Ensure start is within bounds.
|
|
58
|
+
if start[0] < 0 or start[0] >= weights.shape[0] or start[1] < 0 or start[1] >= weights.shape[1]:
|
|
59
|
+
raise ValueError(f'Start of {start} lies outside grid.')
|
|
60
|
+
# Ensure goal is within bounds.
|
|
61
|
+
if goal[0] < 0 or goal[0] >= weights.shape[0] or goal[1] < 0 or goal[1] >= weights.shape[1]:
|
|
62
|
+
raise ValueError(f'Goal of {goal} lies outside grid.')
|
|
63
|
+
|
|
64
|
+
height, width = weights.shape
|
|
65
|
+
start_idx = np.ravel_multi_index(start, (height, width))
|
|
66
|
+
goal_idx = np.ravel_multi_index(goal, (height, width))
|
|
67
|
+
|
|
68
|
+
path = pyastar2d.astar.astar(
|
|
69
|
+
weights.flatten(),
|
|
70
|
+
height,
|
|
71
|
+
width,
|
|
72
|
+
start_idx,
|
|
73
|
+
goal_idx,
|
|
74
|
+
allow_diagonal,
|
|
75
|
+
int(heuristic_override),
|
|
76
|
+
)
|
|
77
|
+
return path
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyastar2d
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: A simple implementation of the A* algorithm for path-finding on a two-dimensional grid.
|
|
5
|
+
Home-page: https://github.com/hjweide/pyastar2d
|
|
6
|
+
Author: Hendrik Weideman
|
|
7
|
+
Author-email: hjweide@gmail.com
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: imageio
|
|
12
|
+
Requires-Dist: numpy
|
|
13
|
+
Dynamic: author
|
|
14
|
+
Dynamic: author-email
|
|
15
|
+
Dynamic: description
|
|
16
|
+
Dynamic: description-content-type
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
Dynamic: requires-dist
|
|
20
|
+
Dynamic: requires-python
|
|
21
|
+
Dynamic: summary
|
|
22
|
+
|
|
1
23
|
[](https://travis-ci.com/hjweide/pyastar2d)
|
|
2
24
|
[](https://coveralls.io/github/hjweide/pyastar2d?branch=master)
|
|
3
25
|
[](https://badge.fury.io/py/pyastar2d)
|
|
@@ -93,7 +115,7 @@ python examples/maze_solver.py --input mazes/maze_small.png --output solns/maze_
|
|
|
93
115
|
python examples/maze_solver.py --input mazes/maze_large.png --output solns/maze_large.png
|
|
94
116
|
```
|
|
95
117
|
|
|
96
|
-
### Small Maze (1802 x 1802):
|
|
118
|
+
### Small Maze (1802 x 1802):
|
|
97
119
|
```bash
|
|
98
120
|
time python examples/maze_solver.py --input mazes/maze_small.png --output solns/maze_small.png
|
|
99
121
|
Loaded maze of shape (1802, 1802) from mazes/maze_small.png
|
|
@@ -108,7 +130,7 @@ sys 0m0.606s
|
|
|
108
130
|
The solution found for the small maze is shown below:
|
|
109
131
|
<img src="https://github.com/hjweide/pyastar2d/raw/master/solns/maze_small_soln.png" alt="Maze Small Solution" style="width: 100%"/>
|
|
110
132
|
|
|
111
|
-
### Large Maze (4002 x 4002):
|
|
133
|
+
### Large Maze (4002 x 4002):
|
|
112
134
|
```bash
|
|
113
135
|
time python examples/maze_solver.py --input mazes/maze_large.png --output solns/maze_large.png
|
|
114
136
|
Loaded maze of shape (4002, 4002) from mazes/maze_large.png
|
|
@@ -148,12 +170,34 @@ pip install -r requirements-dev.txt
|
|
|
148
170
|
```
|
|
149
171
|
before running
|
|
150
172
|
```bash
|
|
151
|
-
|
|
173
|
+
pytest
|
|
152
174
|
```
|
|
153
175
|
The tests are fairly basic but cover some of the
|
|
154
176
|
more common pitfalls. Pull requests for more extensive tests are welcome.
|
|
155
177
|
|
|
178
|
+
## Code Formatting
|
|
179
|
+
|
|
180
|
+
It's recommended that you use `pre-commit` to ensure linting procedures are
|
|
181
|
+
run on any code you write. See [pre-commit.com](https://pre-commit.com/) for
|
|
182
|
+
more information.
|
|
183
|
+
|
|
184
|
+
Reference [pre-commit's installation instructions](https://pre-commit.com/#install)
|
|
185
|
+
for software installation on your OS/platform. After you have the software
|
|
186
|
+
installed, run `pre-commit install` on the command line. Now every time you commit
|
|
187
|
+
to this project's code base the linter procedures will automatically run over the
|
|
188
|
+
changed files. To run pre-commit on files preemtively from the command line use:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
pip install -r requirements-dev.txt
|
|
192
|
+
pre-commit run --all-files
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The code base has been formatted by [Black](https://black.readthedocs.io/en/stable/).
|
|
196
|
+
Furthermore, try to conform to `PEP8`. You should set up your preferred editor to
|
|
197
|
+
use `flake8` as its Python linter, but pre-commit will ensure compliance before a
|
|
198
|
+
git commit is completed. This will use the `flake8` configuration within
|
|
199
|
+
`.flake8`, which ignores several errors and stylistic considerations.
|
|
200
|
+
|
|
156
201
|
## References
|
|
157
202
|
1. [A\* search algorithm on Wikipedia](https://en.wikipedia.org/wiki/A*_search_algorithm#Pseudocode)
|
|
158
203
|
2. [Pathfinding with A* on Red Blob Games](http://www.redblobgames.com/pathfinding/a-star/introduction.html)
|
|
159
|
-
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
LICENSE
|
|
2
2
|
MANIFEST.in
|
|
3
3
|
README.md
|
|
4
|
+
pyproject.toml
|
|
4
5
|
requirements.txt
|
|
6
|
+
setup.cfg
|
|
5
7
|
setup.py
|
|
6
8
|
src/cpp/astar.cpp
|
|
7
9
|
src/cpp/experimental_heuristics.cpp
|
|
@@ -12,4 +14,5 @@ src/pyastar2d.egg-info/PKG-INFO
|
|
|
12
14
|
src/pyastar2d.egg-info/SOURCES.txt
|
|
13
15
|
src/pyastar2d.egg-info/dependency_links.txt
|
|
14
16
|
src/pyastar2d.egg-info/requires.txt
|
|
15
|
-
src/pyastar2d.egg-info/top_level.txt
|
|
17
|
+
src/pyastar2d.egg-info/top_level.txt
|
|
18
|
+
tests/test_astar.py
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
import pyastar2d
|
|
5
|
+
from pyastar2d import Heuristic
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_small():
|
|
9
|
+
weights = np.array(
|
|
10
|
+
[
|
|
11
|
+
[1, 3, 3, 3, 3],
|
|
12
|
+
[2, 1, 3, 3, 3],
|
|
13
|
+
[2, 2, 1, 3, 3],
|
|
14
|
+
[2, 2, 2, 1, 3],
|
|
15
|
+
[2, 2, 2, 2, 1],
|
|
16
|
+
],
|
|
17
|
+
dtype=np.float32,
|
|
18
|
+
)
|
|
19
|
+
# Run down the diagonal.
|
|
20
|
+
path = pyastar2d.astar_path(weights, (0, 0), (4, 4), allow_diagonal=True)
|
|
21
|
+
expected = np.array([[0, 0], [1, 1], [2, 2], [3, 3], [4, 4]])
|
|
22
|
+
|
|
23
|
+
assert np.all(path == expected)
|
|
24
|
+
|
|
25
|
+
# Down, right, down, right, etc.
|
|
26
|
+
path = pyastar2d.astar_path(weights, (0, 0), (4, 4), allow_diagonal=False)
|
|
27
|
+
expected = np.array([[0, 0], [1, 0], [1, 1], [2, 1], [2, 2], [3, 2], [3, 3], [4, 3], [4, 4]])
|
|
28
|
+
|
|
29
|
+
assert np.all(path == expected)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_no_solution():
|
|
33
|
+
# Vertical wall.
|
|
34
|
+
weights = np.ones((5, 5), dtype=np.float32)
|
|
35
|
+
weights[:, 2] = np.inf
|
|
36
|
+
|
|
37
|
+
path = pyastar2d.astar_path(weights, (0, 0), (4, 4), allow_diagonal=True)
|
|
38
|
+
assert not path
|
|
39
|
+
|
|
40
|
+
# Horizontal wall.
|
|
41
|
+
weights = np.ones((5, 5), dtype=np.float32)
|
|
42
|
+
weights[2, :] = np.inf
|
|
43
|
+
|
|
44
|
+
path = pyastar2d.astar_path(weights, (0, 0), (4, 4), allow_diagonal=True)
|
|
45
|
+
assert not path
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_match_reverse():
|
|
49
|
+
# Might fail if there are multiple paths, but this should be rare.
|
|
50
|
+
h, w = 25, 25
|
|
51
|
+
weights = (1.0 + 5.0 * np.random.random((h, w))).astype(np.float32)
|
|
52
|
+
|
|
53
|
+
fwd = pyastar2d.astar_path(weights, (0, 0), (h - 1, w - 1))
|
|
54
|
+
rev = pyastar2d.astar_path(weights, (h - 1, w - 1), (0, 0))
|
|
55
|
+
|
|
56
|
+
assert np.all(fwd[::-1] == rev)
|
|
57
|
+
|
|
58
|
+
fwd = pyastar2d.astar_path(weights, (0, 0), (h - 1, w - 1), allow_diagonal=True)
|
|
59
|
+
rev = pyastar2d.astar_path(weights, (h - 1, w - 1), (0, 0), allow_diagonal=True)
|
|
60
|
+
|
|
61
|
+
assert np.all(fwd[::-1] == rev)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_narrow():
|
|
65
|
+
# Column weights.
|
|
66
|
+
weights = np.ones((5, 1), dtype=np.float32)
|
|
67
|
+
path = pyastar2d.astar_path(weights, (0, 0), (4, 0))
|
|
68
|
+
|
|
69
|
+
expected = np.array([[0, 0], [1, 0], [2, 0], [3, 0], [4, 0]])
|
|
70
|
+
|
|
71
|
+
assert np.all(path == expected)
|
|
72
|
+
|
|
73
|
+
# Row weights.
|
|
74
|
+
weights = np.ones((1, 5), dtype=np.float32)
|
|
75
|
+
path = pyastar2d.astar_path(weights, (0, 0), (0, 4))
|
|
76
|
+
|
|
77
|
+
expected = np.array([[0, 0], [0, 1], [0, 2], [0, 3], [0, 4]])
|
|
78
|
+
|
|
79
|
+
assert np.all(path == expected)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_bad_heuristic():
|
|
83
|
+
# For valid heuristics, the cost to move must be at least 1.
|
|
84
|
+
weights = (1.0 + 5.0 * np.random.random((10, 10))).astype(np.float32)
|
|
85
|
+
# An element smaller than 1 should raise a ValueError.
|
|
86
|
+
bad_cost = np.random.random() / 2.0
|
|
87
|
+
weights[4, 4] = bad_cost
|
|
88
|
+
|
|
89
|
+
with pytest.raises(ValueError) as exc:
|
|
90
|
+
pyastar2d.astar_path(weights, (0, 0), (9, 9))
|
|
91
|
+
assert '.f' % bad_cost in exc.value.args[0]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_invalid_start_and_goal():
|
|
95
|
+
weights = (1.0 + 5.0 * np.random.random((10, 10))).astype(np.float32)
|
|
96
|
+
# Test bad start indices.
|
|
97
|
+
with pytest.raises(ValueError) as exc:
|
|
98
|
+
pyastar2d.astar_path(weights, (-1, 0), (9, 9))
|
|
99
|
+
assert '-1' in exc.value.args[0]
|
|
100
|
+
with pytest.raises(ValueError) as exc:
|
|
101
|
+
pyastar2d.astar_path(weights, (10, 0), (9, 9))
|
|
102
|
+
assert '10' in exc.value.args[0]
|
|
103
|
+
with pytest.raises(ValueError) as exc:
|
|
104
|
+
pyastar2d.astar_path(weights, (0, -1), (9, 9))
|
|
105
|
+
assert '-1' in exc.value.args[0]
|
|
106
|
+
with pytest.raises(ValueError) as exc:
|
|
107
|
+
pyastar2d.astar_path(weights, (0, 10), (9, 9))
|
|
108
|
+
assert '10' in exc.value.args[0]
|
|
109
|
+
# Test bad goal indices.
|
|
110
|
+
with pytest.raises(ValueError) as exc:
|
|
111
|
+
pyastar2d.astar_path(weights, (0, 0), (-1, 9))
|
|
112
|
+
assert '-1' in exc.value.args[0]
|
|
113
|
+
with pytest.raises(ValueError) as exc:
|
|
114
|
+
pyastar2d.astar_path(weights, (0, 0), (10, 9))
|
|
115
|
+
assert '10' in exc.value.args[0]
|
|
116
|
+
with pytest.raises(ValueError) as exc:
|
|
117
|
+
pyastar2d.astar_path(weights, (0, 0), (0, -1))
|
|
118
|
+
assert '-1' in exc.value.args[0]
|
|
119
|
+
with pytest.raises(ValueError) as exc:
|
|
120
|
+
pyastar2d.astar_path(weights, (0, 0), (0, 10))
|
|
121
|
+
assert '10' in exc.value.args[0]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_bad_weights_dtype():
|
|
125
|
+
weights = np.array([[1, 2, 3], [1, 2, 3], [1, 2, 3]], dtype=np.float64)
|
|
126
|
+
with pytest.raises(AssertionError) as exc:
|
|
127
|
+
pyastar2d.astar_path(weights, (0, 0), (2, 2))
|
|
128
|
+
assert 'float64' in exc.value.args[0]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_orthogonal_x():
|
|
132
|
+
weights = np.ones((5, 5), dtype=np.float32)
|
|
133
|
+
path = pyastar2d.astar_path(
|
|
134
|
+
weights,
|
|
135
|
+
(0, 0),
|
|
136
|
+
(4, 4),
|
|
137
|
+
allow_diagonal=False,
|
|
138
|
+
heuristic_override=Heuristic.ORTHOGONAL_X,
|
|
139
|
+
)
|
|
140
|
+
expected = np.array([[0, 0], [1, 0], [2, 0], [2, 1], [2, 2], [2, 3], [2, 4], [3, 4], [4, 4]])
|
|
141
|
+
|
|
142
|
+
assert np.all(path == expected)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_orthogonal_y():
|
|
146
|
+
weights = np.ones((5, 5), dtype=np.float32)
|
|
147
|
+
path = pyastar2d.astar_path(
|
|
148
|
+
weights,
|
|
149
|
+
(0, 0),
|
|
150
|
+
(4, 4),
|
|
151
|
+
allow_diagonal=False,
|
|
152
|
+
heuristic_override=Heuristic.ORTHOGONAL_Y,
|
|
153
|
+
)
|
|
154
|
+
expected = np.array([[0, 0], [0, 1], [0, 2], [1, 2], [2, 2], [3, 2], [4, 2], [4, 3], [4, 4]])
|
|
155
|
+
|
|
156
|
+
assert np.all(path == expected)
|
pyastar2d-1.0.6/setup.cfg
DELETED
pyastar2d-1.0.6/setup.py
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import setuptools
|
|
2
|
-
from distutils.core import Extension
|
|
3
|
-
from setuptools import dist
|
|
4
|
-
dist.Distribution().fetch_build_eggs(["numpy"])
|
|
5
|
-
import numpy
|
|
6
|
-
|
|
7
|
-
astar_module = Extension(
|
|
8
|
-
'pyastar2d.astar', sources=['src/cpp/astar.cpp', 'src/cpp/experimental_heuristics.cpp'],
|
|
9
|
-
include_dirs=[
|
|
10
|
-
numpy.get_include(), # for numpy/arrayobject.h
|
|
11
|
-
'src/cpp' # for experimental_heuristics.h
|
|
12
|
-
],
|
|
13
|
-
extra_compile_args=["-O3", "-Wall", "-shared", "-fpic"],
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
with open("requirements.txt", "r") as fh:
|
|
18
|
-
install_requires = fh.readlines()
|
|
19
|
-
|
|
20
|
-
with open("README.md", "r") as fh:
|
|
21
|
-
long_description = fh.read()
|
|
22
|
-
|
|
23
|
-
setuptools.setup(
|
|
24
|
-
name="pyastar2d",
|
|
25
|
-
version="1.0.6",
|
|
26
|
-
author="Hendrik Weideman",
|
|
27
|
-
author_email="hjweide@gmail.com",
|
|
28
|
-
description=(
|
|
29
|
-
"A simple implementation of the A* algorithm for "
|
|
30
|
-
"path-finding on a two-dimensional grid."),
|
|
31
|
-
long_description=long_description,
|
|
32
|
-
long_description_content_type="text/markdown",
|
|
33
|
-
url="https://github.com/hjweide/pyastar2d",
|
|
34
|
-
install_requires=install_requires,
|
|
35
|
-
packages=setuptools.find_packages(where="src", exclude=("tests",)),
|
|
36
|
-
package_dir={"": "src"},
|
|
37
|
-
ext_modules=[astar_module],
|
|
38
|
-
classifiers=[
|
|
39
|
-
"Programming Language :: Python :: 3",
|
|
40
|
-
"License :: OSI Approved :: MIT License",
|
|
41
|
-
"Operating System :: OS Independent",
|
|
42
|
-
],
|
|
43
|
-
python_requires='>=3.7',
|
|
44
|
-
)
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import ctypes
|
|
2
|
-
import numpy as np
|
|
3
|
-
import pyastar2d.astar
|
|
4
|
-
from enum import IntEnum
|
|
5
|
-
from typing import Optional, Tuple
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
# Define array types
|
|
9
|
-
ndmat_f_type = np.ctypeslib.ndpointer(
|
|
10
|
-
dtype=np.float32, ndim=1, flags="C_CONTIGUOUS")
|
|
11
|
-
ndmat_i2_type = np.ctypeslib.ndpointer(
|
|
12
|
-
dtype=np.int32, ndim=2, flags="C_CONTIGUOUS")
|
|
13
|
-
|
|
14
|
-
# Define input/output types
|
|
15
|
-
pyastar2d.astar.restype = ndmat_i2_type # Nx2 (i, j) coordinates or None
|
|
16
|
-
pyastar2d.astar.argtypes = [
|
|
17
|
-
ndmat_f_type, # weights
|
|
18
|
-
ctypes.c_int, # height
|
|
19
|
-
ctypes.c_int, # width
|
|
20
|
-
ctypes.c_int, # start index in flattened grid
|
|
21
|
-
ctypes.c_int, # goal index in flattened grid
|
|
22
|
-
ctypes.c_bool, # allow diagonal
|
|
23
|
-
ctypes.c_int, # heuristic_override
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
class Heuristic(IntEnum):
|
|
27
|
-
"""The supported heuristics."""
|
|
28
|
-
|
|
29
|
-
DEFAULT = 0
|
|
30
|
-
ORTHOGONAL_X = 1
|
|
31
|
-
ORTHOGONAL_Y = 2
|
|
32
|
-
|
|
33
|
-
def astar_path(
|
|
34
|
-
weights: np.ndarray,
|
|
35
|
-
start: Tuple[int, int],
|
|
36
|
-
goal: Tuple[int, int],
|
|
37
|
-
allow_diagonal: bool = False,
|
|
38
|
-
heuristic_override: Heuristic = Heuristic.DEFAULT) -> Optional[np.ndarray]:
|
|
39
|
-
"""
|
|
40
|
-
Run astar algorithm on 2d weights.
|
|
41
|
-
|
|
42
|
-
param np.ndarray weights: A grid of weights e.g. np.ones((10, 10), dtype=np.float32)
|
|
43
|
-
param Tuple[int, int] start: (i, j)
|
|
44
|
-
param Tuple[int, int] goal: (i, j)
|
|
45
|
-
param bool allow_diagonal: Whether to allow diagonal moves
|
|
46
|
-
param Heuristic heuristic_override: Override heuristic, see Heuristic(IntEnum)
|
|
47
|
-
|
|
48
|
-
"""
|
|
49
|
-
assert weights.dtype == np.float32, (
|
|
50
|
-
f"weights must have np.float32 data type, but has {weights.dtype}"
|
|
51
|
-
)
|
|
52
|
-
# For the heuristic to be valid, each move must cost at least 1.
|
|
53
|
-
if weights.min(axis=None) < 1.:
|
|
54
|
-
raise ValueError("Minimum cost to move must be 1, but got %f" % (
|
|
55
|
-
weights.min(axis=None)))
|
|
56
|
-
# Ensure start is within bounds.
|
|
57
|
-
if (start[0] < 0 or start[0] >= weights.shape[0] or
|
|
58
|
-
start[1] < 0 or start[1] >= weights.shape[1]):
|
|
59
|
-
raise ValueError(f"Start of {start} lies outside grid.")
|
|
60
|
-
# Ensure goal is within bounds.
|
|
61
|
-
if (goal[0] < 0 or goal[0] >= weights.shape[0] or
|
|
62
|
-
goal[1] < 0 or goal[1] >= weights.shape[1]):
|
|
63
|
-
raise ValueError(f"Goal of {goal} lies outside grid.")
|
|
64
|
-
|
|
65
|
-
height, width = weights.shape
|
|
66
|
-
start_idx = np.ravel_multi_index(start, (height, width))
|
|
67
|
-
goal_idx = np.ravel_multi_index(goal, (height, width))
|
|
68
|
-
|
|
69
|
-
path = pyastar2d.astar.astar(
|
|
70
|
-
weights.flatten(), height, width, start_idx, goal_idx, allow_diagonal,
|
|
71
|
-
int(heuristic_override)
|
|
72
|
-
)
|
|
73
|
-
return path
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|