pyastar2d 1.0.5__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.1.0/MANIFEST.in +2 -0
- {pyastar2d-1.0.5 → pyastar2d-1.1.0}/PKG-INFO +40 -14
- pyastar2d-1.0.5/src/pyastar2d.egg-info/PKG-INFO → pyastar2d-1.1.0/README.md +26 -22
- 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.5 → pyastar2d-1.1.0}/src/cpp/astar.cpp +8 -5
- pyastar2d-1.1.0/src/cpp/experimental_heuristics.h +20 -0
- pyastar2d-1.1.0/src/pyastar2d/__init__.py +3 -0
- pyastar2d-1.1.0/src/pyastar2d/astar_wrapper.py +77 -0
- pyastar2d-1.0.5/README.md → pyastar2d-1.1.0/src/pyastar2d.egg-info/PKG-INFO +48 -4
- {pyastar2d-1.0.5 → pyastar2d-1.1.0}/src/pyastar2d.egg-info/SOURCES.txt +5 -1
- pyastar2d-1.1.0/tests/test_astar.py +156 -0
- pyastar2d-1.0.5/MANIFEST.in +0 -1
- pyastar2d-1.0.5/setup.cfg +0 -4
- pyastar2d-1.0.5/setup.py +0 -44
- pyastar2d-1.0.5/src/pyastar2d/__init__.py +0 -2
- pyastar2d-1.0.5/src/pyastar2d/astar_wrapper.py +0 -73
- {pyastar2d-1.0.5 → pyastar2d-1.1.0}/LICENSE +0 -0
- {pyastar2d-1.0.5 → pyastar2d-1.1.0}/requirements.txt +0 -0
- {pyastar2d-1.0.5 → pyastar2d-1.1.0}/src/cpp/experimental_heuristics.cpp +0 -0
- {pyastar2d-1.0.5 → pyastar2d-1.1.0}/src/pyastar2d.egg-info/dependency_links.txt +0 -0
- {pyastar2d-1.0.5 → pyastar2d-1.1.0}/src/pyastar2d.egg-info/requires.txt +0 -0
- {pyastar2d-1.0.5 → pyastar2d-1.1.0}/src/pyastar2d.egg-info/top_level.txt +0 -0
|
@@ -1,18 +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
|
-
Platform: UNKNOWN
|
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
-
Classifier: Operating System :: OS Independent
|
|
13
|
-
Requires-Python: >=3.7
|
|
8
|
+
Requires-Python: >=3.8
|
|
14
9
|
Description-Content-Type: text/markdown
|
|
15
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
|
|
16
22
|
|
|
17
23
|
[](https://travis-ci.com/hjweide/pyastar2d)
|
|
18
24
|
[](https://coveralls.io/github/hjweide/pyastar2d?branch=master)
|
|
@@ -109,7 +115,7 @@ python examples/maze_solver.py --input mazes/maze_small.png --output solns/maze_
|
|
|
109
115
|
python examples/maze_solver.py --input mazes/maze_large.png --output solns/maze_large.png
|
|
110
116
|
```
|
|
111
117
|
|
|
112
|
-
### Small Maze (1802 x 1802):
|
|
118
|
+
### Small Maze (1802 x 1802):
|
|
113
119
|
```bash
|
|
114
120
|
time python examples/maze_solver.py --input mazes/maze_small.png --output solns/maze_small.png
|
|
115
121
|
Loaded maze of shape (1802, 1802) from mazes/maze_small.png
|
|
@@ -124,7 +130,7 @@ sys 0m0.606s
|
|
|
124
130
|
The solution found for the small maze is shown below:
|
|
125
131
|
<img src="https://github.com/hjweide/pyastar2d/raw/master/solns/maze_small_soln.png" alt="Maze Small Solution" style="width: 100%"/>
|
|
126
132
|
|
|
127
|
-
### Large Maze (4002 x 4002):
|
|
133
|
+
### Large Maze (4002 x 4002):
|
|
128
134
|
```bash
|
|
129
135
|
time python examples/maze_solver.py --input mazes/maze_large.png --output solns/maze_large.png
|
|
130
136
|
Loaded maze of shape (4002, 4002) from mazes/maze_large.png
|
|
@@ -164,14 +170,34 @@ pip install -r requirements-dev.txt
|
|
|
164
170
|
```
|
|
165
171
|
before running
|
|
166
172
|
```bash
|
|
167
|
-
|
|
173
|
+
pytest
|
|
168
174
|
```
|
|
169
175
|
The tests are fairly basic but cover some of the
|
|
170
176
|
more common pitfalls. Pull requests for more extensive tests are welcome.
|
|
171
177
|
|
|
172
|
-
##
|
|
173
|
-
1. [A\* search algorithm on Wikipedia](https://en.wikipedia.org/wiki/A*_search_algorithm#Pseudocode)
|
|
174
|
-
2. [Pathfinding with A* on Red Blob Games](http://www.redblobgames.com/pathfinding/a-star/introduction.html)
|
|
178
|
+
## Code Formatting
|
|
175
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.
|
|
176
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:
|
|
177
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
|
+
|
|
201
|
+
## References
|
|
202
|
+
1. [A\* search algorithm on Wikipedia](https://en.wikipedia.org/wiki/A*_search_algorithm#Pseudocode)
|
|
203
|
+
2. [Pathfinding with A* on Red Blob Games](http://www.redblobgames.com/pathfinding/a-star/introduction.html)
|
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: pyastar2d
|
|
3
|
-
Version: 1.0.5
|
|
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
|
-
License: UNKNOWN
|
|
9
|
-
Platform: UNKNOWN
|
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
-
Classifier: Operating System :: OS Independent
|
|
13
|
-
Requires-Python: >=3.7
|
|
14
|
-
Description-Content-Type: text/markdown
|
|
15
|
-
License-File: LICENSE
|
|
16
|
-
|
|
17
1
|
[](https://travis-ci.com/hjweide/pyastar2d)
|
|
18
2
|
[](https://coveralls.io/github/hjweide/pyastar2d?branch=master)
|
|
19
3
|
[](https://badge.fury.io/py/pyastar2d)
|
|
@@ -109,7 +93,7 @@ python examples/maze_solver.py --input mazes/maze_small.png --output solns/maze_
|
|
|
109
93
|
python examples/maze_solver.py --input mazes/maze_large.png --output solns/maze_large.png
|
|
110
94
|
```
|
|
111
95
|
|
|
112
|
-
### Small Maze (1802 x 1802):
|
|
96
|
+
### Small Maze (1802 x 1802):
|
|
113
97
|
```bash
|
|
114
98
|
time python examples/maze_solver.py --input mazes/maze_small.png --output solns/maze_small.png
|
|
115
99
|
Loaded maze of shape (1802, 1802) from mazes/maze_small.png
|
|
@@ -124,7 +108,7 @@ sys 0m0.606s
|
|
|
124
108
|
The solution found for the small maze is shown below:
|
|
125
109
|
<img src="https://github.com/hjweide/pyastar2d/raw/master/solns/maze_small_soln.png" alt="Maze Small Solution" style="width: 100%"/>
|
|
126
110
|
|
|
127
|
-
### Large Maze (4002 x 4002):
|
|
111
|
+
### Large Maze (4002 x 4002):
|
|
128
112
|
```bash
|
|
129
113
|
time python examples/maze_solver.py --input mazes/maze_large.png --output solns/maze_large.png
|
|
130
114
|
Loaded maze of shape (4002, 4002) from mazes/maze_large.png
|
|
@@ -164,14 +148,34 @@ pip install -r requirements-dev.txt
|
|
|
164
148
|
```
|
|
165
149
|
before running
|
|
166
150
|
```bash
|
|
167
|
-
|
|
151
|
+
pytest
|
|
168
152
|
```
|
|
169
153
|
The tests are fairly basic but cover some of the
|
|
170
154
|
more common pitfalls. Pull requests for more extensive tests are welcome.
|
|
171
155
|
|
|
172
|
-
##
|
|
173
|
-
|
|
174
|
-
|
|
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:
|
|
175
167
|
|
|
168
|
+
```bash
|
|
169
|
+
pip install -r requirements-dev.txt
|
|
170
|
+
pre-commit run --all-files
|
|
171
|
+
```
|
|
176
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.
|
|
177
178
|
|
|
179
|
+
## References
|
|
180
|
+
1. [A\* search algorithm on Wikipedia](https://en.wikipedia.org/wiki/A*_search_algorithm#Pseudocode)
|
|
181
|
+
2. [Pathfinding with A* on Red Blob Games](http://www.redblobgames.com/pathfinding/a-star/introduction.html)
|
|
@@ -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,20 @@
|
|
|
1
|
+
// Please note below heuristics are experimental and only for pretty lines.
|
|
2
|
+
// They may not take the shortest path and require additional cpu cycles.
|
|
3
|
+
|
|
4
|
+
#ifndef EXPERIMENTAL_HEURISTICS_H_
|
|
5
|
+
#define EXPERIMENTAL_HEURISTICS_H_
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
enum Heuristic { DEFAULT, ORTHOGONAL_X, ORTHOGONAL_Y };
|
|
9
|
+
|
|
10
|
+
typedef float (*heuristic_ptr)(int, int, int, int, int, int);
|
|
11
|
+
|
|
12
|
+
heuristic_ptr select_heuristic(int);
|
|
13
|
+
|
|
14
|
+
// Orthogonal x (moves by x first, then half way by y)
|
|
15
|
+
float orthogonal_x(int, int, int, int, int, int);
|
|
16
|
+
|
|
17
|
+
// Orthogonal y (moves by y first, then half way by x)
|
|
18
|
+
float orthogonal_y(int, int, int, int, int, int);
|
|
19
|
+
|
|
20
|
+
#endif
|
|
@@ -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,14 +1,18 @@
|
|
|
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
|
|
10
|
+
src/cpp/experimental_heuristics.h
|
|
8
11
|
src/pyastar2d/__init__.py
|
|
9
12
|
src/pyastar2d/astar_wrapper.py
|
|
10
13
|
src/pyastar2d.egg-info/PKG-INFO
|
|
11
14
|
src/pyastar2d.egg-info/SOURCES.txt
|
|
12
15
|
src/pyastar2d.egg-info/dependency_links.txt
|
|
13
16
|
src/pyastar2d.egg-info/requires.txt
|
|
14
|
-
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.5/MANIFEST.in
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
include requirements.txt
|
pyastar2d-1.0.5/setup.cfg
DELETED
pyastar2d-1.0.5/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.5",
|
|
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
|