-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch_unit_tests.py
480 lines (367 loc) · 16.7 KB
/
search_unit_tests.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
# coding=utf-8
import itertools
import pickle
import random
import unittest
import networkx
from explorable_graph import ExplorableGraph
from submission import a_star, bidirectional_a_star, \
bidirectional_ucs, breadth_first_search, euclidean_dist_heuristic, \
null_heuristic, haversine_dist_heuristic, tridirectional_search, tridirectional_upgraded, \
uniform_cost_search, custom_heuristic
def is_valid(graph, path, start, goal):
"""
Test whether a path is valid or not
"""
if start == goal:
return path == []
else:
if path[0] != start or path[-1] != goal:
return False
for i in range(len(path) -1):
if path[i + 1] not in graph.neighbors(path[i]):
return False
return True
class SearchUnitTests(unittest.TestCase):
"""
Error Diagnostic code courtesy one of our former students - Mac Chan
The following unit tests will check for all pairs on romania and random
points on atlanta.
Comment out any tests that you haven't implemented yet.
If you failed on Gradescope because of non-optimal path, make sure you pass
all the local tests.
Change test_count=-1 if you failed the path test on Gradescope, it will run
tests on atlanta until it finds a set of points that fail.
If you failed on Gradescope because of your explored set is too large,
there is no easy way to test without a reference implementation.
But you can read the pdf slides for the optimized terminal condition.
To run,
nosetests --nocapture -v search_unit_tests.py:SearchUnitTests
nosetests --nocapture -v
search_unit_tests.py:SearchUnitTests.test_bfs_romania
"""
def setUp(self):
"""Setup both atlanta and romania graph data."""
with (open("romania_graph.pickle", "rb")) as romFile:
romania = pickle.load(romFile)
self.romania = ExplorableGraph(romania)
self.romania.reset_search()
with (open("atlanta_osm.pickle", "rb")) as atlFile:
atlanta = pickle.load(atlFile)
self.atlanta = ExplorableGraph(atlanta)
self.atlanta.reset_search()
self.margin_of_error = 1.0e-6
def reference_path(self, graph, src_node, dst_node, weight='weight'):
"""
Path as generated by networkx shortest path.
Args:
graph (ExplorableGraph): Undirected graph to search.
src_node (node): Key for the start node.
dst_node (node): Key for the end node.
weight (:obj:`str`):
If None, every edge has weight/distance/cost 1.
If a string, use this edge attribute as the edge weight.
Any edge attribute not present defaults to 1.
Returns:
Tuple with (cost of path, path as list).
"""
graph.reset_search()
path = networkx.shortest_path(graph, src_node, dst_node, weight=weight)
cost = self.sum_weight(graph, path)
return cost, path
def reference_bfs_path(self, graph, src_node, dst_node):
"""
Breadth First Search as generated by networkx shortest path.
Args:
graph (ExplorableGraph): Undirected graph to search.
src_node (node): Key for the start node.
dst_node (node): Key for the end node.
Returns:
"""
return self.reference_path(graph, src_node, dst_node, weight=None)
@staticmethod
def sum_weight(graph, path):
"""
Calculate the total cost of a path by summing edge weights.
Args:
graph (ExplorableGraph): Graph that contains path.
path (list(nodes)): List of nodes from src to dst.
Returns:
Sum of edge weights in path.
"""
pairs = zip(path, path[1:])
return sum([graph.get_edge_data(a, b)['weight'] for a, b in pairs])
def run_romania_data(self, ref_method, method, **kwargs):
"""
Run the test search against the Romania data.
Args:
ref_method (func): Reference search function to compare test search
method (func): Test search function.
kwargs: Keyword arguments.
Asserts:
True if the path from the test search is equivalent to the
reference search.
"""
keys = self.romania.nodes.keys()
pairs = itertools.permutations(keys, 2)
for src, dst in pairs:
self.romania.reset_search()
path = method(self.romania, src, dst, **kwargs)
ref_len, ref_path = ref_method(self.romania, src, dst)
if path != ref_path:
print (src, dst)
self.assertEqual(ref_path, path)
def run_romania_tri(self, method, **kwargs):
"""
Run the tridirectional test search against the Romania data.
Args:
method (func): Test search function.
kwargs: Keyword arguments.
Asserts:
True if the path from the test search is equivalent to the
reference search.
"""
keys = self.romania.nodes.keys()
triplets = itertools.permutations(keys, 3)
for goals in triplets:
self.romania.reset_search()
path = method(self.romania, goals, **kwargs)
path_len = self.sum_weight(self.romania, path)
s1len, _ = self.reference_path(self.romania, goals[0], goals[1])
s2len, _ = self.reference_path(self.romania, goals[2], goals[1])
s3len, _ = self.reference_path(self.romania, goals[0], goals[2])
min_len = min(s1len + s2len, s1len + s3len, s3len + s2len)
if path_len != min_len:
print (goals)
self.assertEqual(min_len, path_len)
def run_atlanta_data(self, method, test_count=10, **kwargs):
"""
Run the bidirectional test search against the Atlanta data.
In the interest of time and memory, this is not an exhaustive search of
all possible pairs in the graph.
Args:
method (func): Test search function.
test_count (int): Number of tests to run. Default is 10.
kwargs: Keyword arguments.
Asserts:
True if the path from the test search is equivalent to the
reference search.
"""
keys = list(networkx.connected_components(self.atlanta).__next__())
random.shuffle(keys)
for src, dst in list(zip(keys, keys[1:]))[::2]:
self.atlanta.reset_search()
path = method(self.atlanta, src, dst, **kwargs)
path_len = self.sum_weight(self.atlanta, path)
ref_len, ref_path = self.reference_path(self.atlanta, src, dst)
if abs(path_len - ref_len) > self.margin_of_error:
print (src, dst)
self.assertAlmostEqual(path_len, ref_len,
delta=self.margin_of_error)
test_count -= 1
if test_count == 0:
break
def run_atlanta_tri(self, method, test_count=10, **kwargs):
"""
Run the tridirectional test search against the Atlanta data.
In the interest of time and memory, this is not an exhaustive search of
all possible triplets in the graph.
Args:
method (func): Test search function.
test_count (int): Number of tests to run. Default is 10.
kwargs: Keyword arguments.
Asserts:
True if the path from the test search is equivalent to the
reference search.
"""
keys = list(next(networkx.connected_components(self.atlanta)))
random.shuffle(keys)
for goals in list(zip(keys, keys[1:], keys[2:]))[::3]:
self.atlanta.reset_search()
path = method(self.atlanta, goals, **kwargs)
path_len = self.sum_weight(self.atlanta, path)
s1len, _ = self.reference_path(self.atlanta, goals[0], goals[1])
s2len, _ = self.reference_path(self.atlanta, goals[2], goals[1])
s3len, _ = self.reference_path(self.atlanta, goals[0], goals[2])
min_len = min(s1len + s2len, s1len + s3len, s3len + s2len)
if abs(path_len - min_len) > self.margin_of_error:
print (goals)
self.assertAlmostEqual(path_len, min_len,
delta=self.margin_of_error)
test_count -= 1
if test_count == 0:
break
def same_node_bi(self, graph, method, test_count=10, **kwargs):
"""
Run the a bidirectional test search using same start and end node.
Args:
graph (ExplorableGraph): Graph that contains path.
method (func): Test search function.
test_count (int): Number of tests to run. Default is 10.
kwargs: Keyword arguments.
Asserts:
True if the path between the same start and end node is empty.
"""
keys = list(networkx.connected_components(graph).__next__())
random.shuffle(keys)
for i in range(test_count):
path = method(graph, keys[i], keys[i], **kwargs)
self.assertFalse(path)
def test_same_node_bi(self):
"""
Test bidirectional search using the same start and end nodes.
Searches Tested:
breadth_first_search
uniform_cost_search
a_star, null_heuristic
a_star, euclidean_dist_heuristic
bidirectional_ucs
bidirectional_a_star, null_heuristic
bidirectional_a_star, euclidean_dist_heuristic
"""
self.same_node_bi(self.romania, breadth_first_search)
self.same_node_bi(self.romania, uniform_cost_search)
self.same_node_bi(self.romania, a_star, heuristic=null_heuristic)
self.same_node_bi(self.romania, a_star,
heuristic=euclidean_dist_heuristic)
self.same_node_bi(self.romania, bidirectional_ucs)
self.same_node_bi(self.romania, bidirectional_a_star,
heuristic=null_heuristic)
self.same_node_bi(self.romania, bidirectional_a_star,
heuristic=euclidean_dist_heuristic)
def same_node_tri_test(self, graph, method, test_count=10, **kwargs):
"""
Run the tridirectional test search using same start and end nodes
Args:
graph (ExplorableGraph): Graph that contains path.
method (func): Test search function.
test_count (int): Number of tests to run. Default is 10.
kwargs: Keyword arguments.
Asserts:
True if the path between the same start and end node is empty.
"""
keys = list(next(networkx.connected_components(graph)))
random.shuffle(keys)
for i in range(test_count):
path = method(graph, [keys[i], keys[i], keys[i]], **kwargs)
self.assertFalse(path)
def test_same_node_tri(self):
"""
Test bidirectional search using the same start and end nodes.
Searches Tested:
tridirectional_search
tridirectional_upgraded, null_heuristic
tridirectional_upgraded, euclidean_dist_heuristic
"""
self.same_node_tri_test(self.romania, tridirectional_search)
self.same_node_tri_test(self.romania, tridirectional_upgraded,
heuristic=null_heuristic)
self.same_node_tri_test(self.romania, tridirectional_upgraded,
heuristic=euclidean_dist_heuristic)
def test_bfs_romania(self):
"""Test breadth first search with Romania data."""
keys = self.romania.nodes.keys()
pairs = itertools.permutations(keys, 2)
for src in keys:
for dst in keys:
self.romania.reset_search()
path = breadth_first_search(self.romania, src, dst)
ref_len, ref_path = self.reference_bfs_path(self.romania, src, dst)
self.assertTrue(is_valid(self.romania, path, src, dst),
msg="path %s for start '%s' and goal '%s' is not valid" % (path, src, dst))
if src != dst: # we want path == [] if src == dst
self.assertTrue(len(path) == len(ref_path), msg="Path is too long. Real path: %s, your path: %s" % (ref_path, path))
def test_ucs_romania(self):
"""Test uniform cost search with Romania data."""
self.run_romania_data(self.reference_path, uniform_cost_search)
def test_a_star_null_romania(self):
"""Test A* search with Romania data and the Null heuristic."""
self.run_romania_data(self.reference_path, a_star,
heuristic=null_heuristic)
def test_a_star_euclidean_romania(self):
"""Test A* search with Romania data and the Euclidean heuristic."""
self.run_romania_data(self.reference_path, a_star,
heuristic=euclidean_dist_heuristic)
def test_bi_ucs_romania(self):
"""Test Bi-uniform cost search with Romania data."""
self.run_romania_data(self.reference_path, bidirectional_ucs)
def test_bi_ucs_atlanta(self):
"""
Test Bi-uniform cost search with Atlanta data.
To loop test forever, set test_count to -1
"""
self.run_atlanta_data(bidirectional_ucs, test_count=10)
def test_bi_a_star_null_romania(self):
"""Test Bi-A* search with Romania data and the Null heuristic."""
self.run_romania_data(self.reference_path, bidirectional_a_star,
heuristic=null_heuristic)
def test_bi_a_star_null_atlanta(self):
"""
Test Bi-A* search with Atlanta data and the Null heuristic.
To loop test forever, set test_count to -1
"""
self.run_atlanta_data(bidirectional_a_star, heuristic=null_heuristic,
test_count=10)
def test_bi_a_star_euclidean_romania(self):
"""Test Bi-A* search with Romania data and the Euclidean heuristic."""
self.run_romania_data(self.reference_path, bidirectional_a_star,
heuristic=euclidean_dist_heuristic)
def test_bi_a_star_euclidean_atlanta(self):
"""
Test Bi-A* search with Atlanta data and the Euclidean heuristic.
To loop test forever, set test_count to -1
"""
self.run_atlanta_data(bidirectional_a_star,
heuristic=euclidean_dist_heuristic,
test_count=10)
def test_bi_a_star_haversine_atlanta(self):
"""
Test Bi-A* search with Atlanta data and the Haversine heuristic.
To loop test forever, set test_count to -1
"""
self.run_atlanta_data(bidirectional_a_star,
heuristic=haversine_dist_heuristic,
test_count=10)
def test_tri_ucs_romania(self):
"""Test Tri-UC search with Romania data."""
self.run_romania_tri(tridirectional_search)
def test_tri_ucs_atlanta(self):
"""
Test Tri-UC search with Atlanta data.
To loop test forever, set test_count to -1
"""
self.run_atlanta_tri(tridirectional_search, test_count=10)
def test_tri_upgraded_null_romania(self):
"""
Test upgraded tri search with Romania data and the Null heuristic.
"""
self.run_romania_tri(tridirectional_upgraded, heuristic=null_heuristic)
def test_tri_upgraded_null_atlanta(self):
"""
Test upgraded tri search with Atlanta data and the Null heuristic.
To loop test forever, set test_count to -1
"""
self.run_atlanta_tri(tridirectional_upgraded, test_count=10,
heuristic=null_heuristic)
def test_tri_upgraded_euclidean_romania(self):
"""
Test upgraded tri search with Romania data and the Euclidean heuristic.
"""
self.run_romania_tri(tridirectional_upgraded,
heuristic=euclidean_dist_heuristic)
def test_tri_upgraded_euclidean_atlanta(self):
"""
Test upgraded tri search with Atlanta data and the Euclidean heuristic.
To loop test forever, set test_count to -1
"""
self.run_atlanta_tri(tridirectional_upgraded, test_count=10,
heuristic=euclidean_dist_heuristic)
def test_tri_upgraded_haversine_atlanta(self):
"""
Test upgraded tri search with Atlanta data and the Haversine heuristic.
To loop test forever, set test_count to -1
"""
self.run_atlanta_tri(tridirectional_upgraded, test_count=10,
heuristic=haversine_dist_heuristic)
if __name__ == '__main__':
unittest.main()