scgraph.graph

  1from heapq import heappop, heappush
  2from scgraph.bmssp import BmsspSolver
  3
  4
  5class Graph:
  6    @staticmethod
  7    def validate_graph(
  8        graph: list[dict[int, int | float]],
  9        check_symmetry: bool = True,
 10        check_connected: bool = True,
 11    ) -> None:
 12        """
 13        Function:
 14
 15        - Validate that a graph is properly formatted
 16
 17        Required Arguments:
 18
 19        - `graph`:
 20            - Type: list of dictionaries with integer keys and integer or float values
 21            - What: A list of dictionaries where the indicies are origin node ids and the values are dictionaries of destination node indices and graph weights
 22            - Note: All nodes must be included as origins in the graph regardless of if they have any connected destinations
 23            - EG:
 24            ```
 25                [
 26                    # From London (index 0)
 27                    {
 28                        # To Paris (index 1)
 29                        1: 311,
 30                    },
 31                    # From Paris (index 1)
 32                    {
 33                        # To London (index 0)
 34                        0: 311,
 35                        # To Berlin (index 2)
 36                        2: 878,
 37                        # To Rome (index 3)
 38                        3: 1439,
 39                        # To Madrid (index 4)
 40                        4: 1053
 41                    },
 42                    # From Berlin (index 2)
 43                    {
 44                        # To Paris (index 1)
 45                        1: 878,
 46                        # To Rome (index 3)
 47                        3: 1181,
 48                    },
 49                    # From Rome (index 3)
 50                    {
 51                        # To Paris (index 1)
 52                        1: 1439,
 53                        # To Berlin (index 2)
 54                        2: 1181,
 55                    },
 56                    # From Madrid (index 4)
 57                    {
 58                        # To Paris (index 1)
 59                        1: 1053,
 60                        # To Lisbon (index 5)
 61                        5: 623
 62                    },
 63                    # From Lisbon (index 5)
 64                    {
 65                        # To Madrid (index 4)
 66                        4: 623
 67                    }
 68                ]
 69            ```
 70
 71        Optional Arguments:
 72
 73        - `check_symmetry`
 74            - Type: bool
 75            - What: Whether to check that the graph is symmetric
 76            - Default: True
 77            - Note: This is forced to True if `check_connected` is True
 78        - `check_connected`
 79            - Type: bool
 80            - What: Whether to check that the graph is fully connected
 81            - Default: True
 82            - Note: For computational efficiency, only symmetric graphs are checked for connectivity
 83            - Note: If this is True, `check_symmetry` is forced to True and the graph will be checked for symmetry prior to checking for connectivity
 84        """
 85        check_symmetry = check_symmetry or check_connected
 86        assert isinstance(graph, list), "Your graph must be a list"
 87        len_graph = len(graph)
 88        for origin_id, origin_dict in enumerate(graph):
 89            assert isinstance(
 90                origin_dict, dict
 91            ), f"Your graph must be a list of dictionaries but the value for origin {origin_id} is not a dictionary"
 92            destinations = list(origin_dict.keys())
 93            lengths = list(origin_dict.values())
 94            assert all(
 95                [
 96                    (isinstance(i, int) and i >= 0 and i < len_graph)
 97                    for i in destinations
 98                ]
 99            ), f"Destination ids must be non-negative integers and equivalent to an existing index, but graph[{origin_id}] has an error in the destination ids"
