Skip to main content

rust_igraph/algorithms/paths/
radii.rs

1//! Eccentricity / radius / diameter (ALGO-SP-020 + SP-021abc mode-aware).
2//!
3//! Counterpart of:
4//! - `igraph_eccentricity()` from `references/igraph/src/paths/distances.c:257`
5//! - `igraph_radius()`       from `references/igraph/src/paths/distances.c:345`
6//! - `igraph_diameter()`     from `references/igraph/src/paths/shortest_paths.c:1259`
7//!
8//! All three are BFS-based on unweighted graphs and share the same
9//! "distances from each vertex" inner loop. The Phase-1 SP-020 minimal
10//! slice ships the unweighted, undirected (or `IGRAPH_OUT` for directed)
11//! variants. SP-021abc adds the mode-aware `*_with_mode` family — the
12//! `mode` parameter selects which adjacency the BFS follows on directed
13//! graphs (`Out` / `In` / `All`); for undirected graphs every mode
14//! reduces to `All` (every edge is bidirectional).
15//!
16//! Conventions (matching upstream):
17//! - **Eccentricity** of a vertex `v` = max shortest-path distance from
18//!   `v` to any reachable vertex; `0` for isolated vertices. Unreachable
19//!   vertex pairs are ignored (`unconn = true` semantics).
20//! - **Radius** = min eccentricity over all vertices; `None` for n = 0.
21//! - **Diameter** = max eccentricity over all vertices; `None` for n = 0.
22
23use std::collections::VecDeque;
24
25use crate::algorithms::paths::distances::distances;
26use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
27
28/// Mode for direction-aware eccentricity / radius / diameter on directed
29/// graphs. For undirected graphs every mode reduces to [`EccMode::All`].
30///
31/// Counterpart of `igraph_neimode_t` in
32/// `references/igraph/include/igraph_constants.h`.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum EccMode {
35    /// Follow outgoing edges (`IGRAPH_OUT`). Default semantics for
36    /// upstream `igraph_eccentricity` / `igraph_radius` /
37    /// `igraph_diameter` and what the legacy [`eccentricity`] / [`radius`]
38    /// / [`diameter`] APIs use.
39    Out,
40    /// Follow incoming edges (`IGRAPH_IN`).
41    In,
42    /// Ignore direction — treat every edge as bidirectional
43    /// (`IGRAPH_ALL`).
44    All,
45}
46
47/// BFS distances from `source` following edges in `mode` direction.
48/// Pure helper — does not allocate the full distance vector twice and
49/// matches [`distances`] when `mode == EccMode::Out`.
50fn bfs_distances(graph: &Graph, source: VertexId, mode: EccMode) -> IgraphResult<Vec<Option<u32>>> {
51    let n = graph.vcount();
52    if n == 0 {
53        return Ok(Vec::new());
54    }
55    if source >= n {
56        return Err(IgraphError::VertexOutOfRange { id: source, n });
57    }
58
59    let n_us = n as usize;
60    let mut dist: Vec<Option<u32>> = vec![None; n_us];
61    let mut queue: VecDeque<VertexId> = VecDeque::new();
62    dist[source as usize] = Some(0);
63    queue.push_back(source);
64
65    let directed = graph.is_directed();
66    while let Some(v) = queue.pop_front() {
67        let next = dist[v as usize]
68            .expect("dequeued vertex has been visited")
69            .checked_add(1)
70            .ok_or(IgraphError::Internal(
71                "distance overflow (graph diameter exceeds u32::MAX)",
72            ))?;
73        let neighbours: Vec<VertexId> = if directed {
74            match mode {
75                EccMode::Out => graph.out_neighbors_vec(v)?,
76                EccMode::In => graph.in_neighbors_vec(v)?,
77                EccMode::All => {
78                    let mut out = graph.out_neighbors_vec(v)?;
79                    out.extend(graph.in_neighbors_vec(v)?);
80                    out
81                }
82            }
83        } else {
84            // Undirected graphs: every mode is `All`, and `Graph::neighbors`
85            // already returns the merged list.
86            graph.neighbors(v)?
87        };
88        for w in neighbours {
89            if dist[w as usize].is_none() {
90                dist[w as usize] = Some(next);
91                queue.push_back(w);
92            }
93        }
94    }
95
96    Ok(dist)
97}
98
99// ---------------------------------------------------------------
100// Mode-default (OUT) variants — kept as the public legacy surface
101// from SP-020. They simply delegate to the with-mode flavour.
102// ---------------------------------------------------------------
103
104/// Eccentricity of every vertex (length `vcount`). Result `r[v]` is the
105/// maximum shortest-path distance from `v` to any reachable vertex.
106/// Isolated vertices have eccentricity `0`.
107///
108/// Counterpart of `igraph_eccentricity(_, NULL_weights, _, igraph_vss_all(), IGRAPH_OUT)`.
109pub fn eccentricity(graph: &Graph) -> IgraphResult<Vec<u32>> {
110    let n = graph.vcount();
111    let mut out = vec![0u32; n as usize];
112    for v in 0..n {
113        let d = distances(graph, v)?;
114        let max = d.iter().filter_map(|x| *x).max().unwrap_or(0);
115        out[v as usize] = max;
116    }
117    Ok(out)
118}
119
120/// Radius of `graph` — the minimum vertex eccentricity. `None` for a
121/// graph with no vertices (matches upstream's `IGRAPH_NAN` for the
122/// null graph).
123///
124/// Counterpart of `igraph_radius(_, NULL_weights, _, IGRAPH_OUT)`.
125pub fn radius(graph: &Graph) -> IgraphResult<Option<u32>> {
126    let n = graph.vcount();
127    if n == 0 {
128        return Ok(None);
129    }
130    let ecc = eccentricity(graph)?;
131    Ok(ecc.into_iter().min())
132}
133
134/// Diameter of `graph` — the maximum vertex eccentricity. `None` for a
135/// graph with no vertices.
136///
137/// Counterpart of
138/// `igraph_diameter(_, NULL_weights, _, NULL, NULL, NULL, NULL, _, /*unconn=*/true)`.
139///
140/// # Examples
141///
142/// ```
143/// use rust_igraph::{Graph, diameter, radius, eccentricity};
144///
145/// // Path 0-1-2-3-4: longest geodesic is 0→4 of length 4.
146/// let mut g = Graph::with_vertices(5);
147/// for i in 0..4 { g.add_edge(i, i + 1).unwrap(); }
148/// assert_eq!(diameter(&g).unwrap(), Some(4));
149/// // Centre of the path (vertex 2) has eccentricity 2 → radius 2.
150/// assert_eq!(radius(&g).unwrap(), Some(2));
151/// assert_eq!(eccentricity(&g).unwrap(), vec![4, 3, 2, 3, 4]);
152/// ```
153pub fn diameter(graph: &Graph) -> IgraphResult<Option<u32>> {
154    let n = graph.vcount();
155    if n == 0 {
156        return Ok(None);
157    }
158    let ecc = eccentricity(graph)?;
159    Ok(ecc.into_iter().max())
160}
161
162// ---------------------------------------------------------------
163// SP-021abc: mode-aware variants. For undirected graphs every mode
164// behaves identically (matches upstream).
165// ---------------------------------------------------------------
166
167/// Eccentricity of every vertex following `mode`-direction edges.
168///
169/// Counterpart of `igraph_eccentricity(_, NULL_weights, _,
170/// igraph_vss_all(), mode)`. For undirected graphs every mode reduces
171/// to [`EccMode::All`] (every edge is bidirectional).
172///
173/// # Examples
174///
175/// ```
176/// use rust_igraph::{Graph, eccentricity_with_mode, EccMode};
177///
178/// // Directed path 0→1→2: out-mode says ecc[2]=0; in-mode says ecc[0]=0;
179/// // all-mode treats it like an undirected path.
180/// let mut g = Graph::new(3, true).unwrap();
181/// g.add_edge(0, 1).unwrap();
182/// g.add_edge(1, 2).unwrap();
183/// assert_eq!(eccentricity_with_mode(&g, EccMode::Out).unwrap(), vec![2, 1, 0]);
184/// assert_eq!(eccentricity_with_mode(&g, EccMode::In).unwrap(),  vec![0, 1, 2]);
185/// assert_eq!(eccentricity_with_mode(&g, EccMode::All).unwrap(), vec![2, 1, 2]);
186/// ```
187pub fn eccentricity_with_mode(graph: &Graph, mode: EccMode) -> IgraphResult<Vec<u32>> {
188    let n = graph.vcount();
189    let mut out = vec![0u32; n as usize];
190    for v in 0..n {
191        let d = bfs_distances(graph, v, mode)?;
192        let max = d.iter().filter_map(|x| *x).max().unwrap_or(0);
193        out[v as usize] = max;
194    }
195    Ok(out)
196}
197
198/// Radius of `graph` under the given `mode`. `None` for `vcount == 0`.
199///
200/// Counterpart of `igraph_radius(_, NULL_weights, _, mode)`.
201pub fn radius_with_mode(graph: &Graph, mode: EccMode) -> IgraphResult<Option<u32>> {
202    if graph.vcount() == 0 {
203        return Ok(None);
204    }
205    let ecc = eccentricity_with_mode(graph, mode)?;
206    Ok(ecc.into_iter().min())
207}
208
209/// Diameter of `graph` under the given `mode`. `None` for `vcount == 0`.
210///
211/// Counterpart of `igraph_diameter(_, NULL_weights, _, NULL, NULL, NULL,
212/// NULL, mode == directed ? IGRAPH_OUT : IGRAPH_ALL, /*unconn=*/true)`.
213pub fn diameter_with_mode(graph: &Graph, mode: EccMode) -> IgraphResult<Option<u32>> {
214    if graph.vcount() == 0 {
215        return Ok(None);
216    }
217    let ecc = eccentricity_with_mode(graph, mode)?;
218    Ok(ecc.into_iter().max())
219}
220
221// ---------------------------------------------------------------
222// SP-021..023: weighted (Dijkstra-based) ecc/radius/diameter.
223// Mirrors `igraph_eccentricity / igraph_radius / igraph_diameter`
224// when called with a non-NULL `weights` argument.
225// ---------------------------------------------------------------
226
227fn ecc_mode_to_dijkstra(mode: EccMode) -> crate::algorithms::paths::dijkstra::DijkstraMode {
228    use crate::algorithms::paths::dijkstra::DijkstraMode;
229    match mode {
230        EccMode::Out => DijkstraMode::Out,
231        EccMode::In => DijkstraMode::In,
232        EccMode::All => DijkstraMode::All,
233    }
234}
235
236/// Mode-aware weighted eccentricity. For each vertex `v`, returns
237/// `max_{u reachable from v} dist(v, u)`, where distances are
238/// weighted shortest-path lengths (Dijkstra). Unreachable vertex
239/// pairs are ignored (matches upstream's `unconn=true`); isolated
240/// vertices have eccentricity `0.0`.
241///
242/// Counterpart of `igraph_eccentricity(_, weights, _, igraph_vss_all(), mode)`.
243///
244/// Edge weights must be non-negative and not NaN.
245///
246/// # Examples
247///
248/// ```
249/// use rust_igraph::{Graph, eccentricity_weighted_with_mode, EccMode};
250///
251/// // Path 0-1-2 with weights (1.0, 2.5): vertex 0 has ecc 3.5,
252/// // vertex 1 has ecc 2.5, vertex 2 has ecc 3.5.
253/// let mut g = Graph::with_vertices(3);
254/// g.add_edge(0, 1).unwrap();
255/// g.add_edge(1, 2).unwrap();
256/// let r = eccentricity_weighted_with_mode(&g, &[1.0, 2.5], EccMode::All).unwrap();
257/// assert_eq!(r, vec![3.5, 2.5, 3.5]);
258/// ```
259pub fn eccentricity_weighted_with_mode(
260    graph: &Graph,
261    weights: &[f64],
262    mode: EccMode,
263) -> IgraphResult<Vec<f64>> {
264    use crate::algorithms::paths::dijkstra::dijkstra_distances_with_mode;
265    let n = graph.vcount();
266    let mut out = vec![0.0_f64; n as usize];
267    for v in 0..n {
268        let d = dijkstra_distances_with_mode(graph, v, weights, ecc_mode_to_dijkstra(mode))?;
269        let max = d
270            .iter()
271            .filter_map(|x| *x)
272            .fold(0.0_f64, |a, b| if b > a { b } else { a });
273        out[v as usize] = max;
274    }
275    Ok(out)
276}
277
278/// Mode-aware weighted radius. `None` for `vcount == 0`.
279///
280/// Counterpart of `igraph_radius(_, weights, _, mode)`.
281pub fn radius_weighted_with_mode(
282    graph: &Graph,
283    weights: &[f64],
284    mode: EccMode,
285) -> IgraphResult<Option<f64>> {
286    if graph.vcount() == 0 {
287        return Ok(None);
288    }
289    let ecc = eccentricity_weighted_with_mode(graph, weights, mode)?;
290    Ok(ecc
291        .into_iter()
292        .fold(None, |acc, x| Some(acc.map_or(x, |a: f64| a.min(x)))))
293}
294
295/// Mode-aware weighted diameter. `None` for `vcount == 0`.
296///
297/// Counterpart of `igraph_diameter(_, weights, _, NULL, NULL, NULL,
298/// NULL, mode == directed ? IGRAPH_OUT : IGRAPH_ALL, /*unconn=*/true)`.
299pub fn diameter_weighted_with_mode(
300    graph: &Graph,
301    weights: &[f64],
302    mode: EccMode,
303) -> IgraphResult<Option<f64>> {
304    if graph.vcount() == 0 {
305        return Ok(None);
306    }
307    let ecc = eccentricity_weighted_with_mode(graph, weights, mode)?;
308    Ok(ecc
309        .into_iter()
310        .fold(None, |acc, x| Some(acc.map_or(x, |a: f64| a.max(x)))))
311}
312
313/// OUT-mode default for [`eccentricity_weighted_with_mode`]. Counterpart
314/// of `igraph_eccentricity(_, weights, _, igraph_vss_all(), IGRAPH_OUT)`.
315pub fn eccentricity_weighted(graph: &Graph, weights: &[f64]) -> IgraphResult<Vec<f64>> {
316    eccentricity_weighted_with_mode(graph, weights, EccMode::Out)
317}
318
319/// OUT-mode default for [`radius_weighted_with_mode`].
320pub fn radius_weighted(graph: &Graph, weights: &[f64]) -> IgraphResult<Option<f64>> {
321    radius_weighted_with_mode(graph, weights, EccMode::Out)
322}
323
324/// OUT-mode default for [`diameter_weighted_with_mode`].
325pub fn diameter_weighted(graph: &Graph, weights: &[f64]) -> IgraphResult<Option<f64>> {
326    diameter_weighted_with_mode(graph, weights, EccMode::Out)
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    fn empty_graph_radii_are_none() {
335        let g = Graph::with_vertices(0);
336        assert_eq!(radius(&g).unwrap(), None);
337        assert_eq!(diameter(&g).unwrap(), None);
338        assert!(eccentricity(&g).unwrap().is_empty());
339    }
340
341    #[test]
342    fn singleton_has_zero_eccentricity() {
343        let g = Graph::with_vertices(1);
344        assert_eq!(eccentricity(&g).unwrap(), vec![0]);
345        assert_eq!(radius(&g).unwrap(), Some(0));
346        assert_eq!(diameter(&g).unwrap(), Some(0));
347    }
348
349    #[test]
350    fn isolated_vertices_each_have_eccentricity_zero() {
351        let g = Graph::with_vertices(5);
352        assert_eq!(eccentricity(&g).unwrap(), vec![0; 5]);
353        assert_eq!(radius(&g).unwrap(), Some(0));
354        assert_eq!(diameter(&g).unwrap(), Some(0));
355    }
356
357    #[test]
358    fn path_5_diameter_4_radius_2() {
359        let mut g = Graph::with_vertices(5);
360        for i in 0..4 {
361            g.add_edge(i, i + 1).unwrap();
362        }
363        assert_eq!(eccentricity(&g).unwrap(), vec![4, 3, 2, 3, 4]);
364        assert_eq!(radius(&g).unwrap(), Some(2));
365        assert_eq!(diameter(&g).unwrap(), Some(4));
366    }
367
368    #[test]
369    fn cycle_4_eccentricity_uniform_2() {
370        let mut g = Graph::with_vertices(4);
371        for i in 0..4u32 {
372            g.add_edge(i, (i + 1) % 4).unwrap();
373        }
374        assert_eq!(eccentricity(&g).unwrap(), vec![2, 2, 2, 2]);
375        assert_eq!(radius(&g).unwrap(), Some(2));
376        assert_eq!(diameter(&g).unwrap(), Some(2));
377    }
378
379    #[test]
380    fn star_centre_has_eccentricity_1_leaves_have_2() {
381        // 0-1, 0-2, 0-3 → centre 0 has ecc 1; leaves have ecc 2 (via centre).
382        let mut g = Graph::with_vertices(4);
383        for v in 1..4 {
384            g.add_edge(0, v).unwrap();
385        }
386        assert_eq!(eccentricity(&g).unwrap(), vec![1, 2, 2, 2]);
387        assert_eq!(radius(&g).unwrap(), Some(1));
388        assert_eq!(diameter(&g).unwrap(), Some(2));
389    }
390
391    #[test]
392    fn disconnected_components_use_max_within_components() {
393        // Two paths: 0-1-2 (diameter 2) and 3-4 (diameter 1).
394        // Per upstream's `unconn=true` semantics, unreachable pairs are
395        // ignored, so eccentricity[v] is the max over v's component only.
396        let mut g = Graph::with_vertices(5);
397        g.add_edge(0, 1).unwrap();
398        g.add_edge(1, 2).unwrap();
399        g.add_edge(3, 4).unwrap();
400        assert_eq!(eccentricity(&g).unwrap(), vec![2, 1, 2, 1, 1]);
401        assert_eq!(radius(&g).unwrap(), Some(1));
402        assert_eq!(diameter(&g).unwrap(), Some(2));
403    }
404
405    #[test]
406    fn directed_path_uses_out_edges() {
407        // 0 -> 1 -> 2: from 0, ecc = 2; from 2, ecc = 0 (no outgoing).
408        let mut g = Graph::new(3, true).unwrap();
409        g.add_edge(0, 1).unwrap();
410        g.add_edge(1, 2).unwrap();
411        assert_eq!(eccentricity(&g).unwrap(), vec![2, 1, 0]);
412        assert_eq!(diameter(&g).unwrap(), Some(2));
413    }
414
415    #[test]
416    fn self_loop_does_not_inflate_eccentricity() {
417        // 0-self + 0-1: ecc[0] = 1, ecc[1] = 1.
418        let mut g = Graph::with_vertices(2);
419        g.add_edge(0, 0).unwrap();
420        g.add_edge(0, 1).unwrap();
421        assert_eq!(eccentricity(&g).unwrap(), vec![1, 1]);
422        assert_eq!(diameter(&g).unwrap(), Some(1));
423    }
424
425    // ---- SP-021abc mode-aware tests -----
426
427    #[test]
428    fn legacy_apis_match_with_mode_out() {
429        // For undirected graphs every mode is identical; ensure the
430        // legacy default-mode wrappers agree with `_with_mode(Out)`.
431        let mut g = Graph::with_vertices(5);
432        for i in 0..4 {
433            g.add_edge(i, i + 1).unwrap();
434        }
435        assert_eq!(
436            eccentricity(&g).unwrap(),
437            eccentricity_with_mode(&g, EccMode::Out).unwrap()
438        );
439        assert_eq!(
440            radius(&g).unwrap(),
441            radius_with_mode(&g, EccMode::Out).unwrap()
442        );
443        assert_eq!(
444            diameter(&g).unwrap(),
445            diameter_with_mode(&g, EccMode::Out).unwrap()
446        );
447    }
448
449    #[test]
450    fn undirected_all_modes_agree() {
451        let mut g = Graph::with_vertices(4);
452        g.add_edge(0, 1).unwrap();
453        g.add_edge(1, 2).unwrap();
454        g.add_edge(2, 3).unwrap();
455        for m in [EccMode::Out, EccMode::In, EccMode::All] {
456            assert_eq!(
457                eccentricity_with_mode(&g, m).unwrap(),
458                vec![3, 2, 2, 3],
459                "mode {m:?}"
460            );
461            assert_eq!(radius_with_mode(&g, m).unwrap(), Some(2));
462            assert_eq!(diameter_with_mode(&g, m).unwrap(), Some(3));
463        }
464    }
465
466    #[test]
467    fn directed_path_in_mode_reverses() {
468        // 0→1→2: out-mode reaches forward, in-mode reaches backward.
469        let mut g = Graph::new(3, true).unwrap();
470        g.add_edge(0, 1).unwrap();
471        g.add_edge(1, 2).unwrap();
472        assert_eq!(
473            eccentricity_with_mode(&g, EccMode::Out).unwrap(),
474            vec![2, 1, 0]
475        );
476        assert_eq!(
477            eccentricity_with_mode(&g, EccMode::In).unwrap(),
478            vec![0, 1, 2]
479        );
480    }
481
482    #[test]
483    fn directed_path_all_mode_treats_undirected() {
484        let mut g = Graph::new(3, true).unwrap();
485        g.add_edge(0, 1).unwrap();
486        g.add_edge(1, 2).unwrap();
487        // All-mode = ignore direction → undirected path, ecc = [2,1,2].
488        assert_eq!(
489            eccentricity_with_mode(&g, EccMode::All).unwrap(),
490            vec![2, 1, 2]
491        );
492        assert_eq!(radius_with_mode(&g, EccMode::All).unwrap(), Some(1));
493        assert_eq!(diameter_with_mode(&g, EccMode::All).unwrap(), Some(2));
494    }
495
496    #[test]
497    fn directed_cycle_all_modes_uniform() {
498        // 0→1→2→0: every mode visits the whole cycle.
499        // Out / In: max distance from a vertex on a directed 3-cycle is 2.
500        // All: the underlying graph is a triangle (undirected K3) — ecc = 1.
501        let mut g = Graph::new(3, true).unwrap();
502        g.add_edge(0, 1).unwrap();
503        g.add_edge(1, 2).unwrap();
504        g.add_edge(2, 0).unwrap();
505        assert_eq!(
506            eccentricity_with_mode(&g, EccMode::Out).unwrap(),
507            vec![2, 2, 2]
508        );
509        assert_eq!(
510            eccentricity_with_mode(&g, EccMode::In).unwrap(),
511            vec![2, 2, 2]
512        );
513        assert_eq!(
514            eccentricity_with_mode(&g, EccMode::All).unwrap(),
515            vec![1, 1, 1]
516        );
517    }
518
519    #[test]
520    fn directed_disconnected_in_out_is_zero_for_sinks_sources() {
521        // 0→1; isolated vertex 2.
522        let mut g = Graph::new(3, true).unwrap();
523        g.add_edge(0, 1).unwrap();
524        // Out: 0 reaches 1 (1); 1 reaches nothing (0); 2 isolated (0).
525        assert_eq!(
526            eccentricity_with_mode(&g, EccMode::Out).unwrap(),
527            vec![1, 0, 0]
528        );
529        // In: 0 reaches nothing in reverse (0); 1 reaches 0 (1); 2 (0).
530        assert_eq!(
531            eccentricity_with_mode(&g, EccMode::In).unwrap(),
532            vec![0, 1, 0]
533        );
534        // All: 0 and 1 reach each other (1); 2 still isolated (0).
535        assert_eq!(
536            eccentricity_with_mode(&g, EccMode::All).unwrap(),
537            vec![1, 1, 0]
538        );
539    }
540
541    #[test]
542    fn radius_diameter_match_min_max_of_eccentricity() {
543        let mut g = Graph::new(4, true).unwrap();
544        g.add_edge(0, 1).unwrap();
545        g.add_edge(1, 2).unwrap();
546        g.add_edge(2, 3).unwrap();
547        g.add_edge(3, 0).unwrap();
548        for m in [EccMode::Out, EccMode::In, EccMode::All] {
549            let ecc = eccentricity_with_mode(&g, m).unwrap();
550            assert_eq!(radius_with_mode(&g, m).unwrap(), ecc.iter().copied().min());
551            assert_eq!(
552                diameter_with_mode(&g, m).unwrap(),
553                ecc.iter().copied().max()
554            );
555        }
556    }
557
558    #[test]
559    fn empty_graph_with_mode_is_none() {
560        let g_u = Graph::with_vertices(0);
561        let g_d = Graph::new(0, true).unwrap();
562        for m in [EccMode::Out, EccMode::In, EccMode::All] {
563            assert_eq!(radius_with_mode(&g_u, m).unwrap(), None);
564            assert_eq!(diameter_with_mode(&g_u, m).unwrap(), None);
565            assert!(eccentricity_with_mode(&g_u, m).unwrap().is_empty());
566            assert_eq!(radius_with_mode(&g_d, m).unwrap(), None);
567            assert_eq!(diameter_with_mode(&g_d, m).unwrap(), None);
568            assert!(eccentricity_with_mode(&g_d, m).unwrap().is_empty());
569        }
570    }
571
572    // ---- SP-021..023: weighted ecc/rad/diam tests ------------------
573
574    #[test]
575    fn weighted_path_eccentricity() {
576        // Path 0-1-2 with weights (1, 2.5):
577        // ecc[0] = 0+1+2.5 = 3.5; ecc[1] = max(1, 2.5) = 2.5; ecc[2] = 3.5.
578        let mut g = Graph::with_vertices(3);
579        g.add_edge(0, 1).unwrap();
580        g.add_edge(1, 2).unwrap();
581        let r = eccentricity_weighted(&g, &[1.0, 2.5]).unwrap();
582        assert_eq!(r, vec![3.5, 2.5, 3.5]);
583        assert_eq!(radius_weighted(&g, &[1.0, 2.5]).unwrap(), Some(2.5));
584        assert_eq!(diameter_weighted(&g, &[1.0, 2.5]).unwrap(), Some(3.5));
585    }
586
587    #[test]
588    fn weighted_singleton_zero() {
589        let g = Graph::with_vertices(1);
590        assert_eq!(eccentricity_weighted(&g, &[]).unwrap(), vec![0.0]);
591        assert_eq!(radius_weighted(&g, &[]).unwrap(), Some(0.0));
592        assert_eq!(diameter_weighted(&g, &[]).unwrap(), Some(0.0));
593    }
594
595    #[test]
596    fn weighted_isolated_vertices_zero() {
597        let g = Graph::with_vertices(4);
598        assert_eq!(eccentricity_weighted(&g, &[]).unwrap(), vec![0.0; 4]);
599        assert_eq!(radius_weighted(&g, &[]).unwrap(), Some(0.0));
600        assert_eq!(diameter_weighted(&g, &[]).unwrap(), Some(0.0));
601    }
602
603    #[test]
604    fn weighted_disconnected_unconn_true_semantics() {
605        // 0-1 (w 1.0) and 2-3 (w 4.0). Each vertex's ecc = max within its
606        // component (unconn=true ignores cross-component pairs).
607        let mut g = Graph::with_vertices(4);
608        g.add_edge(0, 1).unwrap();
609        g.add_edge(2, 3).unwrap();
610        let r = eccentricity_weighted(&g, &[1.0, 4.0]).unwrap();
611        assert_eq!(r, vec![1.0, 1.0, 4.0, 4.0]);
612        // Diameter is 4.0 (the max), radius is 1.0 (the min).
613        assert_eq!(radius_weighted(&g, &[1.0, 4.0]).unwrap(), Some(1.0));
614        assert_eq!(diameter_weighted(&g, &[1.0, 4.0]).unwrap(), Some(4.0));
615    }
616
617    #[test]
618    fn weighted_directed_in_mode_reverses() {
619        // Directed path 0→1→2 with weights (1, 2). OUT-mode ecc[2]=0
620        // (sink); IN-mode ecc[2]=3 (reaches both predecessors).
621        let mut g = Graph::new(3, true).unwrap();
622        g.add_edge(0, 1).unwrap();
623        g.add_edge(1, 2).unwrap();
624        let w = vec![1.0, 2.0];
625        assert_eq!(
626            eccentricity_weighted_with_mode(&g, &w, EccMode::Out).unwrap(),
627            vec![3.0, 2.0, 0.0]
628        );
629        assert_eq!(
630            eccentricity_weighted_with_mode(&g, &w, EccMode::In).unwrap(),
631            vec![0.0, 1.0, 3.0]
632        );
633        assert_eq!(
634            eccentricity_weighted_with_mode(&g, &w, EccMode::All).unwrap(),
635            vec![3.0, 2.0, 3.0]
636        );
637    }
638
639    #[test]
640    fn weighted_undirected_modes_agree() {
641        // Triangle 0-1, 1-2, 0-2 with weights 1, 2, 4. Min path 0→2 is
642        // via 1: cost 3 < direct 4. ecc[0] = 3, ecc[1] = 2, ecc[2] = 3.
643        let mut g = Graph::with_vertices(3);
644        g.add_edge(0, 1).unwrap();
645        g.add_edge(1, 2).unwrap();
646        g.add_edge(0, 2).unwrap();
647        let w = vec![1.0, 2.0, 4.0];
648        for m in [EccMode::Out, EccMode::In, EccMode::All] {
649            assert_eq!(
650                eccentricity_weighted_with_mode(&g, &w, m).unwrap(),
651                vec![3.0, 2.0, 3.0],
652                "mode {m:?}"
653            );
654        }
655    }
656
657    #[test]
658    fn weighted_negative_weight_errors() {
659        let mut g = Graph::with_vertices(2);
660        g.add_edge(0, 1).unwrap();
661        assert!(eccentricity_weighted(&g, &[-1.0]).is_err());
662    }
663
664    #[test]
665    fn weighted_empty_graph_radius_diameter_none() {
666        let g = Graph::with_vertices(0);
667        assert_eq!(radius_weighted(&g, &[]).unwrap(), None);
668        assert_eq!(diameter_weighted(&g, &[]).unwrap(), None);
669        assert!(eccentricity_weighted(&g, &[]).unwrap().is_empty());
670    }
671
672    #[test]
673    fn weighted_with_mode_default_matches_out() {
674        let mut g = Graph::with_vertices(3);
675        g.add_edge(0, 1).unwrap();
676        g.add_edge(1, 2).unwrap();
677        let w = vec![1.0, 2.5];
678        assert_eq!(
679            eccentricity_weighted(&g, &w).unwrap(),
680            eccentricity_weighted_with_mode(&g, &w, EccMode::Out).unwrap()
681        );
682    }
683}