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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scgraph
3
- Version: 2.6.0
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.6.0"
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
- destionation_id=destination_id,
79
+ destination_id=destination_id,
72
80
  spanning_tree=spanning_tree,
73
81
  )
74
- return Graph.dijkstra_makowski(
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, hard_round, distance_converter, get_line_path
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": hard_round(4, distance_matrix[destination_id]),
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 True:
308
- if len(open_leaves) == 0:
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(open_leaves, (possible_distance, connected_id))
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": hard_round(4, distance_matrix[destination_id]),
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, destionation_id: int, spanning_tree: dict):
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
- - `origin_idx`
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
- - `destination_idx`
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"][destionation_id]
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
- origin_to_spanning_id = []
107
- current_id = origin_id
108
- while current_id != spanning_id:
109
- origin_to_spanning_id.append(current_id)
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
- destination_to_spanning_id = []
113
- current_id = destionation_id
106
+ current_id = origin_id if spanning_id != origin_id else destination_id
107
+ current_path = []
114
108
  while current_id != spanning_id:
115
- destination_to_spanning_id.append(current_id)
109
+ current_path.append(current_id)
116
110
  current_id = spanning_tree["predecessors"][current_id]
117
- # Reverse the destination path to get the correct order
118
- destination_to_spanning_id.reverse()
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": origin_to_spanning_id
122
- + [spanning_id]
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
- try:
37
- # convert decimal degrees to radians
38
- lon1, lat1, lon2, lat2 = map(
39
- math.radians,
40
- [
41
- origin[1],
42
- origin[0],
43
- destination[1],
44
- destination[0],
45
- ],
46
- )
47
- # haversine formula
48
- dlon = lon2 - lon1
49
- dlat = lat2 - lat1
50
- a = (
51
- math.sin(dlat / 2) ** 2
52
- + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
53
- )
54
- c = 2 * math.asin(a**0.5)
55
- # Set the radius of earth based on the units specified
56
- if units == "km":
57
- radius = 6371
58
- elif units == "m":
59
- radius = 6371000
60
- elif units == "mi":
61
- radius = 3959
62
- elif units == "ft":
63
- radius = 3959 * 5280
64
- else:
65
- raise ValueError('Units must be one of "km", "m", "mi", or "ft"')
66
- return c * radius * circuity
67
- except:
68
- print(origin, destination)
69
- raise Exception()
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.6.0
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
 
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = scgraph
3
- version = 2.6.0
3
+ version = 2.7.0
4
4
  description_file = README.md
5
5
 
6
6
  [options]
File without changes