100            assert all(
101                [(isinstance(i, (int, float)) and i >= 0) for i in lengths]
102            ), f"Distances must be integers or floats, but graph[{origin_id}] contains a non-integer or non-float distance"
103            if check_symmetry:
104                for destination_id, distance in origin_dict.items():
105                    assert (
106                        graph[destination_id].get(origin_id) == distance
107                    ), f"Your graph is not symmetric, the distance from node {origin_id} to node {destination_id} is {distance} but the distance from node {destination_id} to node {origin_id} is {graph.get(destination_id, {}).get(origin_id)}"
108        if check_connected:
109            assert Graph.validate_connected(
110                graph
111            ), "Your graph is not fully connected"
112
113    @staticmethod
114    def validate_connected(
115        graph: list[dict[int, int | float]], origin_id: int = 0
116    ) -> bool:
117        """
118        Function:
119
120        - Validate that a graph is fully connected
121            - This means that every node in the graph has a path to every other node in the graph
122            - Note: This assumes that the graph is symmetric
123        - Return True if the graph is fully connected and False if it is not
124
125        Required Arguments:
126
127        - `graph`:
128            - Type: list of dictionaries
129            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
130
131        Optional Arguments:
132
133        - `origin_id`
134            - Type: int
135            - What: The id of the origin node from which to start the connectivity check
136            - Default: 0
137        """
138        visited = [0] * len(graph)
139        open_leaves = [origin_id]
140
141        while open_leaves:
142            current_id = open_leaves.pop()
143            visited[current_id] = 1
144            for connected_id, connected_distance in graph[current_id].items():
145                if visited[connected_id] == 0:
146                    open_leaves.append(connected_id)
147        return min(visited) == 1
148
149    @staticmethod
150    def input_check(
151        graph: list[dict[int, int | float]], origin_id: int, destination_id: int
152    ) -> None:
153        """
154        Function:
155
156        - Check that the inputs passed to the shortest path algorithm are valid
157        - Raises an exception if the inputs passed are not valid
158
159        Required Arguments:
160
161        - `graph`:
162            - Type: list of dictionaries
163            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
164        - `origin_id`
165            - Type: int
166            - What: The id of the origin node from the graph dictionary to start the shortest path from
167        - `destination_id`
168            - Type: int
169            - What: The id of the destination node from the graph dictionary to end the shortest path at
170
171        Optional Arguments:
172
173        - None
174        """
175        if (
176            not isinstance(origin_id, int)
177            and origin_id < len(graph)
178            and origin_id >= 0
179        ):
180            raise Exception(f"Origin node ({origin_id}) is not in the graph")
181        if (
182            not isinstance(destination_id, int)
183            and origin_id < len(graph)
184            and origin_id >= 0
185        ):
186            raise Exception(
187                f"Destination node ({destination_id}) is not in the graph"
188            )
189
190    @staticmethod
191    def reconstruct_path(
192        destination_id: int, predecessor: list[int]
193    ) -> list[int]:
194        """
195        Function:
196
197        - Reconstruct the shortest path from the destination node to the origin node
198        - Return the reconstructed path in the correct order
199        - Given the predecessor list, this function reconstructs the path
200
201        Required Arguments:
202
203        - `destination_id`
204            - Type: int
205            - What: The id of the destination node from the graph dictionary to end the shortest path at
206        - `predecessor`
207            - Type: list[int]
208            - What: The predecessor list that was used to compute the shortest path
209            - This list is used to reconstruct the path from the destination node to the origin node
210            - Note: Nodes with no predecessor should be -1
211
212        Optional Arguments:
213
214        - None
215        """
216        output_path = [destination_id]
217        while predecessor[destination_id] != -1:
218            destination_id = predecessor[destination_id]
219            output_path.append(destination_id)
220        output_path.reverse()
221        return output_path
222
223    @staticmethod
224    def dijkstra(
225        graph: list[dict[int, int | float]], origin_id: int, destination_id: int
226    ) -> dict:
227        """
228        Function:
229
230        - Identify the shortest path between two nodes in a sparse network graph using a modified dijkstra algorithm
231            - Modifications allow for a sparse distance matrix to be used instead of a dense distance matrix
232            - This can dramatically reduce the memory and compute requirements of the algorithm
233            - This algorithm should run in O(n^2) time where n is the number of nodes in the graph
234        - Return a dictionary of various path information including:
235            - `id_path`: A list of node ids in the order they are visited
236            - `path`: A list of node dictionaries (lat + long) in the order they are visited
237
238        Required Arguments:
239
240        - `graph`:
241            - Type: list of dictionaries
242            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
243        - `origin_id`
244            - Type: int
245            - What: The id of the origin node from the graph dictionary to start the shortest path from
246        - `destination_id`
247            - Type: int
248            - What: The id of the destination node from the graph dictionary to end the shortest path at
249
250        Optional Arguments:
251
252        - None
253        """
254        Graph.input_check(
255            graph=graph, origin_id=origin_id, destination_id=destination_id
256        )
257        distance_matrix = [float("inf")] * len(graph)
258        branch_tip_distances = [float("inf")] * len(graph)
259        predecessor = [-1] * len(graph)
260
261        distance_matrix[origin_id] = 0
262        branch_tip_distances[origin_id] = 0
263
264        while True:
265            current_distance = min(branch_tip_distances)
266            if current_distance == float("inf"):
267                raise Exception(
268                    "Something went wrong, the origin and destination nodes are not connected."
269                )
270            current_id = branch_tip_distances.index(current_distance)
271            branch_tip_distances[current_id] = float("inf")
272            if current_id == destination_id:
273                break
274            for connected_id, connected_distance in graph[current_id].items():
275                possible_distance = current_distance + connected_distance
276                if possible_distance < distance_matrix[connected_id]:
277                    distance_matrix[connected_id] = possible_distance
278                    predecessor[connected_id] = current_id
279                    branch_tip_distances[connected_id] = possible_distance
280
281        return {
282            "path": Graph.reconstruct_path(destination_id, predecessor),
283            "length": distance_matrix[destination_id],
284        }
285
286    @staticmethod
287    def dijkstra_makowski(
288        graph: list[dict[int, int | float]], origin_id: int, destination_id: int
289    ) -> dict:
290        """
291        Function:
292
293        - Identify the shortest path between two nodes in a sparse network graph using a modified Dijkstra algorithm
294            - Modifications allow for a sparse distance matrix to be used instead of a dense distance matrix
295            - Improvements include only computing future potential nodes based on the open leaves for each branch
296                - Open leaves are nodes that have not been visited yet but are adjacent to other visited nodes
297            - This can dramatically reduce the memory and compute requirements of the algorithm
298            - This algorithm should run close to O((n+m) log n) time
299                - Where n is the number of nodes in the graph and m is the number of edges in the graph
300        - Return a dictionary of various path information including:
301            - `id_path`: A list of node ids in the order they are visited
302            - `path`: A list of node dictionaries (lat + long) in the order they are visited
303
304        Required Arguments:
305
306        - `graph`:
307            - Type: list of dictionaries
308            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
309        - `origin_id`
310            - Type: int
311            - What: The id of the origin node from the graph dictionary to start the shortest path from
312        - `destination_id`
313            - Type: int
314            - What: The id of the destination node from the graph dictionary to end the shortest path at
315
316        Optional Arguments:
317
318        - None
319        """
320        # Input Validation
321        Graph.input_check(
322            graph=graph, origin_id=origin_id, destination_id=destination_id
323        )
324        # Variable Initialization
325        distance_matrix = [float("inf")] * len(graph)
326        distance_matrix[origin_id] = 0
327        open_leaves = []
328        heappush(open_leaves, (0, origin_id))
329        predecessor = [-1] * len(graph)
330
331        while open_leaves:
332            current_distance, current_id = heappop(open_leaves)
333            if current_id == destination_id:
334                break
335            # Technically, the next line is not necessary but can help with performance
336            if current_distance == distance_matrix[current_id]:
337                for connected_id, connected_distance in graph[
338                    current_id
339                ].items():
340                    possible_distance = current_distance + connected_distance
341                    if possible_distance < distance_matrix[connected_id]:
342                        distance_matrix[connected_id] = possible_distance
343                        predecessor[connected_id] = current_id
344                        heappush(open_leaves, (possible_distance, connected_id))
345        if current_id != destination_id:
346            raise Exception(
347                "Something went wrong, the origin and destination nodes are not connected."
348            )
349
350        return {
351            "path": Graph.reconstruct_path(destination_id, predecessor),
352            "length": distance_matrix[destination_id],
353        }
354
355    @staticmethod
356    def cycle_check(predecessor_matrix, node_id):
357        """
358        Function:
359
360        - Check if a cycle exists in the predecessor matrix starting from the given node_id
361        - Returns None if no cycle is detected
362        - Raises an Exception if a cycle is detected
363
364        Required Arguments:
365
366        - `predecessor_matrix`:
367            - Type: list[int]
368            - What: A list where the index represents the node id and the value at that index is the predecessor node id
369        - `node_id`:
370            - Type: int
371            - What: The node id to start checking for cycles from
372
373        Optional Arguments:
374
375        - None
376        """
377        cycle_id = node_id
378        while True:
379            cycle_id = predecessor_matrix[cycle_id]
380            if cycle_id == -1:
381                return
382            if cycle_id == node_id:
383                raise Exception(
384                    f"Cycle detected in the graph at node {node_id}"
385                )
386
387    @staticmethod
388    def dijkstra_negative(
389        graph: list[dict[int, int | float]],
390        origin_id: int,
391        destination_id: int,
392        cycle_check_iterations: int | None = None,
393    ) -> dict:
394        """
395        Function:
396
397        - Identify the shortest path between two nodes in a sparse network graph using a modified Dijkstra algorithm that catches negative cycles
398            - Negative cycles raise an exception if they are detected
399        - Note: This algorithm is guaranteed to find the shortest path or raise an exception if a negative cycle is detected
400        - Note: This algorithm requires computing the entire spanning tree of the graph and is therefore not able to be terminated early
401            - For non negative weighted graphs, it is recommended to use the `dijkstra_makowski` algorithm instead
402        - Note: For certain graphs with weights that are negative, this algorithm may run far slower than O((n+m) log n)
403
404        - Return a dictionary of various path information including:
405            - `id_path`: A list of node ids in the order they are visited
406            - `path`: A list of node dictionaries (lat + long) in the order they are visited
407
408        Required Arguments:
409
410        - `graph`:
411            - Type: list of dictionaries
412            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
413        - `origin_id`
414            - Type: int
415            - What: The id of the origin node from the graph dictionary to start the shortest path from
416        - `destination_id`
417            - Type: int
418            - What: The id of the destination node from the graph dictionary to end the shortest path at
419
420        Optional Arguments:
421
422        - `cycle_check_iterations`
423            - Type: int | None
424            - Default: None -> The length of the graph is used as the default number of iterations to loop over before checking for negative cycles
425            - What: The number of iterations to loop over before checking for negative cycles
426        """
427        # Input Validation
428        Graph.input_check(
429            graph=graph, origin_id=origin_id, destination_id=destination_id
430        )
431        # Variable Initialization
432        distance_matrix = [float("inf")] * len(graph)
433        distance_matrix[origin_id] = 0
434        open_leaves = []
435        heappush(open_leaves, (0, origin_id))
436        predecessor = [-1] * len(graph)
437
438        # Cycle iteration Variables
439        cycle_iteration = 0
440        if cycle_check_iterations is None:
441            cycle_check_iterations = len(graph)
442
443        while open_leaves:
444            current_distance, current_id = heappop(open_leaves)
445            if current_distance == distance_matrix[current_id]:
446                # Increment the cycle iteration counter and check for negative cycles if the iteration limit is reached
447                cycle_iteration += 1
448                if cycle_iteration >= cycle_check_iterations:
449                    cycle_iteration = 0  # Reset the cycle iteration counter
450                    Graph.cycle_check(
451                        predecessor_matrix=predecessor, node_id=current_id
452                    )
453                for connected_id, connected_distance in graph[
454                    current_id
455                ].items():
456                    possible_distance = current_distance + connected_distance
457                    if possible_distance < distance_matrix[connected_id]:
458                        distance_matrix[connected_id] = possible_distance
459                        predecessor[connected_id] = current_id
460                        heappush(open_leaves, (possible_distance, connected_id))
461
462        if distance_matrix[destination_id] == float("inf"):
463            raise Exception(
464                "Something went wrong, the origin and destination nodes are not connected."
465            )
466
467        return {
468            "path": Graph.reconstruct_path(destination_id, predecessor),
469            "length": distance_matrix[destination_id],
470        }
471
472    @staticmethod
473    def a_star(
474        graph: list[dict[int, int | float]],
475        origin_id: int,
476        destination_id: int,
477        heuristic_fn: callable = None,
478    ) -> dict:
479        """
480        Function:
481
482        - Identify the shortest path between two nodes in a sparse network graph using an A* extension of Makowski's modified Dijkstra algorithm
483        - Return a dictionary of various path information including:
484            - `id_path`: A list of node ids in the order they are visited
485            - `path`: A list of node dictionaries (lat + long) in the order they are visited
486
487        Required Arguments:
488
489        - `graph`:
490            - Type: list of dictionaries
491            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
492        - `origin_id`
493            - Type: int
494            - What: The id of the origin node from the graph dictionary to start the shortest path from
495        - `destination_id`
496            - Type: int
497            - What: The id of the destination node from the graph dictionary to end the shortest path at
498        - `heuristic_fn`
499            - Type: function
500            - What: A heuristic function that takes two node ids and returns an estimated distance between them
501            - Note: If None, returns the shortest path using Makowski's modified Dijkstra algorithm
502            - Default: None
503
504        Optional Arguments:
505
506        - None
507        """
508        if heuristic_fn is None:
509            return Graph.dijkstra_makowski(
510                graph=graph,
511                origin_id=origin_id,
512                destination_id=destination_id,
513            )
514        # Input Validation
515        Graph.input_check(
516            graph=graph, origin_id=origin_id, destination_id=destination_id
517        )
518        # Variable Initialization
519        distance_matrix = [float("inf")] * len(graph)
520        distance_matrix[origin_id] = 0
521        # Using a visited matrix does add a tad bit of overhead but avoids revisiting nodes
522        # and does not require anything extra to be stored in the heap
523        visited = [0] * len(graph)
524        open_leaves = []
525        heappush(open_leaves, (0, origin_id))
526        predecessor = [-1] * len(graph)
527
528        while open_leaves:
529            current_id = heappop(open_leaves)[1]
530            if current_id == destination_id:
531                break
532            if visited[current_id] == 1:
533                continue
534            visited[current_id] = 1
535            current_distance = distance_matrix[current_id]
536            for connected_id, connected_distance in graph[current_id].items():
537                possible_distance = current_distance + connected_distance
538                if possible_distance < distance_matrix[connected_id]:
539                    distance_matrix[connected_id] = possible_distance
540                    predecessor[connected_id] = current_id
541                    heappush(
542                        open_leaves,
543                        (
544                            possible_distance
545                            + heuristic_fn(connected_id, destination_id),
546                            connected_id,
547                        ),
548                    )
549        if current_id != destination_id:
550            raise Exception(
551                "Something went wrong, the origin and destination nodes are not connected."
552            )
553
554        return {
555            "path": Graph.reconstruct_path(destination_id, predecessor),
556            "length": distance_matrix[destination_id],
557        }
558
559    @staticmethod
560    def bellman_ford(
561        graph: list[dict[int, int | float]],
562        origin_id: int,
563        destination_id: int,
564    ) -> dict:
565        """
566        Function:
567
568        - Identify the shortest path between two nodes in a sparse network graph using the Bellman-Ford algorithm
569        - Return a dictionary of various path information including:
570            - `id_path`: A list of node ids in the order they are visited
571            - `path`: A list of node dictionaries (lat + long) in the order they are visited
572
573        Required Arguments:
574
575        - `graph`:
576            - Type: list of dictionaries
577            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
578        - `origin_id`
579            - Type: int
580            - What: The id of the origin node from the graph dictionary to start the shortest path from
581        - `destination_id`
582            - Type: int
583            - What: The id of the destination node from the graph dictionary to end the shortest path at
584
585        Optional Arguments:
586
587        - None
588        """
589        # Input Validation
590        Graph.input_check(
591            graph=graph, origin_id=origin_id, destination_id=destination_id
592        )
593        # Variable Initialization
594        distance_matrix = [float("inf")] * len(graph)
595        distance_matrix[origin_id] = 0
596        predecessor = [-1] * len(graph)
597
598        len_graph = len(graph)
599        for i in range(len_graph):
600            for current_id in range(len(graph)):
601                current_distance = distance_matrix[current_id]
602                for connected_id, connected_distance in graph[
603                    current_id
604                ].items():
605                    possible_distance = current_distance + connected_distance
606                    if possible_distance < distance_matrix[connected_id]:
607                        distance_matrix[connected_id] = possible_distance
608                        predecessor[connected_id] = current_id
609                        if i == len_graph - 1:
610                            raise Exception(
611                                "Graph contains a negative weight cycle"
612                            )
613        # Check if destination is reachable
614        if distance_matrix[destination_id] == float("inf"):
615            raise Exception(
616                "Something went wrong, the origin and destination nodes are not connected."
617            )
618
619        return {
620            "path": Graph.reconstruct_path(destination_id, predecessor),
621            "length": distance_matrix[destination_id],
622        }
623
624    @staticmethod
625    def bmssp(
626        graph: list[dict[int, int | float]], origin_id: int, destination_id: int
627    ):
628        """
629        Function:
630
631        - A Full BMSSP-style shortest path solver with a Dijkstra finalizer for non-relaxed edges.
632        - Return a dictionary of various path information including:
633            - `id_path`: A list of node ids in the order they are visited
634            - `path`: A list of node dictionaries (lat + long) in the order they are visited
635
636        Required Arguments:
637
638        - `graph`:
639            - Type: list of dictionaries
640            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
641        - `origin_id`
642            - Type: int
643            - What: The id of the origin node from the graph dictionary to start the shortest path from
644        - `destination_id`
645            - Type: int
646            - What: The id of the destination node from the graph dictionary to end the shortest path at
647        - `heuristic_fn`
648            - Type: function
649            - What: A heuristic function that takes two node ids and returns an estimated distance between them
650            - Note: If None, returns the shortest path using Makowski's modified Dijkstra algorithm
651            - Default: None
652
653        Optional Arguments:
654
655        - None
656        """
657        # Input Validation
658        Graph.input_check(
659            graph=graph, origin_id=origin_id, destination_id=destination_id
660        )
661        # Run the BMSSP Algorithm to relax as many edges as possible.
662        solver = BmsspSolver(graph, origin_id)
663        if solver.distance_matrix[destination_id] == float("inf"):
664            raise Exception(
665                "Something went wrong, the origin and destination nodes are not connected."
666            )
667
668        return {
669            "path": Graph.reconstruct_path(destination_id, solver.predecessor),
670            "length": solver.distance_matrix[destination_id],
671        }
class Graph:
  6class Graph:
  7    @staticmethod
  8    def validate_graph(
  9        graph: list[dict[int, int | float]],
 10        check_symmetry: bool = True,
 11        check_connected: bool = True,
 12    ) -> None:
 13        """
 14        Function:
 15
 16        - Validate that a graph is properly formatted
 17
 18        Required Arguments:
 19
 20        - `graph`:
 21            - Type: list of dictionaries with integer keys and integer or float values
 22            - What: A list of dictionaries where the indicies are origin node ids and the values are dictionaries of destination node indices and graph weights
 23            - Note: All nodes must be included as origins in the graph regardless of if they have any connected destinations
 24            - EG:
 25            ```
 26                [
 27                    # From London (index 0)
 28                    {
 29                        # To Paris (index 1)
 30                        1: 311,
 31                    },
 32                    # From Paris (index 1)
 33                    {
 34                        # To London (index 0)
 35                        0: 311,
 36                        # To Berlin (index 2)
 37                        2: 878,
 38                        # To Rome (index 3)
 39                        3: 1439,
 40                        # To Madrid (index 4)
 41                        4: 1053
 42                    },
 43                    # From Berlin (index 2)
 44                    {
 45                        # To Paris (index 1)
 46                        1: 878,
 47                        # To Rome (index 3)
 48                        3: 1181,
 49                    },
 50                    # From Rome (index 3)
 51                    {
 52                        # To Paris (index 1)
 53                        1: 1439,
 54                        # To Berlin (index 2)
 55                        2: 1181,
 56                    },
 57                    # From Madrid (index 4)
 58                    {
 59                        # To Paris (index 1)
 60                        1: 1053,
 61                        # To Lisbon (index 5)
 62                        5: 623
 63                    },
 64                    # From Lisbon (index 5)
 65                    {
 66                        # To Madrid (index 4)
 67                        4: 623
 68                    }
 69                ]
 70            ```
 71
 72        Optional Arguments:
 73
 74        - `check_symmetry`
 75            - Type: bool
 76            - What: Whether to check that the graph is symmetric
 77            - Default: True
 78            - Note: This is forced to True if `check_connected` is True
 79        - `check_connected`
 80            - Type: bool
 81            - What: Whether to check that the graph is fully connected
 82            - Default: True
 83            - Note: For computational efficiency, only symmetric graphs are checked for connectivity
 84            - Note: If this is True, `check_symmetry` is forced to True and the graph will be checked for symmetry prior to checking for connectivity
 85        """
 86        check_symmetry = check_symmetry or check_connected
 87        assert isinstance(graph, list), "Your graph must be a list"
 88        len_graph = len(graph)
 89        for origin_id, origin_dict in enumerate(graph):
 90            assert isinstance(
 91                origin_dict, dict
 92            ), f"Your graph must be a list of dictionaries but the value for origin {origin_id} is not a dictionary"
 93            destinations = list(origin_dict.keys())
 94            lengths = list(origin_dict.values())
 95            assert all(
 96                [
 97                    (isinstance(i, int) and i >= 0 and i < len_graph)
 98                    for i in destinations
 99                ]
100            ), f"Destination ids must be non-negative integers and equivalent to an existing index, but graph[{origin_id}] has an error in the destination ids"
101            assert all(
102                [(isinstance(i, (int, float)) and i >= 0) for i in lengths]
103            ), f"Distances must be integers or floats, but graph[{origin_id}] contains a non-integer or non-float distance"
104            if check_symmetry:
105                for destination_id, distance in origin_dict.items():
106                    assert (
107                        graph[destination_id].get(origin_id) == distance
108                    ), f"Your graph is not symmetric, the distance from node {origin_id} to node {destination_id} is {distance} but the distance from node {destination_id} to node {origin_id} is {graph.get(destination_id, {}).get(origin_id)}"
109        if check_connected:
110            assert Graph.validate_connected(
111                graph
112            ), "Your graph is not fully connected"
113
114    @staticmethod
115    def validate_connected(
116        graph: list[dict[int, int | float]], origin_id: int = 0
117    ) -> bool:
118        """
119        Function:
120
121        - Validate that a graph is fully connected
122            - This means that every node in the graph has a path to every other node in the graph
123            - Note: This assumes that the graph is symmetric
124        - Return True if the graph is fully connected and False if it is not
125
126        Required Arguments:
127
128        - `graph`:
129            - Type: list of dictionaries
130            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
131
132        Optional Arguments:
133
134        - `origin_id`
135            - Type: int
136            - What: The id of the origin node from which to start the connectivity check
137            - Default: 0
138        """
139        visited = [0] * len(graph)
140        open_leaves = [origin_id]
141
142        while open_leaves:
143            current_id = open_leaves.pop()
144            visited[current_id] = 1
145            for connected_id, connected_distance in graph[current_id].items():
146                if visited[connected_id] == 0:
147                    open_leaves.append(connected_id)
148        return min(visited) == 1
149
150    @staticmethod
151    def input_check(
152        graph: list[dict[int, int | float]], origin_id: int, destination_id: int
153    ) -> None:
154        """
155        Function:
156
157        - Check that the inputs passed to the shortest path algorithm are valid
158        - Raises an exception if the inputs passed are not valid
159
160        Required Arguments:
161
162        - `graph`:
163            - Type: list of dictionaries
164            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
165        - `origin_id`
166            - Type: int
167            - What: The id of the origin node from the graph dictionary to start the shortest path from
168        - `destination_id`
169            - Type: int
170            - What: The id of the destination node from the graph dictionary to end the shortest path at
171
172        Optional Arguments:
173
174        - None
175        """
176        if (
177            not isinstance(origin_id, int)
178            and origin_id < len(graph)
179            and origin_id >= 0
180        ):
181            raise Exception(f"Origin node ({origin_id}) is not in the graph")
182        if (
183            not isinstance(destination_id, int)
184            and origin_id < len(graph)
185            and origin_id >= 0
186        ):
187            raise Exception(
188                f"Destination node ({destination_id}) is not in the graph"
189            )
190
191    @staticmethod
192    def reconstruct_path(
193        destination_id: int, predecessor: list[int]
194    ) -> list[int]:
195        """
196        Function:
197
198        - Reconstruct the shortest path from the destination node to the origin node
199        - Return the reconstructed path in the correct order
200        - Given the predecessor list, this function reconstructs the path
201
202        Required Arguments:
203
204        - `destination_id`
205            - Type: int
206            - What: The id of the destination node from the graph dictionary to end the shortest path at
207        - `predecessor`
208            - Type: list[int]
209            - What: The predecessor list that was used to compute the shortest path
210            - This list is used to reconstruct the path from the destination node to the origin node
211            - Note: Nodes with no predecessor should be -1
212
213        Optional Arguments:
214
215        - None
216        """
217        output_path = [destination_id]
218        while predecessor[destination_id] != -1:
219            destination_id = predecessor[destination_id]
220            output_path.append(destination_id)
221        output_path.reverse()
222        return output_path
223
224    @staticmethod
225    def dijkstra(
226        graph: list[dict[int, int | float]], origin_id: int, destination_id: int
227    ) -> dict:
228        """
229        Function:
230
231        - Identify the shortest path between two nodes in a sparse network graph using a modified dijkstra algorithm
232            - Modifications allow for a sparse distance matrix to be used instead of a dense distance matrix
233            - This can dramatically reduce the memory and compute requirements of the algorithm
234            - This algorithm should run in O(n^2) time where n is the number of nodes in the graph
235        - Return a dictionary of various path information including:
236            - `id_path`: A list of node ids in the order they are visited
237            - `path`: A list of node dictionaries (lat + long) in the order they are visited
238
239        Required Arguments:
240
241        - `graph`:
242            - Type: list of dictionaries
243            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
244        - `origin_id`
245            - Type: int
246            - What: The id of the origin node from the graph dictionary to start the shortest path from
247        - `destination_id`
248            - Type: int
249            - What: The id of the destination node from the graph dictionary to end the shortest path at
250
251        Optional Arguments:
252
253        - None
254        """
255        Graph.input_check(
256            graph=graph, origin_id=origin_id, destination_id=destination_id
257        )
258        distance_matrix = [float("inf")] * len(graph)
259        branch_tip_distances = [float("inf")] * len(graph)
260        predecessor = [-1] * len(graph)
261
262        distance_matrix[origin_id] = 0
263        branch_tip_distances[origin_id] = 0
264
265        while True:
266            current_distance = min(branch_tip_distances)
267            if current_distance == float("inf"):
268                raise Exception(
269                    "Something went wrong, the origin and destination nodes are not connected."
270                )
271            current_id = branch_tip_distances.index(current_distance)
272            branch_tip_distances[current_id] = float("inf")
273            if current_id == destination_id:
274                break
275            for connected_id, connected_distance in graph[current_id].items():
276                possible_distance = current_distance + connected_distance
277                if possible_distance < distance_matrix[connected_id]:
278                    distance_matrix[connected_id] = possible_distance
279                    predecessor[connected_id] = current_id
280                    branch_tip_distances[connected_id] = possible_distance
281
282        return {
283            "path": Graph.reconstruct_path(destination_id, predecessor),
284            "length": distance_matrix[destination_id],
285        }
286
287    @staticmethod
288    def dijkstra_makowski(
289        graph: list[dict[int, int | float]], origin_id: int, destination_id: int
290    ) -> dict:
291        """
292        Function:
293
294        - Identify the shortest path between two nodes in a sparse network graph using a modified Dijkstra algorithm
295            - Modifications allow for a sparse distance matrix to be used instead of a dense distance matrix
296            - Improvements include only computing future potential nodes based on the open leaves for each branch
297                - Open leaves are nodes that have not been visited yet but are adjacent to other visited nodes
298            - This can dramatically reduce the memory and compute requirements of the algorithm
299            - This algorithm should run close to O((n+m) log n) time
300                - Where n is the number of nodes in the graph and m is the number of edges in the graph
301        - Return a dictionary of various path information including:
302            - `id_path`: A list of node ids in the order they are visited
303            - `path`: A list of node dictionaries (lat + long) in the order they are visited
304
305        Required Arguments:
306
307        - `graph`:
308            - Type: list of dictionaries
309            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
310        - `origin_id`
311            - Type: int
312            - What: The id of the origin node from the graph dictionary to start the shortest path from
313        - `destination_id`
314            - Type: int
315            - What: The id of the destination node from the graph dictionary to end the shortest path at
316
317        Optional Arguments:
318
319        - None
320        """
321        # Input Validation
322        Graph.input_check(
323            graph=graph, origin_id=origin_id, destination_id=destination_id
324        )
325        # Variable Initialization
326        distance_matrix = [float("inf")] * len(graph)
327        distance_matrix[origin_id] = 0
328        open_leaves = []
329        heappush(open_leaves, (0, origin_id))
330        predecessor = [-1] * len(graph)
331
332        while open_leaves:
333            current_distance, current_id = heappop(open_leaves)
334            if current_id == destination_id:
335                break
336            # Technically, the next line is not necessary but can help with performance
337            if current_distance == distance_matrix[current_id]:
338                for connected_id, connected_distance in graph[
339                    current_id
340                ].items():
341                    possible_distance = current_distance + connected_distance
342                    if possible_distance < distance_matrix[connected_id]:
343                        distance_matrix[connected_id] = possible_distance
344                        predecessor[connected_id] = current_id
345                        heappush(open_leaves, (possible_distance, connected_id))
346        if current_id != destination_id:
347            raise Exception(
348                "Something went wrong, the origin and destination nodes are not connected."
349            )
350
351        return {
352            "path": Graph.reconstruct_path(destination_id, predecessor),
353            "length": distance_matrix[destination_id],
354        }
355
356    @staticmethod
357    def cycle_check(predecessor_matrix, node_id):
358        """
359        Function:
360
361        - Check if a cycle exists in the predecessor matrix starting from the given node_id
362        - Returns None if no cycle is detected
363        - Raises an Exception if a cycle is detected
364
365        Required Arguments:
366
367        - `predecessor_matrix`:
368            - Type: list[int]
369            - What: A list where the index represents the node id and the value at that index is the predecessor node id
370        - `node_id`:
371            - Type: int
372            - What: The node id to start checking for cycles from
373
374        Optional Arguments:
375
376        - None
377        """
378        cycle_id = node_id
379        while True:
380            cycle_id = predecessor_matrix[cycle_id]
381            if cycle_id == -1:
382                return
383            if cycle_id == node_id:
384                raise Exception(
385                    f"Cycle detected in the graph at node {node_id}"
386                )
387
388    @staticmethod
389    def dijkstra_negative(
390        graph: list[dict[int, int | float]],
391        origin_id: int,
392        destination_id: int,
393        cycle_check_iterations: int | None = None,
394    ) -> dict:
395        """
396        Function:
397
398        - Identify the shortest path between two nodes in a sparse network graph using a modified Dijkstra algorithm that catches negative cycles
399            - Negative cycles raise an exception if they are detected
400        - Note: This algorithm is guaranteed to find the shortest path or raise an exception if a negative cycle is detected
401        - Note: This algorithm requires computing the entire spanning tree of the graph and is therefore not able to be terminated early
402            - For non negative weighted graphs, it is recommended to use the `dijkstra_makowski` algorithm instead
403        - Note: For certain graphs with weights that are negative, this algorithm may run far slower than O((n+m) log n)
404
405        - Return a dictionary of various path information including:
406            - `id_path`: A list of node ids in the order they are visited
407            - `path`: A list of node dictionaries (lat + long) in the order they are visited
408
409        Required Arguments:
410
411        - `graph`:
412            - Type: list of dictionaries
413            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
414        - `origin_id`
415            - Type: int
416            - What: The id of the origin node from the graph dictionary to start the shortest path from
417        - `destination_id`
418            - Type: int
419            - What: The id of the destination node from the graph dictionary to end the shortest path at
420
421        Optional Arguments:
422
423        - `cycle_check_iterations`
424            - Type: int | None
425            - Default: None -> The length of the graph is used as the default number of iterations to loop over before checking for negative cycles
426            - What: The number of iterations to loop over before checking for negative cycles
427        """
428        # Input Validation
429        Graph.input_check(
430            graph=graph, origin_id=origin_id, destination_id=destination_id
431        )
432        # Variable Initialization
433        distance_matrix = [float("inf")] * len(graph)
434        distance_matrix[origin_id] = 0
435        open_leaves = []
436        heappush(open_leaves, (0, origin_id))
437        predecessor = [-1] * len(graph)
438
439        # Cycle iteration Variables
440        cycle_iteration = 0
441        if cycle_check_iterations is None:
442            cycle_check_iterations = len(graph)
443
444        while open_leaves:
445            current_distance, current_id = heappop(open_leaves)
446            if current_distance == distance_matrix[current_id]:
447                # Increment the cycle iteration counter and check for negative cycles if the iteration limit is reached
448                cycle_iteration += 1
449                if cycle_iteration >= cycle_check_iterations:
450                    cycle_iteration = 0  # Reset the cycle iteration counter
451                    Graph.cycle_check(
452                        predecessor_matrix=predecessor, node_id=current_id
453                    )
454                for connected_id, connected_distance in graph[
455                    current_id
456                ].items():
457                    possible_distance = current_distance + connected_distance
458                    if possible_distance < distance_matrix[connected_id]:
459                        distance_matrix[connected_id] = possible_distance
460                        predecessor[connected_id] = current_id
461                        heappush(open_leaves, (possible_distance, connected_id))
462
463        if distance_matrix[destination_id] == float("inf"):
464            raise Exception(
465                "Something went wrong, the origin and destination nodes are not connected."
466            )
467
468        return {
469            "path": Graph.reconstruct_path(destination_id, predecessor),
470            "length": distance_matrix[destination_id],
471        }
472
473    @staticmethod
474    def a_star(
475        graph: list[dict[int, int | float]],
476        origin_id: int,
477        destination_id: int,
478        heuristic_fn: callable = None,
479    ) -> dict:
480        """
481        Function:
482
483        - Identify the shortest path between two nodes in a sparse network graph using an A* extension of Makowski's modified Dijkstra algorithm
484        - Return a dictionary of various path information including:
485            - `id_path`: A list of node ids in the order they are visited
486            - `path`: A list of node dictionaries (lat + long) in the order they are visited
487
488        Required Arguments:
489
490        - `graph`:
491            - Type: list of dictionaries
492            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
493        - `origin_id`
494            - Type: int
495            - What: The id of the origin node from the graph dictionary to start the shortest path from
496        - `destination_id`
497            - Type: int
498            - What: The id of the destination node from the graph dictionary to end the shortest path at
499        - `heuristic_fn`
500            - Type: function
501            - What: A heuristic function that takes two node ids and returns an estimated distance between them
502            - Note: If None, returns the shortest path using Makowski's modified Dijkstra algorithm
503            - Default: None
504
505        Optional Arguments:
506
507        - None
508        """
509        if heuristic_fn is None:
510            return Graph.dijkstra_makowski(
511                graph=graph,
512                origin_id=origin_id,
513                destination_id=destination_id,
514            )
515        # Input Validation
516        Graph.input_check(
517            graph=graph, origin_id=origin_id, destination_id=destination_id
518        )
519        # Variable Initialization
520        distance_matrix = [float("inf")] * len(graph)
521        distance_matrix[origin_id] = 0
522        # Using a visited matrix does add a tad bit of overhead but avoids revisiting nodes
523        # and does not require anything extra to be stored in the heap
524        visited = [0] * len(graph)
525        open_leaves = []
526        heappush(open_leaves, (0, origin_id))
527        predecessor = [-1] * len(graph)
528
529        while open_leaves:
530            current_id = heappop(open_leaves)[1]
531            if current_id == destination_id:
532                break
533            if visited[current_id] == 1:
534                continue
535            visited[current_id] = 1
536            current_distance = distance_matrix[current_id]
537            for connected_id, connected_distance in graph[current_id].items():
538                possible_distance = current_distance + connected_distance
539                if possible_distance < distance_matrix[connected_id]:
540                    distance_matrix[connected_id] = possible_distance
541                    predecessor[connected_id] = current_id
542                    heappush(
543                        open_leaves,
544                        (
545                            possible_distance
546                            + heuristic_fn(connected_id, destination_id),
547                            connected_id,
548                        ),
549                    )
550        if current_id != destination_id:
551            raise Exception(
552                "Something went wrong, the origin and destination nodes are not connected."
553            )
554
555        return {
556            "path": Graph.reconstruct_path(destination_id, predecessor),
557            "length": distance_matrix[destination_id],
558        }
559
560    @staticmethod
561    def bellman_ford(
562        graph: list[dict[int, int | float]],
563        origin_id: int,
564        destination_id: int,
565    ) -> dict:
566        """
567        Function:
568
569        - Identify the shortest path between two nodes in a sparse network graph using the Bellman-Ford algorithm
570        - Return a dictionary of various path information including:
571            - `id_path`: A list of node ids in the order they are visited
572            - `path`: A list of node dictionaries (lat + long) in the order they are visited
573
574        Required Arguments:
575
576        - `graph`:
577            - Type: list of dictionaries
578            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
579        - `origin_id`
580            - Type: int
581            - What: The id of the origin node from the graph dictionary to start the shortest path from
582        - `destination_id`
583            - Type: int
584            - What: The id of the destination node from the graph dictionary to end the shortest path at
585
586        Optional Arguments:
587
588        - None
589        """
590        # Input Validation
591        Graph.input_check(
592            graph=graph, origin_id=origin_id, destination_id=destination_id
593        )
594        # Variable Initialization
595        distance_matrix = [float("inf")] * len(graph)
596        distance_matrix[origin_id] = 0
597        predecessor = [-1] * len(graph)
598
599        len_graph = len(graph)
600        for i in range(len_graph):
601            for current_id in range(len(graph)):
602                current_distance = distance_matrix[current_id]
603                for connected_id, connected_distance in graph[
604                    current_id
605                ].items():
606                    possible_distance = current_distance + connected_distance
607                    if possible_distance < distance_matrix[connected_id]:
608                        distance_matrix[connected_id] = possible_distance
609                        predecessor[connected_id] = current_id
610                        if i == len_graph - 1:
611                            raise Exception(
612                                "Graph contains a negative weight cycle"
613                            )
614        # Check if destination is reachable
615        if distance_matrix[destination_id] == float("inf"):
616            raise Exception(
617                "Something went wrong, the origin and destination nodes are not connected."
618            )
619
620        return {
621            "path": Graph.reconstruct_path(destination_id, predecessor),
622            "length": distance_matrix[destination_id],
623        }
624
625    @staticmethod
626    def bmssp(
627        graph: list[dict[int, int | float]], origin_id: int, destination_id: int
628    ):
629        """
630        Function:
631
632        - A Full BMSSP-style shortest path solver with a Dijkstra finalizer for non-relaxed edges.
633        - Return a dictionary of various path information including:
634            - `id_path`: A list of node ids in the order they are visited
635            - `path`: A list of node dictionaries (lat + long) in the order they are visited
636
637        Required Arguments:
638
639        - `graph`:
640            - Type: list of dictionaries
641            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
642        - `origin_id`
643            - Type: int
644            - What: The id of the origin node from the graph dictionary to start the shortest path from
645        - `destination_id`
646            - Type: int
647            - What: The id of the destination node from the graph dictionary to end the shortest path at
648        - `heuristic_fn`
649            - Type: function
650            - What: A heuristic function that takes two node ids and returns an estimated distance between them
651            - Note: If None, returns the shortest path using Makowski's modified Dijkstra algorithm
652            - Default: None
653
654        Optional Arguments:
655
656        - None
657        """
658        # Input Validation
659        Graph.input_check(
660            graph=graph, origin_id=origin_id, destination_id=destination_id
661        )
662        # Run the BMSSP Algorithm to relax as many edges as possible.
663        solver = BmsspSolver(graph, origin_id)
664        if solver.distance_matrix[destination_id] == float("inf"):
665            raise Exception(
666                "Something went wrong, the origin and destination nodes are not connected."
667            )
668
669        return {
670            "path": Graph.reconstruct_path(destination_id, solver.predecessor),
671            "length": solver.distance_matrix[destination_id],
672        }
@staticmethod
def validate_graph( graph: list[dict[int, int | float]], check_symmetry: bool = True, check_connected: bool = True) -> None:
  7    @staticmethod
  8    def validate_graph(
  9        graph: list[dict[int, int | float]],
 10        check_symmetry: bool = True,
 11        check_connected: bool = True,
 12    ) -> None:
 13        """
 14        Function:
 15
 16        - Validate that a graph is properly formatted
 17
 18        Required Arguments:
 19
 20        - `graph`:
 21            - Type: list of dictionaries with integer keys and integer or float values
 22            - What: A list of dictionaries where the indicies are origin node ids and the values are dictionaries of destination node indices and graph weights
 23            - Note: All nodes must be included as origins in the graph regardless of if they have any connected destinations
 24            - EG:
 25            ```
 26                [
 27                    # From London (index 0)
 28                    {
 29                        # To Paris (index 1)
 30                        1: 311,
 31                    },
 32                    # From Paris (index 1)
 33                    {
 34                        # To London (index 0)
 35                        0: 311,
 36                        # To Berlin (index 2)
 37                        2: 878,
 38                        # To Rome (index 3)
 39                        3: 1439,
 40                        # To Madrid (index 4)
 41                        4: 1053
 42                    },
 43                    # From Berlin (index 2)
 44                    {
 45                        # To Paris (index 1)
 46                        1: 878,
 47                        # To Rome (index 3)
 48                        3: 1181,
 49                    },
 50                    # From Rome (index 3)
 51                    {
 52                        # To Paris (index 1)
 53                        1: 1439,
 54                        # To Berlin (index 2)
 55                        2: 1181,
 56                    },
 57                    # From Madrid (index 4)
 58                    {
 59                        # To Paris (index 1)
 60                        1: 1053,
 61                        # To Lisbon (index 5)
 62                        5: 623
 63                    },
 64                    # From Lisbon (index 5)
 65                    {
 66                        # To Madrid (index 4)
 67                        4: 623
 68                    }
 69                ]
 70            ```
 71
 72        Optional Arguments:
 73
 74        - `check_symmetry`
 75            - Type: bool
 76            - What: Whether to check that the graph is symmetric
 77            - Default: True
 78            - Note: This is forced to True if `check_connected` is True
 79        - `check_connected`
 80            - Type: bool
 81            - What: Whether to check that the graph is fully connected
 82            - Default: True
 83            - Note: For computational efficiency, only symmetric graphs are checked for connectivity
 84            - Note: If this is True, `check_symmetry` is forced to True and the graph will be checked for symmetry prior to checking for connectivity
 85        """
 86        check_symmetry = check_symmetry or check_connected
 87        assert isinstance(graph, list), "Your graph must be a list"
 88        len_graph = len(graph)
 89        for origin_id, origin_dict in enumerate(graph):
 90            assert isinstance(
 91                origin_dict, dict
 92            ), f"Your graph must be a list of dictionaries but the value for origin {origin_id} is not a dictionary"
 93            destinations = list(origin_dict.keys())
 94            lengths = list(origin_dict.values())
 95            assert all(
 96                [
 97                    (isinstance(i, int) and i >= 0 and i < len_graph)
 98                    for i in destinations
 99                ]
100            ), f"Destination ids must be non-negative integers and equivalent to an existing index, but graph[{origin_id}] has an error in the destination ids"
101            assert all(
102                [(isinstance(i, (int, float)) and i >= 0) for i in lengths]
103            ), f"Distances must be integers or floats, but graph[{origin_id}] contains a non-integer or non-float distance"
104            if check_symmetry:
105                for destination_id, distance in origin_dict.items():
106                    assert (
107                        graph[destination_id].get(origin_id) == distance
108                    ), f"Your graph is not symmetric, the distance from node {origin_id} to node {destination_id} is {distance} but the distance from node {destination_id} to node {origin_id} is {graph.get(destination_id, {}).get(origin_id)}"
109        if check_connected:
110            assert Graph.validate_connected(
111                graph
112            ), "Your graph is not fully connected"

