edsger 0.1.1__pp310-pypy310_pp73-macosx_10_9_x86_64.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/.gitignore +1 -0
- edsger/__init__.py +1 -0
- edsger/_version.py +1 -0
- edsger/commons.c +8391 -0
- edsger/commons.pxd +25 -0
- edsger/commons.pypy310-pp73-darwin.so +0 -0
- edsger/commons.pyx +34 -0
- edsger/dijkstra.c +35847 -0
- edsger/dijkstra.pypy310-pp73-darwin.so +0 -0
- edsger/dijkstra.pyx +504 -0
- edsger/networks.py +414 -0
- edsger/path.py +786 -0
- edsger/path_tracking.c +30240 -0
- edsger/path_tracking.pypy310-pp73-darwin.so +0 -0
- edsger/path_tracking.pyx +93 -0
- edsger/pq_4ary_dec_0b.c +35868 -0
- edsger/pq_4ary_dec_0b.pxd +33 -0
- edsger/pq_4ary_dec_0b.pypy310-pp73-darwin.so +0 -0
- edsger/pq_4ary_dec_0b.pyx +692 -0
- edsger/spiess_florian.c +34519 -0
- edsger/spiess_florian.pypy310-pp73-darwin.so +0 -0
- edsger/spiess_florian.pyx +368 -0
- edsger/star.c +33895 -0
- edsger/star.pypy310-pp73-darwin.so +0 -0
- edsger/star.pyx +356 -0
- edsger/utils.py +63 -0
- edsger-0.1.1.dist-info/METADATA +111 -0
- edsger-0.1.1.dist-info/RECORD +32 -0
- edsger-0.1.1.dist-info/WHEEL +5 -0
- edsger-0.1.1.dist-info/licenses/AUTHORS.rst +5 -0
- edsger-0.1.1.dist-info/licenses/LICENSE +21 -0
- edsger-0.1.1.dist-info/top_level.txt +1 -0
edsger/path.py
ADDED
@@ -0,0 +1,786 @@
|
|
1
|
+
"""
|
2
|
+
Path-related methods.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import warnings
|
6
|
+
|
7
|
+
import numpy as np
|
8
|
+
import pandas as pd
|
9
|
+
|
10
|
+
from edsger.commons import (
|
11
|
+
A_VERY_SMALL_TIME_INTERVAL_PY,
|
12
|
+
DTYPE_INF_PY,
|
13
|
+
DTYPE_PY,
|
14
|
+
INF_FREQ_PY,
|
15
|
+
MIN_FREQ_PY,
|
16
|
+
)
|
17
|
+
from edsger.dijkstra import (
|
18
|
+
compute_sssp,
|
19
|
+
compute_sssp_w_path,
|
20
|
+
compute_stsp,
|
21
|
+
compute_stsp_w_path,
|
22
|
+
)
|
23
|
+
from edsger.path_tracking import compute_path
|
24
|
+
from edsger.spiess_florian import compute_SF_in
|
25
|
+
from edsger.star import (
|
26
|
+
convert_graph_to_csc_float64,
|
27
|
+
convert_graph_to_csc_uint32,
|
28
|
+
convert_graph_to_csr_float64,
|
29
|
+
convert_graph_to_csr_uint32,
|
30
|
+
)
|
31
|
+
|
32
|
+
|
33
|
+
class Dijkstra:
|
34
|
+
"""
|
35
|
+
Dijkstra's algorithm for finding the shortest paths between nodes in directed graphs with
|
36
|
+
positive edge weights.
|
37
|
+
|
38
|
+
Parameters:
|
39
|
+
-----------
|
40
|
+
edges: pandas.DataFrame
|
41
|
+
DataFrame containing the edges of the graph. It should have three columns: 'tail', 'head',
|
42
|
+
and 'weight'. The 'tail' column should contain the IDs of the starting nodes, the 'head'
|
43
|
+
column should contain the IDs of the ending nodes, and the 'weight' column should contain
|
44
|
+
the (positive) weights of the edges.
|
45
|
+
tail: str, optional (default='tail')
|
46
|
+
The name of the column in the DataFrame that contains the IDs of the edge starting nodes.
|
47
|
+
head: str, optional (default='head')
|
48
|
+
The name of the column in the DataFrame that contains the IDs of the edge ending nodes.
|
49
|
+
weight: str, optional (default='weight')
|
50
|
+
The name of the column in the DataFrame that contains the (positive) weights of the edges.
|
51
|
+
orientation: str, optional (default='out')
|
52
|
+
The orientation of Dijkstra's algorithm. It can be either 'out' for single source shortest
|
53
|
+
paths or 'in' for single target shortest path.
|
54
|
+
check_edges: bool, optional (default=False)
|
55
|
+
Whether to check if the edges DataFrame is well-formed. If set to True, the edges DataFrame
|
56
|
+
will be checked for missing values and invalid data types.
|
57
|
+
permute: bool, optional (default=False)
|
58
|
+
Whether to permute the IDs of the nodes. If set to True, the node IDs will be reindexed to
|
59
|
+
start from 0 and be contiguous.
|
60
|
+
"""
|
61
|
+
|
62
|
+
def __init__(
|
63
|
+
self,
|
64
|
+
edges,
|
65
|
+
tail="tail",
|
66
|
+
head="head",
|
67
|
+
weight="weight",
|
68
|
+
orientation="out",
|
69
|
+
check_edges=False,
|
70
|
+
permute=False,
|
71
|
+
):
|
72
|
+
# load the edges
|
73
|
+
if check_edges:
|
74
|
+
self._check_edges(edges, tail, head, weight)
|
75
|
+
self._edges = edges[[tail, head, weight]].copy(deep=True)
|
76
|
+
self._n_edges = len(self._edges)
|
77
|
+
|
78
|
+
# reindex the vertices
|
79
|
+
self._permute = permute
|
80
|
+
if self._permute:
|
81
|
+
self.__n_vertices_init = self._edges[[tail, head]].max(axis=0).max() + 1
|
82
|
+
self._permutation = self._permute_graph(tail, head)
|
83
|
+
self._n_vertices = len(self._permutation)
|
84
|
+
else:
|
85
|
+
self._permutation = None
|
86
|
+
self._n_vertices = self._edges[[tail, head]].max(axis=0).max() + 1
|
87
|
+
self.__n_vertices_init = self._n_vertices
|
88
|
+
|
89
|
+
# convert to CSR/CSC:
|
90
|
+
# __indices: numpy.ndarray
|
91
|
+
# 1D array containing the indices of the indices of the forward or reverse star of
|
92
|
+
# the graph in compressed format.
|
93
|
+
# __indptr: numpy.ndarray
|
94
|
+
# 1D array containing the indices of the pointer of the forward or reverse star of
|
95
|
+
# the graph in compressed format.
|
96
|
+
# __edge_weights: numpy.ndarray
|
97
|
+
# 1D array containing the weights of the edges in the graph.
|
98
|
+
|
99
|
+
self._check_orientation(orientation)
|
100
|
+
self._orientation = orientation
|
101
|
+
if self._orientation == "out":
|
102
|
+
fs_indptr, fs_indices, fs_data = convert_graph_to_csr_float64(
|
103
|
+
self._edges, tail, head, weight, self._n_vertices
|
104
|
+
)
|
105
|
+
self.__indices = fs_indices.astype(np.uint32)
|
106
|
+
self.__indptr = fs_indptr.astype(np.uint32)
|
107
|
+
self.__edge_weights = fs_data.astype(DTYPE_PY)
|
108
|
+
else:
|
109
|
+
rs_indptr, rs_indices, rs_data = convert_graph_to_csc_float64(
|
110
|
+
self._edges, tail, head, weight, self._n_vertices
|
111
|
+
)
|
112
|
+
self.__indices = rs_indices.astype(np.uint32)
|
113
|
+
self.__indptr = rs_indptr.astype(np.uint32)
|
114
|
+
self.__edge_weights = rs_data.astype(DTYPE_PY)
|
115
|
+
|
116
|
+
self._path_links = None
|
117
|
+
|
118
|
+
@property
|
119
|
+
def edges(self):
|
120
|
+
"""
|
121
|
+
Getter for the graph edge dataframe.
|
122
|
+
|
123
|
+
Returns
|
124
|
+
-------
|
125
|
+
edges: pandas.DataFrame
|
126
|
+
DataFrame containing the edges of the graph.
|
127
|
+
"""
|
128
|
+
return self._edges
|
129
|
+
|
130
|
+
@property
|
131
|
+
def n_edges(self):
|
132
|
+
"""
|
133
|
+
Getter for the number of graph edges.
|
134
|
+
|
135
|
+
Returns
|
136
|
+
-------
|
137
|
+
n_edges: int
|
138
|
+
The number of edges in the graph.
|
139
|
+
"""
|
140
|
+
return self._n_edges
|
141
|
+
|
142
|
+
@property
|
143
|
+
def n_vertices(self):
|
144
|
+
"""
|
145
|
+
Getter for the number of graph vertices.
|
146
|
+
|
147
|
+
Returns
|
148
|
+
-------
|
149
|
+
n_vertices: int
|
150
|
+
The number of nodes in the graph (after permutation, if _permute is True).
|
151
|
+
"""
|
152
|
+
return self._n_vertices
|
153
|
+
|
154
|
+
@property
|
155
|
+
def orientation(self):
|
156
|
+
"""
|
157
|
+
Getter of Dijkstra's algorithm orientation ("in" or "out").
|
158
|
+
|
159
|
+
Returns
|
160
|
+
-------
|
161
|
+
orientation : str
|
162
|
+
The orientation of Dijkstra's algorithm.
|
163
|
+
"""
|
164
|
+
return self._orientation
|
165
|
+
|
166
|
+
@property
|
167
|
+
def permute(self):
|
168
|
+
"""
|
169
|
+
Getter for the graph permutation/reindexing option.
|
170
|
+
|
171
|
+
Returns
|
172
|
+
-------
|
173
|
+
permute : bool
|
174
|
+
Whether to permute the IDs of the nodes.
|
175
|
+
"""
|
176
|
+
return self._permute
|
177
|
+
|
178
|
+
@property
|
179
|
+
def path_links(self):
|
180
|
+
"""
|
181
|
+
Getter for the graph permutation/reindexing option.
|
182
|
+
|
183
|
+
Returns
|
184
|
+
-------
|
185
|
+
path_links: numpy.ndarray
|
186
|
+
predecessors or successors node index if the path tracking is activated.
|
187
|
+
"""
|
188
|
+
return self._path_links
|
189
|
+
|
190
|
+
def _check_edges(self, edges, tail, head, weight):
|
191
|
+
"""Checks if the edges DataFrame is well-formed. If not, raises an appropriate error."""
|
192
|
+
if not isinstance(edges, pd.core.frame.DataFrame):
|
193
|
+
raise TypeError("edges should be a pandas DataFrame")
|
194
|
+
|
195
|
+
if tail not in edges:
|
196
|
+
raise KeyError(
|
197
|
+
f"edge tail column '{tail}' not found in graph edges dataframe"
|
198
|
+
)
|
199
|
+
|
200
|
+
if head not in edges:
|
201
|
+
raise KeyError(
|
202
|
+
f"edge head column '{head}' not found in graph edges dataframe"
|
203
|
+
)
|
204
|
+
|
205
|
+
if weight not in edges:
|
206
|
+
raise KeyError(
|
207
|
+
f"edge weight column '{weight}' not found in graph edges dataframe"
|
208
|
+
)
|
209
|
+
|
210
|
+
if edges[[tail, head, weight]].isna().any().any():
|
211
|
+
raise ValueError(
|
212
|
+
" ".join(
|
213
|
+
[
|
214
|
+
f"edges[[{tail}, {head}, {weight}]] ",
|
215
|
+
"should not have any missing value",
|
216
|
+
]
|
217
|
+
)
|
218
|
+
)
|
219
|
+
|
220
|
+
for col in [tail, head]:
|
221
|
+
if not pd.api.types.is_integer_dtype(edges[col].dtype):
|
222
|
+
raise TypeError(f"edges['{col}'] should be of integer type")
|
223
|
+
|
224
|
+
if not pd.api.types.is_numeric_dtype(edges[weight].dtype):
|
225
|
+
raise TypeError(f"edges['{weight}'] should be of numeric type")
|
226
|
+
|
227
|
+
if edges[weight].min() < 0.0:
|
228
|
+
raise ValueError(f"edges['{weight}'] should be nonnegative")
|
229
|
+
|
230
|
+
if not np.isfinite(edges[weight]).all():
|
231
|
+
raise ValueError(f"edges['{weight}'] should be finite")
|
232
|
+
|
233
|
+
def _permute_graph(self, tail, head):
|
234
|
+
"""Permute the IDs of the nodes to start from 0 and be contiguous.
|
235
|
+
Returns a DataFrame with the permuted IDs."""
|
236
|
+
|
237
|
+
permutation = pd.DataFrame(
|
238
|
+
data={
|
239
|
+
"vert_idx": np.union1d(
|
240
|
+
self._edges[tail].values, self._edges[head].values
|
241
|
+
)
|
242
|
+
}
|
243
|
+
)
|
244
|
+
permutation["vert_idx_new"] = permutation.index
|
245
|
+
permutation.index.name = "index"
|
246
|
+
|
247
|
+
self._edges = pd.merge(
|
248
|
+
self._edges,
|
249
|
+
permutation[["vert_idx", "vert_idx_new"]],
|
250
|
+
left_on=tail,
|
251
|
+
right_on="vert_idx",
|
252
|
+
how="left",
|
253
|
+
)
|
254
|
+
self._edges.drop([tail, "vert_idx"], axis=1, inplace=True)
|
255
|
+
self._edges.rename(columns={"vert_idx_new": tail}, inplace=True)
|
256
|
+
|
257
|
+
self._edges = pd.merge(
|
258
|
+
self._edges,
|
259
|
+
permutation[["vert_idx", "vert_idx_new"]],
|
260
|
+
left_on=head,
|
261
|
+
right_on="vert_idx",
|
262
|
+
how="left",
|
263
|
+
)
|
264
|
+
self._edges.drop([head, "vert_idx"], axis=1, inplace=True)
|
265
|
+
self._edges.rename(columns={"vert_idx_new": head}, inplace=True)
|
266
|
+
|
267
|
+
permutation.rename(columns={"vert_idx": "vert_idx_old"}, inplace=True)
|
268
|
+
permutation.reset_index(drop=True, inplace=True)
|
269
|
+
permutation.sort_values(by="vert_idx_new", inplace=True)
|
270
|
+
|
271
|
+
permutation.index.name = "index"
|
272
|
+
self._edges.index.name = "index"
|
273
|
+
|
274
|
+
return permutation
|
275
|
+
|
276
|
+
def _check_orientation(self, orientation):
|
277
|
+
"""Checks the orientation attribute."""
|
278
|
+
if orientation not in ["in", "out"]:
|
279
|
+
raise ValueError("orientation should be either 'in' on 'out'")
|
280
|
+
|
281
|
+
def run(
|
282
|
+
self,
|
283
|
+
vertex_idx,
|
284
|
+
path_tracking=False,
|
285
|
+
return_inf=True,
|
286
|
+
return_series=False,
|
287
|
+
heap_length_ratio=1.0,
|
288
|
+
):
|
289
|
+
"""
|
290
|
+
Runs shortest path algorithm between a given vertex and all other vertices in the graph.
|
291
|
+
|
292
|
+
Parameters
|
293
|
+
----------
|
294
|
+
vertex_idx : int
|
295
|
+
The index of the source/target vertex.
|
296
|
+
path_tracking : bool, optional (default=False)
|
297
|
+
Whether to track the shortest path(s) from the source vertex to all other vertices in
|
298
|
+
the graph.
|
299
|
+
return_inf : bool, optional (default=True)
|
300
|
+
Whether to return path length(s) as infinity (np.inf) when no path exists.
|
301
|
+
return_series : bool, optional (default=False)
|
302
|
+
Whether to return a Pandas Series object indexed by vertex indices with path length(s)
|
303
|
+
as values.
|
304
|
+
heap_length_ratio : float, optional (default=1.0)
|
305
|
+
The heap length as a fraction of the number of vertices. Must be in the range (0, 1].
|
306
|
+
|
307
|
+
Returns
|
308
|
+
-------
|
309
|
+
path_length_values or path_lengths_series : array_like or Pandas Series
|
310
|
+
If `return_series=False`, a 1D Numpy array of shape (n_vertices,) with the shortest
|
311
|
+
path length from the source vertex to each vertex in the graph (`orientation="out"`), or
|
312
|
+
from each vertex to the target vertex (`orientation="in"`). If `return_series=True`, a
|
313
|
+
Pandas Series object with the same data and the vertex indices as index.
|
314
|
+
|
315
|
+
"""
|
316
|
+
# validate the input arguments
|
317
|
+
if not isinstance(vertex_idx, int):
|
318
|
+
try:
|
319
|
+
vertex_idx = int(vertex_idx)
|
320
|
+
except ValueError as exc:
|
321
|
+
raise TypeError(
|
322
|
+
f"argument 'vertex_idx={vertex_idx}' must be an integer"
|
323
|
+
) from exc
|
324
|
+
if vertex_idx < 0:
|
325
|
+
raise ValueError(f"argument 'vertex_idx={vertex_idx}' must be positive")
|
326
|
+
if self._permute:
|
327
|
+
if vertex_idx not in self._permutation.vert_idx_old.values:
|
328
|
+
raise ValueError(f"vertex {vertex_idx} not found in graph")
|
329
|
+
vertex_new = self._permutation.loc[
|
330
|
+
self._permutation.vert_idx_old == vertex_idx, "vert_idx_new"
|
331
|
+
].iloc[0]
|
332
|
+
else:
|
333
|
+
if vertex_idx >= self._n_vertices:
|
334
|
+
raise ValueError(f"vertex {vertex_idx} not found in graph")
|
335
|
+
vertex_new = vertex_idx
|
336
|
+
if not isinstance(path_tracking, bool):
|
337
|
+
raise TypeError(
|
338
|
+
f"argument 'path_tracking=f{path_tracking}' must be of bool type"
|
339
|
+
)
|
340
|
+
if not isinstance(return_inf, bool):
|
341
|
+
raise TypeError(f"argument 'return_inf=f{return_inf}' must be of bool type")
|
342
|
+
if not isinstance(return_series, bool):
|
343
|
+
raise TypeError(
|
344
|
+
f"argument 'return_series=f{return_series}' must be of bool type"
|
345
|
+
)
|
346
|
+
if not isinstance(heap_length_ratio, float):
|
347
|
+
raise TypeError(
|
348
|
+
f"argument 'heap_length_ratio=f{heap_length_ratio}' must be of float type"
|
349
|
+
)
|
350
|
+
|
351
|
+
heap_length_ratio = np.amin([heap_length_ratio, 1.0])
|
352
|
+
if heap_length_ratio <= 0.0:
|
353
|
+
raise ValueError(
|
354
|
+
f"argument 'heap_length_ratio={heap_length_ratio}' must be strictly positive "
|
355
|
+
)
|
356
|
+
heap_length = int(np.rint(heap_length_ratio * self._n_vertices))
|
357
|
+
|
358
|
+
# compute path length
|
359
|
+
if not path_tracking:
|
360
|
+
self._path_links = None
|
361
|
+
if self._orientation == "in":
|
362
|
+
path_length_values = compute_stsp(
|
363
|
+
self.__indptr,
|
364
|
+
self.__indices,
|
365
|
+
self.__edge_weights,
|
366
|
+
vertex_new,
|
367
|
+
self._n_vertices,
|
368
|
+
heap_length,
|
369
|
+
)
|
370
|
+
else:
|
371
|
+
path_length_values = compute_sssp(
|
372
|
+
self.__indptr,
|
373
|
+
self.__indices,
|
374
|
+
self.__edge_weights,
|
375
|
+
vertex_new,
|
376
|
+
self._n_vertices,
|
377
|
+
heap_length,
|
378
|
+
)
|
379
|
+
else:
|
380
|
+
self._path_links = np.arange(0, self._n_vertices, dtype=np.uint32)
|
381
|
+
if self._orientation == "in":
|
382
|
+
path_length_values = compute_stsp_w_path(
|
383
|
+
self.__indptr,
|
384
|
+
self.__indices,
|
385
|
+
self.__edge_weights,
|
386
|
+
self._path_links,
|
387
|
+
vertex_new,
|
388
|
+
self._n_vertices,
|
389
|
+
heap_length,
|
390
|
+
)
|
391
|
+
else:
|
392
|
+
path_length_values = compute_sssp_w_path(
|
393
|
+
self.__indptr,
|
394
|
+
self.__indices,
|
395
|
+
self.__edge_weights,
|
396
|
+
self._path_links,
|
397
|
+
vertex_new,
|
398
|
+
self._n_vertices,
|
399
|
+
heap_length,
|
400
|
+
)
|
401
|
+
|
402
|
+
if self._permute:
|
403
|
+
# permute back the path vertex indices
|
404
|
+
path_df = pd.DataFrame(
|
405
|
+
data={
|
406
|
+
"vertex_idx": np.arange(self._n_vertices),
|
407
|
+
"associated_idx": self._path_links,
|
408
|
+
}
|
409
|
+
)
|
410
|
+
path_df = pd.merge(
|
411
|
+
path_df,
|
412
|
+
self._permutation,
|
413
|
+
left_on="vertex_idx",
|
414
|
+
right_on="vert_idx_new",
|
415
|
+
how="left",
|
416
|
+
)
|
417
|
+
path_df.drop(["vertex_idx", "vert_idx_new"], axis=1, inplace=True)
|
418
|
+
path_df.rename(columns={"vert_idx_old": "vertex_idx"}, inplace=True)
|
419
|
+
path_df = pd.merge(
|
420
|
+
path_df,
|
421
|
+
self._permutation,
|
422
|
+
left_on="associated_idx",
|
423
|
+
right_on="vert_idx_new",
|
424
|
+
how="left",
|
425
|
+
)
|
426
|
+
path_df.drop(["associated_idx", "vert_idx_new"], axis=1, inplace=True)
|
427
|
+
path_df.rename(columns={"vert_idx_old": "associated_idx"}, inplace=True)
|
428
|
+
|
429
|
+
if return_series:
|
430
|
+
path_df.set_index("vertex_idx", inplace=True)
|
431
|
+
self._path_links = path_df.associated_idx.astype(np.uint32)
|
432
|
+
else:
|
433
|
+
self._path_links = np.arange(
|
434
|
+
self.__n_vertices_init, dtype=np.uint32
|
435
|
+
)
|
436
|
+
self._path_links[path_df.vertex_idx.values] = (
|
437
|
+
path_df.associated_idx.values
|
438
|
+
)
|
439
|
+
|
440
|
+
# deal with infinity
|
441
|
+
if return_inf:
|
442
|
+
path_length_values = np.where(
|
443
|
+
path_length_values == DTYPE_INF_PY, np.inf, path_length_values
|
444
|
+
)
|
445
|
+
|
446
|
+
# reorder path lengths
|
447
|
+
if return_series:
|
448
|
+
if self._permute:
|
449
|
+
self._permutation["path_length"] = path_length_values
|
450
|
+
path_lengths_df = self._permutation[
|
451
|
+
["vert_idx_old", "path_length"]
|
452
|
+
].sort_values(by="vert_idx_old")
|
453
|
+
path_lengths_df.set_index("vert_idx_old", drop=True, inplace=True)
|
454
|
+
path_lengths_df.index.name = "vertex_idx"
|
455
|
+
path_lengths_series = path_lengths_df.path_length
|
456
|
+
else:
|
457
|
+
path_lengths_series = pd.Series(path_length_values)
|
458
|
+
path_lengths_series.index.name = "vertex_idx"
|
459
|
+
path_lengths_series.name = "path_length"
|
460
|
+
|
461
|
+
return path_lengths_series
|
462
|
+
|
463
|
+
if self._permute:
|
464
|
+
self._permutation["path_length"] = path_length_values
|
465
|
+
if return_inf:
|
466
|
+
path_length_values = np.inf * np.ones(self.__n_vertices_init)
|
467
|
+
else:
|
468
|
+
path_length_values = DTYPE_INF_PY * np.ones(self.__n_vertices_init)
|
469
|
+
path_length_values[self._permutation.vert_idx_old.values] = (
|
470
|
+
self._permutation.path_length.values
|
471
|
+
)
|
472
|
+
|
473
|
+
return path_length_values
|
474
|
+
|
475
|
+
def get_vertices(self):
|
476
|
+
"""
|
477
|
+
Get the unique vertices from the graph.
|
478
|
+
|
479
|
+
If the graph has been permuted, this method returns the vertices based on the original
|
480
|
+
indexing. Otherwise, it returns the union of tail and head vertices from the edges.
|
481
|
+
|
482
|
+
Returns
|
483
|
+
-------
|
484
|
+
vertices : ndarray
|
485
|
+
A 1-D array containing the unique vertices.
|
486
|
+
"""
|
487
|
+
if self._permute:
|
488
|
+
return self._permutation.vert_idx_old.values
|
489
|
+
return np.union1d(self._edges["tail"], self._edges["head"])
|
490
|
+
|
491
|
+
def get_path(self, vertex_idx):
|
492
|
+
"""Compute path from predecessors or successors.
|
493
|
+
|
494
|
+
Parameters:
|
495
|
+
-----------
|
496
|
+
|
497
|
+
vertex_idx : int
|
498
|
+
source or target vertex index.
|
499
|
+
|
500
|
+
Returns
|
501
|
+
-------
|
502
|
+
|
503
|
+
path_vertices : numpy.ndarray
|
504
|
+
Array of np.uint32 type storing the path from or to the given vertex index. If we are
|
505
|
+
dealing with the sssp algorithm, the input vertex is the target vertex and the path to
|
506
|
+
the source is given backward from the target to the source using the predecessors. If
|
507
|
+
we are dealing with the stsp algorithm, the input vertex is the source vertex and the
|
508
|
+
path to the target is given backward from the target to the source using the
|
509
|
+
successors.
|
510
|
+
|
511
|
+
"""
|
512
|
+
if self._path_links is None:
|
513
|
+
warnings.warn(
|
514
|
+
"Current Dijkstra instance has not path attribute : \
|
515
|
+
make sure path_tracking is set to True, and run the \
|
516
|
+
shortest path algorithm",
|
517
|
+
UserWarning,
|
518
|
+
)
|
519
|
+
return None
|
520
|
+
if isinstance(self._path_links, pd.Series):
|
521
|
+
path_vertices = compute_path(self._path_links.values, vertex_idx)
|
522
|
+
else:
|
523
|
+
path_vertices = compute_path(self._path_links, vertex_idx)
|
524
|
+
return path_vertices
|
525
|
+
|
526
|
+
|
527
|
+
class HyperpathGenerating:
|
528
|
+
"""
|
529
|
+
A class for constructing and managing hyperpath-based routing and analysis in transportation
|
530
|
+
or graph-based systems.
|
531
|
+
|
532
|
+
Parameters
|
533
|
+
----------
|
534
|
+
edges : pandas.DataFrame
|
535
|
+
A DataFrame containing graph edge information with columns specified by `tail`, `head`,
|
536
|
+
`trav_time`, and `freq`. Must not contain missing values.
|
537
|
+
tail : str, optional
|
538
|
+
Name of the column in `edges` representing the tail nodes (source nodes), by default "tail".
|
539
|
+
head : str, optional
|
540
|
+
Name of the column in `edges` representing the head nodes (target nodes), by default "head".
|
541
|
+
trav_time : str, optional
|
542
|
+
Name of the column in `edges` representing travel times for edges, by default "trav_time".
|
543
|
+
freq : str, optional
|
544
|
+
Name of the column in `edges` representing frequencies of edges, by default "freq".
|
545
|
+
check_edges : bool, optional
|
546
|
+
Whether to validate the structure and data types of `edges`, by default False.
|
547
|
+
orientation : {"in", "out"}, optional
|
548
|
+
Determines the orientation of the graph structure for traversal.
|
549
|
+
- "in": Graph traversal is from destination to origin.
|
550
|
+
- "out": Graph traversal is from origin to destination.
|
551
|
+
By default "in".
|
552
|
+
|
553
|
+
Attributes
|
554
|
+
----------
|
555
|
+
edge_count : int
|
556
|
+
The number of edges in the graph.
|
557
|
+
vertex_count : int
|
558
|
+
The total number of vertices in the graph.
|
559
|
+
u_i_vec : numpy.ndarray
|
560
|
+
An array storing the least travel time for each vertex after running the algorithm.
|
561
|
+
_edges : pandas.DataFrame
|
562
|
+
Internal DataFrame containing the edges with additional metadata.
|
563
|
+
_trav_time : numpy.ndarray
|
564
|
+
Array of travel times for edges.
|
565
|
+
_freq : numpy.ndarray
|
566
|
+
Array of frequencies for edges.
|
567
|
+
_tail : numpy.ndarray
|
568
|
+
Array of tail nodes (source nodes) for edges.
|
569
|
+
_head : numpy.ndarray
|
570
|
+
Array of head nodes (target nodes) for edges.
|
571
|
+
__indptr : numpy.ndarray
|
572
|
+
Array for compressed row (or column) pointers in the CSR/CSC representation.
|
573
|
+
_edge_idx : numpy.ndarray
|
574
|
+
Array of edge indices in the CSR/CSC representation.
|
575
|
+
|
576
|
+
Methods
|
577
|
+
-------
|
578
|
+
run(origin, destination, volume, return_inf=False)
|
579
|
+
Computes the hyperpath and updates edge volumes based on the input demand and configuration.
|
580
|
+
_check_vertex_idx(idx)
|
581
|
+
Validates a vertex index to ensure it is within the graph's bounds.
|
582
|
+
_check_volume(v)
|
583
|
+
Validates a volume value to ensure it is a non-negative float.
|
584
|
+
_check_edges(edges, tail, head, trav_time, freq)
|
585
|
+
Validates the structure and data types of the input edges DataFrame.
|
586
|
+
"""
|
587
|
+
|
588
|
+
def __init__(
|
589
|
+
self,
|
590
|
+
edges,
|
591
|
+
tail="tail",
|
592
|
+
head="head",
|
593
|
+
trav_time="trav_time",
|
594
|
+
freq="freq",
|
595
|
+
check_edges=False,
|
596
|
+
orientation="in",
|
597
|
+
):
|
598
|
+
# load the edges
|
599
|
+
if check_edges:
|
600
|
+
self._check_edges(edges, tail, head, trav_time, freq)
|
601
|
+
self._edges = edges[[tail, head, trav_time, freq]].copy(deep=True)
|
602
|
+
self.edge_count = len(self._edges)
|
603
|
+
|
604
|
+
# remove inf values if any, and values close to zero
|
605
|
+
self._edges[trav_time] = np.where(
|
606
|
+
self._edges[trav_time] > DTYPE_INF_PY, DTYPE_INF_PY, self._edges[trav_time]
|
607
|
+
)
|
608
|
+
self._edges[trav_time] = np.where(
|
609
|
+
self._edges[trav_time] < A_VERY_SMALL_TIME_INTERVAL_PY,
|
610
|
+
A_VERY_SMALL_TIME_INTERVAL_PY,
|
611
|
+
self._edges[trav_time],
|
612
|
+
)
|
613
|
+
self._edges[freq] = np.where(
|
614
|
+
self._edges[freq] > INF_FREQ_PY, INF_FREQ_PY, self._edges[freq]
|
615
|
+
)
|
616
|
+
self._edges[freq] = np.where(
|
617
|
+
self._edges[freq] < MIN_FREQ_PY, MIN_FREQ_PY, self._edges[freq]
|
618
|
+
)
|
619
|
+
|
620
|
+
# create an edge index column
|
621
|
+
self._edges = self._edges.reset_index(drop=True)
|
622
|
+
data_col = "edge_idx"
|
623
|
+
self._edges[data_col] = self._edges.index
|
624
|
+
|
625
|
+
# convert to CSR/CSC format
|
626
|
+
self.vertex_count = self._edges[[tail, head]].max().max() + 1
|
627
|
+
assert orientation in ["out", "in"]
|
628
|
+
self._orientation = orientation
|
629
|
+
if self._orientation == "out":
|
630
|
+
fs_indptr, _, fs_data = convert_graph_to_csr_uint32(
|
631
|
+
self._edges, tail, head, data_col, self.vertex_count
|
632
|
+
)
|
633
|
+
self.__indptr = fs_indptr.astype(np.uint32)
|
634
|
+
self._edge_idx = fs_data.astype(np.uint32)
|
635
|
+
else:
|
636
|
+
rs_indptr, _, rs_data = convert_graph_to_csc_uint32(
|
637
|
+
self._edges, tail, head, data_col, self.vertex_count
|
638
|
+
)
|
639
|
+
self.__indptr = rs_indptr.astype(np.uint32)
|
640
|
+
self._edge_idx = rs_data.astype(np.uint32)
|
641
|
+
|
642
|
+
# edge attributes
|
643
|
+
self._trav_time = self._edges[trav_time].values.astype(DTYPE_PY)
|
644
|
+
self._freq = self._edges[freq].values.astype(DTYPE_PY)
|
645
|
+
self._tail = self._edges[tail].values.astype(np.uint32)
|
646
|
+
self._head = self._edges[head].values.astype(np.uint32)
|
647
|
+
|
648
|
+
# node attribute
|
649
|
+
self.u_i_vec = None
|
650
|
+
|
651
|
+
def run(self, origin, destination, volume, return_inf=False):
|
652
|
+
"""
|
653
|
+
Computes the hyperpath and updates edge volumes based on the input demand and configuration.
|
654
|
+
|
655
|
+
Parameters
|
656
|
+
----------
|
657
|
+
origin : int or list of int
|
658
|
+
The starting vertex or vertices of the demand. If `self._orientation` is "in",
|
659
|
+
this can be a list of origins corresponding to the demand volumes.
|
660
|
+
destination : int or list of int
|
661
|
+
The target vertex or vertices of the demand. If `self._orientation` is "out",
|
662
|
+
this can be a list of destinations corresponding to the demand volumes.
|
663
|
+
volume : float or list of float
|
664
|
+
The demand volume associated with each origin or destination. Must be non-negative.
|
665
|
+
If a single float is provided, it is applied to a single origin-destination pair.
|
666
|
+
return_inf : bool, optional
|
667
|
+
If True, returns additional information from the computation (not yet implemented).
|
668
|
+
Default is False.
|
669
|
+
|
670
|
+
Raises
|
671
|
+
------
|
672
|
+
NotImplementedError
|
673
|
+
If `self._orientation` is "out", as the one-to-many algorithm is not yet implemented.
|
674
|
+
AssertionError
|
675
|
+
If the lengths of `origin` or `destination` and `volume` do not match.
|
676
|
+
If any vertex index or volume is invalid.
|
677
|
+
TypeError
|
678
|
+
If `volume` is not a float or list of floats.
|
679
|
+
ValueError
|
680
|
+
If any volume value is negative.
|
681
|
+
|
682
|
+
Notes
|
683
|
+
-----
|
684
|
+
- The method modifies the `self._edges` DataFrame by adding a "volume" column representing
|
685
|
+
edge volumes based on the computed hyperpath.
|
686
|
+
- The `self.u_i_vec` array is updated to store the least travel time for each vertex.
|
687
|
+
- Only "in" orientation is currently supported.
|
688
|
+
"""
|
689
|
+
# column storing the resulting edge volumes
|
690
|
+
self._edges["volume"] = 0.0
|
691
|
+
self.u_i_vec = None
|
692
|
+
|
693
|
+
# vertex least travel time
|
694
|
+
u_i_vec = DTYPE_INF_PY * np.ones(self.vertex_count, dtype=DTYPE_PY)
|
695
|
+
|
696
|
+
# input check
|
697
|
+
if not isinstance(volume, list):
|
698
|
+
volume = [volume]
|
699
|
+
if self._orientation == "out":
|
700
|
+
self._check_vertex_idx(origin)
|
701
|
+
if not isinstance(destination, list):
|
702
|
+
destination = [destination]
|
703
|
+
assert len(destination) == len(volume)
|
704
|
+
for i, item in enumerate(destination):
|
705
|
+
self._check_vertex_idx(item)
|
706
|
+
self._check_volume(volume[i])
|
707
|
+
demand_indices = np.array(destination, dtype=np.uint32)
|
708
|
+
elif self._orientation == "in":
|
709
|
+
if not isinstance(origin, list):
|
710
|
+
origin = [origin]
|
711
|
+
assert len(origin) == len(volume)
|
712
|
+
for i, item in enumerate(origin):
|
713
|
+
self._check_vertex_idx(item)
|
714
|
+
self._check_volume(volume[i])
|
715
|
+
self._check_vertex_idx(destination)
|
716
|
+
demand_indices = np.array(origin, dtype=np.uint32)
|
717
|
+
assert isinstance(return_inf, bool)
|
718
|
+
|
719
|
+
demand_values = np.array(volume, dtype=DTYPE_PY)
|
720
|
+
|
721
|
+
if self._orientation == "out":
|
722
|
+
raise NotImplementedError(
|
723
|
+
"one-to-many Spiess & Florian's algorithm not implemented yet"
|
724
|
+
)
|
725
|
+
|
726
|
+
compute_SF_in(
|
727
|
+
self.__indptr,
|
728
|
+
self._edge_idx,
|
729
|
+
self._trav_time,
|
730
|
+
self._freq,
|
731
|
+
self._tail,
|
732
|
+
self._head,
|
733
|
+
demand_indices, # source vertex indices
|
734
|
+
demand_values,
|
735
|
+
self._edges["volume"].values,
|
736
|
+
u_i_vec,
|
737
|
+
self.vertex_count,
|
738
|
+
destination,
|
739
|
+
)
|
740
|
+
self.u_i_vec = u_i_vec
|
741
|
+
|
742
|
+
def _check_vertex_idx(self, idx):
|
743
|
+
assert isinstance(idx, int)
|
744
|
+
assert idx >= 0
|
745
|
+
assert idx < self.vertex_count
|
746
|
+
|
747
|
+
def _check_volume(self, v):
|
748
|
+
assert isinstance(v, float)
|
749
|
+
assert v >= 0.0
|
750
|
+
|
751
|
+
def _check_edges(self, edges, tail, head, trav_time, freq):
|
752
|
+
if not isinstance(edges, pd.core.frame.DataFrame):
|
753
|
+
raise TypeError("edges should be a pandas DataFrame")
|
754
|
+
|
755
|
+
for col in [tail, head, trav_time, freq]:
|
756
|
+
if col not in edges:
|
757
|
+
raise KeyError(
|
758
|
+
f"edge column '{col}' not found in graph edges dataframe"
|
759
|
+
)
|
760
|
+
|
761
|
+
if edges[[tail, head, trav_time, freq]].isna().any().any():
|
762
|
+
raise ValueError(
|
763
|
+
" ".join(
|
764
|
+
[
|
765
|
+
f"edges[[{tail}, {head}, {trav_time}, {freq}]] ",
|
766
|
+
"should not have any missing value",
|
767
|
+
]
|
768
|
+
)
|
769
|
+
)
|
770
|
+
|
771
|
+
for col in [tail, head]:
|
772
|
+
if not pd.api.types.is_integer_dtype(edges[col].dtype):
|
773
|
+
raise TypeError(f"column '{col}' should be of integer type")
|
774
|
+
|
775
|
+
for col in [trav_time, freq]:
|
776
|
+
if not pd.api.types.is_numeric_dtype(edges[col].dtype):
|
777
|
+
raise TypeError(f"column '{col}' should be of numeric type")
|
778
|
+
|
779
|
+
if edges[col].min() < 0.0:
|
780
|
+
raise ValueError(f"column '{col}' should be nonnegative")
|
781
|
+
|
782
|
+
|
783
|
+
# author : Francois Pacull
|
784
|
+
# copyright : Architecture & Performance
|
785
|
+
# email: francois.pacull@architecture-performance.fr
|
786
|
+
# license : MIT
|