edsger 0.1.4__cp311-cp311-win32.whl → 0.1.5__cp311-cp311-win32.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/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,