Function:

  • Validate that a graph is properly formatted

Required Arguments:

  • graph:

    • Type: list of dictionaries with integer keys and integer or float values
    • What: A list of dictionaries where the indicies are origin node ids and the values are dictionaries of destination node indices and graph weights
    • Note: All nodes must be included as origins in the graph regardless of if they have any connected destinations
    • EG:
        [
            # From London (index 0)
            {
                # To Paris (index 1)
                1: 311,
            },
            # From Paris (index 1)
            {
                # To London (index 0)
                0: 311,
                # To Berlin (index 2)
                2: 878,
                # To Rome (index 3)
                3: 1439,
                # To Madrid (index 4)
                4: 1053
            },
            # From Berlin (index 2)
            {
                # To Paris (index 1)
                1: 878,
                # To Rome (index 3)
                3: 1181,
            },
            # From Rome (index 3)
            {
                # To Paris (index 1)
                1: 1439,
                # To Berlin (index 2)
                2: 1181,
            },
            # From Madrid (index 4)
            {
                # To Paris (index 1)
                1: 1053,
                # To Lisbon (index 5)
                5: 623
            },
            # From Lisbon (index 5)
            {
                # To Madrid (index 4)
                4: 623
            }
        ]
    

Optional Arguments:

  • check_symmetry
    • Type: bool
    • What: Whether to check that the graph is symmetric
    • Default: True
    • Note: This is forced to True if check_connected is True
  • check_connected
    • Type: bool
    • What: Whether to check that the graph is fully connected
    • Default: True
    • Note: For computational efficiency, only symmetric graphs are checked for connectivity
    • Note: If this is True, check_symmetry is forced to True and the graph will be checked for symmetry prior to checking for connectivity
