Skip to content

Commit 84460a8

Browse files
Merge pull request #2 from MarekSuchanek/speedup
Speedup via Cython
2 parents 7136f19 + 028d3da commit 84460a8

File tree

9 files changed

+201
-59
lines changed

9 files changed

+201
-59
lines changed

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,13 @@ ENV/
9090

9191
# PyCharm
9292
.idea
93+
94+
# MI-PYT tests
95+
tests2
96+
97+
# Own
98+
maze.c
99+
maze.cpp
100+
maze.html
101+
generate.py
102+
random.csv

.travis.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,13 @@ install:
55
- pip install -U pip
66
- pip install -r requirements.txt
77
script:
8+
- python setup.py develop
89
- python -m pytest
10+
addons:
11+
apt:
12+
sources:
13+
- ubuntu-toolchain-r-test
14+
packages:
15+
- gcc-4.8
16+
- g++-4.8
17+
- python3-dev

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ have this version of Python). Then you can install dependencies via:
3535
pip install -r requirements.txt
3636
```
3737

38+
For speedup is analysis written in Cython so you need to compile it with
39+
via:
40+
41+
```
42+
python setup.py develop
43+
```
44+
45+
or
46+
47+
```
48+
python setup.py install
49+
```
50+
3851
## Usage
3952

4053
```
@@ -65,7 +78,7 @@ maze.NoPathExistsException
6578

6679
## Testing
6780

68-
After installing dependencies you can run tests with `pytest`:
81+
After installing dependencies and compilation you can run tests with `pytest`:
6982

