Skip to content

Commit

Permalink
Merge pull request #2 from MarekSuchanek/speedup
Browse files Browse the repository at this point in the history
Speedup via Cython
  • Loading branch information
MarekSuchanek authored Dec 5, 2016
2 parents 7136f19 + 028d3da commit 84460a8
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 59 deletions.
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,13 @@ ENV/

# PyCharm
.idea

# MI-PYT tests
tests2

# Own
maze.c
maze.cpp
maze.html
generate.py
random.csv
9 changes: 9 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,13 @@ install:
- pip install -U pip
- pip install -r requirements.txt
script:
- python setup.py develop
- python -m pytest
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- gcc-4.8
- g++-4.8
- python3-dev
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ have this version of Python). Then you can install dependencies via:
pip install -r requirements.txt
```

For speedup is analysis written in Cython so you need to compile it with
via:

```
python setup.py develop
```

or

```
python setup.py install
```

## Usage

```
Expand Down Expand Up @@ -65,7 +78,7 @@ maze.NoPathExistsException

## Testing

After installing dependencies you can run tests with `pytest`:
After installing dependencies and compilation you can run tests with `pytest`:

```
python -m pytest
Expand Down
14 changes: 14 additions & 0 deletions generate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from random import randint

if __name__ == '__main__':
w = 1000
h = 1000
min_val = -1000
max_val = 2000
for i in range(h):
for j in range(w-1):
if randint(0,10) < 8:
print(randint(min_val, max_val), end=',')
else:
print(1, end=',')
print(randint(min_val, max_val))
56 changes: 0 additions & 56 deletions maze.py

This file was deleted.

107 changes: 107 additions & 0 deletions maze.pyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# distutils: language=c++
import numpy as np
cimport numpy as np
import cython
from libcpp cimport bool
from libcpp.queue cimport queue
from libcpp.vector cimport vector
from libcpp.pair cimport pair
from libcpp.map cimport map

cdef struct coords:
int x
int y

cdef struct qitem:
int x
int y
int distance


@cython.boundscheck(False)
@cython.wraparound(False)
cdef bool directions2reachable(np.ndarray[np.int8_t, ndim=2] directions, int w, int h):
with cython.nogil:
for x in range(w):
for y in range(h):
with cython.gil:
if directions[x, y] == ord(b' '):
return False
return True


@cython.boundscheck(False)
@cython.wraparound(False)
cpdef flood(np.ndarray[np.int8_t, ndim=2] maze, int w, int h):
cdef int x, y, i
cdef queue[qitem] q
cdef np.ndarray[np.int32_t, ndim=2] distances
cdef np.ndarray[np.int8_t, ndim=2] directions
distances = np.full((w, h), -1, dtype='int32')
directions = np.full((w, h), b' ', dtype=('a', 1))

with cython.nogil:
for x in range(w):
for y in range(h):
with cython.gil:
if maze[x, y] < 0:
directions[x, y] = b'#'
elif maze[x, y] == 1:
q.push(qitem(x, y, 0))
distances[x, y] = 0
directions[x, y] = b'X'

cdef coords *dir_offsets = [coords(1, 0), coords(-1, 0), coords(0, 1), coords(0, -1)]
cdef np.int8_t *dir_chars = [b'^', b'v', b'<', b'>']
while not q.empty():
item = q.front()
q.pop()
for i in range(4):
offset = dir_offsets[i]
x = item.x + offset.x
y = item.y + offset.y
if 0 <= x < w and 0 <= y < h:
if maze[x, y] >= 0 and distances[x, y] == -1:
distances[x, y] = item.distance+1
directions[x, y] = dir_chars[i]
q.push(qitem(x, y, item.distance+1))

return distances, directions, directions2reachable(directions, w, h)


@cython.boundscheck(False)
@cython.wraparound(False)
cpdef build_path(np.ndarray[np.int8_t, ndim=2] directions, int row, int column):
if directions[row, column] == b'#' or directions[row, column] == b' ':
raise NoPathExistsException
cdef vector[pair[int,int]] path
cdef map[char,coords] dirs
dirs.insert(pair[char,coords](b'v', coords(1, 0)))
dirs.insert(pair[char,coords](b'^', coords(-1, 0)))
dirs.insert(pair[char,coords](b'>', coords(0, 1)))
dirs.insert(pair[char,coords](b'<', coords(0, -1)))
path.push_back(pair[int,int](row, column))
while directions[row, column] != b'X':
d = dirs[directions[row, column]]
row += d.x
column += d.y
path.push_back(pair[int,int](row, column))
return path


cdef class NoPathExistsException(Exception):
pass


class MazeAnalysis:

def __init__(self, maze):
maze = np.atleast_2d(maze.astype('int8')) # fix matrix type & dims
self.distances, self.directions, self.is_reachable = flood(maze, *maze.shape)

def path(self, row, column):
return build_path(self.directions, row, column)


cpdef analyze(maze):
return MazeAnalysis(maze)
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
numpy>==1.11.2
numpy>=1.11.2
py>=1.4.31
pytest>=3.0.4
Cython>=0.25.1
44 changes: 44 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from setuptools import setup, find_packages
from Cython.Build import cythonize
import numpy

with open('README.md') as f:
long_description = ''.join(f.readlines())

setup(
name='maze',
version=0.2,
keywords='maze analysis matrix cython',
description='Simple python maze analyzer for finding shortest path',
long_description=long_description,
author='Marek Suchánek',
author_email='[email protected]',
license='MIT',
packages=find_packages(),
ext_modules=cythonize(
'maze.pyx',
language_level=3,
include_dirs=[numpy.get_include()],
language="c++"
),
include_dirs=[numpy.get_include()],
install_requires=[
'Cython>=0.25.1',
'numpy>=1.11.2',
'py>=1.4.31',
],
setup_requires=[
'pytest-runner'
],
tests_require=[
'pytest',
],
classifiers=[
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Topic :: Scientific/Engineering :: Mathematics',
],
)
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def load_mazes(folder):
path = os.path.join(folder, name)
if os.path.isfile(path) and name.endswith('.csv'):
maze_num = int(name[:-4])
result[maze_num] = np.loadtxt(path, delimiter=',')
result[maze_num] = np.loadtxt(path, delimiter=',', dtype='int8')
return result


Expand Down

0 comments on commit 84460a8

Please sign in to comment.