Skip to main content

rust_igraph/algorithms/paths/
widest_path.rs

1//! Widest-path widths (ALGO-SP-010).
2//!
3//! Counterpart of `igraph_widest_path_widths_dijkstra()` from
4//! `references/igraph/src/paths/widest_paths.c:596`. Given an
5//! edge-weighted graph, returns for each vertex `v` the maximum
6//! **bottleneck width** of any path from `source` to `v` — that is,
7//! the largest minimum-edge-weight along any such path.
8//!
9//! Algorithm: a max-priority-queue variant of Dijkstra. Instead of
10//! relaxing `dist[u] = min(dist[u], dist[v] + w)` we relax
11//! `width[u] = max(width[u], min(width[v], w))`. The pop order
12//! processes vertices in decreasing width, so once a vertex is
13//! settled the recorded width is optimal.
14//!
15//! Time complexity: `O((V + E) log V)` — same shape as Dijkstra.
16//!
17//! Convention:
18//! - `widths[source] == Some(f64::INFINITY)` (no edge constraints
19//!   yet; matches upstream's `IGRAPH_INFINITY` sentinel)
20//! - `widths[v] == None` if `v` is unreachable from `source`
21//!   (upstream uses `-IGRAPH_INFINITY`)
22//! - Edges with weight `-f64::INFINITY` are ignored (matches
23//!   upstream)
24
25use std::cmp::Ordering;
26use std::collections::BinaryHeap;
27
28use crate::algorithms::paths::dijkstra::DijkstraMode;
29use crate::core::graph::EdgeId;
30use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
31
32/// Max-heap entry: pop yields the largest `width` first, with smaller
33/// vertex id breaking ties. NaN widths are excluded by validation.
34#[derive(Copy, Clone)]
35struct WidestFrontier(f64, VertexId);
36
37impl PartialEq for WidestFrontier {
38    fn eq(&self, other: &Self) -> bool {
39        self.0 == other.0 && self.1 == other.1
40    }
41}
42impl Eq for WidestFrontier {}
43impl Ord for WidestFrontier {
44    fn cmp(&self, other: &Self) -> Ordering {
45        // Natural order: larger width is "greater" so BinaryHeap pops it
46        // first. Smaller vertex id tiebreaks (deterministic for ties).
47        self.0.total_cmp(&other.0).then(other.1.cmp(&self.1))
48    }
49}
50impl PartialOrd for WidestFrontier {
51    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
52        Some(self.cmp(other))
53    }
54}
55
56fn validate_weights(graph: &Graph, weights: &[f64]) -> IgraphResult<()> {
57    let m = graph.ecount();
58    if weights.len() != m {
59        return Err(IgraphError::InvalidArgument(format!(
60            "weights vector size ({}) differs from edge count ({})",
61            weights.len(),
62            m
63        )));
64    }
65    for (e, &w) in weights.iter().enumerate() {
66        if w.is_nan() {
67            return Err(IgraphError::InvalidArgument(format!(
68                "weight at edge {e} is NaN"
69            )));
70        }
71    }
72    Ok(())
73}
74
75fn incident_for_mode(graph: &Graph, v: VertexId, mode: DijkstraMode) -> IgraphResult<Vec<EdgeId>> {
76    if !graph.is_directed() {
77        return graph.incident(v);
78    }
79    match mode {
80        DijkstraMode::Out => graph.incident(v),
81        DijkstraMode::In => graph.incident_in(v),
82        DijkstraMode::All => {
83            let mut out = graph.incident(v)?;
84            out.extend(graph.incident_in(v)?);
85            Ok(out)
86        }
87    }
88}
89
90/// Single-source widest-path widths on `graph` from `source`,
91/// following out-edges on directed graphs.
92///
93/// Returns `widths[v]`:
94/// - `Some(f64::INFINITY)` for `v == source` (no path constraint yet)
95/// - `Some(w)` if `v` is reachable, with `w` the maximum bottleneck
96///   width of any `source → v` path
97/// - `None` if `v` is unreachable
98///
99/// A path's *bottleneck width* is the minimum edge weight along it;
100/// the *widest path* maximises this bottleneck across all
101/// source→target paths. Useful in network-capacity problems.
102///
103/// `weights[e]` is the width of edge `e`; length must equal
104/// `graph.ecount()`. NaN widths are rejected. Edges with weight
105/// `-f64::INFINITY` are treated as "edge absent" (matches upstream).
106///
107/// Counterpart of `igraph_widest_path_widths_dijkstra(_, _,
108/// vss(source), vss_all(), weights, IGRAPH_OUT)`.
109///
110/// # Examples
111///
112/// ```
113/// use rust_igraph::{Graph, widest_path_widths};
114///
115/// // Triangle 0-1-2 with edge weights 1, 4, 2.
116/// // Widest 0→2 path: direct edge (width 4) beats 0-1-2 (min(1,2) = 1).
117/// let mut g = Graph::with_vertices(3);
118/// g.add_edge(0, 1).unwrap();  // edge 0, width 1
119/// g.add_edge(0, 2).unwrap();  // edge 1, width 4
120/// g.add_edge(1, 2).unwrap();  // edge 2, width 2
121/// let w = widest_path_widths(&g, 0, &[1.0, 4.0, 2.0]).unwrap();
122/// assert_eq!(w[0], Some(f64::INFINITY));
123/// assert_eq!(w[1], Some(2.0));  // via 0-2-1: min(4, 2) = 2
124/// assert_eq!(w[2], Some(4.0));  // direct
125/// ```
126pub fn widest_path_widths(
127    graph: &Graph,
128    source: VertexId,
129    weights: &[f64],
130) -> IgraphResult<Vec<Option<f64>>> {
131    widest_path_widths_with_mode(graph, source, weights, DijkstraMode::Out)
132}
133
134/// Widest-path widths with directed-mode selection. Mirrors
135/// [`widest_path_widths`] but lets you choose OUT/IN/ALL traversal
136/// for directed graphs (ignored on undirected).
137///
138/// Counterpart of `igraph_widest_path_widths_dijkstra(_, _,
139/// vss(source), vss_all(), weights, mode)`.
140pub fn widest_path_widths_with_mode(
141    graph: &Graph,
142    source: VertexId,
143    weights: &[f64],
144    mode: DijkstraMode,
145) -> IgraphResult<Vec<Option<f64>>> {
146    let (widths, _) = widest_inner(graph, source, weights, mode)?;
147    Ok(widths
148        .into_iter()
149        .map(|w| {
150            if w == f64::NEG_INFINITY {
151                None
152            } else {
153                Some(w)
154            }
155        })
156        .collect())
157}
158
159/// Shared SPFA-style loop. Returns the raw widths vector (sentinel
160/// `-∞` = unreachable, `+∞` = source itself) and the per-vertex
161/// inbound edge from the widest-paths tree (`None` for source and
162/// unreachable vertices). The public APIs strip the sentinels and
163/// either drop or use the parent-edge chain.
164fn widest_inner(
165    graph: &Graph,
166    source: VertexId,
167    weights: &[f64],
168    mode: DijkstraMode,
169) -> IgraphResult<(Vec<f64>, Vec<Option<EdgeId>>)> {
170    let n = graph.vcount();
171    if source >= n {
172        return Err(IgraphError::VertexOutOfRange { id: source, n });
173    }
174    validate_weights(graph, weights)?;
175
176    let n_usize = n as usize;
177    let mut widths: Vec<f64> = vec![f64::NEG_INFINITY; n_usize];
178    widths[source as usize] = f64::INFINITY;
179    let mut parent_eid: Vec<Option<EdgeId>> = vec![None; n_usize];
180
181    let mut heap: BinaryHeap<WidestFrontier> = BinaryHeap::new();
182    heap.push(WidestFrontier(f64::INFINITY, source));
183
184    while let Some(WidestFrontier(w, v)) = heap.pop() {
185        if w < widths[v as usize] {
186            continue;
187        }
188
189        let incidents = incident_for_mode(graph, v, mode)?;
190        for eid in incidents {
191            let edge_w = weights[eid as usize];
192            if edge_w == f64::NEG_INFINITY {
193                continue;
194            }
195            let other = graph.edge_other(eid, v)?;
196            let alt = w.min(edge_w);
197            if alt > widths[other as usize] {
198                widths[other as usize] = alt;
199                parent_eid[other as usize] = Some(eid);
200                heap.push(WidestFrontier(alt, other));
201            }
202        }
203    }
204
205    Ok((widths, parent_eid))
206}
207
208/// Widest path from `from` to `to`: returns the vertex sequence and
209/// the edge sequence along the widest (maximum-bottleneck) path.
210///
211/// Returns `Some((vertices, edges))` on success, with
212/// `vertices[0] == from`, `*vertices.last().unwrap() == to`, and
213/// `edges.len() == vertices.len() - 1`. Returns `None` if `to` is
214/// unreachable from `from`. Self-target (`from == to`) returns
215/// `Some((vec![from], vec![]))` — the trivial zero-edge path.
216///
217/// Same semantics as [`widest_path_widths`] for weights: negative
218/// finite weights act as small bottlenecks, `-f64::INFINITY` weights
219/// are ignored, NaN is rejected.
220///
221/// Counterpart of `igraph_get_widest_path(_, _, _, from, to,
222/// weights, IGRAPH_OUT)`.
223///
224/// # Examples
225///
226/// ```
227/// use rust_igraph::{Graph, widest_path};
228///
229/// // Triangle 0-1-2 with weights 1, 4, 2 — widest 0→1 path goes via 2.
230/// let mut g = Graph::with_vertices(3);
231/// g.add_edge(0, 1).unwrap();  // edge 0, width 1
232/// g.add_edge(0, 2).unwrap();  // edge 1, width 4
233/// g.add_edge(1, 2).unwrap();  // edge 2, width 2
234/// let path = widest_path(&g, 0, 1, &[1.0, 4.0, 2.0]).unwrap().unwrap();
235/// assert_eq!(path.0, vec![0, 2, 1]);
236/// assert_eq!(path.1, vec![1, 2]);
237/// ```
238pub fn widest_path(
239    graph: &Graph,
240    from: VertexId,
241    to: VertexId,
242    weights: &[f64],
243) -> IgraphResult<Option<(Vec<VertexId>, Vec<EdgeId>)>> {
244    widest_path_with_mode(graph, from, to, weights, DijkstraMode::Out)
245}
246
247/// Sidecar outputs from a single-source widest-paths run. Carries
248/// the widths vector plus the parent-pointer SPT (vertex-side and
249/// edge-side). Counterpart of the `parents` and `inbound_edges`
250/// outputs of `igraph_get_widest_paths`. Source itself has
251/// `parents[source] == None` and `inbound_edges[source] == None`;
252/// unreachable vertices also have both `None`. To disambiguate the
253/// source from unreachable targets, consult `widths`: source has
254/// `Some(f64::INFINITY)`, unreachable has `None`.
255#[derive(Debug, Clone, PartialEq)]
256pub struct WidestPaths {
257    /// `widths[v]`: `Some(f64::INFINITY)` for `v == source`,
258    /// `Some(w)` for reachable, `None` for unreachable.
259    pub widths: Vec<Option<f64>>,
260    /// `parents[v]` is the predecessor of `v` in the widest-paths
261    /// spanning tree. `None` for source and unreachable.
262    pub parents: Vec<Option<VertexId>>,
263    /// `inbound_edges[v]` is the edge id `v` was reached through.
264    /// `None` for source and unreachable.
265    pub inbound_edges: Vec<Option<EdgeId>>,
266}
267
268/// Single-source widest-paths sidecar: widths plus the parent-pointer
269/// SPT. Convenient when you want **all** of widths, parent vertices,
270/// and inbound edge ids in one call without re-running the SPT.
271///
272/// Behaves like [`widest_path_widths`] for weight semantics: NaN
273/// rejected, `-f64::INFINITY` edges ignored, negative finite weights
274/// act as small bottlenecks.
275///
276/// Counterpart of `igraph_get_widest_paths(_, NULL, NULL, source,
277/// vss_all(), weights, IGRAPH_OUT, parents, inbound_edges)` from
278/// `references/igraph/src/paths/widest_paths.c:102`.
279///
280/// # Examples
281///
282/// ```
283/// use rust_igraph::{Graph, widest_paths};
284///
285/// // Triangle 0-1-2 weights (1, 4, 2). Widest 0→1 routes via vertex 2.
286/// let mut g = Graph::with_vertices(3);
287/// g.add_edge(0, 1).unwrap();  // edge 0, width 1
288/// g.add_edge(0, 2).unwrap();  // edge 1, width 4
289/// g.add_edge(1, 2).unwrap();  // edge 2, width 2
290/// let sp = widest_paths(&g, 0, &[1.0, 4.0, 2.0]).unwrap();
291/// // Source itself
292/// assert_eq!(sp.widths[0], Some(f64::INFINITY));
293/// assert_eq!(sp.parents[0], None);
294/// assert_eq!(sp.inbound_edges[0], None);
295/// // Vertex 2 reached directly via edge 1 (widest direct edge)
296/// assert_eq!(sp.parents[2], Some(0));
297/// assert_eq!(sp.inbound_edges[2], Some(1));
298/// // Vertex 1 reached via 2 (bottleneck min(4, 2) = 2 beats direct edge 0 with width 1)
299/// assert_eq!(sp.parents[1], Some(2));
300/// assert_eq!(sp.inbound_edges[1], Some(2));
301/// ```
302pub fn widest_paths(graph: &Graph, from: VertexId, weights: &[f64]) -> IgraphResult<WidestPaths> {
303    widest_paths_with_mode(graph, from, weights, DijkstraMode::Out)
304}
305
306/// Mode-aware variant of [`widest_paths`].
307///
308/// Counterpart of `igraph_get_widest_paths(_, NULL, NULL, source,
309/// vss_all(), weights, mode, parents, inbound_edges)`.
310pub fn widest_paths_with_mode(
311    graph: &Graph,
312    from: VertexId,
313    weights: &[f64],
314    mode: DijkstraMode,
315) -> IgraphResult<WidestPaths> {
316    let (raw_widths, parent_eid) = widest_inner(graph, from, weights, mode)?;
317    let n = raw_widths.len();
318    let mut parents: Vec<Option<VertexId>> = vec![None; n];
319    let widths: Vec<Option<f64>> = raw_widths
320        .iter()
321        .map(|&w| {
322            if w == f64::NEG_INFINITY {
323                None
324            } else {
325                Some(w)
326            }
327        })
328        .collect();
329    // Derive parent vertices from parent edges: `edge_other(eid, v)`
330    // gives the predecessor of v in the SPT. Source's parent stays
331    // None (its parent_eid entry is None by construction).
332    for v in 0..n {
333        if let Some(eid) = parent_eid[v] {
334            let v_u32 = u32::try_from(v)
335                .map_err(|_| IgraphError::Internal("vertex index exceeds u32::MAX"))?;
336            let prev = graph.edge_other(eid, v_u32)?;
337            parents[v] = Some(prev);
338        }
339    }
340    Ok(WidestPaths {
341        widths,
342        parents,
343        inbound_edges: parent_eid,
344    })
345}
346
347/// Widest path with mode selection. Mirrors [`widest_path`] but lets
348/// you pick OUT/IN/ALL traversal on directed graphs.
349///
350/// Counterpart of `igraph_get_widest_path(_, _, _, from, to, weights, mode)`.
351pub fn widest_path_with_mode(
352    graph: &Graph,
353    from: VertexId,
354    to: VertexId,
355    weights: &[f64],
356    mode: DijkstraMode,
357) -> IgraphResult<Option<(Vec<VertexId>, Vec<EdgeId>)>> {
358    let n = graph.vcount();
359    if to >= n {
360        return Err(IgraphError::VertexOutOfRange { id: to, n });
361    }
362    // `from` is validated inside `widest_inner`.
363    let (widths, parent_eid) = widest_inner(graph, from, weights, mode)?;
364    reconstruct_one(graph, from, to, &widths, &parent_eid)
365}
366
367/// Walk back along `parent_eid` from `to` to `from`, building the
368/// vertex+edge chains. Returns `None` if `to` is unreachable; for
369/// `from == to` returns the trivial single-vertex zero-edge chain.
370fn reconstruct_one(
371    graph: &Graph,
372    from: VertexId,
373    to: VertexId,
374    widths: &[f64],
375    parent_eid: &[Option<EdgeId>],
376) -> IgraphResult<Option<(Vec<VertexId>, Vec<EdgeId>)>> {
377    if from == to {
378        return Ok(Some((vec![from], Vec::new())));
379    }
380    if widths[to as usize] == f64::NEG_INFINITY {
381        return Ok(None);
382    }
383    let mut edges: Vec<EdgeId> = Vec::new();
384    let mut vertices: Vec<VertexId> = vec![to];
385    let mut cur = to;
386    while cur != from {
387        let eid = parent_eid[cur as usize].ok_or(IgraphError::Internal(
388            "missing parent edge while walking widest path",
389        ))?;
390        let prev = graph.edge_other(eid, cur)?;
391        edges.push(eid);
392        vertices.push(prev);
393        cur = prev;
394    }
395    vertices.reverse();
396    edges.reverse();
397    Ok(Some((vertices, edges)))
398}
399
400/// One entry of [`widest_paths_to`]'s output: `Some((vertices,
401/// edges))` for a reachable target, `None` for unreachable. Each
402/// vertex chain begins with the source and ends with the target;
403/// each edge chain has length one less.
404pub type WidestPathResult = Option<(Vec<VertexId>, Vec<EdgeId>)>;
405
406/// Widest paths from a single source to multiple targets. Returns
407/// one [`WidestPathResult`] per element of `targets`, in the same
408/// order; `None` means the target is unreachable from `from`.
409///
410/// Self-target entries (`from == targets[i]`) return the trivial
411/// `Some((vec![from], vec![]))`. Repeating the same target id in
412/// `targets` is allowed — both entries get the same path.
413///
414/// Same weight semantics as [`widest_path_widths`]: NaN rejected,
415/// `-f64::INFINITY` edges ignored, negative finite weights act as
416/// small bottlenecks.
417///
418/// Counterpart of `igraph_get_widest_paths(_, vertices, edges,
419/// from, to, weights, IGRAPH_OUT, parents=NULL, inbound_edges=NULL)`
420/// from `references/igraph/src/paths/widest_paths.c:102`.
421///
422/// # Examples
423///
424/// ```
425/// use rust_igraph::{Graph, widest_paths_to};
426///
427/// // Triangle 0-1-2 weights (1, 4, 2). From 0, paths to 1 and 2.
428/// let mut g = Graph::with_vertices(3);
429/// g.add_edge(0, 1).unwrap();  // edge 0, width 1
430/// g.add_edge(0, 2).unwrap();  // edge 1, width 4
431/// g.add_edge(1, 2).unwrap();  // edge 2, width 2
432/// let paths = widest_paths_to(&g, 0, &[1, 2], &[1.0, 4.0, 2.0]).unwrap();
433/// // 0→1 goes via the shortcut at 2 (bottleneck 2 beats direct 1)
434/// assert_eq!(paths[0].as_ref().unwrap().0, vec![0, 2, 1]);
435/// // 0→2 takes the direct edge (width 4 is widest)
436/// assert_eq!(paths[1].as_ref().unwrap().0, vec![0, 2]);
437/// ```
438pub fn widest_paths_to(
439    graph: &Graph,
440    from: VertexId,
441    targets: &[VertexId],
442    weights: &[f64],
443) -> IgraphResult<Vec<WidestPathResult>> {
444    widest_paths_to_with_mode(graph, from, targets, weights, DijkstraMode::Out)
445}
446
447/// Mode-aware variant of [`widest_paths_to`].
448///
449/// Counterpart of `igraph_get_widest_paths(_, vertices, edges,
450/// from, to, weights, mode, parents=NULL, inbound_edges=NULL)`.
451pub fn widest_paths_to_with_mode(
452    graph: &Graph,
453    from: VertexId,
454    targets: &[VertexId],
455    weights: &[f64],
456    mode: DijkstraMode,
457) -> IgraphResult<Vec<WidestPathResult>> {
458    let n = graph.vcount();
459    for &t in targets {
460        if t >= n {
461            return Err(IgraphError::VertexOutOfRange { id: t, n });
462        }
463    }
464    // `from` is validated inside `widest_inner`.
465    let (widths, parent_eid) = widest_inner(graph, from, weights, mode)?;
466    let mut out = Vec::with_capacity(targets.len());
467    for &t in targets {
468        out.push(reconstruct_one(graph, from, t, &widths, &parent_eid)?);
469    }
470    Ok(out)
471}
472
473// ---------------------------------------------------------------
474// ALGO-SP-012: Floyd-Warshall-based all-pairs widest widths.
475// O(V³) — better than V invocations of the Dijkstra-style variant
476// on dense graphs.
477// ---------------------------------------------------------------
478
479/// All-pairs widest-path widths via the Floyd-Warshall recurrence.
480///
481/// Returns a `vcount × vcount` matrix where `result[u][v]` is the
482/// maximum bottleneck width of any `u → v` path:
483/// - `Some(f64::INFINITY)` on the diagonal (`u == v`)
484/// - `Some(w)` for reachable pairs with `w` the bottleneck width
485/// - `None` for unreachable pairs
486///
487/// `weights[e]` is the width of edge `e`; length must equal
488/// `graph.ecount()`. NaN is rejected; edges with weight
489/// `-f64::INFINITY` are ignored (matches upstream). Parallel edges
490/// are merged by the wider-wins rule when seeding the matrix.
491///
492/// Use this on **dense** graphs (`|E| ~ V²`); for sparse graphs the
493/// Dijkstra-based [`widest_path_widths`] called from every source is
494/// asymptotically faster.
495///
496/// Counterpart of `igraph_widest_path_widths_floyd_warshall(_, _,
497/// vss_all(), vss_all(), weights, IGRAPH_OUT)`.
498///
499/// # Examples
500///
501/// ```
502/// use rust_igraph::{Graph, widest_path_widths_floyd_warshall};
503///
504/// // Undirected triangle (1, 4, 2) — same all-pairs result the
505/// // Dijkstra variant produces when run from every source.
506/// let mut g = Graph::with_vertices(3);
507/// g.add_edge(0, 1).unwrap();
508/// g.add_edge(0, 2).unwrap();
509/// g.add_edge(1, 2).unwrap();
510/// let m = widest_path_widths_floyd_warshall(&g, &[1.0, 4.0, 2.0]).unwrap();
511/// assert_eq!(m[0][2], Some(4.0));   // direct
512/// assert_eq!(m[0][1], Some(2.0));   // via vertex 2: min(4, 2)
513/// assert_eq!(m[0][0], Some(f64::INFINITY));
514/// ```
515pub fn widest_path_widths_floyd_warshall(
516    graph: &Graph,
517    weights: &[f64],
518) -> IgraphResult<Vec<Vec<Option<f64>>>> {
519    widest_path_widths_floyd_warshall_with_mode(graph, weights, DijkstraMode::Out)
520}
521
522/// Mode-aware variant of [`widest_path_widths_floyd_warshall`].
523/// Mode selects how directed edges contribute to the adjacency
524/// matrix:
525/// - [`DijkstraMode::Out`] populates `M[s][t]` for edge `s → t`
526/// - [`DijkstraMode::In`] populates `M[t][s]` for edge `s → t`
527/// - [`DijkstraMode::All`] populates both directions
528///
529/// On undirected graphs every mode collapses to `All` (matches
530/// upstream).
531///
532/// Counterpart of `igraph_widest_path_widths_floyd_warshall(_, _,
533/// vss_all(), vss_all(), weights, mode)`.
534pub fn widest_path_widths_floyd_warshall_with_mode(
535    graph: &Graph,
536    weights: &[f64],
537    mode: DijkstraMode,
538) -> IgraphResult<Vec<Vec<Option<f64>>>> {
539    validate_weights(graph, weights)?;
540    let vcount = graph.vcount();
541    let n_us = vcount as usize;
542
543    // Normalise mode: undirected graph → ALL (matches upstream).
544    let effective_mode = if graph.is_directed() {
545        mode
546    } else {
547        DijkstraMode::All
548    };
549    let (use_out, use_in) = match effective_mode {
550        DijkstraMode::Out => (true, false),
551        DijkstraMode::In => (false, true),
552        DijkstraMode::All => (true, true),
553    };
554
555    // Init: -∞ everywhere; +∞ on the diagonal.
556    let mut mat: Vec<Vec<f64>> = vec![vec![f64::NEG_INFINITY; n_us]; n_us];
557    for (i, row) in mat.iter_mut().enumerate().take(n_us) {
558        row[i] = f64::INFINITY;
559    }
560
561    // Seed from edges (wider-wins for parallel edges).
562    for (e, &w) in weights.iter().enumerate() {
563        if w == f64::NEG_INFINITY {
564            continue;
565        }
566        let eid = u32::try_from(e)
567            .map_err(|_| IgraphError::Internal("edge id exceeds u32::MAX in FW widest"))?;
568        let (s, t) = graph.edge(eid)?;
569        let (si, ti) = (s as usize, t as usize);
570        if use_out && mat[si][ti] < w {
571            mat[si][ti] = w;
572        }
573        if use_in && mat[ti][si] < w {
574            mat[ti][si] = w;
575        }
576    }
577
578    // Modified FW: relax via every intermediate k.
579    // alt = min(M[i][k], M[k][j]); M[i][j] = max(M[i][j], alt).
580    // The triple-nested index access is inherent to the recurrence —
581    // iterator-style rewrites obscure it.
582    #[allow(clippy::needless_range_loop)]
583    for k in 0..n_us {
584        for j in 0..n_us {
585            let width_kj = mat[k][j];
586            if j == k || width_kj == f64::NEG_INFINITY {
587                continue;
588            }
589            for i in 0..n_us {
590                if i == j || i == k {
591                    continue;
592                }
593                let alt = mat[i][k].min(width_kj);
594                if alt > mat[i][j] {
595                    mat[i][j] = alt;
596                }
597            }
598        }
599    }
600
601    Ok(mat
602        .into_iter()
603        .map(|row| {
604            row.into_iter()
605                .map(|w| {
606                    if w == f64::NEG_INFINITY {
607                        None
608                    } else {
609                        Some(w)
610                    }
611                })
612                .collect()
613        })
614        .collect())
615}
616
617#[cfg(test)]
618mod tests {
619    use super::*;
620
621    #[test]
622    fn triangle_picks_direct_edge_when_wider() {
623        let mut g = Graph::with_vertices(3);
624        g.add_edge(0, 1).unwrap(); // width 1
625        g.add_edge(0, 2).unwrap(); // width 4
626        g.add_edge(1, 2).unwrap(); // width 2
627        let w = widest_path_widths(&g, 0, &[1.0, 4.0, 2.0]).unwrap();
628        // Source: ∞
629        assert_eq!(w[0], Some(f64::INFINITY));
630        // 0 → 1 direct = 1, vs 0-2-1 = min(4, 2) = 2. Widest = 2.
631        assert_eq!(w[1], Some(2.0));
632        // 0 → 2 direct = 4, vs 0-1-2 = min(1, 2) = 1. Widest = 4.
633        assert_eq!(w[2], Some(4.0));
634    }
635
636    #[test]
637    fn chain_bottleneck_is_minimum_weight() {
638        // 0-1-2-3 with weights 5, 1, 3.
639        let mut g = Graph::with_vertices(4);
640        g.add_edge(0, 1).unwrap();
641        g.add_edge(1, 2).unwrap();
642        g.add_edge(2, 3).unwrap();
643        let w = widest_path_widths(&g, 0, &[5.0, 1.0, 3.0]).unwrap();
644        assert_eq!(w[0], Some(f64::INFINITY));
645        assert_eq!(w[1], Some(5.0));
646        // 0→2 bottleneck = min(5, 1) = 1
647        assert_eq!(w[2], Some(1.0));
648        // 0→3 bottleneck = min(5, 1, 3) = 1
649        assert_eq!(w[3], Some(1.0));
650    }
651
652    #[test]
653    fn unreachable_vertex_is_none() {
654        let mut g = Graph::with_vertices(4);
655        g.add_edge(0, 1).unwrap();
656        g.add_edge(2, 3).unwrap();
657        let w = widest_path_widths(&g, 0, &[2.0, 3.0]).unwrap();
658        assert_eq!(w[0], Some(f64::INFINITY));
659        assert_eq!(w[1], Some(2.0));
660        assert_eq!(w[2], None);
661        assert_eq!(w[3], None);
662    }
663
664    #[test]
665    fn negative_infinity_edge_ignored() {
666        // 0-1 has -∞ width → effectively absent. 0-2 via 0-1-2 not
667        // possible; only direct 0-2 if it exists.
668        let mut g = Graph::with_vertices(3);
669        g.add_edge(0, 1).unwrap(); // -∞ width
670        g.add_edge(1, 2).unwrap();
671        let w = widest_path_widths(&g, 0, &[f64::NEG_INFINITY, 1.0]).unwrap();
672        assert_eq!(w[0], Some(f64::INFINITY));
673        assert_eq!(w[1], None); // edge 0-1 ignored
674        assert_eq!(w[2], None);
675    }
676
677    #[test]
678    fn directed_out_mode_default() {
679        // 0 → 1 → 2 with weights 5, 3. From 0: w[1]=5, w[2]=3.
680        let mut g = Graph::new(3, true).unwrap();
681        g.add_edge(0, 1).unwrap();
682        g.add_edge(1, 2).unwrap();
683        let w = widest_path_widths(&g, 0, &[5.0, 3.0]).unwrap();
684        assert_eq!(w[0], Some(f64::INFINITY));
685        assert_eq!(w[1], Some(5.0));
686        assert_eq!(w[2], Some(3.0));
687    }
688
689    #[test]
690    fn directed_in_mode_reverses() {
691        // 0 → 1 → 2 from 2 with IN mode: 2 reaches 1 (width 3), then 0 (min(3, 5) = 3).
692        let mut g = Graph::new(3, true).unwrap();
693        g.add_edge(0, 1).unwrap();
694        g.add_edge(1, 2).unwrap();
695        let w = widest_path_widths_with_mode(&g, 2, &[5.0, 3.0], DijkstraMode::In).unwrap();
696        assert_eq!(w[0], Some(3.0));
697        assert_eq!(w[1], Some(3.0));
698        assert_eq!(w[2], Some(f64::INFINITY));
699    }
700
701    #[test]
702    fn source_out_of_range_errors() {
703        let g = Graph::with_vertices(3);
704        let err = widest_path_widths(&g, 99, &[]).unwrap_err();
705        assert!(matches!(
706            err,
707            IgraphError::VertexOutOfRange { id: 99, n: 3 }
708        ));
709    }
710
711    #[test]
712    fn nan_weight_errors() {
713        let mut g = Graph::with_vertices(2);
714        g.add_edge(0, 1).unwrap();
715        let err = widest_path_widths(&g, 0, &[f64::NAN]).unwrap_err();
716        assert!(matches!(err, IgraphError::InvalidArgument(_)));
717    }
718
719    #[test]
720    fn weights_size_mismatch_errors() {
721        let mut g = Graph::with_vertices(2);
722        g.add_edge(0, 1).unwrap();
723        let err = widest_path_widths(&g, 0, &[1.0, 2.0]).unwrap_err();
724        assert!(matches!(err, IgraphError::InvalidArgument(_)));
725    }
726
727    #[test]
728    fn empty_graph_no_edges() {
729        let g = Graph::with_vertices(3);
730        let w = widest_path_widths(&g, 0, &[]).unwrap();
731        assert_eq!(w[0], Some(f64::INFINITY));
732        assert_eq!(w[1], None);
733        assert_eq!(w[2], None);
734    }
735
736    #[test]
737    fn negative_weights_allowed_as_bottleneck() {
738        // A negative *finite* edge weight is allowed; it just acts as
739        // a small bottleneck. -∞ is the ignore sentinel.
740        let mut g = Graph::with_vertices(3);
741        g.add_edge(0, 1).unwrap();
742        g.add_edge(1, 2).unwrap();
743        let w = widest_path_widths(&g, 0, &[-1.0, 5.0]).unwrap();
744        assert_eq!(w[0], Some(f64::INFINITY));
745        assert_eq!(w[1], Some(-1.0));
746        // 0 → 2 bottleneck = min(-1, 5) = -1
747        assert_eq!(w[2], Some(-1.0));
748    }
749
750    #[test]
751    fn multiple_parallel_edges_pick_widest() {
752        let mut g = Graph::with_vertices(2);
753        g.add_edge(0, 1).unwrap(); // width 1
754        g.add_edge(0, 1).unwrap(); // width 5 — should win
755        g.add_edge(0, 1).unwrap(); // width 3
756        let w = widest_path_widths(&g, 0, &[1.0, 5.0, 3.0]).unwrap();
757        assert_eq!(w[0], Some(f64::INFINITY));
758        assert_eq!(w[1], Some(5.0));
759    }
760
761    // -------- ALGO-SP-011: widest_path path construction --------
762
763    #[test]
764    fn widest_path_triangle_via_shortcut() {
765        // Same setup as widest_path_widths triangle: 0→1 via 0-2-1
766        // is wider (bottleneck 2) than the direct edge (width 1).
767        let mut g = Graph::with_vertices(3);
768        g.add_edge(0, 1).unwrap(); // edge 0, width 1
769        g.add_edge(0, 2).unwrap(); // edge 1, width 4
770        g.add_edge(1, 2).unwrap(); // edge 2, width 2
771        let (vs, es) = widest_path(&g, 0, 1, &[1.0, 4.0, 2.0])
772            .unwrap()
773            .expect("0→1 is reachable");
774        assert_eq!(vs, vec![0, 2, 1]);
775        assert_eq!(es, vec![1, 2]);
776    }
777
778    #[test]
779    fn widest_path_direct_edge_when_widest() {
780        let mut g = Graph::with_vertices(3);
781        g.add_edge(0, 1).unwrap(); // edge 0, width 1
782        g.add_edge(0, 2).unwrap(); // edge 1, width 4 — direct is widest
783        g.add_edge(1, 2).unwrap(); // edge 2, width 2
784        let (vs, es) = widest_path(&g, 0, 2, &[1.0, 4.0, 2.0])
785            .unwrap()
786            .expect("0→2 reachable");
787        assert_eq!(vs, vec![0, 2]);
788        assert_eq!(es, vec![1]);
789    }
790
791    #[test]
792    fn widest_path_self_target_returns_trivial() {
793        let mut g = Graph::with_vertices(3);
794        g.add_edge(0, 1).unwrap();
795        let (vs, es) = widest_path(&g, 0, 0, &[5.0]).unwrap().unwrap();
796        assert_eq!(vs, vec![0]);
797        assert!(es.is_empty());
798    }
799
800    #[test]
801    fn widest_path_unreachable_target_returns_none() {
802        let mut g = Graph::with_vertices(4);
803        g.add_edge(0, 1).unwrap();
804        g.add_edge(2, 3).unwrap();
805        let result = widest_path(&g, 0, 2, &[1.0, 1.0]).unwrap();
806        assert!(result.is_none());
807    }
808
809    #[test]
810    fn widest_path_chain_returns_full_chain() {
811        // 0-1-2-3 unit widths; widest path is the chain itself.
812        let mut g = Graph::with_vertices(4);
813        g.add_edge(0, 1).unwrap();
814        g.add_edge(1, 2).unwrap();
815        g.add_edge(2, 3).unwrap();
816        let (vs, es) = widest_path(&g, 0, 3, &[1.0, 1.0, 1.0]).unwrap().unwrap();
817        assert_eq!(vs, vec![0, 1, 2, 3]);
818        assert_eq!(es, vec![0, 1, 2]);
819    }
820
821    #[test]
822    fn widest_path_directed_respects_direction() {
823        // Directed 0 → 1 → 2: 0 → 2 reachable in OUT mode.
824        let mut g = Graph::new(3, true).unwrap();
825        g.add_edge(0, 1).unwrap();
826        g.add_edge(1, 2).unwrap();
827        let (vs, _) = widest_path(&g, 0, 2, &[5.0, 3.0]).unwrap().unwrap();
828        assert_eq!(vs, vec![0, 1, 2]);
829        // Reverse direction: not reachable in OUT mode.
830        assert!(widest_path(&g, 2, 0, &[5.0, 3.0]).unwrap().is_none());
831        // IN mode from 2 to 0 reaches via reverse traversal.
832        let (vs, _) = widest_path_with_mode(&g, 2, 0, &[5.0, 3.0], DijkstraMode::In)
833            .unwrap()
834            .unwrap();
835        assert_eq!(vs, vec![2, 1, 0]);
836    }
837
838    #[test]
839    fn widest_path_from_out_of_range_errors() {
840        let g = Graph::with_vertices(3);
841        let err = widest_path(&g, 99, 0, &[]).unwrap_err();
842        assert!(matches!(
843            err,
844            IgraphError::VertexOutOfRange { id: 99, n: 3 }
845        ));
846    }
847
848    #[test]
849    fn widest_path_to_out_of_range_errors() {
850        let g = Graph::with_vertices(3);
851        let err = widest_path(&g, 0, 99, &[]).unwrap_err();
852        assert!(matches!(
853            err,
854            IgraphError::VertexOutOfRange { id: 99, n: 3 }
855        ));
856    }
857
858    #[test]
859    fn widest_path_negative_infinity_edge_breaks_chain() {
860        // 0-1 has -∞ width → effectively missing; path can't use it.
861        let mut g = Graph::with_vertices(3);
862        g.add_edge(0, 1).unwrap();
863        g.add_edge(1, 2).unwrap();
864        let r = widest_path(&g, 0, 2, &[f64::NEG_INFINITY, 1.0]).unwrap();
865        assert!(r.is_none());
866    }
867
868    // -------- ALGO-SP-012: FW all-pairs widest widths --------
869
870    #[test]
871    fn fw_widest_triangle_matches_dijkstra_per_source() {
872        let mut g = Graph::with_vertices(3);
873        g.add_edge(0, 1).unwrap();
874        g.add_edge(0, 2).unwrap();
875        g.add_edge(1, 2).unwrap();
876        let weights = [1.0, 4.0, 2.0];
877        let fw = widest_path_widths_floyd_warshall(&g, &weights).unwrap();
878        // Compare each row to the Dijkstra-based result.
879        for u in 0..3u32 {
880            let dij = widest_path_widths(&g, u, &weights).unwrap();
881            assert_eq!(fw[u as usize], dij, "row {u} mismatch");
882        }
883    }
884
885    #[test]
886    fn fw_widest_diagonal_is_infinity() {
887        let g = Graph::with_vertices(3);
888        let m = widest_path_widths_floyd_warshall(&g, &[]).unwrap();
889        for (i, row) in m.iter().enumerate() {
890            for (j, entry) in row.iter().enumerate() {
891                if i == j {
892                    assert_eq!(*entry, Some(f64::INFINITY));
893                } else {
894                    assert_eq!(*entry, None);
895                }
896            }
897        }
898    }
899
900    #[test]
901    fn fw_widest_unreachable_components() {
902        let mut g = Graph::with_vertices(4);
903        g.add_edge(0, 1).unwrap();
904        g.add_edge(2, 3).unwrap();
905        let m = widest_path_widths_floyd_warshall(&g, &[5.0, 7.0]).unwrap();
906        assert_eq!(m[0][1], Some(5.0));
907        assert_eq!(m[0][2], None);
908        assert_eq!(m[2][3], Some(7.0));
909        assert_eq!(m[1][3], None);
910    }
911
912    #[test]
913    fn fw_widest_directed_respects_mode() {
914        let mut g = Graph::new(3, true).unwrap();
915        g.add_edge(0, 1).unwrap();
916        g.add_edge(1, 2).unwrap();
917        let weights = [5.0, 3.0];
918        // OUT mode: 0 reaches 1 (5) and 2 (3); 2 doesn't reach back.
919        let out = widest_path_widths_floyd_warshall(&g, &weights).unwrap();
920        assert_eq!(out[0][1], Some(5.0));
921        assert_eq!(out[0][2], Some(3.0));
922        assert_eq!(out[2][0], None);
923        // IN mode: reversed.
924        let in_m =
925            widest_path_widths_floyd_warshall_with_mode(&g, &weights, DijkstraMode::In).unwrap();
926        assert_eq!(in_m[0][1], None);
927        assert_eq!(in_m[2][0], Some(3.0));
928        // ALL mode: bidirectional.
929        let all =
930            widest_path_widths_floyd_warshall_with_mode(&g, &weights, DijkstraMode::All).unwrap();
931        assert_eq!(all[0][2], Some(3.0));
932        assert_eq!(all[2][0], Some(3.0));
933    }
934
935    #[test]
936    fn fw_widest_parallel_edges_keep_widest() {
937        let mut g = Graph::with_vertices(2);
938        g.add_edge(0, 1).unwrap(); // width 1
939        g.add_edge(0, 1).unwrap(); // width 5 — wins
940        g.add_edge(0, 1).unwrap(); // width 3
941        let m = widest_path_widths_floyd_warshall(&g, &[1.0, 5.0, 3.0]).unwrap();
942        assert_eq!(m[0][1], Some(5.0));
943        assert_eq!(m[1][0], Some(5.0));
944    }
945
946    #[test]
947    fn fw_widest_negative_infinity_edge_ignored() {
948        let mut g = Graph::with_vertices(3);
949        g.add_edge(0, 1).unwrap(); // -∞ → ignored
950        g.add_edge(1, 2).unwrap();
951        let m = widest_path_widths_floyd_warshall(&g, &[f64::NEG_INFINITY, 1.0]).unwrap();
952        // 0 can't reach 1 or 2 — the bridge edge is absent.
953        assert_eq!(m[0][1], None);
954        assert_eq!(m[0][2], None);
955        assert_eq!(m[1][2], Some(1.0));
956    }
957
958    #[test]
959    fn fw_widest_nan_weight_errors() {
960        let mut g = Graph::with_vertices(2);
961        g.add_edge(0, 1).unwrap();
962        let err = widest_path_widths_floyd_warshall(&g, &[f64::NAN]).unwrap_err();
963        assert!(matches!(err, IgraphError::InvalidArgument(_)));
964    }
965
966    #[test]
967    fn fw_widest_empty_graph_empty_matrix() {
968        let g = Graph::with_vertices(0);
969        let m = widest_path_widths_floyd_warshall(&g, &[]).unwrap();
970        assert!(m.is_empty());
971    }
972
973    // -------- ALGO-SP-013: widest_paths_to multi-target --------
974
975    #[test]
976    fn widest_paths_to_triangle_two_targets() {
977        let mut g = Graph::with_vertices(3);
978        g.add_edge(0, 1).unwrap(); // edge 0, width 1
979        g.add_edge(0, 2).unwrap(); // edge 1, width 4
980        g.add_edge(1, 2).unwrap(); // edge 2, width 2
981        let paths = widest_paths_to(&g, 0, &[1, 2], &[1.0, 4.0, 2.0]).unwrap();
982        assert_eq!(paths.len(), 2);
983        // 0→1 via shortcut at 2: bottleneck min(4, 2) = 2 beats direct 1
984        let (vs1, es1) = paths[0].as_ref().unwrap();
985        assert_eq!(vs1, &vec![0, 2, 1]);
986        assert_eq!(es1, &vec![1, 2]);
987        // 0→2 direct: width 4 wins
988        let (vs2, es2) = paths[1].as_ref().unwrap();
989        assert_eq!(vs2, &vec![0, 2]);
990        assert_eq!(es2, &vec![1]);
991    }
992
993    #[test]
994    fn widest_paths_to_includes_unreachable_as_none() {
995        let mut g = Graph::with_vertices(4);
996        g.add_edge(0, 1).unwrap();
997        g.add_edge(2, 3).unwrap();
998        // Targets [1, 2, 3]: only 1 reachable.
999        let paths = widest_paths_to(&g, 0, &[1, 2, 3], &[1.0, 1.0]).unwrap();
1000        assert!(paths[0].is_some());
1001        assert!(paths[1].is_none());
1002        assert!(paths[2].is_none());
1003    }
1004
1005    #[test]
1006    fn widest_paths_to_empty_targets_returns_empty() {
1007        let g = Graph::with_vertices(3);
1008        let paths = widest_paths_to(&g, 0, &[], &[]).unwrap();
1009        assert!(paths.is_empty());
1010    }
1011
1012    #[test]
1013    fn widest_paths_to_self_target_is_trivial() {
1014        let mut g = Graph::with_vertices(3);
1015        g.add_edge(0, 1).unwrap();
1016        // from == target[0]: trivial path.
1017        let paths = widest_paths_to(&g, 1, &[1, 0], &[5.0]).unwrap();
1018        let (vs0, es0) = paths[0].as_ref().unwrap();
1019        assert_eq!(vs0, &vec![1]);
1020        assert!(es0.is_empty());
1021        // 1 → 0: single edge.
1022        let (vs1, es1) = paths[1].as_ref().unwrap();
1023        assert_eq!(vs1, &vec![1, 0]);
1024        assert_eq!(es1, &vec![0]);
1025    }
1026
1027    #[test]
1028    fn widest_paths_to_duplicate_targets_return_same_path() {
1029        let mut g = Graph::with_vertices(3);
1030        g.add_edge(0, 1).unwrap();
1031        g.add_edge(1, 2).unwrap();
1032        let paths = widest_paths_to(&g, 0, &[2, 2, 2], &[5.0, 3.0]).unwrap();
1033        assert_eq!(paths.len(), 3);
1034        for p in &paths {
1035            let (vs, _) = p.as_ref().unwrap();
1036            assert_eq!(vs, &vec![0, 1, 2]);
1037        }
1038    }
1039
1040    #[test]
1041    fn widest_paths_to_target_out_of_range_errors() {
1042        let g = Graph::with_vertices(3);
1043        let err = widest_paths_to(&g, 0, &[1, 99], &[]).unwrap_err();
1044        assert!(matches!(
1045            err,
1046            IgraphError::VertexOutOfRange { id: 99, n: 3 }
1047        ));
1048    }
1049
1050    #[test]
1051    fn widest_paths_to_directed_in_mode_reverses() {
1052        let mut g = Graph::new(3, true).unwrap();
1053        g.add_edge(0, 1).unwrap();
1054        g.add_edge(1, 2).unwrap();
1055        // IN mode from 2: reaches 1 and 0 by walking reverse edges.
1056        let paths =
1057            widest_paths_to_with_mode(&g, 2, &[1, 0], &[5.0, 3.0], DijkstraMode::In).unwrap();
1058        let (vs1, _) = paths[0].as_ref().unwrap();
1059        assert_eq!(vs1, &vec![2, 1]);
1060        let (vs0, _) = paths[1].as_ref().unwrap();
1061        assert_eq!(vs0, &vec![2, 1, 0]);
1062    }
1063
1064    #[test]
1065    fn widest_paths_to_negative_infinity_edge_blocks_target() {
1066        let mut g = Graph::with_vertices(3);
1067        g.add_edge(0, 1).unwrap(); // -∞ → ignored
1068        g.add_edge(1, 2).unwrap();
1069        let paths = widest_paths_to(&g, 0, &[1, 2], &[f64::NEG_INFINITY, 1.0]).unwrap();
1070        assert!(paths[0].is_none());
1071        assert!(paths[1].is_none());
1072    }
1073
1074    // -------- ALGO-SP-014: WidestPaths struct (widths + SPT) --------
1075
1076    #[test]
1077    fn widest_paths_triangle_struct_fields_consistent() {
1078        let mut g = Graph::with_vertices(3);
1079        g.add_edge(0, 1).unwrap(); // edge 0, width 1
1080        g.add_edge(0, 2).unwrap(); // edge 1, width 4
1081        g.add_edge(1, 2).unwrap(); // edge 2, width 2
1082        let sp = widest_paths(&g, 0, &[1.0, 4.0, 2.0]).unwrap();
1083        // Source-side sentinels
1084        assert_eq!(sp.widths[0], Some(f64::INFINITY));
1085        assert_eq!(sp.parents[0], None);
1086        assert_eq!(sp.inbound_edges[0], None);
1087        // Vertex 2 reached directly via edge 1 (width 4)
1088        assert_eq!(sp.widths[2], Some(4.0));
1089        assert_eq!(sp.parents[2], Some(0));
1090        assert_eq!(sp.inbound_edges[2], Some(1));
1091        // Vertex 1 reached via 2 (bottleneck min(4, 2) = 2 beats direct width 1)
1092        assert_eq!(sp.widths[1], Some(2.0));
1093        assert_eq!(sp.parents[1], Some(2));
1094        assert_eq!(sp.inbound_edges[1], Some(2));
1095    }
1096
1097    #[test]
1098    fn widest_paths_widths_match_widest_path_widths() {
1099        // The struct's `widths` field must match the standalone function.
1100        let mut g = Graph::with_vertices(4);
1101        g.add_edge(0, 1).unwrap();
1102        g.add_edge(1, 2).unwrap();
1103        g.add_edge(2, 3).unwrap();
1104        let weights = [5.0, 1.0, 3.0];
1105        let sp = widest_paths(&g, 0, &weights).unwrap();
1106        let standalone = widest_path_widths(&g, 0, &weights).unwrap();
1107        assert_eq!(sp.widths, standalone);
1108    }
1109
1110    #[test]
1111    fn widest_paths_unreachable_has_none_in_all_three_fields() {
1112        let mut g = Graph::with_vertices(4);
1113        g.add_edge(0, 1).unwrap();
1114        g.add_edge(2, 3).unwrap();
1115        let sp = widest_paths(&g, 0, &[5.0, 7.0]).unwrap();
1116        // Unreachable vertex 2: widths/parents/inbound_edges all None.
1117        assert_eq!(sp.widths[2], None);
1118        assert_eq!(sp.parents[2], None);
1119        assert_eq!(sp.inbound_edges[2], None);
1120        // Reachable vertex 1 (via edge 0) — parent is source.
1121        assert_eq!(sp.parents[1], Some(0));
1122        assert_eq!(sp.inbound_edges[1], Some(0));
1123    }
1124
1125    #[test]
1126    fn widest_paths_directed_in_mode() {
1127        let mut g = Graph::new(3, true).unwrap();
1128        g.add_edge(0, 1).unwrap();
1129        g.add_edge(1, 2).unwrap();
1130        // IN mode from 2: reaches 1 (edge 1) and 0 (via edge 0).
1131        let sp = widest_paths_with_mode(&g, 2, &[5.0, 3.0], DijkstraMode::In).unwrap();
1132        assert_eq!(sp.widths[2], Some(f64::INFINITY));
1133        assert_eq!(sp.widths[1], Some(3.0));
1134        assert_eq!(sp.widths[0], Some(3.0));
1135        // Parents under IN mode: 1's predecessor reached via edge 1 → from
1136        // 2's side that is vertex 2 (edge_other(1, 1) = 2).
1137        assert_eq!(sp.parents[1], Some(2));
1138        assert_eq!(sp.parents[0], Some(1));
1139    }
1140
1141    #[test]
1142    fn widest_paths_source_out_of_range_errors() {
1143        let g = Graph::with_vertices(3);
1144        let err = widest_paths(&g, 99, &[]).unwrap_err();
1145        assert!(matches!(
1146            err,
1147            IgraphError::VertexOutOfRange { id: 99, n: 3 }
1148        ));
1149    }
1150
1151    #[test]
1152    fn widest_paths_nan_weight_errors() {
1153        let mut g = Graph::with_vertices(2);
1154        g.add_edge(0, 1).unwrap();
1155        let err = widest_paths(&g, 0, &[f64::NAN]).unwrap_err();
1156        assert!(matches!(err, IgraphError::InvalidArgument(_)));
1157    }
1158
1159    #[test]
1160    fn widest_paths_spt_endpoints_match_widest_path_chain() {
1161        // Walking back from a target via parents/inbound_edges must
1162        // reconstruct exactly the chain returned by widest_path.
1163        let mut g = Graph::with_vertices(4);
1164        g.add_edge(0, 1).unwrap();
1165        g.add_edge(1, 2).unwrap();
1166        g.add_edge(2, 3).unwrap();
1167        let weights = [5.0, 1.0, 3.0];
1168        let sp = widest_paths(&g, 0, &weights).unwrap();
1169        let path = widest_path(&g, 0, 3, &weights).unwrap().unwrap();
1170        // Reconstruct via SPT from target 3.
1171        let mut reconstructed: Vec<u32> = vec![3];
1172        let mut cur = 3usize;
1173        while let Some(prev) = sp.parents[cur] {
1174            reconstructed.push(prev);
1175            cur = prev as usize;
1176        }
1177        reconstructed.reverse();
1178        assert_eq!(reconstructed, path.0);
1179    }
1180}