Skip to content

Commit 986676a

Browse files
authored
Merge pull request #41 from Tjstretchalot/master
Add dijkstra pathfinding
2 parents c988139 + 73d8dff commit 986676a

File tree

10 files changed

+427
-13
lines changed

10 files changed

+427
-13
lines changed

CONTIRBUTORS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@
1212
- [dstark85](https://github.com/dstark85)
1313
- Songzhuozhuo '[souo](https://github.com/souo)'
1414
- Emil '[Skeen](https://github.com/Skeen)' Madsen
15-
- Ian '[IanDoarn](https://github.com/IanDoarn)' Doarn
15+
- Ian '[IanDoarn](https://github.com/IanDoarn)' Doarn
16+
- Timothy '[Tjstretchalot](https://github.com/Tjstretchalot)' Moore

docs/Data_Structure.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,12 @@ Graph
172172
.. autoclass:: WeightedGraph
173173
:members:
174174

175+
176+
Weighted Undirected Graph
177+
-------------------------
178+
.. autoclass:: WeightedUndirectedGraph
179+
:members:
180+
175181

176182
Topological Sort
177183
----------------
@@ -190,6 +196,7 @@ Graph
190196
.. autoclass:: CheckCycleUndirectedGraph
191197
:members:
192198

199+
193200
Heap
194201
----
195202

docs/Pathing.rst

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
====
2+
Pathing
3+
====
4+
5+
Some pathfinding algorithms and their implementations
6+
7+
Quick Start Guide
8+
-----------------
9+
10+
.. code-block:: python
11+
12+
# import the required pathing algorithm
13+
from pygorithm.pathing import dijkstra
14+
15+
# import a graph data structure
16+
from pygorithm.data_structures import graph
17+
18+
# initialize the graph with nodes from (0, 0) to (4, 4)
19+
# with weight corresponding to distance (orthogonal
20+
# is 1, diagonal is sqrt(2))
21+
my_graph = graph.WeightedUndirectedGraph()
22+
my_graph.gridify(5, 1)
23+
24+
# make the graph more interesting by removing along the
25+
# x=2 column except for (2,4)
26+
my_graph.remove_edge((2, 0))
27+
my_graph.remove_edge((2, 1))
28+
my_graph.remove_edge((2, 2))
29+
my_graph.remove_edge((2, 3))
30+
31+
# calculate a path
32+
my_path = dijkstra.find_path(my_graph, (0, 0), (3, 0))
33+
34+
# print path
35+
print(' -> '.join(my_path))
36+
# (0, 0) -> (1, 1) -> (0, 2) -> (1, 3) -> (2, 4) -> (3, 3) -> (3, 2) -> (3, 1) -> (3, 0)
37+
38+
Features
39+
--------
40+
41+
* Algorithms available:
42+
- Dijkstra (dijkstra)
43+
44+
45+
* To see all the available functions in a module there is a `modules()` function available. For example,
46+
47+
.. code:: python
48+
49+
>>> from pygorithm.pathfinding import modules
50+
>>> modules.modules()
51+
['dijkstra']
52+
53+
* Get the code used for any of the algorithm
54+
55+
.. code-block:: python
56+
57+
from pygorithm.pathing import dijkstra
58+
59+
# for printing the source code of Dijkstra object
60+
print(dijkstra.Dijikstra.get_code())
61+
62+
Dijkstra
63+
---
64+
65+
* Functions and their uses
66+
67+
.. function:: dijkstra.find_path(pygorithm.data_structures.WeightedUndirectedGraph, vertex, vertex)
68+
69+
- **pygorithm.data_structures.WeightedUndirectedGraph** : acts like an object with `graph` (see WeightedUndirectedGraph)
70+
- **vertex** : any hashable type for the start of the path
71+
- **vertex** : any hashable type for the end of the path
72+
- **Return Value** : returns a `List` of vertexes (of the same type as the graph) starting with from and going to to. This algorithm does *not* respect weights.
73+
74+
.. function:: dijkstra.get_code()
75+
76+
- **Return Value** : returns the code for the ``Dijkstra`` object

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Quick Links
2727
Data_Structure
2828
Fibonacci
2929
Math
30+
Pathing
3031

3132
Quick Start Guide
3233
-----------------

pygorithm/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
Songzhuozhuo 'souo'
2727
Emil 'Skeen' Madsen
2828
Ian 'IanDoarn' Doarn
29+
Timothy 'Tjsretchalot' Moore
2930
3031
"""
3132

@@ -51,7 +52,8 @@
5152
"dstark85",
5253
"Songzhuozhuo 'souo'",
5354
"Emil 'Skeen' Madsen",
54-
"Ian 'IanDoarn' Doarn"
55+
"Ian 'IanDoarn' Doarn",
56+
"Timothy 'Tjstretchalot' Moore"
5557
]
5658

5759
__all__ = [
@@ -60,5 +62,6 @@
6062
'math',
6163
'searching',
6264
'sorting',
63-
'string'
65+
'string',
66+
'pathing',
6467
]

pygorithm/data_structures/graph.py

Lines changed: 122 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55
from collections import defaultdict
66
import inspect
7+
import math
78

89

910
class Graph(object):
@@ -14,14 +15,14 @@ class Graph(object):
1415
def __init__(self):
1516
self.graph = defaultdict(list)
1617
self.count = 0
17-
18+
1819
def print_graph(self):
1920
"""
2021
Prints the contents of the graph
2122
"""
2223
for i in self.graph:
2324
print(i, '->', ' -> '.join([str(j) for j in self.graph[i]]))
24-
25+
2526
def add_edge(self, from_vertex, to_vertex):
2627
"""
2728
Adds an edge in the graph
@@ -121,7 +122,117 @@ def kruskal_code(cls):
121122
"""
122123
return inspect.getsource(cls.kruskal_mst)
123124

124-
125+
class WeightedUndirectedGraph(object):
126+
"""WeightedUndirectedGraph object
127+
A graph with a numerical value (weight) on edges, which
128+
is the same for both directions in an undirected graph.
129+
"""
130+
131+
def __init__(self):
132+
self.graph = {}
133+
self.weights = {}
134+
135+
def add_edge(self, u, v, weight):
136+
"""
137+
Adds the specified edge to this graph. If the edge already exists,
138+
this will only modify the weight (not create duplicates).
139+
:param u: from vertex
140+
:param v: to vertex
141+
:param weight: weight of the edge - type : numeric
142+
"""
143+
144+
changing_weight = (u, v) in self.weights.keys()
145+
146+
self.weights[(u, v)] = weight
147+
self.weights[(v, u)] = weight
148+
149+
if changing_weight:
150+
return
151+
152+
if u in self.graph.keys():
153+
self.graph[u].append(v)
154+
else:
155+
self.graph[u] = [v]
156+
157+
if v in self.graph.keys():
158+
self.graph[v].append(u)
159+
else:
160+
self.graph[v] = [u]
161+
162+
def get_edge_weight(self, u, v):
163+
"""
164+
Gets the weight between u and v if such an edge
165+
exists, or None if it does not.
166+
:param u: one edge
167+
:param v: the other edge
168+
:return: numeric or None
169+
"""
170+
return self.weights.get((u, v), None)
171+
172+
def remove_edge(self, edge, other_edge_or_none=None):
173+
"""
174+
Removes the specified edge from the grid entirely or,
175+
if specified, the connection with one other edge.
176+
Behavior is undefined if the connection does not
177+
exist.
178+
:param edge: the edge to remove
179+
:param other_edge_or_none: an edge connected to edge or none
180+
"""
181+
182+
if other_edge_or_none is not None:
183+
del self.weights[(edge, other_edge_or_none)]
184+
del self.weights[(other_edge_or_none, edge)]
185+
186+
edge_list = self.graph[edge]
187+
other_edge_list = self.graph[other_edge_or_none]
188+
189+
if len(edge_list) == 1:
190+
del self.graph[edge]
191+
else:
192+
self.graph[edge].remove(other_edge_or_none)
193+
194+
if len(other_edge_list) == 1:
195+
del self.graph[other_edge_or_none]
196+
else:
197+
self.graph[other_edge_or_none].remove(edge)
198+
else:
199+
edge_list = self.graph[edge]
200+
del self.graph[edge]
201+
for other_edge in edge_list:
202+
del self.weights[(edge, other_edge)]
203+
del self.weights[(other_edge, edge)]
204+
205+
other_edge_list = self.graph[other_edge]
206+
if len(other_edge_list) == 1:
207+
del self.graph[other_edge]
208+
else:
209+
other_edge_list.remove(edge)
210+
211+
212+
def gridify(self, size, weight):
213+
"""
214+
Constructs connections from a square grid starting at (0, 0)
215+
until (size-1, size-1) with connections between adjacent and
216+
diagonal nodes. Diagonal nodes have a weight of weight*sqrt(2)
217+
:param size: the size of the square grid to construct - type : integer
218+
:param weight: the weight between orthogonal nodes. - type: numeric
219+
:return: None
220+
"""
221+
rt2 = math.sqrt(2)
222+
acceptable_offsets = [
223+
(-1, -1, rt2), (-1, 0, 1), (-1, 1, rt2),
224+
(0, -1, 1), (0, 1, 1),
225+
(1, -1, rt2), (1, 0, 1), (1, 1, rt2)
226+
]
227+
228+
for x in range(0, size):
229+
for y in range(0, size):
230+
for offset in acceptable_offsets:
231+
nx = x + offset[0]
232+
ny = y + offset[1]
233+
if nx >= 0 and ny >= 0 and nx < size and ny < size:
234+
self.add_edge((x, y), (nx, ny), weight * offset[2])
235+
125236
class TopologicalSort(Graph):
126237

127238
def topological_sort(self):
@@ -241,25 +352,28 @@ class CheckCycleUndirectedGraph(object):
241352
def __init__(self):
242353
self.graph = {}
243354
self.count = 0
244-
355+
245356
def print_graph(self):
246357
"""
247358
for printing the contents of the graph
248359
"""
249360
for i in self.graph:
250361
print(i, '->', ' -> '.join([str(j) for j in self.graph[i]]))
251-
362+
252363
def add_edge(self, from_vertex, to_vertex):
253364
"""
254365
for adding the edge between two vertices
255366
"""
256367
# check if vertex is already present,
257-
if from_vertex in self.graph.keys() and to_vertex in self.graph.keys():
368+
if from_vertex in self.graph.keys():
258369
self.graph[from_vertex].append(to_vertex)
259-
self.graph[to_vertex].append(from_vertex)
260370
else:
261-
# else make a new vertex
371+
# otherwise add it to the graph
262372
self.graph[from_vertex] = [to_vertex]
373+
374+
if to_vertex in self.graph.keys():
375+
self.graph[to_vertex].append(from_vertex)
376+
else:
263377
self.graph[to_vertex] = [from_vertex]
264378

265379
def check_cycle(self):

pygorithm/pathing/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""
2+
Collection of pathfinding examples
3+
"""
4+
from . import dijkstra
5+
6+
__all__ = [
7+
'dijkstra'
8+
]

pygorithm/pathing/dijkstra.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import heapq
2+
class Dijkstra(object):
3+
"""Dijkstra object
4+
Finds the optimal path between two nodes on
5+
a graph."""
6+
7+
def __init__(self):
8+
pass
9+
10+
def reverse_path(self, node):
11+
result = []
12+
while node is not None:
13+
result.insert(0, node['vertex'])
14+
node = node['parent']
15+
return result
16+
17+
def find_path(self, graph, start, end):
18+
"""
19+
Calculates the optimal path from start to end
20+
on the graph. Weights are ignored.
21+
22+
:param graph: object contains `graphs` as per pygorithm.data_structures.WeightedUndirectedGraph
23+
weights are ignored.
24+
:param start: the start vertex (which is the same type of the verticies in the graph)
25+
:param end: the end vertex (which is the same type of the vertices in the graph)
26+
:return: a list starting with `start` and ending with `end`, or None if no path is possible.
27+
"""
28+
29+
open = []
30+
closed = set()
31+
32+
# the first element in the tuple is the distance from the source. This is used as the primary
33+
# key for sorting. The second element in the tuple is just a counter and is used to avoid having
34+
# to hash the dictionary when the distance from the source is not unique.
35+
36+
counter = 0
37+
heapq.heappush(open, (0, counter, { 'vertex': start, 'parent': None }))
38+
counter += 1
39+
40+
while len(open) > 0:
41+
current = heapq.heappop(open)
42+
closed.update(current[2]['vertex'])
43+
44+
if current[2]['vertex'] == end:
45+
return self.reverse_path(current[2])
46+
47+
neighbors = graph.graph[current[2]['vertex']]
48+
for neighbor in neighbors:
49+
if neighbor not in closed:
50+
heapq.heappush(open, (current[0] + 1, counter, { 'vertex': neighbor, 'parent': current[2] }))
51+
counter += 1
52+
53+
54+
return None
55+
56+
@staticmethod
57+
def get_code(self):
58+
"""
59+
returns the code for the current class
60+
"""
61+
return inspect.getsource(Dijkstra)

0 commit comments

Comments
 (0)