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