7083
```
7184
python -m pytest

generate.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from random import randint
2+
3+
if __name__ == '__main__':
4+
w = 1000
5+
h = 1000
6+
min_val = -1000
7+
max_val = 2000
8+
for i in range(h):
9+
for j in range(w-1):
10+
if randint(0,10) < 8:
11+
print(randint(min_val, max_val), end=',')
12+
else:
13+
print(1, end=',')
14+
print(randint(min_val, max_val))

maze.py

Lines changed: 0 additions & 56 deletions
This file was deleted.

maze.pyx

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# distutils: language=c++
2+
import numpy as np
3+
cimport numpy as np
4+
import cython
5+
from libcpp cimport bool
6+
from libcpp.queue cimport queue
7+
from libcpp.vector cimport vector
8+
from libcpp.pair cimport pair
9+
from libcpp.map cimport map
10+
11+
cdef struct coords:
12+
int x
13+
int y
14+
15+
cdef struct qitem:
16+
int x
17+
int y
18+
int distance
19+
20+
21+
@cython.boundscheck(False)
22+
@cython.wraparound(False)
23+
cdef bool directions2reachable(np.ndarray[np.int8_t, ndim=2] directions, int w, int h):
24+
with cython.nogil:
25+
for x in range(w):
26+
for y in range(h):
27+
with cython.gil:
28+
if directions[x, y] == ord(b' '):
29+
return False
30+
return True
31+
32+
33+
@cython.boundscheck(False)
34+
@cython.wraparound(False)
35+
cpdef flood(np.ndarray[np.int8_t, ndim=2] maze, int w, int h):
36+
cdef int x, y, i
37+
cdef queue[qitem] q
38+
cdef np.ndarray[np.int32_t, ndim=2] distances
39+
cdef np.ndarray[np.int8_t, ndim=2] directions
40+
distances = np.full((w, h), -1, dtype='int32')
41+
directions = np.full((w, h), b' ', dtype=('a', 1))
42+
43+
with cython.nogil:
44+
for x in range(w):
45+
for y in range(h):
46+
with cython.gil:
47+
if maze[x, y] < 0:
48+
directions[x, y] = b'#'
49+
elif maze[x, y] == 1:
50+
q.push(qitem(x, y, 0))
51+
distances[x, y] = 0
52+
directions[x, y] = b'X'
53+
54+
cdef coords *dir_offsets = [coords(1, 0), coords(-1, 0), coords(0, 1), coords(0, -1)]
55+
cdef np.int8_t *dir_chars = [b'^', b'v', b'<', b'>']
56+
while not q.empty():
57+
item = q.front()
58+
q.pop()
59+
for i in range(4):
60+
offset = dir_offsets[i]
61+
x = item.x + offset.x
62+
y = item.y + offset.y
63+
if 0 <= x < w and 0 <= y < h:
64+
if maze[x, y] >= 0 and distances[x, y] == -1:
65+
distances[x, y] = item.distance+1
66+
directions[x, y] = dir_chars[i]
67+
q.push(qitem(x, y, item.distance+1))
68+
69+
return distances, directions, directions2reachable(directions, w, h)
70+
71+
72+
@cython.boundscheck(False)
73+
@cython.wraparound(False)
74+
cpdef build_path(np.ndarray[np.int8_t, ndim=2] directions, int row, int column):
75+
if directions[row, column] == b'#' or directions[row, column] == b' ':
76+
raise NoPathExistsException
77+
cdef vector[pair[int,int]] path
78+
cdef map[char,coords] dirs
79+
dirs.insert(pair[char,coords](b'v', coords(1, 0)))
80+
dirs.insert(pair[char,coords](b'^', coords(-1, 0)))
81+
dirs.insert(pair[char,coords](b'>', coords(0, 1)))
82+
dirs.insert(pair[char,coords](b'<', coords(0, -1)))
83+
path.push_back(pair[int,int](row, column))
84+
while directions[row, column] != b'X':
85+
d = dirs[directions[row, column]]
86+
row += d.x
87+
column += d.y
88+
path.push_back(pair[int,int](row, column))
89+
return path
90+
91+
92+
cdef class NoPathExistsException(Exception):
93+
pass
94+
95+
96+
class MazeAnalysis:
97+
98+
def __init__(self, maze):
99+
maze = np.atleast_2d(maze.astype('int8')) # fix matrix type & dims
100+
self.distances, self.directions, self.is_reachable = flood(maze, *maze.shape)
101+
102+
def path(self, row, column):
103+
return build_path(self.directions, row, column)
104+
105+
106+
cpdef analyze(maze):
107+
return MazeAnalysis(maze)

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
numpy>==1.11.2
1+
numpy>=1.11.2
22
py>=1.4.31
33
pytest>=3.0.4
4+
Cython>=0.25.1

setup.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from setuptools import setup, find_packages
2+
from Cython.Build import cythonize
3+
import numpy
4+
5+
with open('README.md') as f:
6+
long_description = ''.join(f.readlines())
7+
8+
setup(
9+
name='maze',
10+
version=0.2,
11+
keywords='maze analysis matrix cython',
12+
description='Simple python maze analyzer for finding shortest path',
13+
long_description=long_description,
14+
author='Marek Suchánek',
15+
author_email='[email protected]',
16+
license='MIT',
17+
packages=find_packages(),
18+
ext_modules=cythonize(
19+
'maze.pyx',
20+
language_level=3,
21+
include_dirs=[numpy.get_include()],
22+
language="c++"
23+
),
24+
include_dirs=[numpy.get_include()],
25+
install_requires=[
26+
'Cython>=0.25.1',
27+
'numpy>=1.11.2',
28+
'py>=1.4.31',
29+
],
30+
setup_requires=[
31+
'pytest-runner'
32+
],
33+
tests_require=[
34+
'pytest',
35+
],
36+
classifiers=[
37+
'License :: OSI Approved :: MIT License',
38+
'Programming Language :: Python',
39+
'Programming Language :: Python :: Implementation :: CPython',
40+
'Programming Language :: Python :: 3',
41+
'Programming Language :: Python :: 3.5',
42+
'Topic :: Scientific/Engineering :: Mathematics',
43+
],
44+
)

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def load_mazes(folder):
1515
path = os.path.join(folder, name)
1616
if os.path.isfile(path) and name.endswith('.csv'):
1717
maze_num = int(name[:-4])
18-
result[maze_num] = np.loadtxt(path, delimiter=',')
18+
result[maze_num] = np.loadtxt(path, delimiter=',', dtype='int8')
1919
return result
2020

2121

0 commit comments

Comments
 (0)