scgraph 2.6.0__tar.gz → 2.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {scgraph-2.6.0/scgraph.egg-info → scgraph-2.7.0}/PKG-INFO +12 -3
- {scgraph-2.6.0 → scgraph-2.7.0}/README.md +11 -2
- {scgraph-2.6.0 → scgraph-2.7.0}/pyproject.toml +1 -1
- {scgraph-2.6.0 → scgraph-2.7.0}/scgraph/__init__.py +11 -2
- {scgraph-2.6.0 → scgraph-2.7.0}/scgraph/cache.py +11 -2
- {scgraph-2.6.0 → scgraph-2.7.0}/scgraph/core.py +128 -11
- {scgraph-2.6.0 → scgraph-2.7.0}/scgraph/grid.py +49 -0
- {scgraph-2.6.0 → scgraph-2.7.0}/scgraph/spanning.py +21 -26
- {scgraph-2.6.0 → scgraph-2.7.0}/scgraph/utils.py +99 -34
- {scgraph-2.6.0 → scgraph-2.7.0/scgraph.egg-info}/PKG-INFO +12 -3
- {scgraph-2.6.0 → scgraph-2.7.0}/setup.cfg +1 -1
- {scgraph-2.6.0 → scgraph-2.7.0}/LICENSE +0 -0
- {scgraph-2.6.0 → scgraph-2.7.0}/scgraph/geographs/__init__.py +0 -0
- {scgraph-2.6.0 → scgraph-2.7.0}/scgraph/geographs/marnet.py +0 -0
- {scgraph-2.6.0 → scgraph-2.7.0}/scgraph/geographs/north_america_rail.py +0 -0
- {scgraph-2.6.0 → scgraph-2.7.0}/scgraph/geographs/oak_ridge_maritime.py +0 -0
- {scgraph-2.6.0 → scgraph-2.7.0}/scgraph/geographs/us_freeway.py +0 -0
- {scgraph-2.6.0 → scgraph-2.7.0}/scgraph/helpers/__init__.py +0 -0
- {scgraph-2.6.0 → scgraph-2.7.0}/scgraph/helpers/shape_mover_utils.py +0 -0
- {scgraph-2.6.0 → scgraph-2.7.0}/scgraph.egg-info/SOURCES.txt +0 -0
- {scgraph-2.6.0 → scgraph-2.7.0}/scgraph.egg-info/dependency_links.txt +0 -0
- {scgraph-2.6.0 → scgraph-2.7.0}/scgraph.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scgraph
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: Determine an approximate route between two points on earth.
|
|
5
5
|
Author-email: Connor Makowski <conmak@mit.edu>
|
|
6
6
|
Project-URL: Homepage, https://github.com/connor-makowski/scgraph
|
|
@@ -43,6 +43,9 @@ Low Level: https://connor-makowski.github.io/scgraph/scgraph/core.html
|
|
|
43
43
|
- Modified to support sparse network data structures
|
|
44
44
|
- Makowski's Modified Sparse Dijkstra algorithm
|
|
45
45
|
- Modified for O(n) performance on particularly sparse networks
|
|
46
|
+
- A* algorithm (Extension of Makowski's Modified Sparse Dijkstra)
|
|
47
|
+
- Uses a heuristic function to improve performance on large graphs
|
|
48
|
+
- Note: The heuristic function is optional and defaults to Dijkstra's algorithm
|
|
46
49
|
- Possible future support for other algorithms
|
|
47
50
|
- Distances:
|
|
48
51
|
- Uses the [haversine formula](https://en.wikipedia.org/wiki/Haversine_formula) to calculate the distance between two points on earth
|
|
@@ -89,7 +92,8 @@ In this case, calculate the shortest maritime path between Shanghai, China and S
|
|
|
89
92
|
# Use a maritime network geograph
|
|
90
93
|
from scgraph.geographs.marnet import marnet_geograph
|
|
91
94
|
|
|
92
|
-
# Get the shortest path between
|
|
95
|
+
# Get the shortest maritime path between Shanghai, China and Savannah, Georgia, USA
|
|
96
|
+
# Note: The origin and destination nodes can be any latitude / longitude pair
|
|
93
97
|
output = marnet_geograph.get_shortest_path(
|
|
94
98
|
origin_node={"latitude": 31.23,"longitude": 121.47},
|
|
95
99
|
destination_node={"latitude": 32.08,"longitude": -81.09},
|
|
@@ -194,11 +198,16 @@ Using `scgraph_data` geographs:
|
|
|
194
198
|
- Note: Make sure to install the `scgraph_data` package before using these geographs
|
|
195
199
|
```py
|
|
196
200
|
from scgraph_data.world_railways import world_railways_geograph
|
|
201
|
+
from scgraph import Graph
|
|
197
202
|
|
|
198
203
|
# Get the shortest path between Kalamazoo Michigan and Detroit Michigan by Train
|
|
199
204
|
output = world_railways_geograph.get_shortest_path(
|
|
200
205
|
origin_node={"latitude": 42.29,"longitude": -85.58},
|
|
201
|
-
destination_node={"latitude": 42.33,"longitude": -83.05}
|
|
206
|
+
destination_node={"latitude": 42.33,"longitude": -83.05},
|
|
207
|
+
# Optional: Use the A* algorithm
|
|
208
|
+
algorithm_fn=Graph.a_star,
|
|
209
|
+
# Optional: Pass the haversine function as the heuristic function to the A* algorithm
|
|
210
|
+
algorithm_kwargs={"heuristic_fn": world_railways_geograph.haversine},
|
|
202
211
|
)
|
|
203
212
|
```
|
|
204
213
|
|
|
@@ -27,6 +27,9 @@ Low Level: https://connor-makowski.github.io/scgraph/scgraph/core.html
|
|
|
27
27
|
- Modified to support sparse network data structures
|
|
28
28
|
- Makowski's Modified Sparse Dijkstra algorithm
|
|
29
29
|
- Modified for O(n) performance on particularly sparse networks
|
|
30
|
+
- A* algorithm (Extension of Makowski's Modified Sparse Dijkstra)
|
|
31
|
+
- Uses a heuristic function to improve performance on large graphs
|
|
32
|
+
- Note: The heuristic function is optional and defaults to Dijkstra's algorithm
|
|
30
33
|
- Possible future support for other algorithms
|
|
31
34
|
- Distances:
|
|
32
35
|
- Uses the [haversine formula](https://en.wikipedia.org/wiki/Haversine_formula) to calculate the distance between two points on earth
|
|
@@ -73,7 +76,8 @@ In this case, calculate the shortest maritime path between Shanghai, China and S
|
|
|
73
76
|
# Use a maritime network geograph
|
|
74
77
|
from scgraph.geographs.marnet import marnet_geograph
|
|
75
78
|
|
|
76
|
-
# Get the shortest path between
|
|
79
|
+
# Get the shortest maritime path between Shanghai, China and Savannah, Georgia, USA
|
|
80
|
+
# Note: The origin and destination nodes can be any latitude / longitude pair
|
|
77
81
|
output = marnet_geograph.get_shortest_path(
|
|
78
82
|
origin_node={"latitude": 31.23,"longitude": 121.47},
|
|
79
83
|
destination_node={"latitude": 32.08,"longitude": -81.09},
|
|
@@ -178,11 +182,16 @@ Using `scgraph_data` geographs:
|
|
|
178
182
|
- Note: Make sure to install the `scgraph_data` package before using these geographs
|
|
179
183
|
```py
|
|
180
184
|
from scgraph_data.world_railways import world_railways_geograph
|
|
185
|
+
from scgraph import Graph
|
|
181
186
|
|
|
182
187
|
# Get the shortest path between Kalamazoo Michigan and Detroit Michigan by Train
|
|
183
188
|
output = world_railways_geograph.get_shortest_path(
|
|
184
189
|
origin_node={"latitude": 42.29,"longitude": -85.58},
|
|
185
|
-
destination_node={"latitude": 42.33,"longitude": -83.05}
|
|
190
|
+
destination_node={"latitude": 42.33,"longitude": -83.05},
|
|
191
|
+
# Optional: Use the A* algorithm
|
|
192
|
+
algorithm_fn=Graph.a_star,
|
|
193
|
+
# Optional: Pass the haversine function as the heuristic function to the A* algorithm
|
|
194
|
+
algorithm_kwargs={"heuristic_fn": world_railways_geograph.haversine},
|
|
186
195
|
)
|
|
187
196
|
```
|
|
188
197
|
|
|
@@ -12,7 +12,7 @@ build-backend = "setuptools.build_meta"
|
|
|
12
12
|
|
|
13
13
|
[project]
|
|
14
14
|
name = "scgraph"
|
|
15
|
-
version = "2.
|
|
15
|
+
version = "2.7.0"
|
|
16
16
|
description = "Determine an approximate route between two points on earth."
|
|
17
17
|
authors = [
|
|
18
18
|
{name="Connor Makowski", email="conmak@mit.edu"}
|
|
@@ -28,6 +28,9 @@ Low Level: https://connor-makowski.github.io/scgraph/scgraph/core.html
|
|
|
28
28
|
- Modified to support sparse network data structures
|
|
29
29
|
- Makowski's Modified Sparse Dijkstra algorithm
|
|
30
30
|
- Modified for O(n) performance on particularly sparse networks
|
|
31
|
+
- A* algorithm (Extension of Makowski's Modified Sparse Dijkstra)
|
|
32
|
+
- Uses a heuristic function to improve performance on large graphs
|
|
33
|
+
- Note: The heuristic function is optional and defaults to Dijkstra's algorithm
|
|
31
34
|
- Possible future support for other algorithms
|
|
32
35
|
- Distances:
|
|
33
36
|
- Uses the [haversine formula](https://en.wikipedia.org/wiki/Haversine_formula) to calculate the distance between two points on earth
|
|
@@ -74,7 +77,8 @@ In this case, calculate the shortest maritime path between Shanghai, China and S
|
|
|
74
77
|
# Use a maritime network geograph
|
|
75
78
|
from scgraph.geographs.marnet import marnet_geograph
|
|
76
79
|
|
|
77
|
-
# Get the shortest path between
|
|
80
|
+
# Get the shortest maritime path between Shanghai, China and Savannah, Georgia, USA
|
|
81
|
+
# Note: The origin and destination nodes can be any latitude / longitude pair
|
|
78
82
|
output = marnet_geograph.get_shortest_path(
|
|
79
83
|
origin_node={"latitude": 31.23,"longitude": 121.47},
|
|
80
84
|
destination_node={"latitude": 32.08,"longitude": -81.09},
|
|
@@ -179,11 +183,16 @@ Using `scgraph_data` geographs:
|
|
|
179
183
|
- Note: Make sure to install the `scgraph_data` package before using these geographs
|
|
180
184
|
```py
|
|
181
185
|
from scgraph_data.world_railways import world_railways_geograph
|
|
186
|
+
from scgraph import Graph
|
|
182
187
|
|
|
183
188
|
# Get the shortest path between Kalamazoo Michigan and Detroit Michigan by Train
|
|
184
189
|
output = world_railways_geograph.get_shortest_path(
|
|
185
190
|
origin_node={"latitude": 42.29,"longitude": -85.58},
|
|
186
|
-
destination_node={"latitude": 42.33,"longitude": -83.05}
|
|
191
|
+
destination_node={"latitude": 42.33,"longitude": -83.05},
|
|
192
|
+
# Optional: Use the A* algorithm
|
|
193
|
+
algorithm_fn=Graph.a_star,
|
|
194
|
+
# Optional: Pass the haversine function as the heuristic function to the A* algorithm
|
|
195
|
+
algorithm_kwargs={"heuristic_fn": world_railways_geograph.haversine},
|
|
187
196
|
)
|
|
188
197
|
```
|
|
189
198
|
|
|
@@ -31,6 +31,7 @@ class CacheGraph:
|
|
|
31
31
|
destination_id: int,
|
|
32
32
|
cache: bool = True,
|
|
33
33
|
cache_for: str = "origin",
|
|
34
|
+
heuristic_fn=None,
|
|
34
35
|
):
|
|
35
36
|
"""
|
|
36
37
|
Function:
|
|
@@ -50,6 +51,13 @@ class CacheGraph:
|
|
|
50
51
|
- cache_for: Whether to cache the spanning tree for the origin or destination node
|
|
51
52
|
- Default: 'origin'
|
|
52
53
|
- Options: 'origin', 'destination'
|
|
54
|
+
- heuristic_fn: A heuristic function to use for the A* algorithm if cache is False and the origin or destination node is not in the cache
|
|
55
|
+
- Type: callable | None
|
|
56
|
+
- If None, the A* function will compute with Dijkstra's algorithm instead
|
|
57
|
+
- If a callable is provided, it should take two arguments: origin_id and destination_id
|
|
58
|
+
and return a float representing the heuristic distance between the two nodes
|
|
59
|
+
- Note: This distance should never be greater than the actual distance between the two nodes or you may get suboptimal paths
|
|
60
|
+
|
|
53
61
|
|
|
54
62
|
"""
|
|
55
63
|
spanning_tree = self.cache.get(
|
|
@@ -68,11 +76,12 @@ class CacheGraph:
|
|
|
68
76
|
if spanning_tree is not None:
|
|
69
77
|
return SpanningTree.get_path(
|
|
70
78
|
origin_id=origin_id,
|
|
71
|
-
|
|
79
|
+
destination_id=destination_id,
|
|
72
80
|
spanning_tree=spanning_tree,
|
|
73
81
|
)
|
|
74
|
-
return Graph.
|
|
82
|
+
return Graph.a_star(
|
|
75
83
|
graph=self.graph,
|
|
76
84
|
origin_id=origin_id,
|
|
77
85
|
destination_id=destination_id,
|
|
86
|
+
heuristic_fn=heuristic_fn,
|
|
78
87
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from .utils import haversine,
|
|
1
|
+
from .utils import haversine, distance_converter, get_line_path, cheap_ruler
|
|
2
2
|
import json
|
|
3
3
|
from heapq import heappop, heappush
|
|
4
4
|
|
|
@@ -255,7 +255,7 @@ class Graph:
|
|
|
255
255
|
|
|
256
256
|
return {
|
|
257
257
|
"path": output_path,
|
|
258
|
-
"length":
|
|
258
|
+
"length": distance_matrix[destination_id],
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
@staticmethod
|
|
@@ -294,22 +294,99 @@ class Graph:
|
|
|
294
294
|
|
|
295
295
|
- None
|
|
296
296
|
"""
|
|
297
|
+
# Input Validation
|
|
297
298
|
Graph.input_check(
|
|
298
299
|
graph=graph, origin_id=origin_id, destination_id=destination_id
|
|
299
300
|
)
|
|
301
|
+
# Variable Initialization
|
|
300
302
|
distance_matrix = [float("inf")] * len(graph)
|
|
303
|
+
distance_matrix[origin_id] = 0
|
|
301
304
|
open_leaves = []
|
|
305
|
+
heappush(open_leaves, (0, origin_id))
|
|
302
306
|
predecessor = [-1] * len(graph)
|
|
303
307
|
|
|
308
|
+
while open_leaves:
|
|
309
|
+
current_distance, current_id = heappop(open_leaves)
|
|
310
|
+
if current_id == destination_id:
|
|
311
|
+
break
|
|
312
|
+
for connected_id, connected_distance in graph[current_id].items():
|
|
313
|
+
possible_distance = current_distance + connected_distance
|
|
314
|
+
if possible_distance < distance_matrix[connected_id]:
|
|
315
|
+
distance_matrix[connected_id] = possible_distance
|
|
316
|
+
predecessor[connected_id] = current_id
|
|
317
|
+
heappush(open_leaves, (possible_distance, connected_id))
|
|
318
|
+
if current_id != destination_id:
|
|
319
|
+
raise Exception(
|
|
320
|
+
"Something went wrong, the origin and destination nodes are not connected."
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
output_path = [current_id]
|
|
324
|
+
while predecessor[current_id] != -1:
|
|
325
|
+
current_id = predecessor[current_id]
|
|
326
|
+
output_path.append(current_id)
|
|
327
|
+
|
|
328
|
+
output_path.reverse()
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
"path": output_path,
|
|
332
|
+
"length": distance_matrix[destination_id],
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
@staticmethod
|
|
336
|
+
def a_star(
|
|
337
|
+
graph: list[dict[int, int | float]],
|
|
338
|
+
origin_id: int,
|
|
339
|
+
destination_id: int,
|
|
340
|
+
heuristic_fn=None,
|
|
341
|
+
) -> dict:
|
|
342
|
+
"""
|
|
343
|
+
Function:
|
|
344
|
+
|
|
345
|
+
- Identify the shortest path between two nodes in a sparse network graph using an A* extension of Makowski's modified Dijkstra algorithm
|
|
346
|
+
- Return a dictionary of various path information including:
|
|
347
|
+
- `id_path`: A list of node ids in the order they are visited
|
|
348
|
+
- `path`: A list of node dictionaries (lat + long) in the order they are visited
|
|
349
|
+
|
|
350
|
+
Required Arguments:
|
|
351
|
+
|
|
352
|
+
- `graph`:
|
|
353
|
+
- Type: list of dictionaries
|
|
354
|
+
- See: https://connor-makowski.github.io/scgraph/scgraph/core.html#Graph.validate_graph
|
|
355
|
+
- `origin_id`
|
|
356
|
+
- Type: int
|
|
357
|
+
- What: The id of the origin node from the graph dictionary to start the shortest path from
|
|
358
|
+
- `destination_id`
|
|
359
|
+
- Type: int
|
|
360
|
+
- What: The id of the destination node from the graph dictionary to end the shortest path at
|
|
361
|
+
- `heuristic_fn`
|
|
362
|
+
- Type: function
|
|
363
|
+
- What: A heuristic function that takes two node ids and returns an estimated distance between them
|
|
364
|
+
- Note: If None, returns the shortest path using Makowski's modified Dijkstra algorithm
|
|
365
|
+
- Default: None
|
|
366
|
+
|
|
367
|
+
Optional Arguments:
|
|
368
|
+
|
|
369
|
+
- None
|
|
370
|
+
"""
|
|
371
|
+
if heuristic_fn is None:
|
|
372
|
+
return Graph.dijkstra_makowski(
|
|
373
|
+
graph=graph,
|
|
374
|
+
origin_id=origin_id,
|
|
375
|
+
destination_id=destination_id,
|
|
376
|
+
)
|
|
377
|
+
# Input Validation
|
|
378
|
+
Graph.input_check(
|
|
379
|
+
graph=graph, origin_id=origin_id, destination_id=destination_id
|
|
380
|
+
)
|
|
381
|
+
# Variable Initialization
|
|
382
|
+
distance_matrix = [float("inf")] * len(graph)
|
|
304
383
|
distance_matrix[origin_id] = 0
|
|
384
|
+
open_leaves = []
|
|
305
385
|
heappush(open_leaves, (0, origin_id))
|
|
386
|
+
predecessor = [-1] * len(graph)
|
|
306
387
|
|
|
307
|
-
while
|
|
308
|
-
|
|
309
|
-
raise Exception(
|
|
310
|
-
"Something went wrong, the origin and destination nodes are not connected."
|
|
311
|
-
)
|
|
312
|
-
current_distance, current_id = heappop(open_leaves)
|
|
388
|
+
while open_leaves:
|
|
389
|
+
current_id = heappop(open_leaves)[1]
|
|
313
390
|
if current_id == destination_id:
|
|
314
391
|
break
|
|
315
392
|
current_distance = distance_matrix[current_id]
|
|
@@ -318,7 +395,18 @@ class Graph:
|
|
|
318
395
|
if possible_distance < distance_matrix[connected_id]:
|
|
319
396
|
distance_matrix[connected_id] = possible_distance
|
|
320
397
|
predecessor[connected_id] = current_id
|
|
321
|
-
heappush(
|
|
398
|
+
heappush(
|
|
399
|
+
open_leaves,
|
|
400
|
+
(
|
|
401
|
+
possible_distance
|
|
402
|
+
+ heuristic_fn(connected_id, destination_id),
|
|
403
|
+
connected_id,
|
|
404
|
+
),
|
|
405
|
+
)
|
|
406
|
+
if current_id != destination_id:
|
|
407
|
+
raise Exception(
|
|
408
|
+
"Something went wrong, the origin and destination nodes are not connected."
|
|
409
|
+
)
|
|
322
410
|
|
|
323
411
|
output_path = [current_id]
|
|
324
412
|
while predecessor[current_id] != -1:
|
|
@@ -329,7 +417,7 @@ class Graph:
|
|
|
329
417
|
|
|
330
418
|
return {
|
|
331
419
|
"path": output_path,
|
|
332
|
-
"length":
|
|
420
|
+
"length": distance_matrix[destination_id],
|
|
333
421
|
}
|
|
334
422
|
|
|
335
423
|
|
|
@@ -447,12 +535,38 @@ class GeoGraph:
|
|
|
447
535
|
]
|
|
448
536
|
), "Your nodes must be a list of lists where each sub list has a length of 2 with a latitude [-90,90] and longitude [-180,180] value"
|
|
449
537
|
|
|
538
|
+
def haversine(
|
|
539
|
+
self,
|
|
540
|
+
origin_id: int,
|
|
541
|
+
destination_id: int,
|
|
542
|
+
):
|
|
543
|
+
return haversine(
|
|
544
|
+
origin=self.nodes[origin_id],
|
|
545
|
+
destination=self.nodes[destination_id],
|
|
546
|
+
units="km",
|
|
547
|
+
circuity=1,
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
def cheap_ruler(
|
|
551
|
+
self,
|
|
552
|
+
origin_id: int,
|
|
553
|
+
destination_id: int,
|
|
554
|
+
):
|
|
555
|
+
return cheap_ruler(
|
|
556
|
+
origin=self.nodes[origin_id],
|
|
557
|
+
destination=self.nodes[destination_id],
|
|
558
|
+
units="km",
|
|
559
|
+
# Use a circuity factor of 0.95 to account for the fact that cheap_ruler can overestimate distances
|
|
560
|
+
circuity=0.9,
|
|
561
|
+
)
|
|
562
|
+
|
|
450
563
|
def get_shortest_path(
|
|
451
564
|
self,
|
|
452
565
|
origin_node: dict[float | int],
|
|
453
566
|
destination_node: dict[float | int],
|
|
454
567
|
output_units: str = "km",
|
|
455
568
|
algorithm_fn=Graph.dijkstra_makowski,
|
|
569
|
+
algorithm_kwargs: dict = dict(),
|
|
456
570
|
off_graph_circuity: [float | int] = 1,
|
|
457
571
|
node_addition_type: str = "quadrant",
|
|
458
572
|
node_addition_circuity: [float | int] = 4,
|
|
@@ -505,6 +619,9 @@ class GeoGraph:
|
|
|
505
619
|
- See: https://connor-makowski.github.io/scgraph/scgraph/core.html#Graph.validate_graph
|
|
506
620
|
- `origin`: The id of the origin node from the graph dictionary to start the shortest path from
|
|
507
621
|
- `destination`: The id of the destination node from the graph dictionary to end the shortest path at
|
|
622
|
+
- `algorithm_kwargs`
|
|
623
|
+
- Type: dict
|
|
624
|
+
- What: Additional keyword arguments to pass to the algorithm function assuming it accepts them
|
|
508
625
|
- `off_graph_circuity`
|
|
509
626
|
- Type: int | float
|
|
510
627
|
- What: The circuity factor to apply to any distance calculations between your origin and destination nodes and their connecting nodes in the graph
|
|
@@ -594,12 +711,12 @@ class GeoGraph:
|
|
|
594
711
|
lat_lon_bound=node_addition_lat_lon_bound,
|
|
595
712
|
node_addition_math=node_addition_math,
|
|
596
713
|
)
|
|
597
|
-
|
|
598
714
|
try:
|
|
599
715
|
output = algorithm_fn(
|
|
600
716
|
graph=self.graph,
|
|
601
717
|
origin_id=origin_id,
|
|
602
718
|
destination_id=destination_id,
|
|
719
|
+
**algorithm_kwargs,
|
|
603
720
|
)
|
|
604
721
|
output["coordinate_path"] = self.get_coordinate_path(output["path"])
|
|
605
722
|
output["length"] = self.adujust_circuity_length(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from scgraph.helpers.shape_mover_utils import ShapeMoverUtils
|
|
2
2
|
from scgraph.cache import CacheGraph
|
|
3
|
+
from typing import Literal
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class GridGraph:
|
|
@@ -101,6 +102,7 @@ class GridGraph:
|
|
|
101
102
|
self.conn_data = conn_data
|
|
102
103
|
|
|
103
104
|
self.graph = self.__create_graph__()
|
|
105
|
+
self.nodes = [self.__get_x_y__(idx) for idx in range(len(self.graph))]
|
|
104
106
|
self.cacheGraph = CacheGraph(self.graph)
|
|
105
107
|
|
|
106
108
|
def __get_idx__(self, x: int, y: int):
|
|
@@ -217,6 +219,12 @@ class GridGraph:
|
|
|
217
219
|
Function:
|
|
218
220
|
|
|
219
221
|
- Create a graph from the grid specifications
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
|
|
225
|
+
- `graph`
|
|
226
|
+
- Type: list
|
|
227
|
+
- What: The adjacency list representation of the graph
|
|
220
228
|
"""
|
|
221
229
|
####################
|
|
222
230
|
# Create a list of lists to hold all the possible connections in an easy to access/understand format
|
|
@@ -282,6 +290,34 @@ class GridGraph:
|
|
|
282
290
|
|
|
283
291
|
return graph
|
|
284
292
|
|
|
293
|
+
def euclidean_heuristic(self, origin_id: int, destination_id: int) -> float:
|
|
294
|
+
"""
|
|
295
|
+
Function:
|
|
296
|
+
|
|
297
|
+
- Calculate the Euclidean distance between two nodes in the grid graph
|
|
298
|
+
|
|
299
|
+
Required Arguments:
|
|
300
|
+
|
|
301
|
+
- `origin_id`
|
|
302
|
+
- Type: int
|
|
303
|
+
- What: The id of the origin node
|
|
304
|
+
- `destination_id`
|
|
305
|
+
- Type: int
|
|
306
|
+
- What: The id of the destination node
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
|
|
310
|
+
- `distance`
|
|
311
|
+
- Type: float
|
|
312
|
+
- What: The Euclidean distance between the two nodes
|
|
313
|
+
"""
|
|
314
|
+
origin_location = self.nodes[origin_id]
|
|
315
|
+
destination_location = self.nodes[destination_id]
|
|
316
|
+
return (
|
|
317
|
+
(origin_location[0] - destination_location[0]) ** 2
|
|
318
|
+
+ (origin_location[1] - destination_location[1]) ** 2
|
|
319
|
+
) ** 0.5
|
|
320
|
+
|
|
285
321
|
def get_shortest_path(
|
|
286
322
|
self,
|
|
287
323
|
origin_node: dict[str, int] | tuple[int, int] | list[int],
|
|
@@ -290,6 +326,7 @@ class GridGraph:
|
|
|
290
326
|
cache: bool = False,
|
|
291
327
|
cache_for: str = "origin",
|
|
292
328
|
output_path: bool = False,
|
|
329
|
+
heuristic_fn: callable | Literal["euclidean"] | None = "euclidean",
|
|
293
330
|
**kwargs,
|
|
294
331
|
) -> dict:
|
|
295
332
|
"""
|
|
@@ -336,6 +373,13 @@ class GridGraph:
|
|
|
336
373
|
- Type: bool
|
|
337
374
|
- What: Whether to output the path as a list of graph idxs (mostly for debugging purposes)
|
|
338
375
|
- Default: False
|
|
376
|
+
- `heuristic_fn`
|
|
377
|
+
- Type: callable | Literal['euclidean'] | None
|
|
378
|
+
- What: A heuristic function to use for the A* algorithm if caching is False
|
|
379
|
+
- Default: 'euclidean' (A predefined heuristic function that calculates the Euclidean distance for this grid graph)
|
|
380
|
+
- If None, the A* algorithm will not be used and the Dijkstra's algorithm will be used instead
|
|
381
|
+
- If a callable is provided, it should take two arguments: origin_id and destination_id and return a float representing the heuristic distance between the two nodes
|
|
382
|
+
- Note: This distance should never be greater than the actual distance between the two nodes or you may get suboptimal paths
|
|
339
383
|
- `**kwargs`
|
|
340
384
|
- Additional keyword arguments. These are included for forwards and backwards compatibility reasons, but are not currently used.
|
|
341
385
|
"""
|
|
@@ -369,6 +413,11 @@ class GridGraph:
|
|
|
369
413
|
destination_id=destination_id,
|
|
370
414
|
cache=cache,
|
|
371
415
|
cache_for=cache_for,
|
|
416
|
+
heuristic_fn=(
|
|
417
|
+
self.euclidean_heuristic
|
|
418
|
+
if heuristic_fn == "euclidean"
|
|
419
|
+
else heuristic_fn
|
|
420
|
+
),
|
|
372
421
|
)
|
|
373
422
|
output["coordinate_path"] = self.get_coordinate_path(
|
|
374
423
|
output["path"], output_coordinate_path
|
|
@@ -40,20 +40,15 @@ class SpanningTree:
|
|
|
40
40
|
# Input Validation
|
|
41
41
|
assert isinstance(node_id, int), "node_id must be an integer"
|
|
42
42
|
assert 0 <= node_id < len(graph), "node_id must be a valid node id"
|
|
43
|
+
# Variable Initialization
|
|
43
44
|
distance_matrix = [float("inf")] * len(graph)
|
|
44
|
-
open_leaves = {}
|
|
45
|
-
predecessor = [-1] * len(graph)
|
|
46
|
-
visited = [0] * len(graph)
|
|
47
|
-
|
|
48
45
|
distance_matrix[node_id] = 0
|
|
49
46
|
open_leaves = []
|
|
50
47
|
heappush(open_leaves, (0, node_id))
|
|
48
|
+
predecessor = [-1] * len(graph)
|
|
51
49
|
|
|
52
50
|
while open_leaves:
|
|
53
51
|
current_distance, current_id = heappop(open_leaves)
|
|
54
|
-
if visited[current_id]:
|
|
55
|
-
continue
|
|
56
|
-
visited[current_id] = True
|
|
57
52
|
for connected_id, connected_distance in graph[current_id].items():
|
|
58
53
|
possible_distance = current_distance + connected_distance
|
|
59
54
|
if possible_distance < distance_matrix[connected_id]:
|
|
@@ -68,7 +63,7 @@ class SpanningTree:
|
|
|
68
63
|
}
|
|
69
64
|
|
|
70
65
|
@staticmethod
|
|
71
|
-
def get_path(origin_id: int,
|
|
66
|
+
def get_path(origin_id: int, destination_id: int, spanning_tree: dict):
|
|
72
67
|
"""
|
|
73
68
|
Function:
|
|
74
69
|
|
|
@@ -78,10 +73,10 @@ class SpanningTree:
|
|
|
78
73
|
|
|
79
74
|
Required Arguments:
|
|
80
75
|
|
|
81
|
-
- `
|
|
76
|
+
- `origin_id`
|
|
82
77
|
- Type: int
|
|
83
78
|
- What: The id of the origin node from the graph dictionary to start the shortest path from
|
|
84
|
-
- `
|
|
79
|
+
- `destination_id`
|
|
85
80
|
- Type: int
|
|
86
81
|
- What: The id of the destination node from the graph dictionary to end the shortest path at
|
|
87
82
|
- `spanning_tree`
|
|
@@ -93,7 +88,7 @@ class SpanningTree:
|
|
|
93
88
|
- None
|
|
94
89
|
"""
|
|
95
90
|
spanning_id = spanning_tree["node_id"]
|
|
96
|
-
destination_distance = spanning_tree["distance_matrix"][
|
|
91
|
+
destination_distance = spanning_tree["distance_matrix"][destination_id]
|
|
97
92
|
origin_distance = spanning_tree["distance_matrix"][origin_id]
|
|
98
93
|
|
|
99
94
|
if destination_distance == float("inf") or origin_distance == float(
|
|
@@ -103,23 +98,23 @@ class SpanningTree:
|
|
|
103
98
|
"Something went wrong: One or both of the origin and destination nodes are not connected to this spanning tree."
|
|
104
99
|
)
|
|
105
100
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
current_id = spanning_tree["predecessors"][current_id]
|
|
101
|
+
if spanning_id != origin_id and spanning_id != destination_id:
|
|
102
|
+
raise Exception(
|
|
103
|
+
"Something went wrong: Neither the origin nor the destination node is the same as the spanning node."
|
|
104
|
+
)
|
|
111
105
|
|
|
112
|
-
|
|
113
|
-
|
|
106
|
+
current_id = origin_id if spanning_id != origin_id else destination_id
|
|
107
|
+
current_path = []
|
|
114
108
|
while current_id != spanning_id:
|
|
115
|
-
|
|
109
|
+
current_path.append(current_id)
|
|
116
110
|
current_id = spanning_tree["predecessors"][current_id]
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
111
|
+
current_path.append(spanning_id)
|
|
112
|
+
if spanning_id == origin_id:
|
|
113
|
+
current_path.reverse()
|
|
114
|
+
current_length = spanning_tree["distance_matrix"][destination_id]
|
|
115
|
+
else:
|
|
116
|
+
current_length = spanning_tree["distance_matrix"][origin_id]
|
|
120
117
|
return {
|
|
121
|
-
"path":
|
|
122
|
-
|
|
123
|
-
+ destination_to_spanning_id,
|
|
124
|
-
"length": hard_round(4, origin_distance + destination_distance),
|
|
118
|
+
"path": current_path,
|
|
119
|
+
"length": hard_round(4, current_length),
|
|
125
120
|
}
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import math, json
|
|
2
2
|
|
|
3
|
+
# Constants for haversine and cheap ruler calculations
|
|
4
|
+
earth_radius = {
|
|
5
|
+
"km": 6371,
|
|
6
|
+
"m": 6371000,
|
|
7
|
+
"mi": 3959,
|
|
8
|
+
"ft": 3959 * 5280,
|
|
9
|
+
}
|
|
10
|
+
radians_per_degree = math.pi / 180 # radians per degree
|
|
11
|
+
cheap_e2 = (1 / 298.257223563) * (2 - (1 / 298.257223563))
|
|
12
|
+
|
|
3
13
|
|
|
4
14
|
def haversine(
|
|
5
15
|
origin: list[float | int],
|
|
@@ -33,40 +43,95 @@ def haversine(
|
|
|
33
43
|
- Default: 1
|
|
34
44
|
|
|
35
45
|
"""
|
|
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
|
-
|
|
46
|
+
# convert decimal degrees to radians
|
|
47
|
+
lon1, lat1, lon2, lat2 = map(
|
|
48
|
+
math.radians,
|
|
49
|
+
[
|
|
50
|
+
origin[1],
|
|
51
|
+
origin[0],
|
|
52
|
+
destination[1],
|
|
53
|
+
destination[0],
|
|
54
|
+
],
|
|
55
|
+
)
|
|
56
|
+
# haversine formula
|
|
57
|
+
dlon = lon2 - lon1
|
|
58
|
+
dlat = lat2 - lat1
|
|
59
|
+
a = (
|
|
60
|
+
math.sin(dlat / 2) ** 2
|
|
61
|
+
+ math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
|
|
62
|
+
)
|
|
63
|
+
c = 2 * math.asin(a**0.5)
|
|
64
|
+
# Set the radius of earth based on the units specified
|
|
65
|
+
radius = earth_radius.get(
|
|
66
|
+
units, 6371
|
|
67
|
+
) # Default to kilometers if not specified or invalid
|
|
68
|
+
return c * radius * circuity
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def cheap_ruler(origin, destination, units="km", circuity=1):
|
|
72
|
+
"""
|
|
73
|
+
Function:
|
|
74
|
+
|
|
75
|
+
Calculates a fast approximate distance between two lat/lon points using Mapbox's "cheap ruler" method.
|
|
76
|
+
|
|
77
|
+
Note: In general, this method is considered faster than the haversine formula, but less accurate, especially near the poles and for long distances.
|
|
78
|
+
For this implementation, it tests slower than haversine, but it seems like there might be some room for optimization.
|
|
79
|
+
|
|
80
|
+
Required Arguments
|
|
81
|
+
- `origin`
|
|
82
|
+
- Type: list of two floats | ints
|
|
83
|
+
- What: The origin point as a list of "latitude" and "longitude"
|
|
84
|
+
- `destination`
|
|
85
|
+
- Type: list of two floats | ints
|
|
86
|
+
- What: The destination point as a list of "latitude" and "longitude"
|
|
87
|
+
|
|
88
|
+
Optional Arguments
|
|
89
|
+
|
|
90
|
+
- `units`
|
|
91
|
+
- Type: str
|
|
92
|
+
- What: units to return the distance in? (one of "km", "m", "mi", or "ft")
|
|
93
|
+
- Default: "km"
|
|
94
|
+
origin: [lat, lon] in degrees
|
|
95
|
+
destination: [lat, lon] in degrees
|
|
96
|
+
units: 'km', 'm', 'mi', or 'ft'
|
|
97
|
+
- `circuity`
|
|
98
|
+
- Type: int | float
|
|
99
|
+
- What: Multiplier to increase the calculated distance (to account for circuity)
|
|
100
|
+
- Default: 1
|
|
101
|
+
- Note: Consider using this as less than 1 when you are writing a heuristic function for
|
|
102
|
+
A* as this method can overestimate distances, especially near the Earth's poles.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Distance in the specified units
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
# Constants
|
|
109
|
+
radius = earth_radius.get(
|
|
110
|
+
units, 6371
|
|
111
|
+
) # Default to kilometers if not specified
|
|
112
|
+
lat1, lon1 = origin
|
|
113
|
+
lat2, lon2 = destination
|
|
114
|
+
# Get the adjusted longitude difference
|
|
115
|
+
lon_diff = abs(lon2 - lon1)
|
|
116
|
+
lon_diff = min(360 - lon_diff, lon_diff)
|
|
117
|
+
|
|
118
|
+
# Midpoint latitude in radians
|
|
119
|
+
mid_lat = (lat1 + lat2) / 2 * radians_per_degree
|
|
120
|
+
cos_lat = math.cos(mid_lat)
|
|
121
|
+
|
|
122
|
+
# Radius adjustments
|
|
123
|
+
w_squared = 1 / (1 - cheap_e2 * (1 - cos_lat**2))
|
|
124
|
+
w = w_squared**0.5
|
|
125
|
+
|
|
126
|
+
# Meters per degree at this latitude (scaled for km)
|
|
127
|
+
m = radians_per_degree * radius
|
|
128
|
+
kx = m * w * cos_lat
|
|
129
|
+
ky = m * w * w_squared * (1 - cheap_e2)
|
|
130
|
+
|
|
131
|
+
dx = (lon_diff) * kx
|
|
132
|
+
dy = (lat2 - lat1) * ky
|
|
133
|
+
|
|
134
|
+
return (dx**2 + dy**2) ** 0.5 * circuity
|
|
70
135
|
|
|
71
136
|
|
|
72
137
|
def hard_round(decimal_places: int, a: [float | int]):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scgraph
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: Determine an approximate route between two points on earth.
|
|
5
5
|
Author-email: Connor Makowski <conmak@mit.edu>
|
|
6
6
|
Project-URL: Homepage, https://github.com/connor-makowski/scgraph
|
|
@@ -43,6 +43,9 @@ Low Level: https://connor-makowski.github.io/scgraph/scgraph/core.html
|
|
|
43
43
|
- Modified to support sparse network data structures
|
|
44
44
|
- Makowski's Modified Sparse Dijkstra algorithm
|
|
45
45
|
- Modified for O(n) performance on particularly sparse networks
|
|
46
|
+
- A* algorithm (Extension of Makowski's Modified Sparse Dijkstra)
|
|
47
|
+
- Uses a heuristic function to improve performance on large graphs
|
|
48
|
+
- Note: The heuristic function is optional and defaults to Dijkstra's algorithm
|
|
46
49
|
- Possible future support for other algorithms
|
|
47
50
|
- Distances:
|
|
48
51
|
- Uses the [haversine formula](https://en.wikipedia.org/wiki/Haversine_formula) to calculate the distance between two points on earth
|
|
@@ -89,7 +92,8 @@ In this case, calculate the shortest maritime path between Shanghai, China and S
|
|
|
89
92
|
# Use a maritime network geograph
|
|
90
93
|
from scgraph.geographs.marnet import marnet_geograph
|
|
91
94
|
|
|
92
|
-
# Get the shortest path between
|
|
95
|
+
# Get the shortest maritime path between Shanghai, China and Savannah, Georgia, USA
|
|
96
|
+
# Note: The origin and destination nodes can be any latitude / longitude pair
|
|
93
97
|
output = marnet_geograph.get_shortest_path(
|
|
94
98
|
origin_node={"latitude": 31.23,"longitude": 121.47},
|
|
95
99
|
destination_node={"latitude": 32.08,"longitude": -81.09},
|
|
@@ -194,11 +198,16 @@ Using `scgraph_data` geographs:
|
|
|
194
198
|
- Note: Make sure to install the `scgraph_data` package before using these geographs
|
|
195
199
|
```py
|
|
196
200
|
from scgraph_data.world_railways import world_railways_geograph
|
|
201
|
+
from scgraph import Graph
|
|
197
202
|
|
|
198
203
|
# Get the shortest path between Kalamazoo Michigan and Detroit Michigan by Train
|
|
199
204
|
output = world_railways_geograph.get_shortest_path(
|
|
200
205
|
origin_node={"latitude": 42.29,"longitude": -85.58},
|
|
201
|
-
destination_node={"latitude": 42.33,"longitude": -83.05}
|
|
206
|
+
destination_node={"latitude": 42.33,"longitude": -83.05},
|
|
207
|
+
# Optional: Use the A* algorithm
|
|
208
|
+
algorithm_fn=Graph.a_star,
|
|
209
|
+
# Optional: Pass the haversine function as the heuristic function to the A* algorithm
|
|
210
|
+
algorithm_kwargs={"heuristic_fn": world_railways_geograph.haversine},
|
|
202
211
|
)
|
|
203
212
|
```
|
|
204
213
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|