edsger 0.1.4__cp312-cp312-macosx_11_0_arm64.whl → 0.1.5__cp312-cp312-macosx_11_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- edsger/_version.py +1 -1
- edsger/bellman_ford.c +34907 -0
- edsger/bellman_ford.cpython-312-darwin.so +0 -0
- edsger/bellman_ford.pyx +544 -0
- edsger/commons.c +246 -243
- edsger/commons.cpython-312-darwin.so +0 -0
- edsger/dijkstra.c +277 -262
- edsger/dijkstra.cpython-312-darwin.so +0 -0
- edsger/path.py +622 -23
- edsger/path_tracking.c +259 -244
- edsger/path_tracking.cpython-312-darwin.so +0 -0
- edsger/pq_4ary_dec_0b.c +267 -252
- edsger/pq_4ary_dec_0b.cpython-312-darwin.so +0 -0
- edsger/spiess_florian.c +276 -261
- edsger/spiess_florian.cpython-312-darwin.so +0 -0
- edsger/star.c +259 -244
- edsger/star.cpython-312-darwin.so +0 -0
- edsger/utils.py +68 -4
- {edsger-0.1.4.dist-info → edsger-0.1.5.dist-info}/METADATA +59 -2
- edsger-0.1.5.dist-info/RECORD +36 -0
- edsger-0.1.4.dist-info/RECORD +0 -33
- {edsger-0.1.4.dist-info → edsger-0.1.5.dist-info}/WHEEL +0 -0
- {edsger-0.1.4.dist-info → edsger-0.1.5.dist-info}/licenses/AUTHORS.rst +0 -0
- {edsger-0.1.4.dist-info → edsger-0.1.5.dist-info}/licenses/LICENSE +0 -0
- {edsger-0.1.4.dist-info → edsger-0.1.5.dist-info}/top_level.txt +0 -0
edsger/path.py
CHANGED
@@ -14,6 +14,14 @@ from edsger.commons import (
|
|
14
14
|
INF_FREQ_PY,
|
15
15
|
MIN_FREQ_PY,
|
16
16
|
)
|
17
|
+
from edsger.bellman_ford import (
|
18
|
+
compute_bf_sssp,
|
19
|
+
compute_bf_sssp_w_path,
|
20
|
+
compute_bf_stsp,
|
21
|
+
compute_bf_stsp_w_path,
|
22
|
+
detect_negative_cycle,
|
23
|
+
detect_negative_cycle_csc,
|
24
|
+
)
|
17
25
|
from edsger.dijkstra import (
|
18
26
|
compute_sssp,
|
19
27
|
compute_sssp_w_path,
|
@@ -39,6 +47,9 @@ class Dijkstra:
|
|
39
47
|
Dijkstra's algorithm for finding the shortest paths between nodes in directed graphs with
|
40
48
|
positive edge weights.
|
41
49
|
|
50
|
+
Note: If parallel edges exist between the same pair of vertices, only the edge with the minimum
|
51
|
+
weight will be kept automatically during initialization.
|
52
|
+
|
42
53
|
Parameters:
|
43
54
|
-----------
|
44
55
|
edges: pandas.DataFrame
|
@@ -61,6 +72,8 @@ class Dijkstra:
|
|
61
72
|
permute: bool, optional (default=False)
|
62
73
|
Whether to permute the IDs of the nodes. If set to True, the node IDs will be reindexed to
|
63
74
|
start from 0 and be contiguous.
|
75
|
+
verbose: bool, optional (default=False)
|
76
|
+
Whether to print messages about parallel edge removal.
|
64
77
|
"""
|
65
78
|
|
66
79
|
def __init__(
|
@@ -72,12 +85,17 @@ class Dijkstra:
|
|
72
85
|
orientation="out",
|
73
86
|
check_edges=False,
|
74
87
|
permute=False,
|
88
|
+
verbose=False,
|
75
89
|
):
|
76
90
|
# load the edges
|
77
91
|
if check_edges:
|
78
92
|
self._check_edges(edges, tail, head, weight)
|
79
93
|
self._edges = edges[[tail, head, weight]].copy(deep=True)
|
80
94
|
self._n_edges = len(self._edges)
|
95
|
+
self._verbose = verbose
|
96
|
+
|
97
|
+
# preprocess edges to handle parallel edges
|
98
|
+
self._preprocess_edges(tail, head, weight)
|
81
99
|
|
82
100
|
# reindex the vertices
|
83
101
|
self._permute = permute
|
@@ -191,6 +209,34 @@ class Dijkstra:
|
|
191
209
|
"""
|
192
210
|
return self._path_links
|
193
211
|
|
212
|
+
def _preprocess_edges(self, tail, head, weight):
|
213
|
+
"""
|
214
|
+
Preprocess edges to handle parallel edges by keeping only the minimum weight edge
|
215
|
+
between any pair of vertices.
|
216
|
+
|
217
|
+
Parameters
|
218
|
+
----------
|
219
|
+
tail : str
|
220
|
+
The column name for tail vertices
|
221
|
+
head : str
|
222
|
+
The column name for head vertices
|
223
|
+
weight : str
|
224
|
+
The column name for edge weights
|
225
|
+
"""
|
226
|
+
original_count = len(self._edges)
|
227
|
+
self._edges = self._edges.groupby([tail, head], as_index=False)[weight].min()
|
228
|
+
final_count = len(self._edges)
|
229
|
+
|
230
|
+
if original_count > final_count:
|
231
|
+
parallel_edges_removed = original_count - final_count
|
232
|
+
if self._verbose:
|
233
|
+
print(
|
234
|
+
f"Automatically removed {parallel_edges_removed} parallel edge(s). "
|
235
|
+
f"For each pair of vertices, kept the edge with minimum weight."
|
236
|
+
)
|
237
|
+
|
238
|
+
self._n_edges = len(self._edges)
|
239
|
+
|
194
240
|
def _check_edges(self, edges, tail, head, weight):
|
195
241
|
"""Checks if the edges DataFrame is well-formed. If not, raises an appropriate error."""
|
196
242
|
if not isinstance(edges, pd.core.frame.DataFrame):
|
@@ -370,10 +416,10 @@ class Dijkstra:
|
|
370
416
|
if termination_nodes is not None:
|
371
417
|
try:
|
372
418
|
termination_nodes_array = np.array(termination_nodes, dtype=np.uint32)
|
373
|
-
except (ValueError, TypeError):
|
419
|
+
except (ValueError, TypeError) as exc:
|
374
420
|
raise TypeError(
|
375
421
|
"argument 'termination_nodes' must be array-like of integers"
|
376
|
-
)
|
422
|
+
) from exc
|
377
423
|
|
378
424
|
if termination_nodes_array.ndim != 1:
|
379
425
|
raise ValueError("argument 'termination_nodes' must be 1-dimensional")
|
@@ -632,6 +678,566 @@ class Dijkstra:
|
|
632
678
|
return path_vertices
|
633
679
|
|
634
680
|
|
681
|
+
class BellmanFord:
|
682
|
+
"""
|
683
|
+
Bellman-Ford algorithm for finding the shortest paths between nodes in directed graphs.
|
684
|
+
Supports negative edge weights and detects negative cycles.
|
685
|
+
|
686
|
+
Note: If parallel edges exist between the same pair of vertices, only the edge with the minimum
|
687
|
+
weight will be kept automatically during initialization.
|
688
|
+
|
689
|
+
Parameters:
|
690
|
+
-----------
|
691
|
+
edges: pandas.DataFrame
|
692
|
+
DataFrame containing the edges of the graph. It should have three columns: 'tail', 'head',
|
693
|
+
and 'weight'. The 'tail' column should contain the IDs of the starting nodes, the 'head'
|
694
|
+
column should contain the IDs of the ending nodes, and the 'weight' column should contain
|
695
|
+
the weights of the edges (can be negative).
|
696
|
+
tail: str, optional (default='tail')
|
697
|
+
The name of the column in the DataFrame that contains the IDs of the edge starting nodes.
|
698
|
+
head: str, optional (default='head')
|
699
|
+
The name of the column in the DataFrame that contains the IDs of the edge ending nodes.
|
700
|
+
weight: str, optional (default='weight')
|
701
|
+
The name of the column in the DataFrame that contains the weights of the edges.
|
702
|
+
orientation: str, optional (default='out')
|
703
|
+
The orientation of Bellman-Ford's algorithm. It can be either 'out' for single source
|
704
|
+
shortest paths or 'in' for single target shortest path.
|
705
|
+
check_edges: bool, optional (default=False)
|
706
|
+
Whether to check if the edges DataFrame is well-formed. If set to True, the edges
|
707
|
+
DataFrame will be checked for missing values and invalid data types. Note: negative
|
708
|
+
weights are allowed.
|
709
|
+
permute: bool, optional (default=False)
|
710
|
+
Whether to permute the IDs of the nodes. If set to True, the node IDs will be reindexed to
|
711
|
+
start from 0 and be contiguous.
|
712
|
+
verbose: bool, optional (default=False)
|
713
|
+
Whether to print messages about parallel edge removal.
|
714
|
+
"""
|
715
|
+
|
716
|
+
def __init__(
|
717
|
+
self,
|
718
|
+
edges,
|
719
|
+
tail="tail",
|
720
|
+
head="head",
|
721
|
+
weight="weight",
|
722
|
+
orientation="out",
|
723
|
+
check_edges=False,
|
724
|
+
permute=False,
|
725
|
+
verbose=False,
|
726
|
+
):
|
727
|
+
# load the edges
|
728
|
+
if check_edges:
|
729
|
+
self._check_edges(edges, tail, head, weight)
|
730
|
+
self._edges = edges[[tail, head, weight]].copy(deep=True)
|
731
|
+
self._n_edges = len(self._edges)
|
732
|
+
self._verbose = verbose
|
733
|
+
|
734
|
+
# preprocess edges to handle parallel edges
|
735
|
+
self._preprocess_edges(tail, head, weight)
|
736
|
+
|
737
|
+
# reindex the vertices
|
738
|
+
self._permute = permute
|
739
|
+
if self._permute:
|
740
|
+
self.__n_vertices_init = self._edges[[tail, head]].max(axis=0).max() + 1
|
741
|
+
self._permutation = self._permute_graph(tail, head)
|
742
|
+
self._n_vertices = len(self._permutation)
|
743
|
+
else:
|
744
|
+
self._permutation = None
|
745
|
+
self._n_vertices = self._edges[[tail, head]].max(axis=0).max() + 1
|
746
|
+
self.__n_vertices_init = self._n_vertices
|
747
|
+
|
748
|
+
# convert to CSR/CSC:
|
749
|
+
self._check_orientation(orientation)
|
750
|
+
self._orientation = orientation
|
751
|
+
if self._orientation == "out":
|
752
|
+
fs_indptr, fs_indices, fs_data = convert_graph_to_csr_float64(
|
753
|
+
self._edges, tail, head, weight, self._n_vertices
|
754
|
+
)
|
755
|
+
self.__indices = fs_indices.astype(np.uint32)
|
756
|
+
self.__indptr = fs_indptr.astype(np.uint32)
|
757
|
+
self.__edge_weights = fs_data.astype(DTYPE_PY)
|
758
|
+
else:
|
759
|
+
rs_indptr, rs_indices, rs_data = convert_graph_to_csc_float64(
|
760
|
+
self._edges, tail, head, weight, self._n_vertices
|
761
|
+
)
|
762
|
+
self.__indices = rs_indices.astype(np.uint32)
|
763
|
+
self.__indptr = rs_indptr.astype(np.uint32)
|
764
|
+
self.__edge_weights = rs_data.astype(DTYPE_PY)
|
765
|
+
|
766
|
+
# Check if graph has any negative weights (for optimization)
|
767
|
+
self._has_negative_weights = np.any(self.__edge_weights < 0)
|
768
|
+
|
769
|
+
self._path_links = None
|
770
|
+
self._has_negative_cycle = False
|
771
|
+
|
772
|
+
@property
|
773
|
+
def edges(self):
|
774
|
+
"""
|
775
|
+
Getter for the graph edge dataframe.
|
776
|
+
|
777
|
+
Returns
|
778
|
+
-------
|
779
|
+
edges: pandas.DataFrame
|
780
|
+
DataFrame containing the edges of the graph.
|
781
|
+
"""
|
782
|
+
return self._edges
|
783
|
+
|
784
|
+
@property
|
785
|
+
def n_edges(self):
|
786
|
+
"""
|
787
|
+
Getter for the number of graph edges.
|
788
|
+
|
789
|
+
Returns
|
790
|
+
-------
|
791
|
+
n_edges: int
|
792
|
+
The number of edges in the graph.
|
793
|
+
"""
|
794
|
+
return self._n_edges
|
795
|
+
|
796
|
+
@property
|
797
|
+
def n_vertices(self):
|
798
|
+
"""
|
799
|
+
Getter for the number of graph vertices.
|
800
|
+
|
801
|
+
Returns
|
802
|
+
-------
|
803
|
+
n_vertices: int
|
804
|
+
The number of nodes in the graph (after permutation, if _permute is True).
|
805
|
+
"""
|
806
|
+
return self._n_vertices
|
807
|
+
|
808
|
+
@property
|
809
|
+
def orientation(self):
|
810
|
+
"""
|
811
|
+
Getter of Bellman-Ford's algorithm orientation ("in" or "out").
|
812
|
+
|
813
|
+
Returns
|
814
|
+
-------
|
815
|
+
orientation : str
|
816
|
+
The orientation of Bellman-Ford's algorithm.
|
817
|
+
"""
|
818
|
+
return self._orientation
|
819
|
+
|
820
|
+
@property
|
821
|
+
def permute(self):
|
822
|
+
"""
|
823
|
+
Getter for the graph permutation/reindexing option.
|
824
|
+
|
825
|
+
Returns
|
826
|
+
-------
|
827
|
+
permute : bool
|
828
|
+
Whether to permute the IDs of the nodes.
|
829
|
+
"""
|
830
|
+
return self._permute
|
831
|
+
|
832
|
+
@property
|
833
|
+
def path_links(self):
|
834
|
+
"""
|
835
|
+
Getter for the path links (predecessors or successors).
|
836
|
+
|
837
|
+
Returns
|
838
|
+
-------
|
839
|
+
path_links: numpy.ndarray
|
840
|
+
predecessors or successors node index if the path tracking is activated.
|
841
|
+
"""
|
842
|
+
return self._path_links
|
843
|
+
|
844
|
+
def _preprocess_edges(self, tail, head, weight):
|
845
|
+
"""
|
846
|
+
Preprocess edges to handle parallel edges by keeping only the minimum weight edge
|
847
|
+
between any pair of vertices.
|
848
|
+
|
849
|
+
Parameters
|
850
|
+
----------
|
851
|
+
tail : str
|
852
|
+
The column name for tail vertices
|
853
|
+
head : str
|
854
|
+
The column name for head vertices
|
855
|
+
weight : str
|
856
|
+
The column name for edge weights
|
857
|
+
"""
|
858
|
+
original_count = len(self._edges)
|
859
|
+
self._edges = self._edges.groupby([tail, head], as_index=False)[weight].min()
|
860
|
+
final_count = len(self._edges)
|
861
|
+
|
862
|
+
if original_count > final_count:
|
863
|
+
parallel_edges_removed = original_count - final_count
|
864
|
+
if self._verbose:
|
865
|
+
print(
|
866
|
+
f"Automatically removed {parallel_edges_removed} parallel edge(s). "
|
867
|
+
f"For each pair of vertices, kept the edge with minimum weight."
|
868
|
+
)
|
869
|
+
|
870
|
+
self._n_edges = len(self._edges)
|
871
|
+
|
872
|
+
def _check_edges(self, edges, tail, head, weight):
|
873
|
+
"""Checks if the edges DataFrame is well-formed. If not, raises an appropriate error."""
|
874
|
+
if not isinstance(edges, pd.core.frame.DataFrame):
|
875
|
+
raise TypeError("edges should be a pandas DataFrame")
|
876
|
+
|
877
|
+
if tail not in edges:
|
878
|
+
raise KeyError(
|
879
|
+
f"edge tail column '{tail}' not found in graph edges dataframe"
|
880
|
+
)
|
881
|
+
|
882
|
+
if head not in edges:
|
883
|
+
raise KeyError(
|
884
|
+
f"edge head column '{head}' not found in graph edges dataframe"
|
885
|
+
)
|
886
|
+
|
887
|
+
if weight not in edges:
|
888
|
+
raise KeyError(
|
889
|
+
f"edge weight column '{weight}' not found in graph edges dataframe"
|
890
|
+
)
|
891
|
+
|
892
|
+
if edges[[tail, head, weight]].isna().any().any():
|
893
|
+
raise ValueError(
|
894
|
+
" ".join(
|
895
|
+
[
|
896
|
+
f"edges[[{tail}, {head}, {weight}]] ",
|
897
|
+
"should not have any missing value",
|
898
|
+
]
|
899
|
+
)
|
900
|
+
)
|
901
|
+
|
902
|
+
for col in [tail, head]:
|
903
|
+
if not pd.api.types.is_integer_dtype(edges[col].dtype):
|
904
|
+
raise TypeError(f"edges['{col}'] should be of integer type")
|
905
|
+
|
906
|
+
if not pd.api.types.is_numeric_dtype(edges[weight].dtype):
|
907
|
+
raise TypeError(f"edges['{weight}'] should be of numeric type")
|
908
|
+
|
909
|
+
# Note: Unlike Dijkstra, we allow negative weights for Bellman-Ford
|
910
|
+
if not np.isfinite(edges[weight]).all():
|
911
|
+
raise ValueError(f"edges['{weight}'] should be finite")
|
912
|
+
|
913
|
+
def _permute_graph(self, tail, head):
|
914
|
+
"""Permute the IDs of the nodes to start from 0 and be contiguous.
|
915
|
+
Returns a DataFrame with the permuted IDs."""
|
916
|
+
|
917
|
+
permutation = pd.DataFrame(
|
918
|
+
data={
|
919
|
+
"vert_idx": np.union1d(
|
920
|
+
self._edges[tail].values, self._edges[head].values
|
921
|
+
)
|
922
|
+
}
|
923
|
+
)
|
924
|
+
permutation["vert_idx_new"] = permutation.index
|
925
|
+
permutation.index.name = "index"
|
926
|
+
|
927
|
+
self._edges = pd.merge(
|
928
|
+
self._edges,
|
929
|
+
permutation[["vert_idx", "vert_idx_new"]],
|
930
|
+
left_on=tail,
|
931
|
+
right_on="vert_idx",
|
932
|
+
how="left",
|
933
|
+
)
|
934
|
+
self._edges.drop([tail, "vert_idx"], axis=1, inplace=True)
|
935
|
+
self._edges.rename(columns={"vert_idx_new": tail}, inplace=True)
|
936
|
+
|
937
|
+
self._edges = pd.merge(
|
938
|
+
self._edges,
|
939
|
+
permutation[["vert_idx", "vert_idx_new"]],
|
940
|
+
left_on=head,
|
941
|
+
right_on="vert_idx",
|
942
|
+
how="left",
|
943
|
+
)
|
944
|
+
self._edges.drop([head, "vert_idx"], axis=1, inplace=True)
|
945
|
+
self._edges.rename(columns={"vert_idx_new": head}, inplace=True)
|
946
|
+
|
947
|
+
permutation.rename(columns={"vert_idx": "vert_idx_old"}, inplace=True)
|
948
|
+
permutation.reset_index(drop=True, inplace=True)
|
949
|
+
permutation.sort_values(by="vert_idx_new", inplace=True)
|
950
|
+
|
951
|
+
permutation.index.name = "index"
|
952
|
+
self._edges.index.name = "index"
|
953
|
+
|
954
|
+
return permutation
|
955
|
+
|
956
|
+
def _check_orientation(self, orientation):
|
957
|
+
"""Checks the orientation attribute."""
|
958
|
+
if orientation not in ["in", "out"]:
|
959
|
+
raise ValueError("orientation should be either 'in' on 'out'")
|
960
|
+
|
961
|
+
def run(
|
962
|
+
self,
|
963
|
+
vertex_idx,
|
964
|
+
path_tracking=False,
|
965
|
+
return_inf=True,
|
966
|
+
return_series=False,
|
967
|
+
detect_negative_cycles=True,
|
968
|
+
):
|
969
|
+
"""
|
970
|
+
Runs Bellman-Ford shortest path algorithm between a given vertex and all other vertices
|
971
|
+
in the graph.
|
972
|
+
|
973
|
+
Parameters
|
974
|
+
----------
|
975
|
+
vertex_idx : int
|
976
|
+
The index of the source/target vertex.
|
977
|
+
path_tracking : bool, optional (default=False)
|
978
|
+
Whether to track the shortest path(s) from the source vertex to all other vertices in
|
979
|
+
the graph.
|
980
|
+
return_inf : bool, optional (default=True)
|
981
|
+
Whether to return path length(s) as infinity (np.inf) when no path exists.
|
982
|
+
return_series : bool, optional (default=False)
|
983
|
+
Whether to return a Pandas Series object indexed by vertex indices with path length(s)
|
984
|
+
as values.
|
985
|
+
detect_negative_cycles : bool, optional (default=True)
|
986
|
+
Whether to detect negative cycles in the graph. If True and a negative cycle is
|
987
|
+
detected,
|
988
|
+
raises a ValueError.
|
989
|
+
|
990
|
+
Returns
|
991
|
+
-------
|
992
|
+
path_length_values or path_lengths_series : array_like or Pandas Series
|
993
|
+
If `return_series=False`, a 1D Numpy array of shape (n_vertices,) with the shortest
|
994
|
+
path length from the source vertex to each vertex in the graph (`orientation="out"`), or
|
995
|
+
from each vertex to the target vertex (`orientation="in"`). If `return_series=True`, a
|
996
|
+
Pandas Series object with the same data and the vertex indices as index.
|
997
|
+
|
998
|
+
Raises
|
999
|
+
------
|
1000
|
+
ValueError
|
1001
|
+
If detect_negative_cycles is True and a negative cycle is detected in the graph.
|
1002
|
+
"""
|
1003
|
+
# validate the input arguments
|
1004
|
+
if not isinstance(vertex_idx, int):
|
1005
|
+
try:
|
1006
|
+
vertex_idx = int(vertex_idx)
|
1007
|
+
except ValueError as exc:
|
1008
|
+
raise TypeError(
|
1009
|
+
f"argument 'vertex_idx={vertex_idx}' must be an integer"
|
1010
|
+
) from exc
|
1011
|
+
if vertex_idx < 0:
|
1012
|
+
raise ValueError(f"argument 'vertex_idx={vertex_idx}' must be positive")
|
1013
|
+
if self._permute:
|
1014
|
+
if vertex_idx not in self._permutation.vert_idx_old.values:
|
1015
|
+
raise ValueError(f"vertex {vertex_idx} not found in graph")
|
1016
|
+
vertex_new = self._permutation.loc[
|
1017
|
+
self._permutation.vert_idx_old == vertex_idx, "vert_idx_new"
|
1018
|
+
].iloc[0]
|
1019
|
+
else:
|
1020
|
+
if vertex_idx >= self._n_vertices:
|
1021
|
+
raise ValueError(f"vertex {vertex_idx} not found in graph")
|
1022
|
+
vertex_new = vertex_idx
|
1023
|
+
if not isinstance(path_tracking, bool):
|
1024
|
+
raise TypeError(
|
1025
|
+
f"argument 'path_tracking=f{path_tracking}' must be of bool type"
|
1026
|
+
)
|
1027
|
+
if not isinstance(return_inf, bool):
|
1028
|
+
raise TypeError(f"argument 'return_inf=f{return_inf}' must be of bool type")
|
1029
|
+
if not isinstance(return_series, bool):
|
1030
|
+
raise TypeError(
|
1031
|
+
f"argument 'return_series=f{return_series}' must be of bool type"
|
1032
|
+
)
|
1033
|
+
if not isinstance(detect_negative_cycles, bool):
|
1034
|
+
raise TypeError(
|
1035
|
+
f"argument 'detect_negative_cycles={detect_negative_cycles}' must be of bool type"
|
1036
|
+
)
|
1037
|
+
|
1038
|
+
# compute path length
|
1039
|
+
if not path_tracking:
|
1040
|
+
self._path_links = None
|
1041
|
+
if self._orientation == "in":
|
1042
|
+
path_length_values = compute_bf_stsp(
|
1043
|
+
self.__indptr,
|
1044
|
+
self.__indices,
|
1045
|
+
self.__edge_weights,
|
1046
|
+
vertex_new,
|
1047
|
+
self._n_vertices,
|
1048
|
+
)
|
1049
|
+
else:
|
1050
|
+
path_length_values = compute_bf_sssp(
|
1051
|
+
self.__indptr,
|
1052
|
+
self.__indices,
|
1053
|
+
self.__edge_weights,
|
1054
|
+
vertex_new,
|
1055
|
+
self._n_vertices,
|
1056
|
+
)
|
1057
|
+
else:
|
1058
|
+
self._path_links = np.arange(0, self._n_vertices, dtype=np.uint32)
|
1059
|
+
if self._orientation == "in":
|
1060
|
+
path_length_values = compute_bf_stsp_w_path(
|
1061
|
+
self.__indptr,
|
1062
|
+
self.__indices,
|
1063
|
+
self.__edge_weights,
|
1064
|
+
self._path_links,
|
1065
|
+
vertex_new,
|
1066
|
+
self._n_vertices,
|
1067
|
+
)
|
1068
|
+
else:
|
1069
|
+
path_length_values = compute_bf_sssp_w_path(
|
1070
|
+
self.__indptr,
|
1071
|
+
self.__indices,
|
1072
|
+
self.__edge_weights,
|
1073
|
+
self._path_links,
|
1074
|
+
vertex_new,
|
1075
|
+
self._n_vertices,
|
1076
|
+
)
|
1077
|
+
|
1078
|
+
if self._permute:
|
1079
|
+
# permute back the path vertex indices
|
1080
|
+
path_df = pd.DataFrame(
|
1081
|
+
data={
|
1082
|
+
"vertex_idx": np.arange(self._n_vertices),
|
1083
|
+
"associated_idx": self._path_links,
|
1084
|
+
}
|
1085
|
+
)
|
1086
|
+
path_df = pd.merge(
|
1087
|
+
path_df,
|
1088
|
+
self._permutation,
|
1089
|
+
left_on="vertex_idx",
|
1090
|
+
right_on="vert_idx_new",
|
1091
|
+
how="left",
|
1092
|
+
)
|
1093
|
+
path_df.drop(["vertex_idx", "vert_idx_new"], axis=1, inplace=True)
|
1094
|
+
path_df.rename(columns={"vert_idx_old": "vertex_idx"}, inplace=True)
|
1095
|
+
path_df = pd.merge(
|
1096
|
+
path_df,
|
1097
|
+
self._permutation,
|
1098
|
+
left_on="associated_idx",
|
1099
|
+
right_on="vert_idx_new",
|
1100
|
+
how="left",
|
1101
|
+
)
|
1102
|
+
path_df.drop(["associated_idx", "vert_idx_new"], axis=1, inplace=True)
|
1103
|
+
path_df.rename(columns={"vert_idx_old": "associated_idx"}, inplace=True)
|
1104
|
+
|
1105
|
+
if return_series:
|
1106
|
+
path_df.set_index("vertex_idx", inplace=True)
|
1107
|
+
self._path_links = path_df.associated_idx.astype(np.uint32)
|
1108
|
+
else:
|
1109
|
+
self._path_links = np.arange(
|
1110
|
+
self.__n_vertices_init, dtype=np.uint32
|
1111
|
+
)
|
1112
|
+
self._path_links[path_df.vertex_idx.values] = (
|
1113
|
+
path_df.associated_idx.values
|
1114
|
+
)
|
1115
|
+
|
1116
|
+
# detect negative cycles if requested (only if negative weights exist)
|
1117
|
+
if detect_negative_cycles and self._has_negative_weights:
|
1118
|
+
if self._orientation == "out":
|
1119
|
+
# CSR format - can use detect_negative_cycle directly
|
1120
|
+
self._has_negative_cycle = detect_negative_cycle(
|
1121
|
+
self.__indptr,
|
1122
|
+
self.__indices,
|
1123
|
+
self.__edge_weights,
|
1124
|
+
path_length_values,
|
1125
|
+
self._n_vertices,
|
1126
|
+
)
|
1127
|
+
else:
|
1128
|
+
# CSC format - use CSC-specific negative cycle detection
|
1129
|
+
# Much more efficient than converting CSC→CSR
|
1130
|
+
self._has_negative_cycle = detect_negative_cycle_csc(
|
1131
|
+
self.__indptr,
|
1132
|
+
self.__indices,
|
1133
|
+
self.__edge_weights,
|
1134
|
+
path_length_values,
|
1135
|
+
self._n_vertices,
|
1136
|
+
)
|
1137
|
+
|
1138
|
+
if self._has_negative_cycle:
|
1139
|
+
raise ValueError("Negative cycle detected in the graph")
|
1140
|
+
|
1141
|
+
# deal with infinity
|
1142
|
+
if return_inf:
|
1143
|
+
path_length_values = np.where(
|
1144
|
+
path_length_values == DTYPE_INF_PY, np.inf, path_length_values
|
1145
|
+
)
|
1146
|
+
|
1147
|
+
# reorder path lengths
|
1148
|
+
if return_series:
|
1149
|
+
if self._permute:
|
1150
|
+
path_df = pd.DataFrame(
|
1151
|
+
data={"path_length": path_length_values[: self._n_vertices]}
|
1152
|
+
)
|
1153
|
+
path_df["vert_idx_new"] = path_df.index
|
1154
|
+
path_df = pd.merge(
|
1155
|
+
path_df,
|
1156
|
+
self._permutation,
|
1157
|
+
left_on="vert_idx_new",
|
1158
|
+
right_on="vert_idx_new",
|
1159
|
+
how="left",
|
1160
|
+
)
|
1161
|
+
path_df.drop(["vert_idx_new"], axis=1, inplace=True)
|
1162
|
+
path_df.set_index("vert_idx_old", inplace=True)
|
1163
|
+
path_lengths_series = path_df.path_length.astype(DTYPE_PY)
|
1164
|
+
else:
|
1165
|
+
path_lengths_series = pd.Series(
|
1166
|
+
data=path_length_values[: self._n_vertices], dtype=DTYPE_PY
|
1167
|
+
)
|
1168
|
+
path_lengths_series.index = np.arange(self._n_vertices)
|
1169
|
+
path_lengths_series.index.name = None
|
1170
|
+
return path_lengths_series
|
1171
|
+
|
1172
|
+
# No else needed - de-indent the code
|
1173
|
+
if self._permute:
|
1174
|
+
path_df = pd.DataFrame(
|
1175
|
+
data={"path_length": path_length_values[: self._n_vertices]}
|
1176
|
+
)
|
1177
|
+
path_df["vert_idx_new"] = path_df.index
|
1178
|
+
path_df = pd.merge(
|
1179
|
+
path_df,
|
1180
|
+
self._permutation,
|
1181
|
+
left_on="vert_idx_new",
|
1182
|
+
right_on="vert_idx_new",
|
1183
|
+
how="left",
|
1184
|
+
)
|
1185
|
+
path_df.drop(["vert_idx_new"], axis=1, inplace=True)
|
1186
|
+
path_length_values = np.full(self.__n_vertices_init, DTYPE_INF_PY)
|
1187
|
+
path_length_values[path_df.vert_idx_old.values] = path_df.path_length.values
|
1188
|
+
if return_inf:
|
1189
|
+
path_length_values = np.where(
|
1190
|
+
path_length_values == DTYPE_INF_PY, np.inf, path_length_values
|
1191
|
+
)
|
1192
|
+
return path_length_values
|
1193
|
+
|
1194
|
+
def get_path(self, vertex_idx):
|
1195
|
+
"""Compute path from predecessors or successors.
|
1196
|
+
|
1197
|
+
Parameters:
|
1198
|
+
-----------
|
1199
|
+
|
1200
|
+
vertex_idx : int
|
1201
|
+
source or target vertex index.
|
1202
|
+
|
1203
|
+
Returns
|
1204
|
+
-------
|
1205
|
+
|
1206
|
+
path_vertices : numpy.ndarray
|
1207
|
+
Array of np.uint32 type storing the path from or to the given vertex index. If we are
|
1208
|
+
dealing with the sssp algorithm, the input vertex is the target vertex and the path to
|
1209
|
+
the source is given backward from the target to the source using the predecessors. If
|
1210
|
+
we are dealing with the stsp algorithm, the input vertex is the source vertex and the
|
1211
|
+
path to the target is given backward from the target to the source using the
|
1212
|
+
successors.
|
1213
|
+
|
1214
|
+
"""
|
1215
|
+
if self._path_links is None:
|
1216
|
+
warnings.warn(
|
1217
|
+
"Current BellmanFord instance has not path attribute : \
|
1218
|
+
make sure path_tracking is set to True, and run the \
|
1219
|
+
shortest path algorithm",
|
1220
|
+
UserWarning,
|
1221
|
+
)
|
1222
|
+
return None
|
1223
|
+
if isinstance(self._path_links, pd.Series):
|
1224
|
+
path_vertices = compute_path(self._path_links.values, vertex_idx)
|
1225
|
+
else:
|
1226
|
+
path_vertices = compute_path(self._path_links, vertex_idx)
|
1227
|
+
return path_vertices
|
1228
|
+
|
1229
|
+
def has_negative_cycle(self):
|
1230
|
+
"""
|
1231
|
+
Check if the last run detected a negative cycle.
|
1232
|
+
|
1233
|
+
Returns
|
1234
|
+
-------
|
1235
|
+
has_negative_cycle : bool
|
1236
|
+
True if a negative cycle was detected in the last run, False otherwise.
|
1237
|
+
"""
|
1238
|
+
return self._has_negative_cycle
|
1239
|
+
|
1240
|
+
|
635
1241
|
class HyperpathGenerating:
|
636
1242
|
"""
|
637
1243
|
A class for constructing and managing hyperpath-based routing and analysis in transportation
|
@@ -804,33 +1410,26 @@ class HyperpathGenerating:
|
|
804
1410
|
# input check
|
805
1411
|
if not isinstance(volume, list):
|
806
1412
|
volume = [volume]
|
807
|
-
if self._orientation == "out":
|
808
|
-
self._check_vertex_idx(origin)
|
809
|
-
if not isinstance(destination, list):
|
810
|
-
destination = [destination]
|
811
|
-
assert len(destination) == len(volume)
|
812
|
-
for i, item in enumerate(destination):
|
813
|
-
self._check_vertex_idx(item)
|
814
|
-
self._check_volume(volume[i])
|
815
|
-
demand_indices = np.array(destination, dtype=np.uint32)
|
816
|
-
elif self._orientation == "in":
|
817
|
-
if not isinstance(origin, list):
|
818
|
-
origin = [origin]
|
819
|
-
assert len(origin) == len(volume)
|
820
|
-
for i, item in enumerate(origin):
|
821
|
-
self._check_vertex_idx(item)
|
822
|
-
self._check_volume(volume[i])
|
823
|
-
self._check_vertex_idx(destination)
|
824
|
-
demand_indices = np.array(origin, dtype=np.uint32)
|
825
|
-
assert isinstance(return_inf, bool)
|
826
|
-
|
827
|
-
demand_values = np.array(volume, dtype=DTYPE_PY)
|
828
1413
|
|
829
1414
|
if self._orientation == "out":
|
830
1415
|
raise NotImplementedError(
|
831
1416
|
"one-to-many Spiess & Florian's algorithm not implemented yet"
|
832
1417
|
)
|
833
1418
|
|
1419
|
+
# Only "in" orientation is supported currently
|
1420
|
+
if not isinstance(origin, list):
|
1421
|
+
origin = [origin]
|
1422
|
+
assert len(origin) == len(volume)
|
1423
|
+
for i, item in enumerate(origin):
|
1424
|
+
self._check_vertex_idx(item)
|
1425
|
+
self._check_volume(volume[i])
|
1426
|
+
self._check_vertex_idx(destination)
|
1427
|
+
demand_indices = np.array(origin, dtype=np.uint32)
|
1428
|
+
|
1429
|
+
assert isinstance(return_inf, bool)
|
1430
|
+
|
1431
|
+
demand_values = np.array(volume, dtype=DTYPE_PY)
|
1432
|
+
|
834
1433
|
compute_SF_in(
|
835
1434
|
self.__indptr,
|
836
1435
|
self._edge_idx,
|