@staticmethod
def validate_connected(graph: list[dict[int, int | float]], origin_id: int = 0) -> bool:
114    @staticmethod
115    def validate_connected(
116        graph: list[dict[int, int | float]], origin_id: int = 0
117    ) -> bool:
118        """
119        Function:
120
121        - Validate that a graph is fully connected
122            - This means that every node in the graph has a path to every other node in the graph
123            - Note: This assumes that the graph is symmetric
124        - Return True if the graph is fully connected and False if it is not
125
126        Required Arguments:
127
128        - `graph`:
129            - Type: list of dictionaries
130            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
131
132        Optional Arguments:
133
134        - `origin_id`
135            - Type: int
136            - What: The id of the origin node from which to start the connectivity check
137            - Default: 0
138        """
139        visited = [0] * len(graph)
140        open_leaves = [origin_id]
141
142        while open_leaves:
143            current_id = open_leaves.pop()
144            visited[current_id] = 1
145            for connected_id, connected_distance in graph[current_id].items():
146                if visited[connected_id] == 0:
147                    open_leaves.append(connected_id)
148        return min(visited) == 1

Function:

  • Validate that a graph is fully connected
    • This means that every node in the graph has a path to every other node in the graph
    • Note: This assumes that the graph is symmetric
  • Return True if the graph is fully connected and False if it is not

