edsger 0.1.3__cp311-cp311-win_amd64.whl → 0.1.5__cp311-cp311-win_amd64.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 CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.3"
1
+ __version__ = "0.1.5"
Binary file
@@ -0,0 +1,544 @@
1
+ """
2
+ An implementation of the Bellman-Ford algorithm.
3
+
4
+ cpdef functions:
5
+
6
+ - compute_bf_sssp
7
+ Compute single-source shortest path (from one vertex to all vertices). Does
8
+ not return predecessors. Supports negative weights.
9
+ - compute_bf_sssp_w_path
10
+ Compute single-source shortest path (from one vertex to all vertices).
11
+ Compute predecessors. Supports negative weights.
12
+ - compute_bf_stsp
13
+ Compute single-target shortest path (from all vertices to one vertex). Does
14
+ not return successors. Supports negative weights.
15
+ - compute_bf_stsp_w_path
16
+ Compute single-target shortest path (from all vertices to one vertex).
17
+ Compute successors. Supports negative weights.
18
+ - detect_negative_cycle
19
+ Detect negative cycles in the graph.
20
+ """
21
+
22
+ cimport numpy as cnp
23
+ import numpy as np
24
+
25
+ from edsger.commons cimport DTYPE_INF, DTYPE_t
26
+ from edsger.commons import DTYPE_PY
27
+
28
+
29
+ cpdef cnp.ndarray compute_bf_sssp(
30
+ cnp.uint32_t[::1] csr_indptr,
31
+ cnp.uint32_t[::1] csr_indices,
32
+ DTYPE_t[::1] csr_data,
33
+ int source_vert_idx,
34
+ int vertex_count):
35
+ """
36
+ Compute single-source shortest path using Bellman-Ford algorithm.
37
+
38
+ From one vertex to all vertices. Does not return predecessors.
39
+ Supports negative edge weights.
40
+
41
+ Parameters
42
+ ----------
43
+ csr_indices : cnp.uint32_t[::1]
44
+ indices in the CSR format
45
+ csr_indptr : cnp.uint32_t[::1]
46
+ pointers in the CSR format
47
+ csr_data : DTYPE_t[::1]
48
+ data (edge weights) in the CSR format
49
+ source_vert_idx : int
50
+ source vertex index
51
+ vertex_count : int
52
+ vertex count
53
+
54
+ Returns
55
+ -------
56
+ path_lengths : cnp.ndarray
57
+ shortest path length for each vertex
58
+ """
59
+
60
+ cdef:
61
+ size_t tail_vert_idx, head_vert_idx, idx, v
62
+ DTYPE_t tail_vert_val, new_dist
63
+ cnp.ndarray[DTYPE_t, ndim=1] dist = np.full(
64
+ vertex_count, DTYPE_INF, dtype=DTYPE_PY)
65
+ size_t source = <size_t>source_vert_idx
66
+ bint changed
67
+
68
+ # Initialize source distance
69
+ dist[source] = 0.0
70
+
71
+ with nogil:
72
+ # Relax edges V-1 times
73
+ for v in range(<size_t>(vertex_count - 1)):
74
+ changed = False
75
+
76
+ # Iterate through all vertices
77
+ for tail_vert_idx in range(<size_t>vertex_count):
78
+ tail_vert_val = dist[tail_vert_idx]
79
+
80
+ # Skip if vertex is unreachable
81
+ if tail_vert_val == DTYPE_INF:
82
+ continue
83
+
84
+ # Relax edges from this vertex
85
+ for idx in range(<size_t>csr_indptr[tail_vert_idx],
86
+ <size_t>csr_indptr[tail_vert_idx + 1]):
87
+
88
+ head_vert_idx = <size_t>csr_indices[idx]
89
+ new_dist = tail_vert_val + csr_data[idx]
90
+
91
+ if dist[head_vert_idx] > new_dist:
92
+ dist[head_vert_idx] = new_dist
93
+ changed = True
94
+
95
+ # Early termination if no changes
96
+ if not changed:
97
+ break
98
+
99
+ return dist
100
+
101
+
102
+ cpdef cnp.ndarray compute_bf_sssp_w_path(
103
+ cnp.uint32_t[::1] csr_indptr,
104
+ cnp.uint32_t[::1] csr_indices,
105
+ DTYPE_t[::1] csr_data,
106
+ cnp.uint32_t[::1] predecessor,
107
+ int source_vert_idx,
108
+ int vertex_count):
109
+ """
110
+ Compute single-source shortest path using Bellman-Ford algorithm.
111
+
112
+ From one vertex to all vertices. Compute predecessors.
113
+ Supports negative edge weights.
114
+
115
+ Parameters
116
+ ----------
117
+ csr_indices : cnp.uint32_t[::1]
118
+ indices in the CSR format
119
+ csr_indptr : cnp.uint32_t[::1]
120
+ pointers in the CSR format
121
+ csr_data : DTYPE_t[::1]
122
+ data (edge weights) in the CSR format
123
+ predecessor : cnp.uint32_t[::1]
124
+ array of indices, one for each vertex of the graph. Each vertex'
125
+ entry contains the index of its predecessor in a path from the
126
+ source, through the graph.
127
+ source_vert_idx : int
128
+ source vertex index
129
+ vertex_count : int
130
+ vertex count
131
+
132
+ Returns
133
+ -------
134
+ path_lengths : cnp.ndarray
135
+ shortest path length for each vertex
136
+ """
137
+
138
+ cdef:
139
+ size_t tail_vert_idx, head_vert_idx, idx, v
140
+ DTYPE_t tail_vert_val, new_dist
141
+ cnp.ndarray[DTYPE_t, ndim=1] dist = np.full(
142
+ vertex_count, DTYPE_INF, dtype=DTYPE_PY)
143
+ size_t source = <size_t>source_vert_idx
144
+ bint changed
145
+
146
+ # Initialize source distance
147
+ dist[source] = 0.0
148
+
149
+ with nogil:
150
+ # Relax edges V-1 times
151
+ for v in range(<size_t>(vertex_count - 1)):
152
+ changed = False
153
+
154
+ # Iterate through all vertices
155
+ for tail_vert_idx in range(<size_t>vertex_count):
156
+ tail_vert_val = dist[tail_vert_idx]
157
+
158
+ # Skip if vertex is unreachable
159
+ if tail_vert_val == DTYPE_INF:
160
+ continue
161
+
162
+ # Relax edges from this vertex
163
+ for idx in range(<size_t>csr_indptr[tail_vert_idx],
164
+ <size_t>csr_indptr[tail_vert_idx + 1]):
165
+
166
+ head_vert_idx = <size_t>csr_indices[idx]
167
+ new_dist = tail_vert_val + csr_data[idx]
168
+
169
+ if dist[head_vert_idx] > new_dist:
170
+ dist[head_vert_idx] = new_dist
171
+ predecessor[head_vert_idx] = tail_vert_idx
172
+ changed = True
173
+
174
+ # Early termination if no changes
175
+ if not changed:
176
+ break
177
+
178
+ return dist
179
+
180
+
181
+ cpdef cnp.ndarray compute_bf_stsp(
182
+ cnp.uint32_t[::1] csc_indptr,
183
+ cnp.uint32_t[::1] csc_indices,
184
+ DTYPE_t[::1] csc_data,
185
+ int target_vert_idx,
186
+ int vertex_count):
187
+ """
188
+ Compute single-target shortest path using Bellman-Ford algorithm.
189
+
190
+ From all vertices to one vertex. Does not return successors.
191
+ Supports negative edge weights.
192
+
193
+ Parameters
194
+ ----------
195
+ csc_indices : cnp.uint32_t[::1]
196
+ indices in the CSC format
197
+ csc_indptr : cnp.uint32_t[::1]
198
+ pointers in the CSC format
199
+ csc_data : DTYPE_t[::1]
200
+ data (edge weights) in the CSC format
201
+ target_vert_idx : int
202
+ target vertex index
203
+ vertex_count : int
204
+ vertex count
205
+
206
+ Returns
207
+ -------
208
+ path_lengths : cnp.ndarray
209
+ shortest path length for each vertex
210
+ """
211
+
212
+ cdef:
213
+ size_t tail_vert_idx, head_vert_idx, idx, v
214
+ DTYPE_t head_vert_val, new_dist
215
+ cnp.ndarray[DTYPE_t, ndim=1] dist = np.full(
216
+ vertex_count, DTYPE_INF, dtype=DTYPE_PY)
217
+ size_t target = <size_t>target_vert_idx
218
+ bint changed
219
+
220
+ # Initialize target distance
221
+ dist[target] = 0.0
222
+
223
+ with nogil:
224
+ # Relax edges V-1 times
225
+ for v in range(<size_t>(vertex_count - 1)):
226
+ changed = False
227
+
228
+ # Iterate through all vertices (reversed for incoming edges)
229
+ for head_vert_idx in range(<size_t>vertex_count):
230
+ head_vert_val = dist[head_vert_idx]
231
+
232
+ # Skip if vertex is unreachable
233
+ if head_vert_val == DTYPE_INF:
234
+ continue
235
+
236
+ # Relax incoming edges to this vertex
237
+ for idx in range(<size_t>csc_indptr[head_vert_idx],
238
+ <size_t>csc_indptr[head_vert_idx + 1]):
239
+
240
+ tail_vert_idx = <size_t>csc_indices[idx]
241
+ new_dist = head_vert_val + csc_data[idx]
242
+
243
+ if dist[tail_vert_idx] > new_dist:
244
+ dist[tail_vert_idx] = new_dist
245
+ changed = True
246
+
247
+ # Early termination if no changes
248
+ if not changed:
249
+ break
250
+
251
+ return dist
252
+
253
+
254
+ cpdef cnp.ndarray compute_bf_stsp_w_path(
255
+ cnp.uint32_t[::1] csc_indptr,
256
+ cnp.uint32_t[::1] csc_indices,
257
+ DTYPE_t[::1] csc_data,
258
+ cnp.uint32_t[::1] successor,
259
+ int target_vert_idx,
260
+ int vertex_count):
261
+ """
262
+ Compute single-target shortest path using Bellman-Ford algorithm.
263
+
264
+ From all vertices to one vertex. Compute successors.
265
+ Supports negative edge weights.
266
+
267
+ Parameters
268
+ ----------
269
+ csc_indices : cnp.uint32_t[::1]
270
+ indices in the CSC format
271
+ csc_indptr : cnp.uint32_t[::1]
272
+ pointers in the CSC format
273
+ csc_data : DTYPE_t[::1]
274
+ data (edge weights) in the CSC format
275
+ successor : cnp.uint32_t[::1]
276
+ array of indices, one for each vertex of the graph. Each vertex'
277
+ entry contains the index of its successor in a path to the
278
+ target, through the graph.
279
+ target_vert_idx : int
280
+ target vertex index
281
+ vertex_count : int
282
+ vertex count
283
+
284
+ Returns
285
+ -------
286
+ path_lengths : cnp.ndarray
287
+ shortest path length for each vertex
288
+ """
289
+
290
+ cdef:
291
+ size_t tail_vert_idx, head_vert_idx, idx, v
292
+ DTYPE_t head_vert_val, new_dist
293
+ cnp.ndarray[DTYPE_t, ndim=1] dist = np.full(
294
+ vertex_count, DTYPE_INF, dtype=DTYPE_PY)
295
+ size_t target = <size_t>target_vert_idx
296
+ bint changed
297
+
298
+ # Initialize target distance
299
+ dist[target] = 0.0
300
+
301
+ with nogil:
302
+ # Relax edges V-1 times
303
+ for v in range(<size_t>(vertex_count - 1)):
304
+ changed = False
305
+
306
+ # Iterate through all vertices (reversed for incoming edges)
307
+ for head_vert_idx in range(<size_t>vertex_count):
308
+ head_vert_val = dist[head_vert_idx]
309
+
310
+ # Skip if vertex is unreachable
311
+ if head_vert_val == DTYPE_INF:
312
+ continue
313
+
314
+ # Relax incoming edges to this vertex
315
+ for idx in range(<size_t>csc_indptr[head_vert_idx],
316
+ <size_t>csc_indptr[head_vert_idx + 1]):
317
+
318
+ tail_vert_idx = <size_t>csc_indices[idx]
319
+ new_dist = head_vert_val + csc_data[idx]
320
+
321
+ if dist[tail_vert_idx] > new_dist:
322
+ dist[tail_vert_idx] = new_dist
323
+ successor[tail_vert_idx] = head_vert_idx
324
+ changed = True
325
+
326
+ # Early termination if no changes
327
+ if not changed:
328
+ break
329
+
330
+ return dist
331
+
332
+
333
+ cpdef bint detect_negative_cycle(
334
+ cnp.uint32_t[::1] csr_indptr,
335
+ cnp.uint32_t[::1] csr_indices,
336
+ DTYPE_t[::1] csr_data,
337
+ DTYPE_t[::1] dist_matrix,
338
+ int vertex_count):
339
+ """
340
+ Detect negative cycles using one more iteration of edge relaxation.
341
+
342
+ Parameters
343
+ ----------
344
+ csr_indices : cnp.uint32_t[::1]
345
+ indices in the CSR format
346
+ csr_indptr : cnp.uint32_t[::1]
347
+ pointers in the CSR format
348
+ csr_data : DTYPE_t[::1]
349
+ data (edge weights) in the CSR format
350
+ dist_matrix : DTYPE_t[::1]
351
+ current distance matrix from Bellman-Ford algorithm
352
+ vertex_count : int
353
+ vertex count
354
+
355
+ Returns
356
+ -------
357
+ has_negative_cycle : bool
358
+ True if negative cycle detected, False otherwise
359
+ """
360
+
361
+ cdef:
362
+ size_t tail_vert_idx, head_vert_idx, idx
363
+ DTYPE_t tail_vert_val, new_dist
364
+ bint has_negative_cycle = False
365
+
366
+ with nogil:
367
+ # One more iteration to detect negative cycles
368
+ for tail_vert_idx in range(<size_t>vertex_count):
369
+ tail_vert_val = dist_matrix[tail_vert_idx]
370
+
371
+ # Skip if vertex is unreachable
372
+ if tail_vert_val == DTYPE_INF:
373
+ continue
374
+
375
+ # Check edges from this vertex
376
+ for idx in range(<size_t>csr_indptr[tail_vert_idx],
377
+ <size_t>csr_indptr[tail_vert_idx + 1]):
378
+
379
+ head_vert_idx = <size_t>csr_indices[idx]
380
+ new_dist = tail_vert_val + csr_data[idx]
381
+
382
+ # If we can still relax, there's a negative cycle
383
+ if dist_matrix[head_vert_idx] > new_dist:
384
+ has_negative_cycle = True
385
+ break
386
+
387
+ if has_negative_cycle:
388
+ break
389
+
390
+ return has_negative_cycle
391
+
392
+
393
+ cpdef bint detect_negative_cycle_csc(
394
+ cnp.uint32_t[::1] csc_indptr,
395
+ cnp.uint32_t[::1] csc_indices,
396
+ DTYPE_t[::1] csc_data,
397
+ DTYPE_t[::1] stsp_dist,
398
+ int vertex_count):
399
+ """
400
+ Detect negative cycles using CSC format and STSP distances.
401
+
402
+ For STSP (Single-Target Shortest Path):
403
+ - stsp_dist[u] = distance FROM vertex u TO target vertex
404
+ - Edge (u→v) can be relaxed if: stsp_dist[u] > stsp_dist[v] + weight(u→v)
405
+
406
+ This function performs one additional iteration to check if any edge
407
+ can still be relaxed, which indicates the presence of a negative cycle.
408
+
409
+ Parameters
410
+ ----------
411
+ csc_indptr : cnp.uint32_t[::1]
412
+ Pointers in the CSC format (incoming edges by destination)
413
+ csc_indices : cnp.uint32_t[::1]
414
+ Indices in the CSC format (source vertices)
415
+ csc_data : DTYPE_t[::1]
416
+ Data (edge weights) in the CSC format
417
+ stsp_dist : DTYPE_t[::1]
418
+ Current distance array from STSP algorithm (distances TO target)
419
+ vertex_count : int
420
+ Total number of vertices in the graph
421
+
422
+ Returns
423
+ -------
424
+ has_negative_cycle : bool
425
+ True if negative cycle detected, False otherwise
426
+ """
427
+
428
+ cdef:
429
+ size_t head_vert_idx, tail_vert_idx, idx
430
+ DTYPE_t new_dist
431
+ bint has_negative_cycle = False
432
+
433
+ with nogil:
434
+ # Iterate over destination vertices (CSC organization)
435
+ for head_vert_idx in range(<size_t>vertex_count):
436
+ # Skip unreachable vertices
437
+ if stsp_dist[head_vert_idx] == DTYPE_INF:
438
+ continue
439
+
440
+ # Check all incoming edges to this vertex
441
+ for idx in range(<size_t>csc_indptr[head_vert_idx],
442
+ <size_t>csc_indptr[head_vert_idx + 1]):
443
+
444
+ tail_vert_idx = <size_t>csc_indices[idx] # Source of edge
445
+
446
+ # STSP relaxation: can we improve distance FROM tail TO target?
447
+ new_dist = stsp_dist[head_vert_idx] + csc_data[idx]
448
+
449
+ # If we can still relax this edge, negative cycle exists
450
+ if stsp_dist[tail_vert_idx] > new_dist:
451
+ has_negative_cycle = True
452
+ break
453
+
454
+ if has_negative_cycle:
455
+ break
456
+
457
+ return has_negative_cycle
458
+
459
+
460
+ # ============================================================================ #
461
+ # tests #
462
+ # ============================================================================ #
463
+
464
+ cdef generate_negative_edge_network_csr():
465
+ """
466
+ Generate a network with negative edges (no negative cycle) in CSR format.
467
+
468
+ Graph structure:
469
+ 0 -> 1 (weight: 1)
470
+ 0 -> 2 (weight: 4)
471
+ 1 -> 2 (weight: -2)
472
+ 1 -> 3 (weight: 5)
473
+ 2 -> 3 (weight: 1)
474
+ 3 -> 4 (weight: 3)
475
+
476
+ This network has 6 edges and 5 vertices.
477
+ """
478
+
479
+ csr_indptr = np.array([0, 2, 4, 5, 6, 6], dtype=np.uint32)
480
+ csr_indices = np.array([1, 2, 2, 3, 3, 4], dtype=np.uint32)
481
+ csr_data = np.array([1., 4., -2., 5., 1., 3.], dtype=DTYPE_PY)
482
+
483
+ return csr_indptr, csr_indices, csr_data
484
+
485
+
486
+ cdef generate_negative_cycle_network_csr():
487
+ """
488
+ Generate a network with a negative cycle in CSR format.
489
+
490
+ Graph structure:
491
+ 0 -> 1 (weight: 1)
492
+ 1 -> 2 (weight: -2)
493
+ 2 -> 0 (weight: -1)
494
+ 2 -> 3 (weight: 1)
495
+
496
+ Cycle 0->1->2->0 has total weight -2 (negative cycle)
497
+ This network has 4 edges and 4 vertices.
498
+ """
499
+
500
+ csr_indptr = np.array([0, 1, 2, 4, 4], dtype=np.uint32)
501
+ csr_indices = np.array([1, 2, 0, 3], dtype=np.uint32)
502
+ csr_data = np.array([1., -2., -1., 1.], dtype=DTYPE_PY)
503
+
504
+ return csr_indptr, csr_indices, csr_data
505
+
506
+
507
+ cpdef test_bf_negative_edges():
508
+ """
509
+ Test Bellman-Ford with negative edges (no negative cycle).
510
+ """
511
+
512
+ csr_indptr, csr_indices, csr_data = generate_negative_edge_network_csr()
513
+
514
+ # from vertex 0
515
+ path_lengths = compute_bf_sssp(csr_indptr, csr_indices, csr_data, 0, 5)
516
+ path_lengths_ref = np.array([0., 1., -1., 0., 3.], dtype=DTYPE_PY)
517
+ assert np.allclose(path_lengths_ref, path_lengths)
518
+
519
+ # Test negative cycle detection (should return False)
520
+ has_cycle = detect_negative_cycle(
521
+ csr_indptr, csr_indices, csr_data, path_lengths, 5)
522
+ assert not has_cycle
523
+
524
+
525
+ cpdef test_bf_negative_cycle():
526
+ """
527
+ Test Bellman-Ford negative cycle detection.
528
+ """
529
+
530
+ csr_indptr, csr_indices, csr_data = generate_negative_cycle_network_csr()
531
+
532
+ # from vertex 0
533
+ path_lengths = compute_bf_sssp(csr_indptr, csr_indices, csr_data, 0, 4)
534
+
535
+ # Test negative cycle detection (should return True)
536
+ has_cycle = detect_negative_cycle(
537
+ csr_indptr, csr_indices, csr_data, path_lengths, 4)
538
+ assert has_cycle
539
+
540
+
541
+ # author : Francois Pacull
542
+ # copyright : Architecture & Performance
543
+ # email: francois.pacull@architecture-performance.fr
544
+ # license : MIT
Binary file
Binary file