pyastar2d 1.0.6__tar.gz → 1.1.1__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.1}/PKG-INFO +42 -13
- pyastar2d-1.0.6/src/pyastar2d.egg-info/PKG-INFO → pyastar2d-1.1.1/README.md +27 -20
- pyastar2d-1.1.1/pyproject.toml +3 -0
- pyastar2d-1.1.1/requirements.txt +2 -0
- pyastar2d-1.1.1/setup.cfg +16 -0
- pyastar2d-1.1.1/setup.py +59 -0
- {pyastar2d-1.0.6 → pyastar2d-1.1.1}/src/cpp/astar.cpp +7 -5
- pyastar2d-1.1.1/src/pyastar2d/__init__.py +3 -0
- pyastar2d-1.1.1/src/pyastar2d/astar_wrapper.py +77 -0
- pyastar2d-1.0.6/README.md → pyastar2d-1.1.1/src/pyastar2d.egg-info/PKG-INFO +49 -6
- {pyastar2d-1.0.6 → pyastar2d-1.1.1}/src/pyastar2d.egg-info/SOURCES.txt +4 -1
- pyastar2d-1.1.1/src/pyastar2d.egg-info/requires.txt +2 -0
- pyastar2d-1.1.1/tests/test_astar.py +156 -0
- pyastar2d-1.0.6/requirements.txt +0 -2
- 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/src/pyastar2d.egg-info/requires.txt +0 -2
- {pyastar2d-1.0.6 → pyastar2d-1.1.1}/LICENSE +0 -0
- {pyastar2d-1.0.6 → pyastar2d-1.1.1}/MANIFEST.in +0 -0
- {pyastar2d-1.0.6 → pyastar2d-1.1.1}/src/cpp/experimental_heuristics.cpp +0 -0
- {pyastar2d-1.0.6 → pyastar2d-1.1.1}/src/cpp/experimental_heuristics.h +0 -0
- {pyastar2d-1.0.6 → pyastar2d-1.1.1}/src/pyastar2d.egg-info/dependency_links.txt +0 -0
- {pyastar2d-1.0.6 → pyastar2d-1.1.1}/src/pyastar2d.egg-info/top_level.txt +0 -0
|
@@ -1,19 +1,26 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pyastar2d
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.1
|
|
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.9
|
|
12
9
|
Description-Content-Type: text/markdown
|
|
13
10
|
License-File: LICENSE
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
Requires-Dist: imageio
|
|
12
|
+
Requires-Dist: numpy>=2.0.0
|
|
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
|
+
|
|
23
|
+
[](https://github.com/hjweide/pyastar2d/actions/workflows/python-publish.yml)
|
|
17
24
|
[](https://badge.fury.io/py/pyastar2d)
|
|
18
25
|
# PyAstar2D
|
|
19
26
|
This is a very simple C++ implementation of the A\* algorithm for pathfinding
|
|
@@ -107,7 +114,7 @@ python examples/maze_solver.py --input mazes/maze_small.png --output solns/maze_
|
|
|
107
114
|
python examples/maze_solver.py --input mazes/maze_large.png --output solns/maze_large.png
|
|
108
115
|
```
|
|
109
116
|
|
|
110
|
-
### Small Maze (1802 x 1802):
|
|
117
|
+
### Small Maze (1802 x 1802):
|
|
111
118
|
```bash
|
|
112
119
|
time python examples/maze_solver.py --input mazes/maze_small.png --output solns/maze_small.png
|
|
113
120
|
Loaded maze of shape (1802, 1802) from mazes/maze_small.png
|
|
@@ -122,7 +129,7 @@ sys 0m0.606s
|
|
|
122
129
|
The solution found for the small maze is shown below:
|
|
123
130
|
<img src="https://github.com/hjweide/pyastar2d/raw/master/solns/maze_small_soln.png" alt="Maze Small Solution" style="width: 100%"/>
|
|
124
131
|
|
|
125
|
-
### Large Maze (4002 x 4002):
|
|
132
|
+
### Large Maze (4002 x 4002):
|
|
126
133
|
```bash
|
|
127
134
|
time python examples/maze_solver.py --input mazes/maze_large.png --output solns/maze_large.png
|
|
128
135
|
Loaded maze of shape (4002, 4002) from mazes/maze_large.png
|
|
@@ -162,12 +169,34 @@ pip install -r requirements-dev.txt
|
|
|
162
169
|
```
|
|
163
170
|
before running
|
|
164
171
|
```bash
|
|
165
|
-
|
|
172
|
+
pytest
|
|
166
173
|
```
|
|
167
174
|
The tests are fairly basic but cover some of the
|
|
168
175
|
more common pitfalls. Pull requests for more extensive tests are welcome.
|
|
169
176
|
|
|
177
|
+
## Code Formatting
|
|
178
|
+
|
|
179
|
+
It's recommended that you use `pre-commit` to ensure linting procedures are
|
|
180
|
+
run on any code you write. See [pre-commit.com](https://pre-commit.com/) for
|
|
181
|
+
more information.
|
|
182
|
+
|
|
183
|
+
Reference [pre-commit's installation instructions](https://pre-commit.com/#install)
|
|
184
|
+
for software installation on your OS/platform. After you have the software
|
|
185
|
+
installed, run `pre-commit install` on the command line. Now every time you commit
|
|
186
|
+
to this project's code base the linter procedures will automatically run over the
|
|
187
|
+
changed files. To run pre-commit on files preemtively from the command line use:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
pip install -r requirements-dev.txt
|
|
191
|
+
pre-commit run --all-files
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The code base has been formatted by [Black](https://black.readthedocs.io/en/stable/).
|
|
195
|
+
Furthermore, try to conform to `PEP8`. You should set up your preferred editor to
|
|
196
|
+
use `flake8` as its Python linter, but pre-commit will ensure compliance before a
|
|
197
|
+
git commit is completed. This will use the `flake8` configuration within
|
|
198
|
+
`.flake8`, which ignores several errors and stylistic considerations.
|
|
199
|
+
|
|
170
200
|
## References
|
|
171
201
|
1. [A\* search algorithm on Wikipedia](https://en.wikipedia.org/wiki/A*_search_algorithm#Pseudocode)
|
|
172
202
|
2. [Pathfinding with A* on Red Blob Games](http://www.redblobgames.com/pathfinding/a-star/introduction.html)
|
|
173
|
-
|
|
@@ -1,19 +1,4 @@
|
|
|
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
|
-
[](https://travis-ci.com/hjweide/pyastar2d)
|
|
16
|
-
[](https://coveralls.io/github/hjweide/pyastar2d?branch=master)
|
|
1
|
+
[](https://github.com/hjweide/pyastar2d/actions/workflows/python-publish.yml)
|
|
17
2
|
[](https://badge.fury.io/py/pyastar2d)
|
|
18
3
|
# PyAstar2D
|
|
19
4
|
This is a very simple C++ implementation of the A\* algorithm for pathfinding
|
|
@@ -107,7 +92,7 @@ python examples/maze_solver.py --input mazes/maze_small.png --output solns/maze_
|
|
|
107
92
|
python examples/maze_solver.py --input mazes/maze_large.png --output solns/maze_large.png
|
|
108
93
|
```
|
|
109
94
|
|
|
110
|
-
### Small Maze (1802 x 1802):
|
|
95
|
+
### Small Maze (1802 x 1802):
|
|
111
96
|
```bash
|
|
112
97
|
time python examples/maze_solver.py --input mazes/maze_small.png --output solns/maze_small.png
|
|
113
98
|
Loaded maze of shape (1802, 1802) from mazes/maze_small.png
|
|
@@ -122,7 +107,7 @@ sys 0m0.606s
|
|
|
122
107
|
The solution found for the small maze is shown below:
|
|
123
108
|
<img src="https://github.com/hjweide/pyastar2d/raw/master/solns/maze_small_soln.png" alt="Maze Small Solution" style="width: 100%"/>
|
|
124
109
|
|
|
125
|
-
### Large Maze (4002 x 4002):
|
|
110
|
+
### Large Maze (4002 x 4002):
|
|
126
111
|
```bash
|
|
127
112
|
time python examples/maze_solver.py --input mazes/maze_large.png --output solns/maze_large.png
|
|
128
113
|
Loaded maze of shape (4002, 4002) from mazes/maze_large.png
|
|
@@ -162,12 +147,34 @@ pip install -r requirements-dev.txt
|
|
|
162
147
|
```
|
|
163
148
|
before running
|
|
164
149
|
```bash
|
|
165
|
-
|
|
150
|
+
pytest
|
|
166
151
|
```
|
|
167
152
|
The tests are fairly basic but cover some of the
|
|
168
153
|
more common pitfalls. Pull requests for more extensive tests are welcome.
|
|
169
154
|
|
|
155
|
+
## Code Formatting
|
|
156
|
+
|
|
157
|
+
It's recommended that you use `pre-commit` to ensure linting procedures are
|
|
158
|
+
run on any code you write. See [pre-commit.com](https://pre-commit.com/) for
|
|
159
|
+
more information.
|
|
160
|
+
|
|
161
|
+
Reference [pre-commit's installation instructions](https://pre-commit.com/#install)
|
|
162
|
+
for software installation on your OS/platform. After you have the software
|
|
163
|
+
installed, run `pre-commit install` on the command line. Now every time you commit
|
|
164
|
+
to this project's code base the linter procedures will automatically run over the
|
|
165
|
+
changed files. To run pre-commit on files preemtively from the command line use:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
pip install -r requirements-dev.txt
|
|
169
|
+
pre-commit run --all-files
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The code base has been formatted by [Black](https://black.readthedocs.io/en/stable/).
|
|
173
|
+
Furthermore, try to conform to `PEP8`. You should set up your preferred editor to
|
|
174
|
+
use `flake8` as its Python linter, but pre-commit will ensure compliance before a
|
|
175
|
+
git commit is completed. This will use the `flake8` configuration within
|
|
176
|
+
`.flake8`, which ignores several errors and stylistic considerations.
|
|
177
|
+
|
|
170
178
|
## References
|
|
171
179
|
1. [A\* search algorithm on Wikipedia](https://en.wikipedia.org/wiki/A*_search_algorithm#Pseudocode)
|
|
172
180
|
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.1/setup.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
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', '0x03090000'),
|
|
31
|
+
('NPY_NO_DEPRECATED_API', 'NPY_2_0_API_VERSION'),
|
|
32
|
+
('NPY_TARGET_VERSION', 'NPY_2_0_API_VERSION'),
|
|
33
|
+
],
|
|
34
|
+
py_limited_api=True,
|
|
35
|
+
include_dirs=[
|
|
36
|
+
'src/cpp',
|
|
37
|
+
get_numpy_include(),
|
|
38
|
+
],
|
|
39
|
+
extra_compile_args=['-O3', '-fpic', '-Wall'],
|
|
40
|
+
language='c++',
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Define package metadata
|
|
44
|
+
setup(
|
|
45
|
+
name='pyastar2d',
|
|
46
|
+
version='1.1.1',
|
|
47
|
+
author='Hendrik Weideman',
|
|
48
|
+
author_email='hjweide@gmail.com',
|
|
49
|
+
description='A simple implementation of the A* algorithm for path-finding on a two-dimensional grid.',
|
|
50
|
+
long_description=long_description,
|
|
51
|
+
long_description_content_type='text/markdown',
|
|
52
|
+
url='https://github.com/hjweide/pyastar2d',
|
|
53
|
+
packages=find_packages(where='src', exclude=('tests',)),
|
|
54
|
+
package_dir={'': 'src'},
|
|
55
|
+
install_requires=install_requires,
|
|
56
|
+
python_requires='>=3.9',
|
|
57
|
+
ext_modules=[astar_module],
|
|
58
|
+
options={'bdist_wheel': {'py_limited_api': 'cp39'}},
|
|
59
|
+
)
|
|
@@ -44,7 +44,7 @@ inline float l1_norm(int i0, int j0, int i1, int j1) {
|
|
|
44
44
|
// diag_ok: if true, allows diagonal moves (8-conn.)
|
|
45
45
|
// paths (output): for each node, stores previous node in path
|
|
46
46
|
static PyObject *astar(PyObject *self, PyObject *args) {
|
|
47
|
-
|
|
47
|
+
PyArrayObject* weights_object;
|
|
48
48
|
int h;
|
|
49
49
|
int w;
|
|
50
50
|
int start;
|
|
@@ -61,7 +61,7 @@ static PyObject *astar(PyObject *self, PyObject *args) {
|
|
|
61
61
|
))
|
|
62
62
|
return NULL;
|
|
63
63
|
|
|
64
|
-
float* weights = (float*) weights_object
|
|
64
|
+
float* weights = (float*) PyArray_DATA(weights_object);
|
|
65
65
|
int* paths = new int[h * w];
|
|
66
66
|
int path_length = -1;
|
|
67
67
|
|
|
@@ -76,7 +76,7 @@ static PyObject *astar(PyObject *self, PyObject *args) {
|
|
|
76
76
|
nodes_to_visit.push(start_node);
|
|
77
77
|
|
|
78
78
|
int* nbrs = new int[8];
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
int goal_i = goal / w;
|
|
81
81
|
int goal_j = goal % w;
|
|
82
82
|
int start_i = start / w;
|
|
@@ -141,11 +141,13 @@ static PyObject *astar(PyObject *self, PyObject *args) {
|
|
|
141
141
|
if (path_length >= 0) {
|
|
142
142
|
npy_intp dims[2] = {path_length, 2};
|
|
143
143
|
PyArrayObject* path = (PyArrayObject*) PyArray_SimpleNew(2, dims, NPY_INT32);
|
|
144
|
+
char* data = (char*) PyArray_BYTES(path);
|
|
145
|
+
npy_intp* strides = PyArray_STRIDES(path);
|
|
144
146
|
npy_int32 *iptr, *jptr;
|
|
145
147
|
int idx = goal;
|
|
146
148
|
for (npy_intp i = dims[0] - 1; i >= 0; --i) {
|
|
147
|
-
iptr = (npy_int32*) (
|
|
148
|
-
jptr = (npy_int32*) (
|
|
149
|
+
iptr = (npy_int32*) (data + i * strides[0]);
|
|
150
|
+
jptr = (npy_int32*) (data + i * strides[0] + strides[1]);
|
|
149
151
|
|
|
150
152
|
*iptr = idx / w;
|
|
151
153
|
*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,5 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyastar2d
|
|
3
|
+
Version: 1.1.1
|
|
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.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: imageio
|
|
12
|
+
Requires-Dist: numpy>=2.0.0
|
|
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
|
+
|
|
23
|
+
[](https://github.com/hjweide/pyastar2d/actions/workflows/python-publish.yml)
|
|
3
24
|
[](https://badge.fury.io/py/pyastar2d)
|
|
4
25
|
# PyAstar2D
|
|
5
26
|
This is a very simple C++ implementation of the A\* algorithm for pathfinding
|
|
@@ -93,7 +114,7 @@ python examples/maze_solver.py --input mazes/maze_small.png --output solns/maze_
|
|
|
93
114
|
python examples/maze_solver.py --input mazes/maze_large.png --output solns/maze_large.png
|
|
94
115
|
```
|
|
95
116
|
|
|
96
|
-
### Small Maze (1802 x 1802):
|
|
117
|
+
### Small Maze (1802 x 1802):
|
|
97
118
|
```bash
|
|
98
119
|
time python examples/maze_solver.py --input mazes/maze_small.png --output solns/maze_small.png
|
|
99
120
|
Loaded maze of shape (1802, 1802) from mazes/maze_small.png
|
|
@@ -108,7 +129,7 @@ sys 0m0.606s
|
|
|
108
129
|
The solution found for the small maze is shown below:
|
|
109
130
|
<img src="https://github.com/hjweide/pyastar2d/raw/master/solns/maze_small_soln.png" alt="Maze Small Solution" style="width: 100%"/>
|
|
110
131
|
|
|
111
|
-
### Large Maze (4002 x 4002):
|
|
132
|
+
### Large Maze (4002 x 4002):
|
|
112
133
|
```bash
|
|
113
134
|
time python examples/maze_solver.py --input mazes/maze_large.png --output solns/maze_large.png
|
|
114
135
|
Loaded maze of shape (4002, 4002) from mazes/maze_large.png
|
|
@@ -148,12 +169,34 @@ pip install -r requirements-dev.txt
|
|
|
148
169
|
```
|
|
149
170
|
before running
|
|
150
171
|
```bash
|
|
151
|
-
|
|
172
|
+
pytest
|
|
152
173
|
```
|
|
153
174
|
The tests are fairly basic but cover some of the
|
|
154
175
|
more common pitfalls. Pull requests for more extensive tests are welcome.
|
|
155
176
|
|
|
177
|
+
## Code Formatting
|
|
178
|
+
|
|
179
|
+
It's recommended that you use `pre-commit` to ensure linting procedures are
|
|
180
|
+
run on any code you write. See [pre-commit.com](https://pre-commit.com/) for
|
|
181
|
+
more information.
|
|
182
|
+
|
|
183
|
+
Reference [pre-commit's installation instructions](https://pre-commit.com/#install)
|
|
184
|
+
for software installation on your OS/platform. After you have the software
|
|
185
|
+
installed, run `pre-commit install` on the command line. Now every time you commit
|
|
186
|
+
to this project's code base the linter procedures will automatically run over the
|
|
187
|
+
changed files. To run pre-commit on files preemtively from the command line use:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
pip install -r requirements-dev.txt
|
|
191
|
+
pre-commit run --all-files
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The code base has been formatted by [Black](https://black.readthedocs.io/en/stable/).
|
|
195
|
+
Furthermore, try to conform to `PEP8`. You should set up your preferred editor to
|
|
196
|
+
use `flake8` as its Python linter, but pre-commit will ensure compliance before a
|
|
197
|
+
git commit is completed. This will use the `flake8` configuration within
|
|
198
|
+
`.flake8`, which ignores several errors and stylistic considerations.
|
|
199
|
+
|
|
156
200
|
## References
|
|
157
201
|
1. [A\* search algorithm on Wikipedia](https://en.wikipedia.org/wiki/A*_search_algorithm#Pseudocode)
|
|
158
202
|
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/requirements.txt
DELETED
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
|