edsger 0.1.1__cp39-cp39-manylinux_2_17_x86_64.manylinux2014_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/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