Skip to content

Commit 9348490

Browse files
committed
Add dijkstra pathfinding algorithm
This adds one test and documentation regarding dijkstras pathfinding algorithm. I'm not sure exactly how the documentation is generated, but this adheres pretty strictly to the pattern set in the other documentation so it should work. This adds weighted undirected graphs to data structures as well, which is a good standard implementation for testing the pathfinding algorithms. * CONTRIBUTORS.md - Add myself * docs/Data_Structure.rst - Add + docs/Pathing.rst - Add documentation for the new pathing/ submodule * docs/index.rst - Add Pathing to the index * pygorithm/__init__.py - Add myself to the contributors * pygorithm/data_structures.py - Add WeightedUndirectedGraph and very minor indentation changes + pygorithm/pathing/__init__.py - In the same format as the others + pygorithm/pathing/dijkstra.py - The simplest of pathfinding algorithms * tests/test_data_structure.py - Add tests for the new graph type + tests/test_pathing.py - Test the example shown in the documentation
1 parent ccfcf84 commit 9348490

File tree

10 files changed

+403
-10
lines changed

10 files changed

+403
-10
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.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: 116 additions & 5 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,14 +352,14 @@ 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

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)