Skip to main content

rust_igraph/algorithms/paths/
bellman_ford.rs

1//! Bellman-Ford single-source shortest paths (ALGO-SP-002).
2//!
3//! Counterpart of `igraph_distances_bellman_ford()` from
4//! `references/igraph/src/paths/bellman_ford.c:69`. Returns the
5//! shortest-path distance from a single source to every vertex,
6//! allowing **negative edge weights** (which Dijkstra forbids).
7//! Detects negative cycles reachable from the source.
8//!
9//! Algorithm: SPFA (Shortest Path Faster Algorithm) — the
10//! queue-based Bellman-Ford variant the upstream `bellman_ford.c`
11//! uses. Each vertex starts in the queue; popping a vertex marks it
12//! "clean" and relaxes its outgoing edges; relaxing an edge marks
13//! the target "dirty" and re-queues it if it was clean. A vertex
14//! popped more than `vcount` times signals a negative cycle.
15//!
16//! Time complexity: `O(V · E)` worst case (the SPFA optimisation
17//! often beats this in practice).
18
19use std::collections::VecDeque;
20
21use crate::algorithms::paths::dijkstra::DijkstraMode;
22use crate::core::graph::EdgeId;
23use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
24
25fn validate_weights(graph: &Graph, weights: &[f64]) -> IgraphResult<()> {
26    let m = graph.ecount();
27    if weights.len() != m {
28        return Err(IgraphError::InvalidArgument(format!(
29            "weights vector size ({}) differs from edge count ({})",
30            weights.len(),
31            m
32        )));
33    }
34    for (e, &w) in weights.iter().enumerate() {
35        if w.is_nan() {
36            return Err(IgraphError::InvalidArgument(format!(
37                "weight at edge {e} is NaN"
38            )));
39        }
40    }
41    Ok(())
42}
43
44fn incident_for_mode(graph: &Graph, v: VertexId, mode: DijkstraMode) -> IgraphResult<Vec<EdgeId>> {
45    if !graph.is_directed() {
46        return graph.incident(v);
47    }
48    match mode {
49        DijkstraMode::Out => graph.incident(v),
50        DijkstraMode::In => graph.incident_in(v),
51        DijkstraMode::All => {
52            let mut out = graph.incident(v)?;
53            out.extend(graph.incident_in(v)?);
54            Ok(out)
55        }
56    }
57}
58
59/// Single-source Bellman-Ford shortest distances on `graph` from
60/// `source`, following out-edges on directed graphs.
61///
62/// Returns `distances[v]`: `Some(d)` if `v` is reachable from
63/// `source`, `None` otherwise. `distances[source] == Some(0.0)`.
64///
65/// `weights[e]` is the weight of edge `e`; length must equal
66/// `graph.ecount()`. Negative weights are allowed; NaN weights are
67/// rejected ([`IgraphError::InvalidArgument`]). If a negative cycle
68/// is reachable from the source, returns
69/// [`IgraphError::InvalidArgument`] with a "negative cycle"
70/// message (matches upstream's `IGRAPH_ENEGCYCLE` semantics).
71///
72/// Use this instead of [`crate::dijkstra_distances`] when edge
73/// weights can be negative. For non-negative weights Dijkstra is
74/// asymptotically faster (`O((V+E) log V)` vs Bellman-Ford's
75/// `O(V·E)`).
76///
77/// Counterpart of `igraph_distances_bellman_ford(_, _, vss(source),
78/// vss_all(), weights, IGRAPH_OUT)`.
79///
80/// # Examples
81///
82/// ```
83/// use rust_igraph::{Graph, bellman_ford_distances};
84///
85/// // Directed graph 0 → 1 → 2 with weights 3, -1 — Bellman-Ford
86/// // handles the negative edge that would break Dijkstra's
87/// // monotonicity assumption.
88/// let mut g = Graph::new(3, true).unwrap();
89/// g.add_edge(0, 1).unwrap();  // edge 0, weight 3
90/// g.add_edge(1, 2).unwrap();  // edge 1, weight -1
91/// let d = bellman_ford_distances(&g, 0, &[3.0, -1.0]).unwrap();
92/// assert_eq!(d, vec![Some(0.0), Some(3.0), Some(2.0)]);
93/// ```
94pub fn bellman_ford_distances(
95    graph: &Graph,
96    source: VertexId,
97    weights: &[f64],
98) -> IgraphResult<Vec<Option<f64>>> {
99    bellman_ford_distances_with_mode(graph, source, weights, DijkstraMode::Out)
100}
101
102/// Single-source Bellman-Ford with directed-mode selection.
103///
104/// `mode` selects how directed edges are followed:
105/// - [`DijkstraMode::Out`] follows out-edges (default for
106///   [`bellman_ford_distances`]),
107/// - [`DijkstraMode::In`] follows in-edges (i.e. shortest paths
108///   *into* `source`),
109/// - [`DijkstraMode::All`] ignores edge direction.
110///
111/// On undirected graphs the mode is ignored.
112///
113/// Counterpart of `igraph_distances_bellman_ford(_, _, vss(source),
114/// vss_all(), weights, mode)`.
115pub fn bellman_ford_distances_with_mode(
116    graph: &Graph,
117    source: VertexId,
118    weights: &[f64],
119    mode: DijkstraMode,
120) -> IgraphResult<Vec<Option<f64>>> {
121    let n = graph.vcount();
122    if source >= n {
123        return Err(IgraphError::VertexOutOfRange { id: source, n });
124    }
125    validate_weights(graph, weights)?;
126
127    let n_usize = n as usize;
128    let mut dist: Vec<f64> = vec![f64::INFINITY; n_usize];
129    dist[source as usize] = 0.0;
130
131    // SPFA: queue all vertices initially; pop, relax edges of clean
132    // vertices, re-queue targets that get relaxed.
133    let mut queue: VecDeque<VertexId> = VecDeque::with_capacity(n_usize);
134    let mut in_queue: Vec<bool> = vec![true; n_usize];
135    let mut num_queued: Vec<u32> = vec![0; n_usize];
136    for v in 0..n {
137        queue.push_back(v);
138    }
139
140    let n_u32 = n;
141    while let Some(j) = queue.pop_front() {
142        let j_idx = j as usize;
143        in_queue[j_idx] = false;
144        num_queued[j_idx] = num_queued[j_idx]
145            .checked_add(1)
146            .ok_or(IgraphError::Internal("num_queued overflow"))?;
147        // A vertex queued more than vcount times indicates a negative
148        // cycle (upstream uses the same `> n` threshold).
149        if num_queued[j_idx] > n_u32 {
150            return Err(IgraphError::InvalidArgument(
151                "negative cycle reachable from source while running Bellman-Ford".to_string(),
152            ));
153        }
154
155        // No point relaxing edges from an unreachable vertex.
156        if !dist[j_idx].is_finite() {
157            continue;
158        }
159
160        let incidents = incident_for_mode(graph, j, mode)?;
161        for eid in incidents {
162            let w = weights[eid as usize];
163            // Positive-infinite weights are ignored (matches upstream).
164            if w == f64::INFINITY {
165                continue;
166            }
167            let target = graph.edge_other(eid, j)?;
168            let target_idx = target as usize;
169            let altdist = dist[j_idx] + w;
170            if altdist < dist[target_idx] {
171                dist[target_idx] = altdist;
172                if !in_queue[target_idx] {
173                    in_queue[target_idx] = true;
174                    queue.push_back(target);
175                }
176            }
177        }
178    }
179
180    Ok(dist
181        .into_iter()
182        .map(|d| if d.is_finite() { Some(d) } else { None })
183        .collect())
184}
185
186/// Returns the shortest path from `source` to `target` using
187/// Bellman-Ford, following out-edges on directed graphs.
188///
189/// Returns `Some((vertices, edges))` if a finite-weight path exists,
190/// `None` if `target` is unreachable. When `source == target`, returns
191/// `Some((vec![source], vec![]))`.
192///
193/// Supports negative edge weights; detects negative cycles reachable
194/// from the source.
195///
196/// Counterpart of `igraph_get_shortest_path_bellman_ford(_, vertices,
197/// edges, from, to, weights, IGRAPH_OUT)`.
198///
199/// # Examples
200///
201/// ```
202/// use rust_igraph::{Graph, bellman_ford_path_to};
203///
204/// let mut g = Graph::new(4, true).unwrap();
205/// g.add_edge(0, 1).unwrap(); // edge 0, weight 3
206/// g.add_edge(1, 2).unwrap(); // edge 1, weight -1
207/// g.add_edge(0, 2).unwrap(); // edge 2, weight 5
208/// g.add_edge(2, 3).unwrap(); // edge 3, weight 1
209/// let (vs, es) = bellman_ford_path_to(&g, 0, 3, &[3.0, -1.0, 5.0, 1.0])
210///     .unwrap()
211///     .unwrap();
212/// assert_eq!(vs, vec![0, 1, 2, 3]);
213/// assert_eq!(es, vec![0, 1, 3]);
214/// ```
215pub fn bellman_ford_path_to(
216    graph: &Graph,
217    source: VertexId,
218    target: VertexId,
219    weights: &[f64],
220) -> IgraphResult<Option<(Vec<VertexId>, Vec<EdgeId>)>> {
221    bellman_ford_path_to_with_mode(graph, source, target, weights, DijkstraMode::Out)
222}
223
224/// Returns the shortest path from `source` to `target` using
225/// Bellman-Ford with directed-mode selection.
226///
227/// `mode` selects how directed edges are followed:
228/// - [`DijkstraMode::Out`] follows out-edges,
229/// - [`DijkstraMode::In`] follows in-edges,
230/// - [`DijkstraMode::All`] ignores edge direction.
231///
232/// Returns `Some((vertices, edges))` if a finite-weight path exists,
233/// `None` if `target` is unreachable.
234pub fn bellman_ford_path_to_with_mode(
235    graph: &Graph,
236    source: VertexId,
237    target: VertexId,
238    weights: &[f64],
239    mode: DijkstraMode,
240) -> IgraphResult<Option<(Vec<VertexId>, Vec<EdgeId>)>> {
241    let n = graph.vcount();
242    if source >= n {
243        return Err(IgraphError::VertexOutOfRange { id: source, n });
244    }
245    if target >= n {
246        return Err(IgraphError::VertexOutOfRange { id: target, n });
247    }
248    validate_weights(graph, weights)?;
249
250    if source == target {
251        return Ok(Some((vec![source], vec![])));
252    }
253
254    let n_usize = n as usize;
255    let mut dist: Vec<f64> = vec![f64::INFINITY; n_usize];
256    dist[source as usize] = 0.0;
257
258    // Parent edge for reconstructing the path.
259    let mut parent_edge: Vec<Option<EdgeId>> = vec![None; n_usize];
260
261    let mut queue: VecDeque<VertexId> = VecDeque::with_capacity(n_usize);
262    let mut in_queue: Vec<bool> = vec![true; n_usize];
263    let mut num_queued: Vec<u32> = vec![0; n_usize];
264    for v in 0..n {
265        queue.push_back(v);
266    }
267
268    while let Some(j) = queue.pop_front() {
269        let j_idx = j as usize;
270        in_queue[j_idx] = false;
271        num_queued[j_idx] = num_queued[j_idx]
272            .checked_add(1)
273            .ok_or(IgraphError::Internal("num_queued overflow"))?;
274        if num_queued[j_idx] > n {
275            return Err(IgraphError::InvalidArgument(
276                "negative cycle reachable from source while running Bellman-Ford".to_string(),
277            ));
278        }
279
280        if !dist[j_idx].is_finite() {
281            continue;
282        }
283
284        let incidents = incident_for_mode(graph, j, mode)?;
285        for eid in incidents {
286            let w = weights[eid as usize];
287            if w == f64::INFINITY {
288                continue;
289            }
290            let neighbor = graph.edge_other(eid, j)?;
291            let neighbor_idx = neighbor as usize;
292            let altdist = dist[j_idx] + w;
293            if altdist < dist[neighbor_idx] {
294                dist[neighbor_idx] = altdist;
295                parent_edge[neighbor_idx] = Some(eid);
296                if !in_queue[neighbor_idx] {
297                    in_queue[neighbor_idx] = true;
298                    queue.push_back(neighbor);
299                }
300            }
301        }
302    }
303
304    // If target is unreachable, return None.
305    if !dist[target as usize].is_finite() {
306        return Ok(None);
307    }
308
309    // Reconstruct path from target back to source.
310    let mut edges_rev: Vec<EdgeId> = Vec::new();
311    let mut vertices_rev: Vec<VertexId> = Vec::new();
312    let mut cur = target;
313    vertices_rev.push(cur);
314    while cur != source {
315        let eid = parent_edge[cur as usize].ok_or(IgraphError::Internal(
316            "bellman_ford_path_to: missing parent edge in path reconstruction",
317        ))?;
318        edges_rev.push(eid);
319        cur = graph.edge_other(eid, cur)?;
320        vertices_rev.push(cur);
321    }
322
323    vertices_rev.reverse();
324    edges_rev.reverse();
325    Ok(Some((vertices_rev, edges_rev)))
326}
327
328/// One entry in the result of [`bellman_ford_paths`]: `Some((vertices,
329/// edges))` if the target is reachable, `None` otherwise.
330pub type BellmanFordPathEntry = Option<(Vec<VertexId>, Vec<EdgeId>)>;
331
332/// Shortest paths from `source` to each vertex in `targets` using
333/// Bellman-Ford (handles negative edge weights).
334///
335/// Counterpart of `igraph_get_shortest_paths_bellman_ford` in
336/// `references/igraph/src/paths/bellman_ford.c:296`. Runs the
337/// SSSP computation once and reconstructs a path for each target.
338///
339/// Returns a `Vec` with one entry per target. Each entry is
340/// `Some((vertices, edges))` if the target is reachable from `source`,
341/// or `None` if it is not.
342///
343/// # Errors
344///
345/// * [`IgraphError::VertexOutOfRange`] if `source` or any target is
346///   outside `[0, vcount())`.
347/// * [`IgraphError::InvalidArgument`] if a negative cycle is reachable
348///   from `source`, or `weights.len() != ecount()`, or any weight is NaN.
349///
350/// # Examples
351///
352/// ```
353/// use rust_igraph::{Graph, bellman_ford_paths};
354///
355/// let mut g = Graph::new(4, true).unwrap();
356/// g.add_edge(0, 1).unwrap(); // edge 0, weight 3
357/// g.add_edge(1, 2).unwrap(); // edge 1, weight -1
358/// g.add_edge(0, 2).unwrap(); // edge 2, weight 5
359/// g.add_edge(2, 3).unwrap(); // edge 3, weight 1
360/// let results = bellman_ford_paths(&g, 0, &[2, 3], &[3.0, -1.0, 5.0, 1.0]).unwrap();
361/// // Path to vertex 2: 0→1→2 (cost 2, via negative edge)
362/// let (vs, es) = results[0].as_ref().unwrap();
363/// assert_eq!(*vs, vec![0, 1, 2]);
364/// assert_eq!(*es, vec![0, 1]);
365/// // Path to vertex 3: 0→1→2→3 (cost 3)
366/// let (vs, es) = results[1].as_ref().unwrap();
367/// assert_eq!(*vs, vec![0, 1, 2, 3]);
368/// assert_eq!(*es, vec![0, 1, 3]);
369/// ```
370pub fn bellman_ford_paths(
371    graph: &Graph,
372    source: VertexId,
373    targets: &[VertexId],
374    weights: &[f64],
375) -> IgraphResult<Vec<BellmanFordPathEntry>> {
376    bellman_ford_paths_with_mode(graph, source, targets, weights, DijkstraMode::Out)
377}
378
379/// Multi-target Bellman-Ford shortest paths with directed-mode selection.
380///
381/// Like [`bellman_ford_paths`] but allows choosing how directed edges
382/// are followed:
383/// - [`DijkstraMode::Out`] follows out-edges (default),
384/// - [`DijkstraMode::In`] follows in-edges,
385/// - [`DijkstraMode::All`] ignores edge direction.
386pub fn bellman_ford_paths_with_mode(
387    graph: &Graph,
388    source: VertexId,
389    targets: &[VertexId],
390    weights: &[f64],
391    mode: DijkstraMode,
392) -> IgraphResult<Vec<BellmanFordPathEntry>> {
393    let n = graph.vcount();
394    if source >= n {
395        return Err(IgraphError::VertexOutOfRange { id: source, n });
396    }
397    for &t in targets {
398        if t >= n {
399            return Err(IgraphError::VertexOutOfRange { id: t, n });
400        }
401    }
402    validate_weights(graph, weights)?;
403
404    let n_usize = n as usize;
405
406    // Run SSSP once from source.
407    let mut dist: Vec<f64> = vec![f64::INFINITY; n_usize];
408    dist[source as usize] = 0.0;
409
410    let mut parent_edge: Vec<Option<EdgeId>> = vec![None; n_usize];
411
412    let mut queue: VecDeque<VertexId> = VecDeque::with_capacity(n_usize);
413    let mut in_queue: Vec<bool> = vec![true; n_usize];
414    let mut num_queued: Vec<u32> = vec![0; n_usize];
415    for v in 0..n {
416        queue.push_back(v);
417    }
418
419    while let Some(j) = queue.pop_front() {
420        let j_idx = j as usize;
421        in_queue[j_idx] = false;
422        num_queued[j_idx] = num_queued[j_idx]
423            .checked_add(1)
424            .ok_or(IgraphError::Internal("num_queued overflow"))?;
425        if num_queued[j_idx] > n {
426            return Err(IgraphError::InvalidArgument(
427                "negative cycle reachable from source while running Bellman-Ford".to_string(),
428            ));
429        }
430
431        if !dist[j_idx].is_finite() {
432            continue;
433        }
434
435        let incidents = incident_for_mode(graph, j, mode)?;
436        for eid in incidents {
437            let w = weights[eid as usize];
438            if w == f64::INFINITY {
439                continue;
440            }
441            let neighbor = graph.edge_other(eid, j)?;
442            let neighbor_idx = neighbor as usize;
443            let altdist = dist[j_idx] + w;
444            if altdist < dist[neighbor_idx] {
445                dist[neighbor_idx] = altdist;
446                parent_edge[neighbor_idx] = Some(eid);
447                if !in_queue[neighbor_idx] {
448                    in_queue[neighbor_idx] = true;
449                    queue.push_back(neighbor);
450                }
451            }
452        }
453    }
454
455    // Reconstruct paths for each target.
456    let mut results: Vec<Option<(Vec<VertexId>, Vec<EdgeId>)>> = Vec::with_capacity(targets.len());
457    for &target in targets {
458        if target == source {
459            results.push(Some((vec![source], vec![])));
460            continue;
461        }
462        if !dist[target as usize].is_finite() {
463            results.push(None);
464            continue;
465        }
466        let mut edges_rev: Vec<EdgeId> = Vec::new();
467        let mut vertices_rev: Vec<VertexId> = Vec::new();
468        let mut cur = target;
469        vertices_rev.push(cur);
470        while cur != source {
471            let eid = parent_edge[cur as usize].ok_or(IgraphError::Internal(
472                "bellman_ford_paths: missing parent edge in path reconstruction",
473            ))?;
474            edges_rev.push(eid);
475            cur = graph.edge_other(eid, cur)?;
476            vertices_rev.push(cur);
477        }
478        vertices_rev.reverse();
479        edges_rev.reverse();
480        results.push(Some((vertices_rev, edges_rev)));
481    }
482
483    Ok(results)
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489
490    #[test]
491    fn positive_weights_match_dijkstra_triangle() {
492        // Triangle 0-1-2 with positive weights — Bellman-Ford and
493        // Dijkstra must agree.
494        let mut g = Graph::with_vertices(3);
495        g.add_edge(0, 1).unwrap();
496        g.add_edge(0, 2).unwrap();
497        g.add_edge(1, 2).unwrap();
498        let weights = [1.0, 4.0, 2.0];
499        let bf = bellman_ford_distances(&g, 0, &weights).unwrap();
500        assert_eq!(bf, vec![Some(0.0), Some(1.0), Some(3.0)]);
501    }
502
503    #[test]
504    fn negative_edge_directed_no_cycle() {
505        // Directed 0 → 1 → 2 with weights 3, -1.
506        let mut g = Graph::new(3, true).unwrap();
507        g.add_edge(0, 1).unwrap();
508        g.add_edge(1, 2).unwrap();
509        let d = bellman_ford_distances(&g, 0, &[3.0, -1.0]).unwrap();
510        assert_eq!(d, vec![Some(0.0), Some(3.0), Some(2.0)]);
511    }
512
513    #[test]
514    fn unreachable_vertex_is_none() {
515        // Two components, source in the first.
516        let mut g = Graph::with_vertices(4);
517        g.add_edge(0, 1).unwrap();
518        g.add_edge(2, 3).unwrap();
519        let d = bellman_ford_distances(&g, 0, &[1.0, 1.0]).unwrap();
520        assert_eq!(d, vec![Some(0.0), Some(1.0), None, None]);
521    }
522
523    #[test]
524    fn negative_cycle_directed_errors() {
525        // 0 → 1 → 2 → 0 with weights summing to -1 (negative cycle).
526        let mut g = Graph::new(3, true).unwrap();
527        g.add_edge(0, 1).unwrap();
528        g.add_edge(1, 2).unwrap();
529        g.add_edge(2, 0).unwrap();
530        let err = bellman_ford_distances(&g, 0, &[1.0, 1.0, -3.0]).unwrap_err();
531        assert!(matches!(err, IgraphError::InvalidArgument(_)));
532    }
533
534    #[test]
535    fn negative_cycle_unreachable_does_not_error() {
536        // Negative cycle 2 → 3 → 2 unreachable from source 0.
537        // Although SPFA initially queues every vertex, vertex 2's
538        // distance stays at infinity, so relaxation is skipped and
539        // the cycle never gets explored. Distances for the
540        // unreachable component come back as None.
541        let mut g = Graph::new(4, true).unwrap();
542        g.add_edge(0, 1).unwrap();
543        g.add_edge(2, 3).unwrap();
544        g.add_edge(3, 2).unwrap();
545        let d = bellman_ford_distances(&g, 0, &[1.0, -1.0, -1.0]).unwrap();
546        assert_eq!(d, vec![Some(0.0), Some(1.0), None, None]);
547    }
548
549    #[test]
550    fn zero_weights_ok() {
551        let mut g = Graph::with_vertices(3);
552        g.add_edge(0, 1).unwrap();
553        g.add_edge(1, 2).unwrap();
554        let d = bellman_ford_distances(&g, 0, &[0.0, 0.0]).unwrap();
555        assert_eq!(d, vec![Some(0.0), Some(0.0), Some(0.0)]);
556    }
557
558    #[test]
559    fn source_out_of_range_errors() {
560        let g = Graph::with_vertices(3);
561        let err = bellman_ford_distances(&g, 99, &[]).unwrap_err();
562        assert!(matches!(
563            err,
564            IgraphError::VertexOutOfRange { id: 99, n: 3 }
565        ));
566    }
567
568    #[test]
569    fn weights_size_mismatch_errors() {
570        let mut g = Graph::with_vertices(2);
571        g.add_edge(0, 1).unwrap();
572        let err = bellman_ford_distances(&g, 0, &[1.0, 2.0]).unwrap_err();
573        assert!(matches!(err, IgraphError::InvalidArgument(_)));
574    }
575
576    #[test]
577    fn nan_weight_errors() {
578        let mut g = Graph::with_vertices(2);
579        g.add_edge(0, 1).unwrap();
580        let err = bellman_ford_distances(&g, 0, &[f64::NAN]).unwrap_err();
581        assert!(matches!(err, IgraphError::InvalidArgument(_)));
582    }
583
584    #[test]
585    fn in_mode_walks_reverse_edges() {
586        // Directed 0 → 1 → 2: from 2 with IN mode, paths reach 1 then 0.
587        let mut g = Graph::new(3, true).unwrap();
588        g.add_edge(0, 1).unwrap();
589        g.add_edge(1, 2).unwrap();
590        let d = bellman_ford_distances_with_mode(&g, 2, &[3.0, -1.0], DijkstraMode::In).unwrap();
591        // dist(2→2)=0, dist(2→1)=-1, dist(2→0)=-1+3=2
592        assert_eq!(d, vec![Some(2.0), Some(-1.0), Some(0.0)]);
593    }
594
595    #[test]
596    fn all_mode_ignores_direction() {
597        // Directed 0 → 1, asking ALL from 1 reaches 0 via the reverse.
598        let mut g = Graph::new(2, true).unwrap();
599        g.add_edge(0, 1).unwrap();
600        let d = bellman_ford_distances_with_mode(&g, 1, &[5.0], DijkstraMode::All).unwrap();
601        assert_eq!(d, vec![Some(5.0), Some(0.0)]);
602    }
603
604    #[test]
605    fn infinity_weight_ignored() {
606        // Edge with positive-infinite weight should be skipped (upstream behaviour).
607        let mut g = Graph::with_vertices(3);
608        g.add_edge(0, 1).unwrap();
609        g.add_edge(0, 2).unwrap();
610        let d = bellman_ford_distances(&g, 0, &[1.0, f64::INFINITY]).unwrap();
611        assert_eq!(d, vec![Some(0.0), Some(1.0), None]);
612    }
613
614    // --- bellman_ford_path_to tests ---
615
616    #[test]
617    fn path_to_simple_directed() {
618        let mut g = Graph::new(4, true).unwrap();
619        g.add_edge(0, 1).unwrap(); // 0
620        g.add_edge(1, 2).unwrap(); // 1
621        g.add_edge(0, 2).unwrap(); // 2, w=5
622        g.add_edge(2, 3).unwrap(); // 3
623        let w = [3.0, -1.0, 5.0, 1.0];
624        let (vs, es) = bellman_ford_path_to(&g, 0, 3, &w).unwrap().unwrap();
625        assert_eq!(vs, vec![0, 1, 2, 3]);
626        assert_eq!(es, vec![0, 1, 3]);
627    }
628
629    #[test]
630    fn path_to_source_equals_target() {
631        let mut g = Graph::with_vertices(3);
632        g.add_edge(0, 1).unwrap();
633        let (vs, es) = bellman_ford_path_to(&g, 0, 0, &[1.0]).unwrap().unwrap();
634        assert_eq!(vs, vec![0]);
635        assert!(es.is_empty());
636    }
637
638    #[test]
639    fn path_to_unreachable() {
640        let mut g = Graph::new(3, true).unwrap();
641        g.add_edge(0, 1).unwrap();
642        let result = bellman_ford_path_to(&g, 0, 2, &[1.0]).unwrap();
643        assert!(result.is_none());
644    }
645
646    #[test]
647    fn path_to_negative_cycle_errors() {
648        let mut g = Graph::new(3, true).unwrap();
649        g.add_edge(0, 1).unwrap();
650        g.add_edge(1, 2).unwrap();
651        g.add_edge(2, 0).unwrap();
652        let err = bellman_ford_path_to(&g, 0, 2, &[1.0, 1.0, -3.0]).unwrap_err();
653        assert!(matches!(err, IgraphError::InvalidArgument(_)));
654    }
655
656    #[test]
657    fn path_to_prefers_negative_shortcut() {
658        // 0→1→2 (w=1,1) and 0→2 (w=5); via negative: 0→1→2 is 2 < 5
659        let mut g = Graph::new(3, true).unwrap();
660        g.add_edge(0, 1).unwrap(); // 0, w=1
661        g.add_edge(1, 2).unwrap(); // 1, w=-1
662        g.add_edge(0, 2).unwrap(); // 2, w=5
663        let (vs, es) = bellman_ford_path_to(&g, 0, 2, &[1.0, -1.0, 5.0])
664            .unwrap()
665            .unwrap();
666        assert_eq!(vs, vec![0, 1, 2]);
667        assert_eq!(es, vec![0, 1]);
668    }
669
670    #[test]
671    fn path_to_with_in_mode() {
672        // Directed 0→1→2; from 2 in IN mode finds path 2←1←0
673        let mut g = Graph::new(3, true).unwrap();
674        g.add_edge(0, 1).unwrap(); // 0
675        g.add_edge(1, 2).unwrap(); // 1
676        let (vs, es) = bellman_ford_path_to_with_mode(&g, 2, 0, &[3.0, -1.0], DijkstraMode::In)
677            .unwrap()
678            .unwrap();
679        assert_eq!(vs, vec![2, 1, 0]);
680        assert_eq!(es, vec![1, 0]);
681    }
682
683    #[test]
684    fn path_to_undirected_negative_cycle() {
685        // Undirected graph with a negative edge creates a negative cycle
686        // (traverse edge back-and-forth indefinitely), so BF reports it.
687        let mut g = Graph::with_vertices(3);
688        g.add_edge(0, 1).unwrap(); // 0, w=2
689        g.add_edge(1, 2).unwrap(); // 1, w=-1
690        g.add_edge(0, 2).unwrap(); // 2, w=5
691        let err = bellman_ford_path_to(&g, 0, 2, &[2.0, -1.0, 5.0]).unwrap_err();
692        assert!(matches!(err, IgraphError::InvalidArgument(_)));
693    }
694
695    #[test]
696    fn path_to_multiple_hops() {
697        // 0→1→2→3 all weight 1, direct 0→3 weight 10
698        let mut g = Graph::new(4, true).unwrap();
699        g.add_edge(0, 1).unwrap(); // 0
700        g.add_edge(1, 2).unwrap(); // 1
701        g.add_edge(2, 3).unwrap(); // 2
702        g.add_edge(0, 3).unwrap(); // 3, w=10
703        let (vs, es) = bellman_ford_path_to(&g, 0, 3, &[1.0, 1.0, 1.0, 10.0])
704            .unwrap()
705            .unwrap();
706        assert_eq!(vs, vec![0, 1, 2, 3]);
707        assert_eq!(es, vec![0, 1, 2]);
708    }
709
710    // --- bellman_ford_paths (multi-target) tests ---
711
712    #[test]
713    fn paths_multi_target_directed() {
714        let mut g = Graph::new(4, true).unwrap();
715        g.add_edge(0, 1).unwrap(); // 0, w=3
716        g.add_edge(1, 2).unwrap(); // 1, w=-1
717        g.add_edge(0, 2).unwrap(); // 2, w=5
718        g.add_edge(2, 3).unwrap(); // 3, w=1
719        let w = [3.0, -1.0, 5.0, 1.0];
720        let results = bellman_ford_paths(&g, 0, &[1, 2, 3], &w).unwrap();
721        assert_eq!(results.len(), 3);
722        // path to 1: 0→1
723        let (vs, es) = results[0].as_ref().unwrap();
724        assert_eq!(*vs, vec![0, 1]);
725        assert_eq!(*es, vec![0]);
726        // path to 2: 0→1→2 (cost 2) < 0→2 (cost 5)
727        let (vs, es) = results[1].as_ref().unwrap();
728        assert_eq!(*vs, vec![0, 1, 2]);
729        assert_eq!(*es, vec![0, 1]);
730        // path to 3: 0→1→2→3 (cost 3)
731        let (vs, es) = results[2].as_ref().unwrap();
732        assert_eq!(*vs, vec![0, 1, 2, 3]);
733        assert_eq!(*es, vec![0, 1, 3]);
734    }
735
736    #[test]
737    fn paths_source_in_targets() {
738        let mut g = Graph::new(3, true).unwrap();
739        g.add_edge(0, 1).unwrap();
740        g.add_edge(1, 2).unwrap();
741        let results = bellman_ford_paths(&g, 0, &[0, 2], &[1.0, 1.0]).unwrap();
742        // source to self
743        let (vs, es) = results[0].as_ref().unwrap();
744        assert_eq!(*vs, vec![0]);
745        assert!(es.is_empty());
746        // source to 2
747        let (vs, es) = results[1].as_ref().unwrap();
748        assert_eq!(*vs, vec![0, 1, 2]);
749        assert_eq!(*es, vec![0, 1]);
750    }
751
752    #[test]
753    fn paths_unreachable_target() {
754        let mut g = Graph::new(4, true).unwrap();
755        g.add_edge(0, 1).unwrap();
756        // vertex 2 and 3 unreachable from 0
757        let results = bellman_ford_paths(&g, 0, &[1, 2, 3], &[1.0]).unwrap();
758        assert!(results[0].is_some());
759        assert!(results[1].is_none());
760        assert!(results[2].is_none());
761    }
762
763    #[test]
764    fn paths_empty_targets() {
765        let g = Graph::with_vertices(3);
766        let results = bellman_ford_paths(&g, 0, &[], &[]).unwrap();
767        assert!(results.is_empty());
768    }
769
770    #[test]
771    fn paths_negative_cycle_errors() {
772        let mut g = Graph::new(3, true).unwrap();
773        g.add_edge(0, 1).unwrap();
774        g.add_edge(1, 2).unwrap();
775        g.add_edge(2, 0).unwrap();
776        let err = bellman_ford_paths(&g, 0, &[2], &[1.0, 1.0, -3.0]).unwrap_err();
777        assert!(matches!(err, IgraphError::InvalidArgument(_)));
778    }
779
780    #[test]
781    fn paths_duplicate_target() {
782        let mut g = Graph::new(3, true).unwrap();
783        g.add_edge(0, 1).unwrap();
784        g.add_edge(1, 2).unwrap();
785        let results = bellman_ford_paths(&g, 0, &[2, 2], &[1.0, 1.0]).unwrap();
786        assert_eq!(results.len(), 2);
787        assert_eq!(results[0], results[1]);
788    }
789
790    #[test]
791    fn paths_with_in_mode() {
792        let mut g = Graph::new(3, true).unwrap();
793        g.add_edge(0, 1).unwrap(); // 0
794        g.add_edge(1, 2).unwrap(); // 1
795        let results =
796            bellman_ford_paths_with_mode(&g, 2, &[0, 1], &[3.0, -1.0], DijkstraMode::In).unwrap();
797        // 2←1: edge 1
798        let (vs, es) = results[1].as_ref().unwrap();
799        assert_eq!(*vs, vec![2, 1]);
800        assert_eq!(*es, vec![1]);
801        // 2←1←0: edges 1, 0
802        let (vs, es) = results[0].as_ref().unwrap();
803        assert_eq!(*vs, vec![2, 1, 0]);
804        assert_eq!(*es, vec![1, 0]);
805    }
806
807    #[test]
808    fn paths_agrees_with_path_to() {
809        let mut g = Graph::new(5, true).unwrap();
810        g.add_edge(0, 1).unwrap(); // 0
811        g.add_edge(1, 2).unwrap(); // 1
812        g.add_edge(0, 3).unwrap(); // 2
813        g.add_edge(3, 4).unwrap(); // 3
814        g.add_edge(2, 4).unwrap(); // 4
815        let w = [2.0, -1.0, 3.0, 1.0, 1.0];
816        let multi = bellman_ford_paths(&g, 0, &[1, 2, 3, 4], &w).unwrap();
817        for (i, &target) in [1u32, 2, 3, 4].iter().enumerate() {
818            let single = bellman_ford_path_to(&g, 0, target, &w).unwrap();
819            assert_eq!(multi[i], single, "mismatch for target {target}");
820        }
821    }
822
823    #[test]
824    fn paths_target_out_of_range() {
825        let g = Graph::with_vertices(3);
826        let err = bellman_ford_paths(&g, 0, &[99], &[]).unwrap_err();
827        assert!(matches!(
828            err,
829            IgraphError::VertexOutOfRange { id: 99, n: 3 }
830        ));
831    }
832}