Skip to main content

rust_igraph/algorithms/paths/
dijkstra.rs

1//! Dijkstra single-source shortest distances + paths/parents/cutoff +
2//! mode-aware variants + all-shortest-paths (ALGO-SP-001 / SP-001b /
3//! SP-001c).
4//!
5//! Counterparts of:
6//! - `igraph_distances_dijkstra()`              — `references/igraph/src/paths/dijkstra.c:322`
7//! - `igraph_distances_dijkstra_cutoff()`       — `references/igraph/src/paths/dijkstra.c:107`
8//! - `igraph_get_shortest_paths_dijkstra()`     — `references/igraph/src/paths/dijkstra.c:412`
9//! - `igraph_get_shortest_path_dijkstra()`      — `references/igraph/src/paths/dijkstra.c:689`
10//! - `igraph_get_all_shortest_paths_dijkstra()` — `references/igraph/src/paths/dijkstra.c:797`
11//!
12//! Mode (SP-001c): on directed graphs the [`DijkstraMode`] parameter
13//! selects which adjacency the BFS follows (`Out` / `In` / `All`). The
14//! legacy entry points without `_with_mode` keep `Out` semantics
15//! (matches upstream defaults). For undirected graphs every mode is
16//! identical (every edge is bidirectional). The all-shortest-paths
17//! variant takes a `mode` parameter from the start because it didn't
18//! exist before SP-001c.
19//!
20//! All edge weights must be non-negative and not NaN; otherwise we
21//! return [`IgraphError::InvalidArgument`]. Edges of weight
22//! [`f64::INFINITY`] are rejected at validation time (matches SP-001's
23//! contract — upstream silently skips them in the relax loop, but we've
24//! always required finite weights at the boundary). The relax loop
25//! still defensively checks each edge so changes to validation don't
26//! corrupt the heap with `INF + INF = INF`.
27
28use std::cmp::Ordering;
29use std::collections::BinaryHeap;
30
31use crate::core::graph::EdgeId;
32use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
33
34/// Min-heap entry. `Ord` is reversed so that `BinaryHeap` (a max-heap)
35/// pops the smallest distance first. NaN distances are forbidden by the
36/// public API contract — `total_cmp` is therefore safe.
37#[derive(Copy, Clone)]
38struct Frontier(f64, VertexId);
39
40impl PartialEq for Frontier {
41    fn eq(&self, other: &Self) -> bool {
42        self.0 == other.0 && self.1 == other.1
43    }
44}
45impl Eq for Frontier {}
46impl Ord for Frontier {
47    fn cmp(&self, other: &Self) -> Ordering {
48        // Reverse ordering: smaller distance is "greater" for the heap
49        // so it gets popped first.
50        other.0.total_cmp(&self.0).then(other.1.cmp(&self.1))
51    }
52}
53impl PartialOrd for Frontier {
54    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
55        Some(self.cmp(other))
56    }
57}
58
59/// Direction selector for the mode-aware Dijkstra entry points
60/// (SP-001c). Counterpart of `igraph_neimode_t` restricted to the
61/// three values `igraph_distances_dijkstra` accepts. For undirected
62/// graphs every variant collapses to [`DijkstraMode::All`] (every
63/// edge is bidirectional).
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum DijkstraMode {
66    /// Follow outgoing edges (`IGRAPH_OUT`). Default for the legacy
67    /// entry points without `_with_mode`.
68    Out,
69    /// Follow incoming edges (`IGRAPH_IN`).
70    In,
71    /// Ignore direction — treat every edge as bidirectional
72    /// (`IGRAPH_ALL`).
73    All,
74}
75
76/// Per-vertex mode-aware incident edges. For undirected graphs every
77/// mode collapses to [`Graph::incident`]'s merged adjacency.
78pub(super) fn incident_for_mode(
79    graph: &Graph,
80    v: VertexId,
81    mode: DijkstraMode,
82) -> IgraphResult<Vec<EdgeId>> {
83    if !graph.is_directed() {
84        return graph.incident(v);
85    }
86    match mode {
87        DijkstraMode::Out => graph.incident(v),
88        DijkstraMode::In => graph.incident_in(v),
89        DijkstraMode::All => {
90            let mut out = graph.incident(v)?;
91            out.extend(graph.incident_in(v)?);
92            Ok(out)
93        }
94    }
95}
96
97/// Validate `weights`: length matches `graph.ecount()`, no negative,
98/// no NaN, no non-finite values. Shared by every public entry point.
99pub(super) fn validate_weights(graph: &Graph, weights: &[f64]) -> IgraphResult<()> {
100    let m = graph.ecount();
101    if weights.len() != m {
102        return Err(IgraphError::InvalidArgument(format!(
103            "weights vector size ({}) differs from edge count ({})",
104            weights.len(),
105            m
106        )));
107    }
108    for (e, &w) in weights.iter().enumerate() {
109        if w.is_nan() {
110            return Err(IgraphError::InvalidArgument(format!(
111                "weight at edge {e} is NaN"
112            )));
113        }
114        if w < 0.0 {
115            return Err(IgraphError::InvalidArgument(format!(
116                "weight at edge {e} is negative ({w}); Dijkstra requires non-negative weights"
117            )));
118        }
119        if !w.is_finite() {
120            return Err(IgraphError::InvalidArgument(format!(
121                "weight at edge {e} is not finite ({w})"
122            )));
123        }
124    }
125    Ok(())
126}
127
128/// Inner Dijkstra loop seeded with one or more source vertices at
129/// distance 0. Records distances and (optionally) the inbound edge for
130/// each settled vertex. `cutoff` is `None` to disable, `Some(c)` to
131/// stop relaxing at distances strictly greater than `c` (matches the
132/// `mindist > cutoff` early-skip in `igraph_i_distances_dijkstra_cutoff`).
133fn dijkstra_inner(
134    graph: &Graph,
135    sources: &[VertexId],
136    weights: &[f64],
137    cutoff: Option<f64>,
138    record_inbound: bool,
139    mode: DijkstraMode,
140) -> IgraphResult<(Vec<f64>, Vec<Option<EdgeId>>)> {
141    let n = graph.vcount();
142    let n_us = n as usize;
143    let mut dist = vec![f64::INFINITY; n_us];
144    let mut inbound: Vec<Option<EdgeId>> = if record_inbound {
145        vec![None; n_us]
146    } else {
147        Vec::new()
148    };
149    let mut visited = vec![false; n_us];
150
151    let mut heap: BinaryHeap<Frontier> = BinaryHeap::new();
152    for &s in sources {
153        if s >= n {
154            return Err(IgraphError::VertexOutOfRange { id: s, n });
155        }
156        if dist[s as usize] > 0.0 {
157            dist[s as usize] = 0.0;
158            heap.push(Frontier(0.0, s));
159        }
160    }
161
162    while let Some(Frontier(d, v)) = heap.pop() {
163        let v_us = v as usize;
164        if visited[v_us] {
165            continue;
166        }
167        if let Some(c) = cutoff {
168            if d > c {
169                continue;
170            }
171        }
172        visited[v_us] = true;
173
174        for eid in incident_for_mode(graph, v, mode)? {
175            let w = weights[eid as usize];
176            if !w.is_finite() {
177                continue;
178            }
179            let other = graph.edge_other(eid as EdgeId, v)?;
180            let nd = d + w;
181            if nd < dist[other as usize] {
182                dist[other as usize] = nd;
183                if record_inbound {
184                    inbound[other as usize] = Some(eid as EdgeId);
185                }
186                heap.push(Frontier(nd, other));
187            }
188        }
189    }
190
191    Ok((dist, inbound))
192}
193
194fn dist_vec_to_options(dist: Vec<f64>) -> Vec<Option<f64>> {
195    dist.into_iter()
196        .map(|d| if d.is_infinite() { None } else { Some(d) })
197        .collect()
198}
199
200/// Single-source Dijkstra distances.
201///
202/// Returns `Vec<Option<f64>>` of length `vcount`: `result[v] = Some(d)`
203/// if there is a path from `source` to `v` with weighted distance `d`,
204/// `None` if `v` is unreachable. The source's own distance is
205/// `Some(0.0)` whenever `source` is a valid vertex.
206///
207/// `weights[e]` is the weight of edge `e`; `weights.len()` must equal
208/// `graph.ecount()`. All weights must be `>= 0` and finite (no NaN, no
209/// infinity).
210///
211/// # Examples
212///
213/// ```
214/// use rust_igraph::{Graph, dijkstra_distances};
215///
216/// // Triangle 0-1-2 with weights 1, 4, 2 → dist(0→2) = 1+2 via 0-1-2
217/// // (3.0) is shorter than the direct 0-2 edge weight 4.0.
218/// let mut g = Graph::with_vertices(3);
219/// g.add_edge(0, 1).unwrap();   // edge 0, weight 1
220/// g.add_edge(0, 2).unwrap();   // edge 1, weight 4
221/// g.add_edge(1, 2).unwrap();   // edge 2, weight 2
222/// let d = dijkstra_distances(&g, 0, &[1.0, 4.0, 2.0]).unwrap();
223/// assert_eq!(d, vec![Some(0.0), Some(1.0), Some(3.0)]);
224/// ```
225pub fn dijkstra_distances(
226    graph: &Graph,
227    source: VertexId,
228    weights: &[f64],
229) -> IgraphResult<Vec<Option<f64>>> {
230    validate_weights(graph, weights)?;
231    let (dist, _) = dijkstra_inner(graph, &[source], weights, None, false, DijkstraMode::Out)?;
232    Ok(dist_vec_to_options(dist))
233}
234
235/// Output of [`dijkstra_paths`]. `parents[v]` is the predecessor of
236/// `v` along the shortest-path tree from the single source (or `None`
237/// for the source itself and for unreachable vertices — the caller can
238/// disambiguate via `distances[v]`). `inbound_edges[v]` is the edge id
239/// `v` was reached through; `None` for the source and unreachable
240/// vertices.
241///
242/// Counterpart of the `vertices`+`parents`+`inbound_edges` outputs of
243/// `igraph_get_shortest_paths_dijkstra`. Upstream uses sentinels
244/// `-1`/`-2` instead of `Option`s; this Rust port uses `None` for both
245/// "source" and "unreachable", so callers should consult `distances`
246/// to distinguish them.
247#[derive(Debug, Clone, PartialEq)]
248pub struct DijkstraPaths {
249    /// `distances[v]` is `Some(d)` if `v` is reachable from the
250    /// source, else `None`. `distances[source] == Some(0.0)`.
251    pub distances: Vec<Option<f64>>,
252    /// `parents[v]` is the vertex `u` such that the shortest-path
253    /// tree edge into `v` goes `u → v`. `None` for the source and
254    /// for unreachable vertices.
255    pub parents: Vec<Option<VertexId>>,
256    /// `inbound_edges[v]` is the edge id used to reach `v` in the
257    /// shortest-path tree. `None` for the source and for unreachable
258    /// vertices.
259    pub inbound_edges: Vec<Option<EdgeId>>,
260}
261
262/// Single-source Dijkstra returning distances + parents + inbound
263/// edges over every vertex.
264///
265/// Counterpart of `igraph_get_shortest_paths_dijkstra(..., to=igraph_vss_all(),
266/// IGRAPH_OUT, parents, inbound_edges)`.
267///
268/// # Examples
269///
270/// ```
271/// use rust_igraph::{Graph, dijkstra_paths};
272///
273/// // Triangle 0-1-2 with weights 1, 4, 2 → SPT from 0: parent[1]=0,
274/// // parent[2]=1 (via the shortcut), inbound[1]=edge0, inbound[2]=edge2.
275/// let mut g = Graph::with_vertices(3);
276/// g.add_edge(0, 1).unwrap();
277/// g.add_edge(0, 2).unwrap();
278/// g.add_edge(1, 2).unwrap();
279/// let p = dijkstra_paths(&g, 0, &[1.0, 4.0, 2.0]).unwrap();
280/// assert_eq!(p.distances, vec![Some(0.0), Some(1.0), Some(3.0)]);
281/// assert_eq!(p.parents,   vec![None,      Some(0),   Some(1)]);
282/// assert_eq!(p.inbound_edges, vec![None, Some(0), Some(2)]);
283/// ```
284pub fn dijkstra_paths(
285    graph: &Graph,
286    source: VertexId,
287    weights: &[f64],
288) -> IgraphResult<DijkstraPaths> {
289    validate_weights(graph, weights)?;
290    let (dist, inbound) = dijkstra_inner(graph, &[source], weights, None, true, DijkstraMode::Out)?;
291    let mut parents = vec![None; graph.vcount() as usize];
292    for v in 0..graph.vcount() {
293        if let Some(eid) = inbound[v as usize] {
294            let other = graph.edge_other(eid, v)?;
295            parents[v as usize] = Some(other);
296        }
297    }
298    Ok(DijkstraPaths {
299        distances: dist_vec_to_options(dist),
300        parents,
301        inbound_edges: inbound,
302    })
303}
304
305/// Reconstruct a single source-to-target path returning vertex and
306/// edge sequences. `Ok(None)` if `target` is unreachable; the source
307/// vertex appears as the first element when reachable.
308///
309/// Counterpart of `igraph_get_shortest_path_dijkstra`.
310///
311/// # Examples
312///
313/// ```
314/// use rust_igraph::{Graph, dijkstra_path_to};
315///
316/// let mut g = Graph::with_vertices(4);
317/// g.add_edge(0, 1).unwrap();   // edge 0, weight 1
318/// g.add_edge(1, 2).unwrap();   // edge 1, weight 1
319/// g.add_edge(2, 3).unwrap();   // edge 2, weight 1
320/// g.add_edge(0, 3).unwrap();   // edge 3, weight 10 — heavier
321/// let (vs, es) = dijkstra_path_to(&g, 0, 3, &[1.0, 1.0, 1.0, 10.0])
322///     .unwrap()
323///     .expect("target reachable");
324/// assert_eq!(vs, vec![0, 1, 2, 3]);
325/// assert_eq!(es, vec![0, 1, 2]);
326/// ```
327pub fn dijkstra_path_to(
328    graph: &Graph,
329    source: VertexId,
330    target: VertexId,
331    weights: &[f64],
332) -> IgraphResult<Option<(Vec<VertexId>, Vec<EdgeId>)>> {
333    let n = graph.vcount();
334    if target >= n {
335        return Err(IgraphError::VertexOutOfRange { id: target, n });
336    }
337    let p = dijkstra_paths(graph, source, weights)?;
338    if p.distances[target as usize].is_none() {
339        return Ok(None);
340    }
341    // Walk parents back from target to source.
342    let mut vs = Vec::new();
343    let mut es = Vec::new();
344    let mut cur = target;
345    while let Some(eid) = p.inbound_edges[cur as usize] {
346        es.push(eid);
347        let Some(parent) = p.parents[cur as usize] else {
348            return Err(IgraphError::Internal(
349                "inbound edge exists but parent is None",
350            ));
351        };
352        vs.push(cur);
353        cur = parent;
354    }
355    vs.push(cur); // push source last
356    vs.reverse();
357    es.reverse();
358    Ok(Some((vs, es)))
359}
360
361/// Single-source Dijkstra distances with an optional `cutoff`. When
362/// `cutoff = Some(c)`, vertices whose shortest-path distance exceeds
363/// `c` are returned as `None` (unreachable-within-cutoff); the search
364/// stops relaxing edges out of any vertex whose `mindist > c`.
365///
366/// `cutoff = None` is identical to [`dijkstra_distances`].
367///
368/// Counterpart of `igraph_distances_dijkstra_cutoff(..., IGRAPH_OUT, cutoff)`
369/// for a single source. Upstream uses `cutoff < 0` to disable; this
370/// Rust port uses `None`.
371///
372/// # Examples
373///
374/// ```
375/// use rust_igraph::{Graph, dijkstra_distances_cutoff};
376///
377/// let mut g = Graph::with_vertices(3);
378/// g.add_edge(0, 1).unwrap();
379/// g.add_edge(1, 2).unwrap();
380/// let d = dijkstra_distances_cutoff(&g, 0, &[1.0, 10.0], Some(5.0)).unwrap();
381/// assert!(d[0].is_some()); // distance 0
382/// assert!(d[1].is_some()); // distance 1
383/// assert!(d[2].is_none()); // distance 11, beyond cutoff
384/// ```
385pub fn dijkstra_distances_cutoff(
386    graph: &Graph,
387    source: VertexId,
388    weights: &[f64],
389    cutoff: Option<f64>,
390) -> IgraphResult<Vec<Option<f64>>> {
391    validate_weights(graph, weights)?;
392    if let Some(c) = cutoff {
393        if c.is_nan() {
394            return Err(IgraphError::InvalidArgument("cutoff is NaN".into()));
395        }
396    }
397    let (dist, _) = dijkstra_inner(graph, &[source], weights, cutoff, false, DijkstraMode::Out)?;
398    let mut out = dist_vec_to_options(dist);
399    if let Some(c) = cutoff {
400        // The relax loop already prunes outgoing edges past `c`, but a
401        // vertex with `dist == c + ε` may have been settled before the
402        // skip kicked in (its outgoing edges were just not relaxed).
403        // To match upstream's "distances within cutoff only" semantics
404        // we mask anything above `c` to `None` after the fact.
405        for d in &mut out {
406            if let Some(v) = *d {
407                if v > c {
408                    *d = None;
409                }
410            }
411        }
412    }
413    Ok(out)
414}
415
416/// Multi-source Dijkstra distances. `result[i][v]` is the shortest
417/// distance from `sources[i]` to `v` (or `None` if unreachable / past
418/// the cutoff).
419///
420/// Counterpart of `igraph_distances_dijkstra_cutoff(..., from=sources,
421/// to=igraph_vss_all(), weights, IGRAPH_OUT, cutoff)`. Each source is
422/// run independently — upstream loops over `fromvit` doing exactly
423/// the same.
424///
425/// # Examples
426///
427/// ```
428/// use rust_igraph::{Graph, dijkstra_distances_multi};
429///
430/// let mut g = Graph::with_vertices(3);
431/// g.add_edge(0, 1).unwrap();
432/// g.add_edge(1, 2).unwrap();
433/// let d = dijkstra_distances_multi(&g, &[0, 2], &[1.0, 3.0], None).unwrap();
434/// assert_eq!(d[0], vec![Some(0.0), Some(1.0), Some(4.0)]);
435/// assert_eq!(d[1], vec![Some(4.0), Some(3.0), Some(0.0)]);
436/// ```
437pub fn dijkstra_distances_multi(
438    graph: &Graph,
439    sources: &[VertexId],
440    weights: &[f64],
441    cutoff: Option<f64>,
442) -> IgraphResult<Vec<Vec<Option<f64>>>> {
443    validate_weights(graph, weights)?;
444    if let Some(c) = cutoff {
445        if c.is_nan() {
446            return Err(IgraphError::InvalidArgument("cutoff is NaN".into()));
447        }
448    }
449    let mut out = Vec::with_capacity(sources.len());
450    for &s in sources {
451        out.push(dijkstra_distances_cutoff(graph, s, weights, cutoff)?);
452    }
453    Ok(out)
454}
455
456// ---------------------------------------------------------------
457// SP-001c: mode-aware variants of every SP-001/SP-001b entry point.
458// On undirected graphs every mode behaves identically (matches
459// upstream — every edge is bidirectional).
460// ---------------------------------------------------------------
461
462/// Mode-aware Dijkstra distances. Counterpart of
463/// `igraph_distances_dijkstra(_, _, source, vss_all(), weights, mode)`.
464///
465/// # Examples
466///
467/// ```
468/// use rust_igraph::{Graph, dijkstra_distances_with_mode, DijkstraMode};
469///
470/// // Directed path 0→1→2: OUT reaches forward, IN reaches backward,
471/// // ALL collapses to undirected.
472/// let mut g = Graph::new(3, true).unwrap();
473/// g.add_edge(0, 1).unwrap();
474/// g.add_edge(1, 2).unwrap();
475/// let w = [1.0, 2.0];
476/// assert_eq!(
477///     dijkstra_distances_with_mode(&g, 0, &w, DijkstraMode::Out).unwrap(),
478///     vec![Some(0.0), Some(1.0), Some(3.0)]
479/// );
480/// assert_eq!(
481///     dijkstra_distances_with_mode(&g, 0, &w, DijkstraMode::In).unwrap(),
482///     vec![Some(0.0), None, None]
483/// );
484/// ```
485pub fn dijkstra_distances_with_mode(
486    graph: &Graph,
487    source: VertexId,
488    weights: &[f64],
489    mode: DijkstraMode,
490) -> IgraphResult<Vec<Option<f64>>> {
491    validate_weights(graph, weights)?;
492    let (dist, _) = dijkstra_inner(graph, &[source], weights, None, false, mode)?;
493    Ok(dist_vec_to_options(dist))
494}
495
496/// Mode-aware [`dijkstra_paths`]. Counterpart of
497/// `igraph_get_shortest_paths_dijkstra(_, _, _, source, vss_all(), weights, mode, parents, inbound_edges)`.
498///
499/// # Examples
500///
501/// ```
502/// use rust_igraph::{Graph, dijkstra_paths_with_mode, DijkstraMode};
503///
504/// let mut g = Graph::with_vertices(3);
505/// g.add_edge(0, 1).unwrap();
506/// g.add_edge(1, 2).unwrap();
507/// let p = dijkstra_paths_with_mode(&g, 0, &[1.0, 1.0], DijkstraMode::All).unwrap();
508/// assert!(p.distances[2].is_some());
509/// ```
510pub fn dijkstra_paths_with_mode(
511    graph: &Graph,
512    source: VertexId,
513    weights: &[f64],
514    mode: DijkstraMode,
515) -> IgraphResult<DijkstraPaths> {
516    validate_weights(graph, weights)?;
517    let (dist, inbound) = dijkstra_inner(graph, &[source], weights, None, true, mode)?;
518    let mut parents = vec![None; graph.vcount() as usize];
519    for v in 0..graph.vcount() {
520        if let Some(eid) = inbound[v as usize] {
521            let other = graph.edge_other(eid, v)?;
522            parents[v as usize] = Some(other);
523        }
524    }
525    Ok(DijkstraPaths {
526        distances: dist_vec_to_options(dist),
527        parents,
528        inbound_edges: inbound,
529    })
530}
531
532/// Mode-aware [`dijkstra_path_to`]. Counterpart of
533/// `igraph_get_shortest_path_dijkstra(_, _, _, source, target, weights, mode)`.
534///
535/// # Examples
536///
537/// ```
538/// use rust_igraph::{Graph, dijkstra_path_to_with_mode, DijkstraMode};
539///
540/// let mut g = Graph::with_vertices(3);
541/// g.add_edge(0, 1).unwrap();
542/// g.add_edge(1, 2).unwrap();
543/// let (vs, es) = dijkstra_path_to_with_mode(&g, 0, 2, &[1.0, 1.0], DijkstraMode::All)
544///     .unwrap().unwrap();
545/// assert_eq!(vs, vec![0, 1, 2]);
546/// ```
547pub fn dijkstra_path_to_with_mode(
548    graph: &Graph,
549    source: VertexId,
550    target: VertexId,
551    weights: &[f64],
552    mode: DijkstraMode,
553) -> IgraphResult<Option<(Vec<VertexId>, Vec<EdgeId>)>> {
554    let n = graph.vcount();
555    if target >= n {
556        return Err(IgraphError::VertexOutOfRange { id: target, n });
557    }
558    let p = dijkstra_paths_with_mode(graph, source, weights, mode)?;
559    if p.distances[target as usize].is_none() {
560        return Ok(None);
561    }
562    let mut vs = Vec::new();
563    let mut es = Vec::new();
564    let mut cur = target;
565    while let Some(eid) = p.inbound_edges[cur as usize] {
566        es.push(eid);
567        let Some(parent) = p.parents[cur as usize] else {
568            return Err(IgraphError::Internal(
569                "inbound edge exists but parent is None",
570            ));
571        };
572        vs.push(cur);
573        cur = parent;
574    }
575    vs.push(cur);
576    vs.reverse();
577    es.reverse();
578    Ok(Some((vs, es)))
579}
580
581/// Mode-aware [`dijkstra_distances_cutoff`]. Counterpart of
582/// `igraph_distances_dijkstra_cutoff(_, _, source, vss_all(), weights, mode, cutoff)`.
583///
584/// # Examples
585///
586/// ```
587/// use rust_igraph::{Graph, dijkstra_distances_cutoff_with_mode, DijkstraMode};
588///
589/// let mut g = Graph::new(3, true).unwrap();
590/// g.add_edge(0, 1).unwrap();
591/// g.add_edge(1, 2).unwrap();
592/// let d = dijkstra_distances_cutoff_with_mode(
593///     &g, 0, &[1.0, 2.0], Some(1.5), DijkstraMode::Out
594/// ).unwrap();
595/// assert_eq!(d, vec![Some(0.0), Some(1.0), None]);
596/// ```
597pub fn dijkstra_distances_cutoff_with_mode(
598    graph: &Graph,
599    source: VertexId,
600    weights: &[f64],
601    cutoff: Option<f64>,
602    mode: DijkstraMode,
603) -> IgraphResult<Vec<Option<f64>>> {
604    validate_weights(graph, weights)?;
605    if let Some(c) = cutoff {
606        if c.is_nan() {
607            return Err(IgraphError::InvalidArgument("cutoff is NaN".into()));
608        }
609    }
610    let (dist, _) = dijkstra_inner(graph, &[source], weights, cutoff, false, mode)?;
611    let mut out = dist_vec_to_options(dist);
612    if let Some(c) = cutoff {
613        for d in &mut out {
614            if let Some(v) = *d {
615                if v > c {
616                    *d = None;
617                }
618            }
619        }
620    }
621    Ok(out)
622}
623
624/// Mode-aware [`dijkstra_distances_multi`].
625///
626/// # Examples
627///
628/// ```
629/// use rust_igraph::{Graph, dijkstra_distances_multi_with_mode, DijkstraMode};
630///
631/// let mut g = Graph::new(3, true).unwrap();
632/// g.add_edge(0, 1).unwrap();
633/// g.add_edge(1, 2).unwrap();
634/// let d = dijkstra_distances_multi_with_mode(
635///     &g, &[0, 2], &[1.0, 2.0], None, DijkstraMode::Out
636/// ).unwrap();
637/// assert_eq!(d[0], vec![Some(0.0), Some(1.0), Some(3.0)]);
638/// assert_eq!(d[1], vec![None, None, Some(0.0)]);
639/// ```
640pub fn dijkstra_distances_multi_with_mode(
641    graph: &Graph,
642    sources: &[VertexId],
643    weights: &[f64],
644    cutoff: Option<f64>,
645    mode: DijkstraMode,
646) -> IgraphResult<Vec<Vec<Option<f64>>>> {
647    validate_weights(graph, weights)?;
648    if let Some(c) = cutoff {
649        if c.is_nan() {
650            return Err(IgraphError::InvalidArgument("cutoff is NaN".into()));
651        }
652    }
653    let mut out = Vec::with_capacity(sources.len());
654    for &s in sources {
655        out.push(dijkstra_distances_cutoff_with_mode(
656            graph, s, weights, cutoff, mode,
657        )?);
658    }
659    Ok(out)
660}
661
662// ---------------------------------------------------------------
663// SP-001c: all-shortest-paths Dijkstra. Multiple parents per vertex
664// (DAG of equal-cost predecessors) + count of geodesics (`nrgeo`).
665// ---------------------------------------------------------------
666
667/// Output of [`dijkstra_all_shortest_paths`].
668#[derive(Debug, Clone, PartialEq)]
669pub struct DijkstraAllPaths {
670    /// `vertex_paths[v]` = every shortest source→v path as a vertex
671    /// chain (including source and v). Empty for unreachable v;
672    /// always `[[source]]` for v == source.
673    pub vertex_paths: Vec<Vec<Vec<VertexId>>>,
674    /// `edge_paths[v]` mirrors [`Self::vertex_paths`] but with edge ids
675    /// (length one less than the matching vertex chain). Empty for
676    /// unreachable v; `[[]]` for v == source.
677    pub edge_paths: Vec<Vec<Vec<EdgeId>>>,
678    /// `nrgeo[v]` = number of distinct shortest paths from source to
679    /// v. Zero for unreachable, one for the source itself.
680    pub nrgeo: Vec<u64>,
681}
682
683/// Tie-tolerant comparison of two distances. Mirrors upstream's
684/// `igraph_cmp_epsilon(a, b, eps)` which returns `sign(a − b)` modulo
685/// the epsilon band (positive if `a > b`, zero within epsilon,
686/// negative if `a < b`).
687fn cmp_eps(a: f64, b: f64) -> i32 {
688    const EPS: f64 = 1e-10; // `IGRAPH_SHORTEST_PATH_EPSILON`
689    if !a.is_finite() || !b.is_finite() {
690        // Treat infinities deterministically: only equal if both are
691        // identical bit-patterns (both +inf, both -inf, or both NaN —
692        // NaN can't appear here because validate_weights rejects it).
693        return match a.total_cmp(&b) {
694            Ordering::Equal => 0,
695            Ordering::Greater => 1,
696            Ordering::Less => -1,
697        };
698    }
699    let scale = a.abs().max(b.abs()).max(1.0);
700    let diff = a - b;
701    if diff.abs() <= EPS * scale {
702        0
703    } else if diff > 0.0 {
704        1
705    } else {
706        -1
707    }
708}
709
710/// All shortest weighted paths from `source` to every vertex,
711/// preserving every tie. Counterpart of
712/// `igraph_get_all_shortest_paths_dijkstra(_, _, _, _, source, vss_all(), weights, mode)`.
713///
714/// Mirrors upstream's tie handling: equal-cost alternative paths are
715/// recorded only when the connecting edge has non-zero weight (avoids
716/// infinite loops via 0-weight edges in undirected graphs). On
717/// undirected graphs every mode behaves identically.
718///
719/// # Examples
720///
721/// ```
722/// use rust_igraph::{Graph, dijkstra_all_shortest_paths, DijkstraMode};
723///
724/// // Diamond 0-1-3 and 0-2-3, all weight 1: two distinct shortest
725/// // paths to vertex 3.
726/// let mut g = Graph::with_vertices(4);
727/// g.add_edge(0, 1).unwrap();
728/// g.add_edge(0, 2).unwrap();
729/// g.add_edge(1, 3).unwrap();
730/// g.add_edge(2, 3).unwrap();
731/// let r = dijkstra_all_shortest_paths(&g, 0, &[1.0, 1.0, 1.0, 1.0], DijkstraMode::Out)
732///     .unwrap();
733/// assert_eq!(r.nrgeo, vec![1, 1, 1, 2]);
734/// assert_eq!(r.vertex_paths[3].len(), 2); // {0,1,3} and {0,2,3}
735/// ```
736pub fn dijkstra_all_shortest_paths(
737    graph: &Graph,
738    source: VertexId,
739    weights: &[f64],
740    mode: DijkstraMode,
741) -> IgraphResult<DijkstraAllPaths> {
742    let n = graph.vcount();
743    if source >= n {
744        return Err(IgraphError::VertexOutOfRange { id: source, n });
745    }
746    validate_weights(graph, weights)?;
747
748    let n_us = n as usize;
749    let mut dist = vec![f64::INFINITY; n_us];
750    // For each vertex: parent (predecessor) vertices in the shortest
751    // path DAG, plus the matching edge ids. Tracked separately so that
752    // a "shorter path found" event can clear both atomically.
753    let mut parents: Vec<Vec<VertexId>> = vec![Vec::new(); n_us];
754    let mut parent_eids: Vec<Vec<EdgeId>> = vec![Vec::new(); n_us];
755    // Order in which vertices were *settled* (popped from the heap with
756    // their final distance). Used as a topological order to compute
757    // `nrgeo` in linear time after the BFS.
758    let mut order: Vec<VertexId> = Vec::new();
759    let mut visited = vec![false; n_us];
760
761    let mut heap: BinaryHeap<Frontier> = BinaryHeap::new();
762    dist[source as usize] = 0.0;
763    heap.push(Frontier(0.0, source));
764
765    while let Some(Frontier(d, v)) = heap.pop() {
766        let v_us = v as usize;
767        if visited[v_us] {
768            continue;
769        }
770        visited[v_us] = true;
771        order.push(v);
772
773        for eid in incident_for_mode(graph, v, mode)? {
774            let w = weights[eid as usize];
775            if !w.is_finite() {
776                continue;
777            }
778            let other = graph.edge_other(eid as EdgeId, v)?;
779            let altdist = d + w;
780            let curdist = dist[other as usize];
781            let cmp = cmp_eps(curdist, altdist);
782            if curdist.is_infinite() {
783                // First time we reach `other`.
784                dist[other as usize] = altdist;
785                parents[other as usize].clear();
786                parents[other as usize].push(v);
787                parent_eids[other as usize].clear();
788                parent_eids[other as usize].push(eid);
789                heap.push(Frontier(altdist, other));
790            } else if cmp == 0 && w > 0.0 {
791                // Equal-cost alternative — record only if the
792                // connecting edge has positive weight (mirrors upstream's
793                // zero-weight loop guard).
794                parents[other as usize].push(v);
795                parent_eids[other as usize].push(eid);
796            } else if cmp > 0 {
797                // Strictly shorter — replace existing parents.
798                dist[other as usize] = altdist;
799                parents[other as usize].clear();
800                parents[other as usize].push(v);
801                parent_eids[other as usize].clear();
802                parent_eids[other as usize].push(eid);
803                heap.push(Frontier(altdist, other));
804            }
805        }
806    }
807
808    // Number of geodesics: source = 1; otherwise sum over parents.
809    // We process vertices in heap-settle order to ensure parents are
810    // counted before children.
811    let mut nrgeo: Vec<u64> = vec![0; n_us];
812    if dist[source as usize].is_finite() {
813        nrgeo[source as usize] = 1;
814    }
815    for &v in order.iter().skip(1) {
816        let mut acc: u64 = 0;
817        for &p in &parents[v as usize] {
818            acc = acc.saturating_add(nrgeo[p as usize]);
819        }
820        nrgeo[v as usize] = acc;
821    }
822
823    // Reconstruct vertex/edge paths by depth-first enumeration through
824    // the predecessor DAG. For each target v we walk every distinct
825    // chain source → ... → v and emit it.
826    let mut vertex_paths: Vec<Vec<Vec<VertexId>>> = vec![Vec::new(); n_us];
827    let mut edge_paths: Vec<Vec<Vec<EdgeId>>> = vec![Vec::new(); n_us];
828    if dist[source as usize].is_finite() {
829        vertex_paths[source as usize].push(vec![source]);
830        edge_paths[source as usize].push(Vec::new());
831    }
832    // Walk vertices in `order` (distance-ascending). For each non-source
833    // settled vertex, build its paths by extending each parent's paths.
834    for &v in order.iter().skip(1) {
835        let v_us = v as usize;
836        let parent_count = parents[v_us].len();
837        for i in 0..parent_count {
838            let p = parents[v_us][i];
839            let e = parent_eids[v_us][i];
840            // Snapshot lengths so we don't iterate over freshly pushed
841            // paths if `p == v_us` somehow; that shouldn't happen but
842            // it's cheap insurance.
843            let par_paths_len = vertex_paths[p as usize].len();
844            for j in 0..par_paths_len {
845                let mut vp = vertex_paths[p as usize][j].clone();
846                vp.push(v);
847                vertex_paths[v_us].push(vp);
848                let mut ep = edge_paths[p as usize][j].clone();
849                ep.push(e);
850                edge_paths[v_us].push(ep);
851            }
852        }
853    }
854
855    Ok(DijkstraAllPaths {
856        vertex_paths,
857        edge_paths,
858        nrgeo,
859    })
860}
861
862#[cfg(test)]
863mod tests {
864    use super::*;
865
866    #[test]
867    fn empty_graph_source_out_of_range() {
868        let g = Graph::with_vertices(0);
869        assert!(dijkstra_distances(&g, 0, &[]).is_err());
870    }
871
872    #[test]
873    fn singleton_distance_zero() {
874        let g = Graph::with_vertices(1);
875        assert_eq!(dijkstra_distances(&g, 0, &[]).unwrap(), vec![Some(0.0)]);
876    }
877
878    #[test]
879    fn unreachable_yields_none() {
880        let mut g = Graph::with_vertices(3);
881        g.add_edge(0, 1).unwrap();
882        let d = dijkstra_distances(&g, 0, &[1.0]).unwrap();
883        assert_eq!(d, vec![Some(0.0), Some(1.0), None]);
884    }
885
886    #[test]
887    fn shortcut_via_smaller_weights() {
888        let mut g = Graph::with_vertices(3);
889        g.add_edge(0, 1).unwrap();
890        g.add_edge(0, 2).unwrap();
891        g.add_edge(1, 2).unwrap();
892        let d = dijkstra_distances(&g, 0, &[1.0, 4.0, 2.0]).unwrap();
893        assert_eq!(d, vec![Some(0.0), Some(1.0), Some(3.0)]);
894    }
895
896    #[test]
897    fn directed_respects_edge_direction() {
898        let mut g = Graph::new(3, true).unwrap();
899        g.add_edge(0, 1).unwrap();
900        g.add_edge(2, 1).unwrap();
901        let d = dijkstra_distances(&g, 0, &[1.0, 1.0]).unwrap();
902        // 0 reaches 1 via direct edge; 2 has no incoming path from 0.
903        assert_eq!(d, vec![Some(0.0), Some(1.0), None]);
904    }
905
906    #[test]
907    fn weights_size_mismatch_errors() {
908        let mut g = Graph::with_vertices(2);
909        g.add_edge(0, 1).unwrap();
910        assert!(dijkstra_distances(&g, 0, &[]).is_err());
911        assert!(dijkstra_distances(&g, 0, &[1.0, 2.0]).is_err());
912    }
913
914    #[test]
915    fn negative_weight_errors() {
916        let mut g = Graph::with_vertices(2);
917        g.add_edge(0, 1).unwrap();
918        assert!(dijkstra_distances(&g, 0, &[-1.0]).is_err());
919    }
920
921    #[test]
922    fn nan_weight_errors() {
923        let mut g = Graph::with_vertices(2);
924        g.add_edge(0, 1).unwrap();
925        assert!(dijkstra_distances(&g, 0, &[f64::NAN]).is_err());
926    }
927
928    #[test]
929    fn infinite_weight_errors() {
930        let mut g = Graph::with_vertices(2);
931        g.add_edge(0, 1).unwrap();
932        assert!(dijkstra_distances(&g, 0, &[f64::INFINITY]).is_err());
933    }
934
935    #[test]
936    fn zero_weights_match_bfs_distance() {
937        // All edges weight 1.0 reduces Dijkstra to BFS distances.
938        let mut g = Graph::with_vertices(5);
939        for u in 0..4u32 {
940            g.add_edge(u, u + 1).unwrap();
941        }
942        let w = vec![1.0; 4];
943        let d = dijkstra_distances(&g, 0, &w).unwrap();
944        assert_eq!(
945            d,
946            vec![Some(0.0), Some(1.0), Some(2.0), Some(3.0), Some(4.0)]
947        );
948    }
949
950    #[test]
951    fn parallel_edges_pick_minimum_weight() {
952        let mut g = Graph::with_vertices(2);
953        g.add_edge(0, 1).unwrap();
954        g.add_edge(0, 1).unwrap();
955        let d = dijkstra_distances(&g, 0, &[5.0, 1.5]).unwrap();
956        assert_eq!(d, vec![Some(0.0), Some(1.5)]);
957    }
958
959    #[test]
960    fn star_graph_distances() {
961        // Star: vertex 0 is centre, vertices 1..=4 attached with weight i.
962        let mut g = Graph::with_vertices(5);
963        g.add_edge(0, 1).unwrap();
964        g.add_edge(0, 2).unwrap();
965        g.add_edge(0, 3).unwrap();
966        g.add_edge(0, 4).unwrap();
967        let w = vec![1.0, 2.5, 0.5, 7.0];
968        let d = dijkstra_distances(&g, 0, &w).unwrap();
969        assert_eq!(
970            d,
971            vec![Some(0.0), Some(1.0), Some(2.5), Some(0.5), Some(7.0)]
972        );
973    }
974
975    // ------------------ SP-001b: paths / parents / cutoff / multi -------------
976
977    fn shortcut_triangle() -> (Graph, Vec<f64>) {
978        let mut g = Graph::with_vertices(3);
979        g.add_edge(0, 1).unwrap(); // edge 0
980        g.add_edge(0, 2).unwrap(); // edge 1
981        g.add_edge(1, 2).unwrap(); // edge 2
982        (g, vec![1.0, 4.0, 2.0])
983    }
984
985    #[test]
986    fn paths_distances_match_dijkstra_distances() {
987        let (g, w) = shortcut_triangle();
988        let p = dijkstra_paths(&g, 0, &w).unwrap();
989        assert_eq!(p.distances, dijkstra_distances(&g, 0, &w).unwrap());
990    }
991
992    #[test]
993    fn paths_parents_and_inbound_edges_record_spt() {
994        let (g, w) = shortcut_triangle();
995        let p = dijkstra_paths(&g, 0, &w).unwrap();
996        // Source has no parent.
997        assert_eq!(p.parents[0], None);
998        assert_eq!(p.inbound_edges[0], None);
999        // 1 reached directly via edge 0.
1000        assert_eq!(p.parents[1], Some(0));
1001        assert_eq!(p.inbound_edges[1], Some(0));
1002        // 2 reached via the 1-2 shortcut, edge id 2.
1003        assert_eq!(p.parents[2], Some(1));
1004        assert_eq!(p.inbound_edges[2], Some(2));
1005    }
1006
1007    #[test]
1008    fn paths_unreachable_has_none_parent() {
1009        let mut g = Graph::with_vertices(3);
1010        g.add_edge(0, 1).unwrap();
1011        let p = dijkstra_paths(&g, 0, &[1.0]).unwrap();
1012        assert_eq!(p.distances, vec![Some(0.0), Some(1.0), None]);
1013        assert_eq!(p.parents, vec![None, Some(0), None]);
1014        assert_eq!(p.inbound_edges, vec![None, Some(0), None]);
1015    }
1016
1017    #[test]
1018    fn path_to_returns_vertex_and_edge_chain() {
1019        let (g, w) = shortcut_triangle();
1020        let (vs, es) = dijkstra_path_to(&g, 0, 2, &w).unwrap().unwrap();
1021        assert_eq!(vs, vec![0, 1, 2]);
1022        assert_eq!(es, vec![0, 2]);
1023    }
1024
1025    #[test]
1026    fn path_to_self_is_singleton() {
1027        let (g, w) = shortcut_triangle();
1028        let (vs, es) = dijkstra_path_to(&g, 0, 0, &w).unwrap().unwrap();
1029        assert_eq!(vs, vec![0]);
1030        assert!(es.is_empty());
1031    }
1032
1033    #[test]
1034    fn path_to_unreachable_is_none() {
1035        let mut g = Graph::with_vertices(3);
1036        g.add_edge(0, 1).unwrap();
1037        assert_eq!(dijkstra_path_to(&g, 0, 2, &[1.0]).unwrap(), None);
1038    }
1039
1040    #[test]
1041    fn path_to_target_out_of_range_errors() {
1042        let (g, w) = shortcut_triangle();
1043        assert!(dijkstra_path_to(&g, 0, 99, &w).is_err());
1044    }
1045
1046    #[test]
1047    fn cutoff_masks_distances_above_cutoff() {
1048        let mut g = Graph::with_vertices(5);
1049        for i in 0..4u32 {
1050            g.add_edge(i, i + 1).unwrap();
1051        }
1052        let w = vec![1.0; 4];
1053        let d = dijkstra_distances_cutoff(&g, 0, &w, Some(2.0)).unwrap();
1054        // Vertices reachable within distance 2: 0, 1, 2; 3 and 4 masked.
1055        assert_eq!(d, vec![Some(0.0), Some(1.0), Some(2.0), None, None]);
1056    }
1057
1058    #[test]
1059    fn cutoff_none_matches_unbounded_dijkstra() {
1060        let (g, w) = shortcut_triangle();
1061        assert_eq!(
1062            dijkstra_distances_cutoff(&g, 0, &w, None).unwrap(),
1063            dijkstra_distances(&g, 0, &w).unwrap()
1064        );
1065    }
1066
1067    #[test]
1068    fn cutoff_zero_keeps_only_source() {
1069        let (g, w) = shortcut_triangle();
1070        let d = dijkstra_distances_cutoff(&g, 0, &w, Some(0.0)).unwrap();
1071        assert_eq!(d, vec![Some(0.0), None, None]);
1072    }
1073
1074    #[test]
1075    fn cutoff_nan_errors() {
1076        let (g, w) = shortcut_triangle();
1077        assert!(dijkstra_distances_cutoff(&g, 0, &w, Some(f64::NAN)).is_err());
1078    }
1079
1080    #[test]
1081    fn multi_source_yields_per_source_distances() {
1082        let (g, w) = shortcut_triangle();
1083        let r = dijkstra_distances_multi(&g, &[0, 1], &w, None).unwrap();
1084        assert_eq!(r.len(), 2);
1085        assert_eq!(r[0], dijkstra_distances(&g, 0, &w).unwrap());
1086        assert_eq!(r[1], dijkstra_distances(&g, 1, &w).unwrap());
1087    }
1088
1089    #[test]
1090    fn multi_source_empty_list_yields_empty_result() {
1091        let (g, w) = shortcut_triangle();
1092        let r = dijkstra_distances_multi(&g, &[], &w, None).unwrap();
1093        assert!(r.is_empty());
1094    }
1095
1096    #[test]
1097    fn multi_source_propagates_cutoff() {
1098        let mut g = Graph::with_vertices(4);
1099        for i in 0..3u32 {
1100            g.add_edge(i, i + 1).unwrap();
1101        }
1102        let w = vec![1.0; 3];
1103        let r = dijkstra_distances_multi(&g, &[0, 3], &w, Some(1.0)).unwrap();
1104        assert_eq!(r[0], vec![Some(0.0), Some(1.0), None, None]);
1105        assert_eq!(r[1], vec![None, None, Some(1.0), Some(0.0)]);
1106    }
1107
1108    #[test]
1109    fn multi_source_invalid_vertex_errors() {
1110        let (g, w) = shortcut_triangle();
1111        assert!(dijkstra_distances_multi(&g, &[0, 99], &w, None).is_err());
1112    }
1113
1114    // ---- SP-001c: mode-aware + all-shortest-paths -----------------------
1115
1116    fn directed_path_3() -> (Graph, Vec<f64>) {
1117        let mut g = Graph::new(3, true).unwrap();
1118        g.add_edge(0, 1).unwrap();
1119        g.add_edge(1, 2).unwrap();
1120        (g, vec![1.0, 2.0])
1121    }
1122
1123    #[test]
1124    fn legacy_apis_match_with_mode_out() {
1125        let (g, w) = shortcut_triangle();
1126        assert_eq!(
1127            dijkstra_distances(&g, 0, &w).unwrap(),
1128            dijkstra_distances_with_mode(&g, 0, &w, DijkstraMode::Out).unwrap()
1129        );
1130        let p = dijkstra_paths(&g, 0, &w).unwrap();
1131        let pm = dijkstra_paths_with_mode(&g, 0, &w, DijkstraMode::Out).unwrap();
1132        assert_eq!(p, pm);
1133    }
1134
1135    #[test]
1136    fn directed_path_in_mode_reverses_reachability() {
1137        let (g, w) = directed_path_3();
1138        // OUT: from 0, reaches forward.
1139        assert_eq!(
1140            dijkstra_distances_with_mode(&g, 0, &w, DijkstraMode::Out).unwrap(),
1141            vec![Some(0.0), Some(1.0), Some(3.0)]
1142        );
1143        // IN: from 0, no incoming edges so nothing else reachable.
1144        assert_eq!(
1145            dijkstra_distances_with_mode(&g, 0, &w, DijkstraMode::In).unwrap(),
1146            vec![Some(0.0), None, None]
1147        );
1148        // ALL: undirected — vertex 2 reaches 0 too.
1149        assert_eq!(
1150            dijkstra_distances_with_mode(&g, 2, &w, DijkstraMode::All).unwrap(),
1151            vec![Some(3.0), Some(2.0), Some(0.0)]
1152        );
1153    }
1154
1155    #[test]
1156    fn undirected_modes_agree() {
1157        let (g, w) = shortcut_triangle();
1158        for m in [DijkstraMode::Out, DijkstraMode::In, DijkstraMode::All] {
1159            assert_eq!(
1160                dijkstra_distances_with_mode(&g, 0, &w, m).unwrap(),
1161                vec![Some(0.0), Some(1.0), Some(3.0)],
1162                "mode {m:?}"
1163            );
1164        }
1165    }
1166
1167    #[test]
1168    fn paths_with_mode_in_records_reverse_parents() {
1169        let (g, w) = directed_path_3();
1170        // IN-mode SPT from vertex 2: parents[1]=2, parents[0]=1.
1171        let p = dijkstra_paths_with_mode(&g, 2, &w, DijkstraMode::In).unwrap();
1172        assert_eq!(p.distances, vec![Some(3.0), Some(2.0), Some(0.0)]);
1173        assert_eq!(p.parents[2], None);
1174        assert_eq!(p.parents[1], Some(2));
1175        assert_eq!(p.parents[0], Some(1));
1176    }
1177
1178    #[test]
1179    fn path_to_with_mode_directed_in() {
1180        let (g, w) = directed_path_3();
1181        let (vs, es) = dijkstra_path_to_with_mode(&g, 2, 0, &w, DijkstraMode::In)
1182            .unwrap()
1183            .unwrap();
1184        assert_eq!(vs, vec![2, 1, 0]);
1185        assert_eq!(es, vec![1, 0]);
1186    }
1187
1188    #[test]
1189    fn path_to_with_mode_unreachable_via_out() {
1190        let (g, w) = directed_path_3();
1191        // OUT from 2: nothing else reachable (sink).
1192        assert_eq!(
1193            dijkstra_path_to_with_mode(&g, 2, 0, &w, DijkstraMode::Out).unwrap(),
1194            None
1195        );
1196    }
1197
1198    #[test]
1199    fn cutoff_with_mode_masks_above() {
1200        let (g, w) = directed_path_3();
1201        let d =
1202            dijkstra_distances_cutoff_with_mode(&g, 0, &w, Some(1.5), DijkstraMode::Out).unwrap();
1203        assert_eq!(d, vec![Some(0.0), Some(1.0), None]);
1204    }
1205
1206    #[test]
1207    fn multi_with_mode_sources_independent() {
1208        let (g, w) = directed_path_3();
1209        let r =
1210            dijkstra_distances_multi_with_mode(&g, &[0, 2], &w, None, DijkstraMode::All).unwrap();
1211        assert_eq!(r[0], vec![Some(0.0), Some(1.0), Some(3.0)]);
1212        assert_eq!(r[1], vec![Some(3.0), Some(2.0), Some(0.0)]);
1213    }
1214
1215    // ---- SP-001c: all-shortest-paths -------------------
1216
1217    #[test]
1218    fn all_paths_diamond_yields_two_geodesics() {
1219        // 0-1-3 and 0-2-3, all weight 1: two distinct shortest paths
1220        // to vertex 3.
1221        let mut g = Graph::with_vertices(4);
1222        g.add_edge(0, 1).unwrap(); // edge 0
1223        g.add_edge(0, 2).unwrap(); // edge 1
1224        g.add_edge(1, 3).unwrap(); // edge 2
1225        g.add_edge(2, 3).unwrap(); // edge 3
1226        let r = dijkstra_all_shortest_paths(&g, 0, &[1.0; 4], DijkstraMode::Out).unwrap();
1227        assert_eq!(r.nrgeo, vec![1, 1, 1, 2]);
1228        assert_eq!(r.vertex_paths[0], vec![vec![0]]);
1229        assert_eq!(r.edge_paths[0], vec![Vec::<EdgeId>::new()]);
1230        assert_eq!(r.vertex_paths[3].len(), 2);
1231        let mut paths: Vec<Vec<u32>> = r.vertex_paths[3].clone();
1232        paths.sort();
1233        assert_eq!(paths, vec![vec![0, 1, 3], vec![0, 2, 3]]);
1234    }
1235
1236    #[test]
1237    fn all_paths_unique_returns_single_chain() {
1238        let (g, w) = shortcut_triangle();
1239        let r = dijkstra_all_shortest_paths(&g, 0, &w, DijkstraMode::Out).unwrap();
1240        assert_eq!(r.nrgeo, vec![1, 1, 1]);
1241        assert_eq!(r.vertex_paths[2], vec![vec![0, 1, 2]]);
1242        assert_eq!(r.edge_paths[2], vec![vec![0, 2]]);
1243    }
1244
1245    #[test]
1246    fn all_paths_unreachable_emits_empty() {
1247        let mut g = Graph::with_vertices(3);
1248        g.add_edge(0, 1).unwrap();
1249        let r = dijkstra_all_shortest_paths(&g, 0, &[1.0], DijkstraMode::Out).unwrap();
1250        assert_eq!(r.nrgeo, vec![1, 1, 0]);
1251        assert!(r.vertex_paths[2].is_empty());
1252        assert!(r.edge_paths[2].is_empty());
1253    }
1254
1255    #[test]
1256    fn all_paths_directed_in_mode() {
1257        let (g, w) = directed_path_3();
1258        let r = dijkstra_all_shortest_paths(&g, 2, &w, DijkstraMode::In).unwrap();
1259        assert_eq!(r.nrgeo, vec![1, 1, 1]);
1260        assert_eq!(r.vertex_paths[0], vec![vec![2, 1, 0]]);
1261        assert_eq!(r.edge_paths[0], vec![vec![1, 0]]);
1262    }
1263
1264    #[test]
1265    fn all_paths_invalid_source_errors() {
1266        let (g, _) = shortcut_triangle();
1267        assert!(dijkstra_all_shortest_paths(&g, 99, &[1.0; 3], DijkstraMode::Out).is_err());
1268    }
1269
1270    #[test]
1271    fn all_paths_zero_weight_alt_is_dropped_by_guard() {
1272        // Triangle with one zero-weight edge (1→2). Upstream's
1273        // alt-equal-paths branch only fires when the *connecting* edge
1274        // has positive weight. So the alt path 0→1→2 (cost 1+0=1)
1275        // ties the direct 0→2 (cost 1) but is *not* recorded — the
1276        // 1→2 hop has weight 0. We end up with the direct edge only.
1277        let mut g = Graph::with_vertices(3);
1278        g.add_edge(0, 1).unwrap(); // edge 0, weight 1
1279        g.add_edge(1, 2).unwrap(); // edge 1, weight 0
1280        g.add_edge(0, 2).unwrap(); // edge 2, weight 1
1281        let r = dijkstra_all_shortest_paths(&g, 0, &[1.0, 0.0, 1.0], DijkstraMode::Out).unwrap();
1282        assert_eq!(r.nrgeo[2], 1);
1283        assert_eq!(r.vertex_paths[2], vec![vec![0, 2]]);
1284    }
1285}