Required Arguments:

Optional Arguments:

  • origin_id
    • Type: int
    • What: The id of the origin node from which to start the connectivity check
    • Default: 0
@staticmethod
def input_check( graph: list[dict[int, int | float]], origin_id: int, destination_id: int) -> None:
150    @staticmethod
151    def input_check(
152        graph: list[dict[int, int | float]], origin_id: int, destination_id: int
153    ) -> None:
154        """
155        Function:
156
157        - Check that the inputs passed to the shortest path algorithm are valid
158        - Raises an exception if the inputs passed are not valid
159
160        Required Arguments:
161
162        - `graph`:
163            - Type: list of dictionaries
164            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
165        - `origin_id`
166            - Type: int
167            - What: The id of the origin node from the graph dictionary to start the shortest path from
168        - `destination_id`
169            - Type: int
170            - What: The id of the destination node from the graph dictionary to end the shortest path at
171
172        Optional Arguments:
173
174        - None
175        """
176        if (
177            not isinstance(origin_id, int)
178            and origin_id < len(graph)
179            and origin_id >= 0
180        ):
181            raise Exception(f"Origin node ({origin_id}) is not in the graph")
182        if (
183            not isinstance(destination_id, int)
184            and origin_id < len(graph)
185            and origin_id >= 0
186        ):
187            raise Exception(
188                f"Destination node ({destination_id}) is not in the graph"
189            )

