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 parent = p.parents[cur as usize].expect("inbound edge implies parent exists");
348        vs.push(cur);
349        cur = parent;
350    }
351    vs.push(cur); // push source last
352    vs.reverse();
353    es.reverse();
354    Ok(Some((vs, es)))
355}
356
357/// Single-source Dijkstra distances with an optional `cutoff`. When
358/// `cutoff = Some(c)`, vertices whose shortest-path distance exceeds
359/// `c` are returned as `None` (unreachable-within-cutoff); the search
360/// stops relaxing edges out of any vertex whose `mindist > c`.
361///
362/// `cutoff = None` is identical to [`dijkstra_distances`].
363///
364/// Counterpart of `igraph_distances_dijkstra_cutoff(..., IGRAPH_OUT, cutoff)`
365/// for a single source. Upstream uses `cutoff < 0` to disable; this
366/// Rust port uses `None`.
367pub fn dijkstra_distances_cutoff(
368    graph: &Graph,
369    source: VertexId,
370    weights: &[f64],
371    cutoff: Option<f64>,
372) -> IgraphResult<Vec<Option<f64>>> {
373    validate_weights(graph, weights)?;
374    if let Some(c) = cutoff {
375        if c.is_nan() {
376            return Err(IgraphError::InvalidArgument("cutoff is NaN".into()));
377        }
378    }
379    let (dist, _) = dijkstra_inner(graph, &[source], weights, cutoff, false, DijkstraMode::Out)?;
380    let mut out = dist_vec_to_options(dist);
381    if let Some(c) = cutoff {
382        // The relax loop already prunes outgoing edges past `c`, but a
383        // vertex with `dist == c + ε` may have been settled before the
384        // skip kicked in (its outgoing edges were just not relaxed).
385        // To match upstream's "distances within cutoff only" semantics
386        // we mask anything above `c` to `None` after the fact.
387        for d in &mut out {
388            if let Some(v) = *d {
389                if v > c {
390                    *d = None;
391                }
392            }
393        }
394    }
395    Ok(out)
396}
397
398/// Multi-source Dijkstra distances. `result[i][v]` is the shortest
399/// distance from `sources[i]` to `v` (or `None` if unreachable / past
400/// the cutoff).
401///
402/// Counterpart of `igraph_distances_dijkstra_cutoff(..., from=sources,
403/// to=igraph_vss_all(), weights, IGRAPH_OUT, cutoff)`. Each source is
404/// run independently — upstream loops over `fromvit` doing exactly
405/// the same.
406pub fn dijkstra_distances_multi(
407    graph: &Graph,
408    sources: &[VertexId],
409    weights: &[f64],
410    cutoff: Option<f64>,
411) -> IgraphResult<Vec<Vec<Option<f64>>>> {
412    validate_weights(graph, weights)?;
413    if let Some(c) = cutoff {
414        if c.is_nan() {
415            return Err(IgraphError::InvalidArgument("cutoff is NaN".into()));
416        }
417    }
418    let mut out = Vec::with_capacity(sources.len());
419    for &s in sources {
420        out.push(dijkstra_distances_cutoff(graph, s, weights, cutoff)?);
421    }
422    Ok(out)
423}
424
425// ---------------------------------------------------------------
426// SP-001c: mode-aware variants of every SP-001/SP-001b entry point.
427// On undirected graphs every mode behaves identically (matches
428// upstream — every edge is bidirectional).
429// ---------------------------------------------------------------
430
431/// Mode-aware Dijkstra distances. Counterpart of
432/// `igraph_distances_dijkstra(_, _, source, vss_all(), weights, mode)`.
433///
434/// # Examples
435///
436/// ```
437/// use rust_igraph::{Graph, dijkstra_distances_with_mode, DijkstraMode};
438///
439/// // Directed path 0→1→2: OUT reaches forward, IN reaches backward,
440/// // ALL collapses to undirected.
441/// let mut g = Graph::new(3, true).unwrap();
442/// g.add_edge(0, 1).unwrap();
443/// g.add_edge(1, 2).unwrap();
444/// let w = [1.0, 2.0];
445/// assert_eq!(
446///     dijkstra_distances_with_mode(&g, 0, &w, DijkstraMode::Out).unwrap(),
447///     vec![Some(0.0), Some(1.0), Some(3.0)]
448/// );
449/// assert_eq!(
450///     dijkstra_distances_with_mode(&g, 0, &w, DijkstraMode::In).unwrap(),
451///     vec![Some(0.0), None, None]
452/// );
453/// ```
454pub fn dijkstra_distances_with_mode(
455    graph: &Graph,
456    source: VertexId,
457    weights: &[f64],
458    mode: DijkstraMode,
459) -> IgraphResult<Vec<Option<f64>>> {
460    validate_weights(graph, weights)?;
461    let (dist, _) = dijkstra_inner(graph, &[source], weights, None, false, mode)?;
462    Ok(dist_vec_to_options(dist))
463}
464
465/// Mode-aware [`dijkstra_paths`]. Counterpart of
466/// `igraph_get_shortest_paths_dijkstra(_, _, _, source, vss_all(), weights, mode, parents, inbound_edges)`.
467pub fn dijkstra_paths_with_mode(
468    graph: &Graph,
469    source: VertexId,
470    weights: &[f64],
471    mode: DijkstraMode,
472) -> IgraphResult<DijkstraPaths> {
473    validate_weights(graph, weights)?;
474    let (dist, inbound) = dijkstra_inner(graph, &[source], weights, None, true, mode)?;
475    let mut parents = vec![None; graph.vcount() as usize];
476    for v in 0..graph.vcount() {
477        if let Some(eid) = inbound[v as usize] {
478            let other = graph.edge_other(eid, v)?;
479            parents[v as usize] = Some(other);
480        }
481    }
482    Ok(DijkstraPaths {
483        distances: dist_vec_to_options(dist),
484        parents,
485        inbound_edges: inbound,
486    })
487}
488
489/// Mode-aware [`dijkstra_path_to`]. Counterpart of
490/// `igraph_get_shortest_path_dijkstra(_, _, _, source, target, weights, mode)`.
491pub fn dijkstra_path_to_with_mode(
492    graph: &Graph,
493    source: VertexId,
494    target: VertexId,
495    weights: &[f64],
496    mode: DijkstraMode,
497) -> IgraphResult<Option<(Vec<VertexId>, Vec<EdgeId>)>> {
498    let n = graph.vcount();
499    if target >= n {
500        return Err(IgraphError::VertexOutOfRange { id: target, n });
501    }
502    let p = dijkstra_paths_with_mode(graph, source, weights, mode)?;
503    if p.distances[target as usize].is_none() {
504        return Ok(None);
505    }
506    let mut vs = Vec::new();
507    let mut es = Vec::new();
508    let mut cur = target;
509    while let Some(eid) = p.inbound_edges[cur as usize] {
510        es.push(eid);
511        let parent = p.parents[cur as usize].expect("inbound edge implies parent exists");
512        vs.push(cur);
513        cur = parent;
514    }
515    vs.push(cur);
516    vs.reverse();
517    es.reverse();
518    Ok(Some((vs, es)))
519}
520
521/// Mode-aware [`dijkstra_distances_cutoff`]. Counterpart of
522/// `igraph_distances_dijkstra_cutoff(_, _, source, vss_all(), weights, mode, cutoff)`.
523pub fn dijkstra_distances_cutoff_with_mode(
524    graph: &Graph,
525    source: VertexId,
526    weights: &[f64],
527    cutoff: Option<f64>,
528    mode: DijkstraMode,
529) -> IgraphResult<Vec<Option<f64>>> {
530    validate_weights(graph, weights)?;
531    if let Some(c) = cutoff {
532        if c.is_nan() {
533            return Err(IgraphError::InvalidArgument("cutoff is NaN".into()));
534        }
535    }
536    let (dist, _) = dijkstra_inner(graph, &[source], weights, cutoff, false, mode)?;
537    let mut out = dist_vec_to_options(dist);
538    if let Some(c) = cutoff {
539        for d in &mut out {
540            if let Some(v) = *d {
541                if v > c {
542                    *d = None;
543                }
544            }
545        }
546    }
547    Ok(out)
548}
549
550/// Mode-aware [`dijkstra_distances_multi`].
551pub fn dijkstra_distances_multi_with_mode(
552    graph: &Graph,
553    sources: &[VertexId],
554    weights: &[f64],
555    cutoff: Option<f64>,
556    mode: DijkstraMode,
557) -> IgraphResult<Vec<Vec<Option<f64>>>> {
558    validate_weights(graph, weights)?;
559    if let Some(c) = cutoff {
560        if c.is_nan() {
561            return Err(IgraphError::InvalidArgument("cutoff is NaN".into()));
562        }
563    }
564    let mut out = Vec::with_capacity(sources.len());
565    for &s in sources {
566        out.push(dijkstra_distances_cutoff_with_mode(
567            graph, s, weights, cutoff, mode,
568        )?);
569    }
570    Ok(out)
571}
572
573// ---------------------------------------------------------------
574// SP-001c: all-shortest-paths Dijkstra. Multiple parents per vertex
575// (DAG of equal-cost predecessors) + count of geodesics (`nrgeo`).
576// ---------------------------------------------------------------
577
578/// Output of [`dijkstra_all_shortest_paths`].
579#[derive(Debug, Clone, PartialEq)]
580pub struct DijkstraAllPaths {
581    /// `vertex_paths[v]` = every shortest source→v path as a vertex
582    /// chain (including source and v). Empty for unreachable v;
583    /// always `[[source]]` for v == source.
584    pub vertex_paths: Vec<Vec<Vec<VertexId>>>,
585    /// `edge_paths[v]` mirrors [`Self::vertex_paths`] but with edge ids
586    /// (length one less than the matching vertex chain). Empty for
587    /// unreachable v; `[[]]` for v == source.
588    pub edge_paths: Vec<Vec<Vec<EdgeId>>>,
589    /// `nrgeo[v]` = number of distinct shortest paths from source to
590    /// v. Zero for unreachable, one for the source itself.
591    pub nrgeo: Vec<u64>,
592}
593
594/// Tie-tolerant comparison of two distances. Mirrors upstream's
595/// `igraph_cmp_epsilon(a, b, eps)` which returns `sign(a − b)` modulo
596/// the epsilon band (positive if `a > b`, zero within epsilon,
597/// negative if `a < b`).
598fn cmp_eps(a: f64, b: f64) -> i32 {
599    const EPS: f64 = 1e-10; // `IGRAPH_SHORTEST_PATH_EPSILON`
600    if !a.is_finite() || !b.is_finite() {
601        // Treat infinities deterministically: only equal if both are
602        // identical bit-patterns (both +inf, both -inf, or both NaN —
603        // NaN can't appear here because validate_weights rejects it).
604        return match a.total_cmp(&b) {
605            Ordering::Equal => 0,
606            Ordering::Greater => 1,
607            Ordering::Less => -1,
608        };
609    }
610    let scale = a.abs().max(b.abs()).max(1.0);
611    let diff = a - b;
612    if diff.abs() <= EPS * scale {
613        0
614    } else if diff > 0.0 {
615        1
616    } else {
617        -1
618    }
619}
620
621/// All shortest weighted paths from `source` to every vertex,
622/// preserving every tie. Counterpart of
623/// `igraph_get_all_shortest_paths_dijkstra(_, _, _, _, source, vss_all(), weights, mode)`.
624///
625/// Mirrors upstream's tie handling: equal-cost alternative paths are
626/// recorded only when the connecting edge has non-zero weight (avoids
627/// infinite loops via 0-weight edges in undirected graphs). On
628/// undirected graphs every mode behaves identically.
629///
630/// # Examples
631///
632/// ```
633/// use rust_igraph::{Graph, dijkstra_all_shortest_paths, DijkstraMode};
634///
635/// // Diamond 0-1-3 and 0-2-3, all weight 1: two distinct shortest
636/// // paths to vertex 3.
637/// let mut g = Graph::with_vertices(4);
638/// g.add_edge(0, 1).unwrap();
639/// g.add_edge(0, 2).unwrap();
640/// g.add_edge(1, 3).unwrap();
641/// g.add_edge(2, 3).unwrap();
642/// let r = dijkstra_all_shortest_paths(&g, 0, &[1.0, 1.0, 1.0, 1.0], DijkstraMode::Out)
643///     .unwrap();
644/// assert_eq!(r.nrgeo, vec![1, 1, 1, 2]);
645/// assert_eq!(r.vertex_paths[3].len(), 2); // {0,1,3} and {0,2,3}
646/// ```
647pub fn dijkstra_all_shortest_paths(
648    graph: &Graph,
649    source: VertexId,
650    weights: &[f64],
651    mode: DijkstraMode,
652) -> IgraphResult<DijkstraAllPaths> {
653    let n = graph.vcount();
654    if source >= n {
655        return Err(IgraphError::VertexOutOfRange { id: source, n });
656    }
657    validate_weights(graph, weights)?;
658
659    let n_us = n as usize;
660    let mut dist = vec![f64::INFINITY; n_us];
661    // For each vertex: parent (predecessor) vertices in the shortest
662    // path DAG, plus the matching edge ids. Tracked separately so that
663    // a "shorter path found" event can clear both atomically.
664    let mut parents: Vec<Vec<VertexId>> = vec![Vec::new(); n_us];
665    let mut parent_eids: Vec<Vec<EdgeId>> = vec![Vec::new(); n_us];
666    // Order in which vertices were *settled* (popped from the heap with
667    // their final distance). Used as a topological order to compute
668    // `nrgeo` in linear time after the BFS.
669    let mut order: Vec<VertexId> = Vec::new();
670    let mut visited = vec![false; n_us];
671
672    let mut heap: BinaryHeap<Frontier> = BinaryHeap::new();
673    dist[source as usize] = 0.0;
674    heap.push(Frontier(0.0, source));
675
676    while let Some(Frontier(d, v)) = heap.pop() {
677        let v_us = v as usize;
678        if visited[v_us] {
679            continue;
680        }
681        visited[v_us] = true;
682        order.push(v);
683
684        for eid in incident_for_mode(graph, v, mode)? {
685            let w = weights[eid as usize];
686            if !w.is_finite() {
687                continue;
688            }
689            let other = graph.edge_other(eid as EdgeId, v)?;
690            let altdist = d + w;
691            let curdist = dist[other as usize];
692            let cmp = cmp_eps(curdist, altdist);
693            if curdist.is_infinite() {
694                // First time we reach `other`.
695                dist[other as usize] = altdist;
696                parents[other as usize].clear();
697                parents[other as usize].push(v);
698                parent_eids[other as usize].clear();
699                parent_eids[other as usize].push(eid);
700                heap.push(Frontier(altdist, other));
701            } else if cmp == 0 && w > 0.0 {
702                // Equal-cost alternative — record only if the
703                // connecting edge has positive weight (mirrors upstream's
704                // zero-weight loop guard).
705                parents[other as usize].push(v);
706                parent_eids[other as usize].push(eid);
707            } else if cmp > 0 {
708                // Strictly shorter — replace existing parents.
709                dist[other as usize] = altdist;
710                parents[other as usize].clear();
711                parents[other as usize].push(v);
712                parent_eids[other as usize].clear();
713                parent_eids[other as usize].push(eid);
714                heap.push(Frontier(altdist, other));
715            }
716        }
717    }
718
719    // Number of geodesics: source = 1; otherwise sum over parents.
720    // We process vertices in heap-settle order to ensure parents are
721    // counted before children.
722    let mut nrgeo: Vec<u64> = vec![0; n_us];
723    if dist[source as usize].is_finite() {
724        nrgeo[source as usize] = 1;
725    }
726    for &v in order.iter().skip(1) {
727        let mut acc: u64 = 0;
728        for &p in &parents[v as usize] {
729            acc = acc.saturating_add(nrgeo[p as usize]);
730        }
731        nrgeo[v as usize] = acc;
732    }
733
734    // Reconstruct vertex/edge paths by depth-first enumeration through
735    // the predecessor DAG. For each target v we walk every distinct
736    // chain source → ... → v and emit it.
737    let mut vertex_paths: Vec<Vec<Vec<VertexId>>> = vec![Vec::new(); n_us];
738    let mut edge_paths: Vec<Vec<Vec<EdgeId>>> = vec![Vec::new(); n_us];
739    if dist[source as usize].is_finite() {
740        vertex_paths[source as usize].push(vec![source]);
741        edge_paths[source as usize].push(Vec::new());
742    }
743    // Walk vertices in `order` (distance-ascending). For each non-source
744    // settled vertex, build its paths by extending each parent's paths.
745    for &v in order.iter().skip(1) {
746        let v_us = v as usize;
747        let parent_count = parents[v_us].len();
748        for i in 0..parent_count {
749            let p = parents[v_us][i];
750            let e = parent_eids[v_us][i];
751            // Snapshot lengths so we don't iterate over freshly pushed
752            // paths if `p == v_us` somehow; that shouldn't happen but
753            // it's cheap insurance.
754            let par_paths_len = vertex_paths[p as usize].len();
755            for j in 0..par_paths_len {
756                let mut vp = vertex_paths[p as usize][j].clone();
757                vp.push(v);
758                vertex_paths[v_us].push(vp);
759                let mut ep = edge_paths[p as usize][j].clone();
760                ep.push(e);
761                edge_paths[v_us].push(ep);
762            }
763        }
764    }
765
766    Ok(DijkstraAllPaths {
767        vertex_paths,
768        edge_paths,
769        nrgeo,
770    })
771}
772
773#[cfg(test)]
774mod tests {
775    use super::*;
776
777    #[test]
778    fn empty_graph_source_out_of_range() {
779        let g = Graph::with_vertices(0);
780        assert!(dijkstra_distances(&g, 0, &[]).is_err());
781    }
782
783    #[test]
784    fn singleton_distance_zero() {
785        let g = Graph::with_vertices(1);
786        assert_eq!(dijkstra_distances(&g, 0, &[]).unwrap(), vec![Some(0.0)]);
787    }
788
789    #[test]
790    fn unreachable_yields_none() {
791        let mut g = Graph::with_vertices(3);
792        g.add_edge(0, 1).unwrap();
793        let d = dijkstra_distances(&g, 0, &[1.0]).unwrap();
794        assert_eq!(d, vec![Some(0.0), Some(1.0), None]);
795    }
796
797    #[test]
798    fn shortcut_via_smaller_weights() {
799        let mut g = Graph::with_vertices(3);
800        g.add_edge(0, 1).unwrap();
801        g.add_edge(0, 2).unwrap();
802        g.add_edge(1, 2).unwrap();
803        let d = dijkstra_distances(&g, 0, &[1.0, 4.0, 2.0]).unwrap();
804        assert_eq!(d, vec![Some(0.0), Some(1.0), Some(3.0)]);
805    }
806
807    #[test]
808    fn directed_respects_edge_direction() {
809        let mut g = Graph::new(3, true).unwrap();
810        g.add_edge(0, 1).unwrap();
811        g.add_edge(2, 1).unwrap();
812        let d = dijkstra_distances(&g, 0, &[1.0, 1.0]).unwrap();
813        // 0 reaches 1 via direct edge; 2 has no incoming path from 0.
814        assert_eq!(d, vec![Some(0.0), Some(1.0), None]);
815    }
816
817    #[test]
818    fn weights_size_mismatch_errors() {
819        let mut g = Graph::with_vertices(2);
820        g.add_edge(0, 1).unwrap();
821        assert!(dijkstra_distances(&g, 0, &[]).is_err());
822        assert!(dijkstra_distances(&g, 0, &[1.0, 2.0]).is_err());
823    }
824
825    #[test]
826    fn negative_weight_errors() {
827        let mut g = Graph::with_vertices(2);
828        g.add_edge(0, 1).unwrap();
829        assert!(dijkstra_distances(&g, 0, &[-1.0]).is_err());
830    }
831
832    #[test]
833    fn nan_weight_errors() {
834        let mut g = Graph::with_vertices(2);
835        g.add_edge(0, 1).unwrap();
836        assert!(dijkstra_distances(&g, 0, &[f64::NAN]).is_err());
837    }
838
839    #[test]
840    fn infinite_weight_errors() {
841        let mut g = Graph::with_vertices(2);
842        g.add_edge(0, 1).unwrap();
843        assert!(dijkstra_distances(&g, 0, &[f64::INFINITY]).is_err());
844    }
845
846    #[test]
847    fn zero_weights_match_bfs_distance() {
848        // All edges weight 1.0 reduces Dijkstra to BFS distances.
849        let mut g = Graph::with_vertices(5);
850        for u in 0..4u32 {
851            g.add_edge(u, u + 1).unwrap();
852        }
853        let w = vec![1.0; 4];
854        let d = dijkstra_distances(&g, 0, &w).unwrap();
855        assert_eq!(
856            d,
857            vec![Some(0.0), Some(1.0), Some(2.0), Some(3.0), Some(4.0)]
858        );
859    }
860
861    #[test]
862    fn parallel_edges_pick_minimum_weight() {
863        let mut g = Graph::with_vertices(2);
864        g.add_edge(0, 1).unwrap();
865        g.add_edge(0, 1).unwrap();
866        let d = dijkstra_distances(&g, 0, &[5.0, 1.5]).unwrap();
867        assert_eq!(d, vec![Some(0.0), Some(1.5)]);
868    }
869
870    #[test]
871    fn star_graph_distances() {
872        // Star: vertex 0 is centre, vertices 1..=4 attached with weight i.
873        let mut g = Graph::with_vertices(5);
874        g.add_edge(0, 1).unwrap();
875        g.add_edge(0, 2).unwrap();
876        g.add_edge(0, 3).unwrap();
877        g.add_edge(0, 4).unwrap();
878        let w = vec![1.0, 2.5, 0.5, 7.0];
879        let d = dijkstra_distances(&g, 0, &w).unwrap();
880        assert_eq!(
881            d,
882            vec![Some(0.0), Some(1.0), Some(2.5), Some(0.5), Some(7.0)]
883        );
884    }
885
886    // ------------------ SP-001b: paths / parents / cutoff / multi -------------
887
888    fn shortcut_triangle() -> (Graph, Vec<f64>) {
889        let mut g = Graph::with_vertices(3);
890        g.add_edge(0, 1).unwrap(); // edge 0
891        g.add_edge(0, 2).unwrap(); // edge 1
892        g.add_edge(1, 2).unwrap(); // edge 2
893        (g, vec![1.0, 4.0, 2.0])
894    }
895
896    #[test]
897    fn paths_distances_match_dijkstra_distances() {
898        let (g, w) = shortcut_triangle();
899        let p = dijkstra_paths(&g, 0, &w).unwrap();
900        assert_eq!(p.distances, dijkstra_distances(&g, 0, &w).unwrap());
901    }
902
903    #[test]
904    fn paths_parents_and_inbound_edges_record_spt() {
905        let (g, w) = shortcut_triangle();
906        let p = dijkstra_paths(&g, 0, &w).unwrap();
907        // Source has no parent.
908        assert_eq!(p.parents[0], None);
909        assert_eq!(p.inbound_edges[0], None);
910        // 1 reached directly via edge 0.
911        assert_eq!(p.parents[1], Some(0));
912        assert_eq!(p.inbound_edges[1], Some(0));
913        // 2 reached via the 1-2 shortcut, edge id 2.
914        assert_eq!(p.parents[2], Some(1));
915        assert_eq!(p.inbound_edges[2], Some(2));
916    }
917
918    #[test]
919    fn paths_unreachable_has_none_parent() {
920        let mut g = Graph::with_vertices(3);
921        g.add_edge(0, 1).unwrap();
922        let p = dijkstra_paths(&g, 0, &[1.0]).unwrap();
923        assert_eq!(p.distances, vec![Some(0.0), Some(1.0), None]);
924        assert_eq!(p.parents, vec![None, Some(0), None]);
925        assert_eq!(p.inbound_edges, vec![None, Some(0), None]);
926    }
927
928    #[test]
929    fn path_to_returns_vertex_and_edge_chain() {
930        let (g, w) = shortcut_triangle();
931        let (vs, es) = dijkstra_path_to(&g, 0, 2, &w).unwrap().unwrap();
932        assert_eq!(vs, vec![0, 1, 2]);
933        assert_eq!(es, vec![0, 2]);
934    }
935
936    #[test]
937    fn path_to_self_is_singleton() {
938        let (g, w) = shortcut_triangle();
939        let (vs, es) = dijkstra_path_to(&g, 0, 0, &w).unwrap().unwrap();
940        assert_eq!(vs, vec![0]);
941        assert!(es.is_empty());
942    }
943
944    #[test]
945    fn path_to_unreachable_is_none() {
946        let mut g = Graph::with_vertices(3);
947        g.add_edge(0, 1).unwrap();
948        assert_eq!(dijkstra_path_to(&g, 0, 2, &[1.0]).unwrap(), None);
949    }
950
951    #[test]
952    fn path_to_target_out_of_range_errors() {
953        let (g, w) = shortcut_triangle();
954        assert!(dijkstra_path_to(&g, 0, 99, &w).is_err());
955    }
956
957    #[test]
958    fn cutoff_masks_distances_above_cutoff() {
959        let mut g = Graph::with_vertices(5);
960        for i in 0..4u32 {
961            g.add_edge(i, i + 1).unwrap();
962        }
963        let w = vec![1.0; 4];
964        let d = dijkstra_distances_cutoff(&g, 0, &w, Some(2.0)).unwrap();
965        // Vertices reachable within distance 2: 0, 1, 2; 3 and 4 masked.
966        assert_eq!(d, vec![Some(0.0), Some(1.0), Some(2.0), None, None]);
967    }
968
969    #[test]
970    fn cutoff_none_matches_unbounded_dijkstra() {
971        let (g, w) = shortcut_triangle();
972        assert_eq!(
973            dijkstra_distances_cutoff(&g, 0, &w, None).unwrap(),
974            dijkstra_distances(&g, 0, &w).unwrap()
975        );
976    }
977
978    #[test]
979    fn cutoff_zero_keeps_only_source() {
980        let (g, w) = shortcut_triangle();
981        let d = dijkstra_distances_cutoff(&g, 0, &w, Some(0.0)).unwrap();
982        assert_eq!(d, vec![Some(0.0), None, None]);
983    }
984
985    #[test]
986    fn cutoff_nan_errors() {
987        let (g, w) = shortcut_triangle();
988        assert!(dijkstra_distances_cutoff(&g, 0, &w, Some(f64::NAN)).is_err());
989    }
990
991    #[test]
992    fn multi_source_yields_per_source_distances() {
993        let (g, w) = shortcut_triangle();
994        let r = dijkstra_distances_multi(&g, &[0, 1], &w, None).unwrap();
995        assert_eq!(r.len(), 2);
996        assert_eq!(r[0], dijkstra_distances(&g, 0, &w).unwrap());
997        assert_eq!(r[1], dijkstra_distances(&g, 1, &w).unwrap());
998    }
999
1000    #[test]
1001    fn multi_source_empty_list_yields_empty_result() {
1002        let (g, w) = shortcut_triangle();
1003        let r = dijkstra_distances_multi(&g, &[], &w, None).unwrap();
1004        assert!(r.is_empty());
1005    }
1006
1007    #[test]
1008    fn multi_source_propagates_cutoff() {
1009        let mut g = Graph::with_vertices(4);
1010        for i in 0..3u32 {
1011            g.add_edge(i, i + 1).unwrap();
1012        }
1013        let w = vec![1.0; 3];
1014        let r = dijkstra_distances_multi(&g, &[0, 3], &w, Some(1.0)).unwrap();
1015        assert_eq!(r[0], vec![Some(0.0), Some(1.0), None, None]);
1016        assert_eq!(r[1], vec![None, None, Some(1.0), Some(0.0)]);
1017    }
1018
1019    #[test]
1020    fn multi_source_invalid_vertex_errors() {
1021        let (g, w) = shortcut_triangle();
1022        assert!(dijkstra_distances_multi(&g, &[0, 99], &w, None).is_err());
1023    }
1024
1025    // ---- SP-001c: mode-aware + all-shortest-paths -----------------------
1026
1027    fn directed_path_3() -> (Graph, Vec<f64>) {
1028        let mut g = Graph::new(3, true).unwrap();
1029        g.add_edge(0, 1).unwrap();
1030        g.add_edge(1, 2).unwrap();
1031        (g, vec![1.0, 2.0])
1032    }
1033
1034    #[test]
1035    fn legacy_apis_match_with_mode_out() {
1036        let (g, w) = shortcut_triangle();
1037        assert_eq!(
1038            dijkstra_distances(&g, 0, &w).unwrap(),
1039            dijkstra_distances_with_mode(&g, 0, &w, DijkstraMode::Out).unwrap()
1040        );
1041        let p = dijkstra_paths(&g, 0, &w).unwrap();
1042        let pm = dijkstra_paths_with_mode(&g, 0, &w, DijkstraMode::Out).unwrap();
1043        assert_eq!(p, pm);
1044    }
1045
1046    #[test]
1047    fn directed_path_in_mode_reverses_reachability() {
1048        let (g, w) = directed_path_3();
1049        // OUT: from 0, reaches forward.
1050        assert_eq!(
1051            dijkstra_distances_with_mode(&g, 0, &w, DijkstraMode::Out).unwrap(),
1052            vec![Some(0.0), Some(1.0), Some(3.0)]
1053        );
1054        // IN: from 0, no incoming edges so nothing else reachable.
1055        assert_eq!(
1056            dijkstra_distances_with_mode(&g, 0, &w, DijkstraMode::In).unwrap(),
1057            vec![Some(0.0), None, None]
1058        );
1059        // ALL: undirected — vertex 2 reaches 0 too.
1060        assert_eq!(
1061            dijkstra_distances_with_mode(&g, 2, &w, DijkstraMode::All).unwrap(),
1062            vec![Some(3.0), Some(2.0), Some(0.0)]
1063        );
1064    }
1065
1066    #[test]
1067    fn undirected_modes_agree() {
1068        let (g, w) = shortcut_triangle();
1069        for m in [DijkstraMode::Out, DijkstraMode::In, DijkstraMode::All] {
1070            assert_eq!(
1071                dijkstra_distances_with_mode(&g, 0, &w, m).unwrap(),
1072                vec![Some(0.0), Some(1.0), Some(3.0)],
1073                "mode {m:?}"
1074            );
1075        }
1076    }
1077
1078    #[test]
1079    fn paths_with_mode_in_records_reverse_parents() {
1080        let (g, w) = directed_path_3();
1081        // IN-mode SPT from vertex 2: parents[1]=2, parents[0]=1.
1082        let p = dijkstra_paths_with_mode(&g, 2, &w, DijkstraMode::In).unwrap();
1083        assert_eq!(p.distances, vec![Some(3.0), Some(2.0), Some(0.0)]);
1084        assert_eq!(p.parents[2], None);
1085        assert_eq!(p.parents[1], Some(2));
1086        assert_eq!(p.parents[0], Some(1));
1087    }
1088
1089    #[test]
1090    fn path_to_with_mode_directed_in() {
1091        let (g, w) = directed_path_3();
1092        let (vs, es) = dijkstra_path_to_with_mode(&g, 2, 0, &w, DijkstraMode::In)
1093            .unwrap()
1094            .unwrap();
1095        assert_eq!(vs, vec![2, 1, 0]);
1096        assert_eq!(es, vec![1, 0]);
1097    }
1098
1099    #[test]
1100    fn path_to_with_mode_unreachable_via_out() {
1101        let (g, w) = directed_path_3();
1102        // OUT from 2: nothing else reachable (sink).
1103        assert_eq!(
1104            dijkstra_path_to_with_mode(&g, 2, 0, &w, DijkstraMode::Out).unwrap(),
1105            None
1106        );
1107    }
1108
1109    #[test]
1110    fn cutoff_with_mode_masks_above() {
1111        let (g, w) = directed_path_3();
1112        let d =
1113            dijkstra_distances_cutoff_with_mode(&g, 0, &w, Some(1.5), DijkstraMode::Out).unwrap();
1114        assert_eq!(d, vec![Some(0.0), Some(1.0), None]);
1115    }
1116
1117    #[test]
1118    fn multi_with_mode_sources_independent() {
1119        let (g, w) = directed_path_3();
1120        let r =
1121            dijkstra_distances_multi_with_mode(&g, &[0, 2], &w, None, DijkstraMode::All).unwrap();
1122        assert_eq!(r[0], vec![Some(0.0), Some(1.0), Some(3.0)]);
1123        assert_eq!(r[1], vec![Some(3.0), Some(2.0), Some(0.0)]);
1124    }
1125
1126    // ---- SP-001c: all-shortest-paths -------------------
1127
1128    #[test]
1129    fn all_paths_diamond_yields_two_geodesics() {
1130        // 0-1-3 and 0-2-3, all weight 1: two distinct shortest paths
1131        // to vertex 3.
1132        let mut g = Graph::with_vertices(4);
1133        g.add_edge(0, 1).unwrap(); // edge 0
1134        g.add_edge(0, 2).unwrap(); // edge 1
1135        g.add_edge(1, 3).unwrap(); // edge 2
1136        g.add_edge(2, 3).unwrap(); // edge 3
1137        let r = dijkstra_all_shortest_paths(&g, 0, &[1.0; 4], DijkstraMode::Out).unwrap();
1138        assert_eq!(r.nrgeo, vec![1, 1, 1, 2]);
1139        assert_eq!(r.vertex_paths[0], vec![vec![0]]);
1140        assert_eq!(r.edge_paths[0], vec![Vec::<EdgeId>::new()]);
1141        assert_eq!(r.vertex_paths[3].len(), 2);
1142        let mut paths: Vec<Vec<u32>> = r.vertex_paths[3].clone();
1143        paths.sort();
1144        assert_eq!(paths, vec![vec![0, 1, 3], vec![0, 2, 3]]);
1145    }
1146
1147    #[test]
1148    fn all_paths_unique_returns_single_chain() {
1149        let (g, w) = shortcut_triangle();
1150        let r = dijkstra_all_shortest_paths(&g, 0, &w, DijkstraMode::Out).unwrap();
1151        assert_eq!(r.nrgeo, vec![1, 1, 1]);
1152        assert_eq!(r.vertex_paths[2], vec![vec![0, 1, 2]]);
1153        assert_eq!(r.edge_paths[2], vec![vec![0, 2]]);
1154    }
1155
1156    #[test]
1157    fn all_paths_unreachable_emits_empty() {
1158        let mut g = Graph::with_vertices(3);
1159        g.add_edge(0, 1).unwrap();
1160        let r = dijkstra_all_shortest_paths(&g, 0, &[1.0], DijkstraMode::Out).unwrap();
1161        assert_eq!(r.nrgeo, vec![1, 1, 0]);
1162        assert!(r.vertex_paths[2].is_empty());
1163        assert!(r.edge_paths[2].is_empty());
1164    }
1165
1166    #[test]
1167    fn all_paths_directed_in_mode() {
1168        let (g, w) = directed_path_3();
1169        let r = dijkstra_all_shortest_paths(&g, 2, &w, DijkstraMode::In).unwrap();
1170        assert_eq!(r.nrgeo, vec![1, 1, 1]);
1171        assert_eq!(r.vertex_paths[0], vec![vec![2, 1, 0]]);
1172        assert_eq!(r.edge_paths[0], vec![vec![1, 0]]);
1173    }
1174
1175    #[test]
1176    fn all_paths_invalid_source_errors() {
1177        let (g, _) = shortcut_triangle();
1178        assert!(dijkstra_all_shortest_paths(&g, 99, &[1.0; 3], DijkstraMode::Out).is_err());
1179    }
1180
1181    #[test]
1182    fn all_paths_zero_weight_alt_is_dropped_by_guard() {
1183        // Triangle with one zero-weight edge (1→2). Upstream's
1184        // alt-equal-paths branch only fires when the *connecting* edge
1185        // has positive weight. So the alt path 0→1→2 (cost 1+0=1)
1186        // ties the direct 0→2 (cost 1) but is *not* recorded — the
1187        // 1→2 hop has weight 0. We end up with the direct edge only.
1188        let mut g = Graph::with_vertices(3);
1189        g.add_edge(0, 1).unwrap(); // edge 0, weight 1
1190        g.add_edge(1, 2).unwrap(); // edge 1, weight 0
1191        g.add_edge(0, 2).unwrap(); // edge 2, weight 1
1192        let r = dijkstra_all_shortest_paths(&g, 0, &[1.0, 0.0, 1.0], DijkstraMode::Out).unwrap();
1193        assert_eq!(r.nrgeo[2], 1);
1194        assert_eq!(r.vertex_paths[2], vec![vec![0, 2]]);
1195    }
1196}