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