Function:

  • Check that the inputs passed to the shortest path algorithm are valid
  • Raises an exception if the inputs passed are not valid

Required Arguments:

Optional Arguments:

  • None
@staticmethod
def reconstruct_path(destination_id: int, predecessor: list[int]) -> list[int]:
191    @staticmethod
192    def reconstruct_path(
193        destination_id: int, predecessor: list[int]
194    ) -> list[int]:
195        """
196        Function:
197
198        - Reconstruct the shortest path from the destination node to the origin node
199        - Return the reconstructed path in the correct order
200        - Given the predecessor list, this function reconstructs the path
201
202        Required Arguments:
203
204        - `destination_id`
205            - Type: int
206            - What: The id of the destination node from the graph dictionary to end the shortest path at
207        - `predecessor`
208            - Type: list[int]
209            - What: The predecessor list that was used to compute the shortest path
210            - This list is used to reconstruct the path from the destination node to the origin node
211            - Note: Nodes with no predecessor should be -1
212
213        Optional Arguments:
214
215        - None
216        """
217        output_path = [destination_id]
218        while predecessor[destination_id] != -1:
219            destination_id = predecessor[destination_id]
220            output_path.append(destination_id)
221        output_path.reverse()
222        return output_path

Function:

  • Reconstruct the shortest path from the destination node to the origin node
  • Return the reconstructed path in the correct order
  • Given the predecessor list, this function reconstructs the path

Required Arguments:

  • destination_id
    • Type: int
    • What: The id of the destination node from the graph dictionary to end the shortest path at
  • predecessor
    • Type: list[int]
    • What: The predecessor list that was used to compute the shortest path
    • This list is used to reconstruct the path from the destination node to the origin node
    • Note: Nodes with no predecessor should be -1

Optional Arguments:

  • None
@staticmethod
def dijkstra( graph: list[dict[int, int | float]], origin_id: int, destination_id: int) -> dict:
224    @staticmethod
225    def dijkstra(
226        graph: list[dict[int, int | float]], origin_id: int, destination_id: int
227    ) -> dict:
228        """
229        Function:
230
231        - Identify the shortest path between two nodes in a sparse network graph using a modified dijkstra algorithm
232            - Modifications allow for a sparse distance matrix to be used instead of a dense distance matrix
233            - This can dramatically reduce the memory and compute requirements of the algorithm
234            - This algorithm should run in O(n^2) time where n is the number of nodes in the graph
235        - Return a dictionary of various path information including:
236            - `id_path`: A list of node ids in the order they are visited
237            - `path`: A list of node dictionaries (lat + long) in the order they are visited
238
239        Required Arguments:
240
241        - `graph`:
242            - Type: list of dictionaries
243            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
244        - `origin_id`
245            - Type: int
246            - What: The id of the origin node from the graph dictionary to start the shortest path from
247        - `destination_id`
248            - Type: int
249            - What: The id of the destination node from the graph dictionary to end the shortest path at
250
251        Optional Arguments:
252
253        - None
254        """
255        Graph.input_check(
256            graph=graph, origin_id=origin_id, destination_id=destination_id
257        )
258        distance_matrix = [float("inf")] * len(graph)
259        branch_tip_distances = [float("inf")] * len(graph)
260        predecessor = [-1] * len(graph)
261
262        distance_matrix[origin_id] = 0
263        branch_tip_distances[origin_id] = 0
264
265        while True:
266            current_distance = min(branch_tip_distances)
267            if current_distance == float("inf"):
268                raise Exception(
269                    "Something went wrong, the origin and destination nodes are not connected."
270                )
271            current_id = branch_tip_distances.index(current_distance)
272            branch_tip_distances[current_id] = float("inf")
273            if current_id == destination_id:
274                break
275            for connected_id, connected_distance in graph[current_id].items():
276                possible_distance = current_distance + connected_distance
277                if possible_distance < distance_matrix[connected_id]:
278                    distance_matrix[connected_id] = possible_distance
279                    predecessor[connected_id] = current_id
280                    branch_tip_distances[connected_id] = possible_distance
281
282        return {
283            "path": Graph.reconstruct_path(destination_id, predecessor),
284            "length": distance_matrix[destination_id],
285        }

