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