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