Function:

  • Identify the shortest path between two nodes in a sparse network graph using a modified dijkstra algorithm
    • Modifications allow for a sparse distance matrix to be used instead of a dense distance matrix
    • This can dramatically reduce the memory and compute requirements of the algorithm
    • This algorithm should run in O(n^2) time where n is the number of nodes in the graph
  • Return a dictionary of various path information including:
    • id_path: A list of node ids in the order they are visited
    • path: A list of node dictionaries (lat + long) in the order they are visited

Required Arguments:

Optional Arguments:

  • None
@staticmethod
def dijkstra_makowski( graph: list[dict[int, int | float]], origin_id: int, destination_id: int) -> dict:
287    @staticmethod
288    def dijkstra_makowski(
289        graph: list[dict[int, int | float]], origin_id: int, destination_id: int
290    ) -> dict:
291        """
292        Function:
293
294        - Identify the shortest path between two nodes in a sparse network graph using a modified Dijkstra algorithm
295            - Modifications allow for a sparse distance matrix to be used instead of a dense distance matrix
296            - Improvements include only computing future potential nodes based on the open leaves for each branch
297                - Open leaves are nodes that have not been visited yet but are adjacent to other visited nodes
298            - This can dramatically reduce the memory and compute requirements of the algorithm
299            - This algorithm should run close to O((n+m) log n) time
300                - Where n is the number of nodes in the graph and m is the number of edges in the graph
301        - Return a dictionary of various path information including:
302            - `id_path`: A list of node ids in the order they are visited
303            - `path`: A list of node dictionaries (lat + long) in the order they are visited
304
305        Required Arguments:
306
307        - `graph`:
308            - Type: list of dictionaries
309            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
310        - `origin_id`
311            - Type: int
312            - What: The id of the origin node from the graph dictionary to start the shortest path from
313        - `destination_id`
314            - Type: int
315            - What: The id of the destination node from the graph dictionary to end the shortest path at
316
317        Optional Arguments:
318
319        - None
320        """
321        # Input Validation
322        Graph.input_check(
323            graph=graph, origin_id=origin_id, destination_id=destination_id
324        )
325        # Variable Initialization
326        distance_matrix = [float("inf")] * len(graph)
327        distance_matrix[origin_id] = 0
328        open_leaves = []
329        heappush(open_leaves, (0, origin_id))
330        predecessor = [-1] * len(graph)
331
332        while open_leaves:
333            current_distance, current_id = heappop(open_leaves)
334            if current_id == destination_id:
335                break
336            # Technically, the next line is not necessary but can help with performance
337            if current_distance == distance_matrix[current_id]:
338                for connected_id, connected_distance in graph[
339                    current_id
340                ].items():
341                    possible_distance = current_distance + connected_distance
342                    if possible_distance < distance_matrix[connected_id]:
343                        distance_matrix[connected_id] = possible_distance
344                        predecessor[connected_id] = current_id
345                        heappush(open_leaves, (possible_distance, connected_id))
346        if current_id != destination_id:
347            raise Exception(
348                "Something went wrong, the origin and destination nodes are not connected."
349            )
350
351        return {
352            "path": Graph.reconstruct_path(destination_id, predecessor),
353            "length": distance_matrix[destination_id],
354        }

Function:

  • Identify the shortest path between two nodes in a sparse network graph using a modified Dijkstra algorithm
    • Modifications allow for a sparse distance matrix to be used instead of a dense distance matrix
    • Improvements include only computing future potential nodes based on the open leaves for each branch
      • Open leaves are nodes that have not been visited yet but are adjacent to other visited nodes
    • This can dramatically reduce the memory and compute requirements of the algorithm
    • This algorithm should run close to O((n+m) log n) time
      • Where n is the number of nodes in the graph and m is the number of edges in the graph
  • Return a dictionary of various path information including:
    • id_path: A list of node ids in the order they are visited
    • path: A list of node dictionaries (lat + long) in the order they are visited

Required Arguments:

Optional Arguments:

  • None
@staticmethod
def cycle_check(predecessor_matrix, node_id):
356    @staticmethod
357    def cycle_check(predecessor_matrix, node_id):
358        """
359        Function:
360
361        - Check if a cycle exists in the predecessor matrix starting from the given node_id
362        - Returns None if no cycle is detected
363        - Raises an Exception if a cycle is detected
364
365        Required Arguments:
366
367        - `predecessor_matrix`:
368            - Type: list[int]
369            - What: A list where the index represents the node id and the value at that index is the predecessor node id
370        - `node_id`:
371            - Type: int
372            - What: The node id to start checking for cycles from
373
374        Optional Arguments:
375
376        - None
377        """
378        cycle_id = node_id
379        while True:
380            cycle_id = predecessor_matrix[cycle_id]
381            if cycle_id == -1:
382                return
383            if cycle_id == node_id:
384                raise Exception(
385                    f"Cycle detected in the graph at node {node_id}"
386                )

Function:

  • Check if a cycle exists in the predecessor matrix starting from the given node_id
  • Returns None if no cycle is detected
  • Raises an Exception if a cycle is detected

Required Arguments:

  • predecessor_matrix:
    • Type: list[int]
    • What: A list where the index represents the node id and the value at that index is the predecessor node id
  • node_id:
    • Type: int
    • What: The node id to start checking for cycles from

Optional Arguments:

  • None
@staticmethod
def dijkstra_negative( graph: list[dict[int, int | float]], origin_id: int, destination_id: int, cycle_check_iterations: int | None = None) -> dict:
388    @staticmethod
389    def dijkstra_negative(
390        graph: list[dict[int, int | float]],
391        origin_id: int,
392        destination_id: int,
393        cycle_check_iterations: int | None = None,
394    ) -> dict:
395        """
396        Function:
397
398        - Identify the shortest path between two nodes in a sparse network graph using a modified Dijkstra algorithm that catches negative cycles
399            - Negative cycles raise an exception if they are detected
400        - Note: This algorithm is guaranteed to find the shortest path or raise an exception if a negative cycle is detected
401        - Note: This algorithm requires computing the entire spanning tree of the graph and is therefore not able to be terminated early
402            - For non negative weighted graphs, it is recommended to use the `dijkstra_makowski` algorithm instead
403        - Note: For certain graphs with weights that are negative, this algorithm may run far slower than O((n+m) log n)
404
405        - Return a dictionary of various path information including:
406            - `id_path`: A list of node ids in the order they are visited
407            - `path`: A list of node dictionaries (lat + long) in the order they are visited
408
409        Required Arguments:
410
411        - `graph`:
412            - Type: list of dictionaries
413            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
414        - `origin_id`
415            - Type: int
416            - What: The id of the origin node from the graph dictionary to start the shortest path from
417        - `destination_id`
418            - Type: int
419            - What: The id of the destination node from the graph dictionary to end the shortest path at
420
421        Optional Arguments:
422
423        - `cycle_check_iterations`
424            - Type: int | None
425            - Default: None -> The length of the graph is used as the default number of iterations to loop over before checking for negative cycles
426            - What: The number of iterations to loop over before checking for negative cycles
427        """
428        # Input Validation
429        Graph.input_check(
430            graph=graph, origin_id=origin_id, destination_id=destination_id
431        )
432        # Variable Initialization
433        distance_matrix = [float("inf")] * len(graph)
434        distance_matrix[origin_id] = 0
435        open_leaves = []
436        heappush(open_leaves, (0, origin_id))
437        predecessor = [-1] * len(graph)
438
439        # Cycle iteration Variables
440        cycle_iteration = 0
441        if cycle_check_iterations is None:
442            cycle_check_iterations = len(graph)
443
444        while open_leaves:
445            current_distance, current_id = heappop(open_leaves)
446            if current_distance == distance_matrix[current_id]:
447                # Increment the cycle iteration counter and check for negative cycles if the iteration limit is reached
448                cycle_iteration += 1
449                if cycle_iteration >= cycle_check_iterations:
450                    cycle_iteration = 0  # Reset the cycle iteration counter
451                    Graph.cycle_check(
452                        predecessor_matrix=predecessor, node_id=current_id
453                    )
454                for connected_id, connected_distance in graph[
455                    current_id
456                ].items():
457                    possible_distance = current_distance + connected_distance
458                    if possible_distance < distance_matrix[connected_id]:
459                        distance_matrix[connected_id] = possible_distance
460                        predecessor[connected_id] = current_id
461                        heappush(open_leaves, (possible_distance, connected_id))
462
463        if distance_matrix[destination_id] == float("inf"):
464            raise Exception(
465                "Something went wrong, the origin and destination nodes are not connected."
466            )
467
468        return {
469            "path": Graph.reconstruct_path(destination_id, predecessor),
470            "length": distance_matrix[destination_id],
471        }

Function:

  • Identify the shortest path between two nodes in a sparse network graph using a modified Dijkstra algorithm that catches negative cycles
    • Negative cycles raise an exception if they are detected
  • Note: This algorithm is guaranteed to find the shortest path or raise an exception if a negative cycle is detected
  • Note: This algorithm requires computing the entire spanning tree of the graph and is therefore not able to be terminated early
    • For non negative weighted graphs, it is recommended to use the dijkstra_makowski algorithm instead
  • Note: For certain graphs with weights that are negative, this algorithm may run far slower than O((n+m) log n)

  • Return a dictionary of various path information including:

    • id_path: A list of node ids in the order they are visited
    • path: A list of node dictionaries (lat + long) in the order they are visited

Required Arguments:

Optional Arguments:

  • cycle_check_iterations
    • Type: int | None
    • Default: None -> The length of the graph is used as the default number of iterations to loop over before checking for negative cycles
    • What: The number of iterations to loop over before checking for negative cycles
@staticmethod
def a_star( graph: list[dict[int, int | float]], origin_id: int, destination_id: int, heuristic_fn: <built-in function callable> = None) -> dict:
473    @staticmethod
474    def a_star(
475        graph: list[dict[int, int | float]],
476        origin_id: int,
477        destination_id: int,
478        heuristic_fn: callable = None,
479    ) -> dict:
480        """
481        Function:
482
483        - Identify the shortest path between two nodes in a sparse network graph using an A* extension of Makowski's modified Dijkstra algorithm
484        - Return a dictionary of various path information including:
485            - `id_path`: A list of node ids in the order they are visited
486            - `path`: A list of node dictionaries (lat + long) in the order they are visited
487
488        Required Arguments:
489
490        - `graph`:
491            - Type: list of dictionaries
492            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
493        - `origin_id`
494            - Type: int
495            - What: The id of the origin node from the graph dictionary to start the shortest path from
496        - `destination_id`
497            - Type: int
498            - What: The id of the destination node from the graph dictionary to end the shortest path at
499        - `heuristic_fn`
500            - Type: function
501            - What: A heuristic function that takes two node ids and returns an estimated distance between them
502            - Note: If None, returns the shortest path using Makowski's modified Dijkstra algorithm
503            - Default: None
504
505        Optional Arguments:
506
507        - None
508        """
509        if heuristic_fn is None:
510            return Graph.dijkstra_makowski(
511                graph=graph,
512                origin_id=origin_id,
513                destination_id=destination_id,
514            )
515        # Input Validation
516        Graph.input_check(
517            graph=graph, origin_id=origin_id, destination_id=destination_id
518        )
519        # Variable Initialization
520        distance_matrix = [float("inf")] * len(graph)
521        distance_matrix[origin_id] = 0
522        # Using a visited matrix does add a tad bit of overhead but avoids revisiting nodes
523        # and does not require anything extra to be stored in the heap
524        visited = [0] * len(graph)
525        open_leaves = []
526        heappush(open_leaves, (0, origin_id))
527        predecessor = [-1] * len(graph)
528
529        while open_leaves:
530            current_id = heappop(open_leaves)[1]
531            if current_id == destination_id:
532                break
533            if visited[current_id] == 1:
534                continue
535            visited[current_id] = 1
536            current_distance = distance_matrix[current_id]
537            for connected_id, connected_distance in graph[current_id].items():
538                possible_distance = current_distance + connected_distance
539                if possible_distance < distance_matrix[connected_id]:
540                    distance_matrix[connected_id] = possible_distance
541                    predecessor[connected_id] = current_id
542                    heappush(
543                        open_leaves,
544                        (
545                            possible_distance
546                            + heuristic_fn(connected_id, destination_id),
547                            connected_id,
548                        ),
549                    )
550        if current_id != destination_id:
551            raise Exception(
552                "Something went wrong, the origin and destination nodes are not connected."
553            )
554
555        return {
556            "path": Graph.reconstruct_path(destination_id, predecessor),
557            "length": distance_matrix[destination_id],
558        }

Function:

  • Identify the shortest path between two nodes in a sparse network graph using an A* extension of Makowski's modified Dijkstra algorithm
  • Return a dictionary of various path information including:
    • id_path: A list of node ids in the order they are visited
    • path: A list of node dictionaries (lat + long) in the order they are visited

Required Arguments:

  • graph:
  • origin_id
    • Type: int
    • What: The id of the origin node from the graph dictionary to start the shortest path from
  • destination_id
    • Type: int
    • What: The id of the destination node from the graph dictionary to end the shortest path at
  • heuristic_fn
    • Type: function
    • What: A heuristic function that takes two node ids and returns an estimated distance between them
    • Note: If None, returns the shortest path using Makowski's modified Dijkstra algorithm
    • Default: None

Optional Arguments:

  • None
@staticmethod
def bellman_ford( graph: list[dict[int, int | float]], origin_id: int, destination_id: int) -> dict:
560    @staticmethod
561    def bellman_ford(
562        graph: list[dict[int, int | float]],
563        origin_id: int,
564        destination_id: int,
565    ) -> dict:
566        """
567        Function:
568
569        - Identify the shortest path between two nodes in a sparse network graph using the Bellman-Ford algorithm
570        - Return a dictionary of various path information including:
571            - `id_path`: A list of node ids in the order they are visited
572            - `path`: A list of node dictionaries (lat + long) in the order they are visited
573
574        Required Arguments:
575
576        - `graph`:
577            - Type: list of dictionaries
578            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
579        - `origin_id`
580            - Type: int
581            - What: The id of the origin node from the graph dictionary to start the shortest path from
582        - `destination_id`
583            - Type: int
584            - What: The id of the destination node from the graph dictionary to end the shortest path at
585
586        Optional Arguments:
587
588        - None
589        """
590        # Input Validation
591        Graph.input_check(
592            graph=graph, origin_id=origin_id, destination_id=destination_id
593        )
594        # Variable Initialization
595        distance_matrix = [float("inf")] * len(graph)
596        distance_matrix[origin_id] = 0
597        predecessor = [-1] * len(graph)
598
599        len_graph = len(graph)
600        for i in range(len_graph):
601            for current_id in range(len(graph)):
602                current_distance = distance_matrix[current_id]
603                for connected_id, connected_distance in graph[
604                    current_id
605                ].items():
606                    possible_distance = current_distance + connected_distance
607                    if possible_distance < distance_matrix[connected_id]:
608                        distance_matrix[connected_id] = possible_distance
609                        predecessor[connected_id] = current_id
610                        if i == len_graph - 1:
611                            raise Exception(
612                                "Graph contains a negative weight cycle"
613                            )
614        # Check if destination is reachable
615        if distance_matrix[destination_id] == float("inf"):
616            raise Exception(
617                "Something went wrong, the origin and destination nodes are not connected."
618            )
619
620        return {
621            "path": Graph.reconstruct_path(destination_id, predecessor),
622            "length": distance_matrix[destination_id],
623        }

Function:

  • Identify the shortest path between two nodes in a sparse network graph using the Bellman-Ford algorithm
  • Return a dictionary of various path information including:
    • id_path: A list of node ids in the order they are visited
    • path: A list of node dictionaries (lat + long) in the order they are visited

Required Arguments:

Optional Arguments:

  • None
@staticmethod
def bmssp( graph: list[dict[int, int | float]], origin_id: int, destination_id: int):
625    @staticmethod
626    def bmssp(
627        graph: list[dict[int, int | float]], origin_id: int, destination_id: int
628    ):
629        """
630        Function:
631
632        - A Full BMSSP-style shortest path solver with a Dijkstra finalizer for non-relaxed edges.
633        - Return a dictionary of various path information including:
634            - `id_path`: A list of node ids in the order they are visited
635            - `path`: A list of node dictionaries (lat + long) in the order they are visited
636
637        Required Arguments:
638
639        - `graph`:
640            - Type: list of dictionaries
641            - See: https://connor-makowski.github.io/scgraph/scgraph/graph.html#Graph.validate_graph
642        - `origin_id`
643            - Type: int
644            - What: The id of the origin node from the graph dictionary to start the shortest path from
645        - `destination_id`
646            - Type: int
647            - What: The id of the destination node from the graph dictionary to end the shortest path at
648        - `heuristic_fn`
649            - Type: function
650            - What: A heuristic function that takes two node ids and returns an estimated distance between them
651            - Note: If None, returns the shortest path using Makowski's modified Dijkstra algorithm
652            - Default: None
653
654        Optional Arguments:
655
656        - None
657        """
658        # Input Validation
659        Graph.input_check(
660            graph=graph, origin_id=origin_id, destination_id=destination_id
661        )
662        # Run the BMSSP Algorithm to relax as many edges as possible.
663        solver = BmsspSolver(graph, origin_id)
664        if solver.distance_matrix[destination_id] == float("inf"):
665            raise Exception(
666                "Something went wrong, the origin and destination nodes are not connected."
667            )
668
669        return {
670            "path": Graph.reconstruct_path(destination_id, solver.predecessor),
671            "length": solver.distance_matrix[destination_id],
672        }

Function:

  • A Full BMSSP-style shortest path solver with a Dijkstra finalizer for non-relaxed edges.
  • Return a dictionary of various path information including:
    • id_path: A list of node ids in the order they are visited
    • path: A list of node dictionaries (lat + long) in the order they are visited

Required Arguments:

  • graph:
  • origin_id
    • Type: int
    • What: The id of the origin node from the graph dictionary to start the shortest path from
  • destination_id
    • Type: int
    • What: The id of the destination node from the graph dictionary to end the shortest path at
  • heuristic_fn
    • Type: function
    • What: A heuristic function that takes two node ids and returns an estimated distance between them
    • Note: If None, returns the shortest path using Makowski's modified Dijkstra algorithm
    • Default: None

Optional Arguments:

  • None