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            .ok_or(IgraphError::Internal("dequeued vertex has no distance"))?
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)`.
109///
110/// # Examples
111///
112/// ```
113/// use rust_igraph::{Graph, eccentricity};
114///
115/// let mut g = Graph::with_vertices(4);
116/// g.add_edge(0, 1).unwrap();
117/// g.add_edge(1, 2).unwrap();
118/// g.add_edge(2, 3).unwrap();
119/// assert_eq!(eccentricity(&g).unwrap(), vec![3, 2, 2, 3]);
120/// ```
121pub fn eccentricity(graph: &Graph) -> IgraphResult<Vec<u32>> {
122    let n = graph.vcount();
123    let mut out = vec![0u32; n as usize];
124    for v in 0..n {
125        let d = distances(graph, v)?;
126        let max = d.iter().filter_map(|x| *x).max().unwrap_or(0);
127        out[v as usize] = max;
128    }
129    Ok(out)
130}
131
132/// Radius of `graph` — the minimum vertex eccentricity. `None` for a
133/// graph with no vertices (matches upstream's `IGRAPH_NAN` for the
134/// null graph).
135///
136/// Counterpart of `igraph_radius(_, NULL_weights, _, IGRAPH_OUT)`.
137///
138/// # Examples
139///
140/// ```
141/// use rust_igraph::{Graph, radius};
142///
143/// let mut g = Graph::with_vertices(4);
144/// g.add_edge(0, 1).unwrap();
145/// g.add_edge(1, 2).unwrap();
146/// g.add_edge(2, 3).unwrap();
147/// assert_eq!(radius(&g).unwrap(), Some(2));
148/// ```
149pub fn radius(graph: &Graph) -> IgraphResult<Option<u32>> {
150    let n = graph.vcount();
151    if n == 0 {
152        return Ok(None);
153    }
154    let ecc = eccentricity(graph)?;
155    Ok(ecc.into_iter().min())
156}
157
158/// Diameter of `graph` — the maximum vertex eccentricity. `None` for a
159/// graph with no vertices.
160///
161/// Counterpart of
162/// `igraph_diameter(_, NULL_weights, _, NULL, NULL, NULL, NULL, _, /*unconn=*/true)`.
163///
164/// # Examples
165///
166/// ```
167/// use rust_igraph::{Graph, diameter, radius, eccentricity};
168///
169/// // Path 0-1-2-3-4: longest geodesic is 0→4 of length 4.
170/// let mut g = Graph::with_vertices(5);
171/// for i in 0..4 { g.add_edge(i, i + 1).unwrap(); }
172/// assert_eq!(diameter(&g).unwrap(), Some(4));
173/// // Centre of the path (vertex 2) has eccentricity 2 → radius 2.
174/// assert_eq!(radius(&g).unwrap(), Some(2));
175/// assert_eq!(eccentricity(&g).unwrap(), vec![4, 3, 2, 3, 4]);
176/// ```
177pub fn diameter(graph: &Graph) -> IgraphResult<Option<u32>> {
178    let n = graph.vcount();
179    if n == 0 {
180        return Ok(None);
181    }
182    let ecc = eccentricity(graph)?;
183    Ok(ecc.into_iter().max())
184}
185
186// ---------------------------------------------------------------
187// SP-021abc: mode-aware variants. For undirected graphs every mode
188// behaves identically (matches upstream).
189// ---------------------------------------------------------------
190
191/// Eccentricity of every vertex following `mode`-direction edges.
192///
193/// Counterpart of `igraph_eccentricity(_, NULL_weights, _,
194/// igraph_vss_all(), mode)`. For undirected graphs every mode reduces
195/// to [`EccMode::All`] (every edge is bidirectional).
196///
197/// # Examples
198///
199/// ```
200/// use rust_igraph::{Graph, eccentricity_with_mode, EccMode};
201///
202/// // Directed path 0→1→2: out-mode says ecc[2]=0; in-mode says ecc[0]=0;
203/// // all-mode treats it like an undirected path.
204/// let mut g = Graph::new(3, true).unwrap();
205/// g.add_edge(0, 1).unwrap();
206/// g.add_edge(1, 2).unwrap();
207/// assert_eq!(eccentricity_with_mode(&g, EccMode::Out).unwrap(), vec![2, 1, 0]);
208/// assert_eq!(eccentricity_with_mode(&g, EccMode::In).unwrap(),  vec![0, 1, 2]);
209/// assert_eq!(eccentricity_with_mode(&g, EccMode::All).unwrap(), vec![2, 1, 2]);
210/// ```
211pub fn eccentricity_with_mode(graph: &Graph, mode: EccMode) -> IgraphResult<Vec<u32>> {
212    let n = graph.vcount();
213    let mut out = vec![0u32; n as usize];
214    for v in 0..n {
215        let d = bfs_distances(graph, v, mode)?;
216        let max = d.iter().filter_map(|x| *x).max().unwrap_or(0);
217        out[v as usize] = max;
218    }
219    Ok(out)
220}
221
222/// Radius of `graph` under the given `mode`. `None` for `vcount == 0`.
223///
224/// Counterpart of `igraph_radius(_, NULL_weights, _, mode)`.
225///
226/// # Examples
227///
228/// ```
229/// use rust_igraph::{Graph, radius_with_mode, EccMode};
230///
231/// let mut g = Graph::with_vertices(3);
232/// g.add_edge(0, 1).unwrap();
233/// g.add_edge(1, 2).unwrap();
234/// assert_eq!(radius_with_mode(&g, EccMode::All).unwrap(), Some(1));
235/// ```
236pub fn radius_with_mode(graph: &Graph, mode: EccMode) -> IgraphResult<Option<u32>> {
237    if graph.vcount() == 0 {
238        return Ok(None);
239    }
240    let ecc = eccentricity_with_mode(graph, mode)?;
241    Ok(ecc.into_iter().min())
242}
243
244/// Diameter of `graph` under the given `mode`. `None` for `vcount == 0`.
245///
246/// Counterpart of `igraph_diameter(_, NULL_weights, _, NULL, NULL, NULL,
247/// NULL, mode == directed ? IGRAPH_OUT : IGRAPH_ALL, /*unconn=*/true)`.
248///
249/// # Examples
250///
251/// ```
252/// use rust_igraph::{Graph, diameter_with_mode, EccMode};
253///
254/// let mut g = Graph::with_vertices(3);
255/// g.add_edge(0, 1).unwrap();
256/// g.add_edge(1, 2).unwrap();
257/// assert_eq!(diameter_with_mode(&g, EccMode::All).unwrap(), Some(2));
258/// ```
259pub fn diameter_with_mode(graph: &Graph, mode: EccMode) -> IgraphResult<Option<u32>> {
260    if graph.vcount() == 0 {
261        return Ok(None);
262    }
263    let ecc = eccentricity_with_mode(graph, mode)?;
264    Ok(ecc.into_iter().max())
265}
266
267// ---------------------------------------------------------------
268// SP-021..023: weighted (Dijkstra-based) ecc/radius/diameter.
269// Mirrors `igraph_eccentricity / igraph_radius / igraph_diameter`
270// when called with a non-NULL `weights` argument.
271// ---------------------------------------------------------------
272
273fn ecc_mode_to_dijkstra(mode: EccMode) -> crate::algorithms::paths::dijkstra::DijkstraMode {
274    use crate::algorithms::paths::dijkstra::DijkstraMode;
275    match mode {
276        EccMode::Out => DijkstraMode::Out,
277        EccMode::In => DijkstraMode::In,
278        EccMode::All => DijkstraMode::All,
279    }
280}
281
282/// Mode-aware weighted eccentricity. For each vertex `v`, returns
283/// `max_{u reachable from v} dist(v, u)`, where distances are
284/// weighted shortest-path lengths (Dijkstra). Unreachable vertex
285/// pairs are ignored (matches upstream's `unconn=true`); isolated
286/// vertices have eccentricity `0.0`.
287///
288/// Counterpart of `igraph_eccentricity(_, weights, _, igraph_vss_all(), mode)`.
289///
290/// Edge weights must be non-negative and not NaN.
291///
292/// # Examples
293///
294/// ```
295/// use rust_igraph::{Graph, eccentricity_weighted_with_mode, EccMode};
296///
297/// // Path 0-1-2 with weights (1.0, 2.5): vertex 0 has ecc 3.5,
298/// // vertex 1 has ecc 2.5, vertex 2 has ecc 3.5.
299/// let mut g = Graph::with_vertices(3);
300/// g.add_edge(0, 1).unwrap();
301/// g.add_edge(1, 2).unwrap();
302/// let r = eccentricity_weighted_with_mode(&g, &[1.0, 2.5], EccMode::All).unwrap();
303/// assert_eq!(r, vec![3.5, 2.5, 3.5]);
304/// ```
305pub fn eccentricity_weighted_with_mode(
306    graph: &Graph,
307    weights: &[f64],
308    mode: EccMode,
309) -> IgraphResult<Vec<f64>> {
310    use crate::algorithms::paths::dijkstra::dijkstra_distances_with_mode;
311    let n = graph.vcount();
312    let mut out = vec![0.0_f64; n as usize];
313    for v in 0..n {
314        let d = dijkstra_distances_with_mode(graph, v, weights, ecc_mode_to_dijkstra(mode))?;
315        let max = d
316            .iter()
317            .filter_map(|x| *x)
318            .fold(0.0_f64, |a, b| if b > a { b } else { a });
319        out[v as usize] = max;
320    }
321    Ok(out)
322}
323
324/// Mode-aware weighted radius. `None` for `vcount == 0`.
325///
326/// Counterpart of `igraph_radius(_, weights, _, mode)`.
327///
328/// # Examples
329///
330/// ```
331/// use rust_igraph::{Graph, radius_weighted_with_mode, EccMode};
332///
333/// let mut g = Graph::with_vertices(3);
334/// g.add_edge(0, 1).unwrap();
335/// g.add_edge(1, 2).unwrap();
336/// let r = radius_weighted_with_mode(&g, &[1.0, 2.0], EccMode::All).unwrap();
337/// assert!((r.unwrap() - 2.0).abs() < 1e-9);
338/// ```
339pub fn radius_weighted_with_mode(
340    graph: &Graph,
341    weights: &[f64],
342    mode: EccMode,
343) -> IgraphResult<Option<f64>> {
344    if graph.vcount() == 0 {
345        return Ok(None);
346    }
347    let ecc = eccentricity_weighted_with_mode(graph, weights, mode)?;
348    Ok(ecc
349        .into_iter()
350        .fold(None, |acc, x| Some(acc.map_or(x, |a: f64| a.min(x)))))
351}
352
353/// Mode-aware weighted diameter. `None` for `vcount == 0`.
354///
355/// Counterpart of `igraph_diameter(_, weights, _, NULL, NULL, NULL,
356/// NULL, mode == directed ? IGRAPH_OUT : IGRAPH_ALL, /*unconn=*/true)`.
357///
358/// # Examples
359///
360/// ```
361/// use rust_igraph::{Graph, diameter_weighted_with_mode, EccMode};
362///
363/// let mut g = Graph::with_vertices(3);
364/// g.add_edge(0, 1).unwrap();
365/// g.add_edge(1, 2).unwrap();
366/// let d = diameter_weighted_with_mode(&g, &[1.0, 2.0], EccMode::All).unwrap();
367/// assert!((d.unwrap() - 3.0).abs() < 1e-9);
368/// ```
369pub fn diameter_weighted_with_mode(
370    graph: &Graph,
371    weights: &[f64],
372    mode: EccMode,
373) -> IgraphResult<Option<f64>> {
374    if graph.vcount() == 0 {
375        return Ok(None);
376    }
377    let ecc = eccentricity_weighted_with_mode(graph, weights, mode)?;
378    Ok(ecc
379        .into_iter()
380        .fold(None, |acc, x| Some(acc.map_or(x, |a: f64| a.max(x)))))
381}
382
383/// OUT-mode default for [`eccentricity_weighted_with_mode`]. Counterpart
384/// of `igraph_eccentricity(_, weights, _, igraph_vss_all(), IGRAPH_OUT)`.
385///
386/// # Examples
387///
388/// ```
389/// use rust_igraph::{Graph, eccentricity_weighted};
390///
391/// let mut g = Graph::with_vertices(3);
392/// g.add_edge(0, 1).unwrap();
393/// g.add_edge(1, 2).unwrap();
394/// let e = eccentricity_weighted(&g, &[1.0, 2.0]).unwrap();
395/// assert!((e[0] - 3.0).abs() < 1e-9);
396/// assert!((e[1] - 2.0).abs() < 1e-9);
397/// ```
398pub fn eccentricity_weighted(graph: &Graph, weights: &[f64]) -> IgraphResult<Vec<f64>> {
399    eccentricity_weighted_with_mode(graph, weights, EccMode::Out)
400}
401
402/// OUT-mode default for [`radius_weighted_with_mode`].
403///
404/// # Examples
405///
406/// ```
407/// use rust_igraph::{Graph, radius_weighted};
408///
409/// let mut g = Graph::with_vertices(3);
410/// g.add_edge(0, 1).unwrap();
411/// g.add_edge(1, 2).unwrap();
412/// let r = radius_weighted(&g, &[1.0, 2.5]).unwrap();
413/// assert_eq!(r, Some(2.5));
414/// ```
415pub fn radius_weighted(graph: &Graph, weights: &[f64]) -> IgraphResult<Option<f64>> {
416    radius_weighted_with_mode(graph, weights, EccMode::Out)
417}
418
419/// OUT-mode default for [`diameter_weighted_with_mode`].
420///
421/// # Examples
422///
423/// ```
424/// use rust_igraph::{Graph, diameter_weighted};
425///
426/// let mut g = Graph::with_vertices(3);
427/// g.add_edge(0, 1).unwrap();
428/// g.add_edge(1, 2).unwrap();
429/// assert_eq!(diameter_weighted(&g, &[1.0, 2.5]).unwrap(), Some(3.5));
430/// ```
431pub fn diameter_weighted(graph: &Graph, weights: &[f64]) -> IgraphResult<Option<f64>> {
432    diameter_weighted_with_mode(graph, weights, EccMode::Out)
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438
439    #[test]
440    fn empty_graph_radii_are_none() {
441        let g = Graph::with_vertices(0);
442        assert_eq!(radius(&g).unwrap(), None);
443        assert_eq!(diameter(&g).unwrap(), None);
444        assert!(eccentricity(&g).unwrap().is_empty());
445    }
446
447    #[test]
448    fn singleton_has_zero_eccentricity() {
449        let g = Graph::with_vertices(1);
450        assert_eq!(eccentricity(&g).unwrap(), vec![0]);
451        assert_eq!(radius(&g).unwrap(), Some(0));
452        assert_eq!(diameter(&g).unwrap(), Some(0));
453    }
454
455    #[test]
456    fn isolated_vertices_each_have_eccentricity_zero() {
457        let g = Graph::with_vertices(5);
458        assert_eq!(eccentricity(&g).unwrap(), vec![0; 5]);
459        assert_eq!(radius(&g).unwrap(), Some(0));
460        assert_eq!(diameter(&g).unwrap(), Some(0));
461    }
462
463    #[test]
464    fn path_5_diameter_4_radius_2() {
465        let mut g = Graph::with_vertices(5);
466        for i in 0..4 {
467            g.add_edge(i, i + 1).unwrap();
468        }
469        assert_eq!(eccentricity(&g).unwrap(), vec![4, 3, 2, 3, 4]);
470        assert_eq!(radius(&g).unwrap(), Some(2));
471        assert_eq!(diameter(&g).unwrap(), Some(4));
472    }
473
474    #[test]
475    fn cycle_4_eccentricity_uniform_2() {
476        let mut g = Graph::with_vertices(4);
477        for i in 0..4u32 {
478            g.add_edge(i, (i + 1) % 4).unwrap();
479        }
480        assert_eq!(eccentricity(&g).unwrap(), vec![2, 2, 2, 2]);
481        assert_eq!(radius(&g).unwrap(), Some(2));
482        assert_eq!(diameter(&g).unwrap(), Some(2));
483    }
484
485    #[test]
486    fn star_centre_has_eccentricity_1_leaves_have_2() {
487        // 0-1, 0-2, 0-3 → centre 0 has ecc 1; leaves have ecc 2 (via centre).
488        let mut g = Graph::with_vertices(4);
489        for v in 1..4 {
490            g.add_edge(0, v).unwrap();
491        }
492        assert_eq!(eccentricity(&g).unwrap(), vec![1, 2, 2, 2]);
493        assert_eq!(radius(&g).unwrap(), Some(1));
494        assert_eq!(diameter(&g).unwrap(), Some(2));
495    }
496
497    #[test]
498    fn disconnected_components_use_max_within_components() {
499        // Two paths: 0-1-2 (diameter 2) and 3-4 (diameter 1).
500        // Per upstream's `unconn=true` semantics, unreachable pairs are
501        // ignored, so eccentricity[v] is the max over v's component only.
502        let mut g = Graph::with_vertices(5);
503        g.add_edge(0, 1).unwrap();
504        g.add_edge(1, 2).unwrap();
505        g.add_edge(3, 4).unwrap();
506        assert_eq!(eccentricity(&g).unwrap(), vec![2, 1, 2, 1, 1]);
507        assert_eq!(radius(&g).unwrap(), Some(1));
508        assert_eq!(diameter(&g).unwrap(), Some(2));
509    }
510
511    #[test]
512    fn directed_path_uses_out_edges() {
513        // 0 -> 1 -> 2: from 0, ecc = 2; from 2, ecc = 0 (no outgoing).
514        let mut g = Graph::new(3, true).unwrap();
515        g.add_edge(0, 1).unwrap();
516        g.add_edge(1, 2).unwrap();
517        assert_eq!(eccentricity(&g).unwrap(), vec![2, 1, 0]);
518        assert_eq!(diameter(&g).unwrap(), Some(2));
519    }
520
521    #[test]
522    fn self_loop_does_not_inflate_eccentricity() {
523        // 0-self + 0-1: ecc[0] = 1, ecc[1] = 1.
524        let mut g = Graph::with_vertices(2);
525        g.add_edge(0, 0).unwrap();
526        g.add_edge(0, 1).unwrap();
527        assert_eq!(eccentricity(&g).unwrap(), vec![1, 1]);
528        assert_eq!(diameter(&g).unwrap(), Some(1));
529    }
530
531    // ---- SP-021abc mode-aware tests -----
532
533    #[test]
534    fn legacy_apis_match_with_mode_out() {
535        // For undirected graphs every mode is identical; ensure the
536        // legacy default-mode wrappers agree with `_with_mode(Out)`.
537        let mut g = Graph::with_vertices(5);
538        for i in 0..4 {
539            g.add_edge(i, i + 1).unwrap();
540        }
541        assert_eq!(
542            eccentricity(&g).unwrap(),
543            eccentricity_with_mode(&g, EccMode::Out).unwrap()
544        );
545        assert_eq!(
546            radius(&g).unwrap(),
547            radius_with_mode(&g, EccMode::Out).unwrap()
548        );
549        assert_eq!(
550            diameter(&g).unwrap(),
551            diameter_with_mode(&g, EccMode::Out).unwrap()
552        );
553    }
554
555    #[test]
556    fn undirected_all_modes_agree() {
557        let mut g = Graph::with_vertices(4);
558        g.add_edge(0, 1).unwrap();
559        g.add_edge(1, 2).unwrap();
560        g.add_edge(2, 3).unwrap();
561        for m in [EccMode::Out, EccMode::In, EccMode::All] {
562            assert_eq!(
563                eccentricity_with_mode(&g, m).unwrap(),
564                vec![3, 2, 2, 3],
565                "mode {m:?}"
566            );
567            assert_eq!(radius_with_mode(&g, m).unwrap(), Some(2));
568            assert_eq!(diameter_with_mode(&g, m).unwrap(), Some(3));
569        }
570    }
571
572    #[test]
573    fn directed_path_in_mode_reverses() {
574        // 0→1→2: out-mode reaches forward, in-mode reaches backward.
575        let mut g = Graph::new(3, true).unwrap();
576        g.add_edge(0, 1).unwrap();
577        g.add_edge(1, 2).unwrap();
578        assert_eq!(
579            eccentricity_with_mode(&g, EccMode::Out).unwrap(),
580            vec![2, 1, 0]
581        );
582        assert_eq!(
583            eccentricity_with_mode(&g, EccMode::In).unwrap(),
584            vec![0, 1, 2]
585        );
586    }
587
588    #[test]
589    fn directed_path_all_mode_treats_undirected() {
590        let mut g = Graph::new(3, true).unwrap();
591        g.add_edge(0, 1).unwrap();
592        g.add_edge(1, 2).unwrap();
593        // All-mode = ignore direction → undirected path, ecc = [2,1,2].
594        assert_eq!(
595            eccentricity_with_mode(&g, EccMode::All).unwrap(),
596            vec![2, 1, 2]
597        );
598        assert_eq!(radius_with_mode(&g, EccMode::All).unwrap(), Some(1));
599        assert_eq!(diameter_with_mode(&g, EccMode::All).unwrap(), Some(2));
600    }
601
602    #[test]
603    fn directed_cycle_all_modes_uniform() {
604        // 0→1→2→0: every mode visits the whole cycle.
605        // Out / In: max distance from a vertex on a directed 3-cycle is 2.
606        // All: the underlying graph is a triangle (undirected K3) — ecc = 1.
607        let mut g = Graph::new(3, true).unwrap();
608        g.add_edge(0, 1).unwrap();
609        g.add_edge(1, 2).unwrap();
610        g.add_edge(2, 0).unwrap();
611        assert_eq!(
612            eccentricity_with_mode(&g, EccMode::Out).unwrap(),
613            vec![2, 2, 2]
614        );
615        assert_eq!(
616            eccentricity_with_mode(&g, EccMode::In).unwrap(),
617            vec![2, 2, 2]
618        );
619        assert_eq!(
620            eccentricity_with_mode(&g, EccMode::All).unwrap(),
621            vec![1, 1, 1]
622        );
623    }
624
625    #[test]
626    fn directed_disconnected_in_out_is_zero_for_sinks_sources() {
627        // 0→1; isolated vertex 2.
628        let mut g = Graph::new(3, true).unwrap();
629        g.add_edge(0, 1).unwrap();
630        // Out: 0 reaches 1 (1); 1 reaches nothing (0); 2 isolated (0).
631        assert_eq!(
632            eccentricity_with_mode(&g, EccMode::Out).unwrap(),
633            vec![1, 0, 0]
634        );
635        // In: 0 reaches nothing in reverse (0); 1 reaches 0 (1); 2 (0).
636        assert_eq!(
637            eccentricity_with_mode(&g, EccMode::In).unwrap(),
638            vec![0, 1, 0]
639        );
640        // All: 0 and 1 reach each other (1); 2 still isolated (0).
641        assert_eq!(
642            eccentricity_with_mode(&g, EccMode::All).unwrap(),
643            vec![1, 1, 0]
644        );
645    }
646
647    #[test]
648    fn radius_diameter_match_min_max_of_eccentricity() {
649        let mut g = Graph::new(4, true).unwrap();
650        g.add_edge(0, 1).unwrap();
651        g.add_edge(1, 2).unwrap();
652        g.add_edge(2, 3).unwrap();
653        g.add_edge(3, 0).unwrap();
654        for m in [EccMode::Out, EccMode::In, EccMode::All] {
655            let ecc = eccentricity_with_mode(&g, m).unwrap();
656            assert_eq!(radius_with_mode(&g, m).unwrap(), ecc.iter().copied().min());
657            assert_eq!(
658                diameter_with_mode(&g, m).unwrap(),
659                ecc.iter().copied().max()
660            );
661        }
662    }
663
664    #[test]
665    fn empty_graph_with_mode_is_none() {
666        let g_u = Graph::with_vertices(0);
667        let g_d = Graph::new(0, true).unwrap();
668        for m in [EccMode::Out, EccMode::In, EccMode::All] {
669            assert_eq!(radius_with_mode(&g_u, m).unwrap(), None);
670            assert_eq!(diameter_with_mode(&g_u, m).unwrap(), None);
671            assert!(eccentricity_with_mode(&g_u, m).unwrap().is_empty());
672            assert_eq!(radius_with_mode(&g_d, m).unwrap(), None);
673            assert_eq!(diameter_with_mode(&g_d, m).unwrap(), None);
674            assert!(eccentricity_with_mode(&g_d, m).unwrap().is_empty());
675        }
676    }
677
678    // ---- SP-021..023: weighted ecc/rad/diam tests ------------------
679
680    #[test]
681    fn weighted_path_eccentricity() {
682        // Path 0-1-2 with weights (1, 2.5):
683        // ecc[0] = 0+1+2.5 = 3.5; ecc[1] = max(1, 2.5) = 2.5; ecc[2] = 3.5.
684        let mut g = Graph::with_vertices(3);
685        g.add_edge(0, 1).unwrap();
686        g.add_edge(1, 2).unwrap();
687        let r = eccentricity_weighted(&g, &[1.0, 2.5]).unwrap();
688        assert_eq!(r, vec![3.5, 2.5, 3.5]);
689        assert_eq!(radius_weighted(&g, &[1.0, 2.5]).unwrap(), Some(2.5));
690        assert_eq!(diameter_weighted(&g, &[1.0, 2.5]).unwrap(), Some(3.5));
691    }
692
693    #[test]
694    fn weighted_singleton_zero() {
695        let g = Graph::with_vertices(1);
696        assert_eq!(eccentricity_weighted(&g, &[]).unwrap(), vec![0.0]);
697        assert_eq!(radius_weighted(&g, &[]).unwrap(), Some(0.0));
698        assert_eq!(diameter_weighted(&g, &[]).unwrap(), Some(0.0));
699    }
700
701    #[test]
702    fn weighted_isolated_vertices_zero() {
703        let g = Graph::with_vertices(4);
704        assert_eq!(eccentricity_weighted(&g, &[]).unwrap(), vec![0.0; 4]);
705        assert_eq!(radius_weighted(&g, &[]).unwrap(), Some(0.0));
706        assert_eq!(diameter_weighted(&g, &[]).unwrap(), Some(0.0));
707    }
708
709    #[test]
710    fn weighted_disconnected_unconn_true_semantics() {
711        // 0-1 (w 1.0) and 2-3 (w 4.0). Each vertex's ecc = max within its
712        // component (unconn=true ignores cross-component pairs).
713        let mut g = Graph::with_vertices(4);
714        g.add_edge(0, 1).unwrap();
715        g.add_edge(2, 3).unwrap();
716        let r = eccentricity_weighted(&g, &[1.0, 4.0]).unwrap();
717        assert_eq!(r, vec![1.0, 1.0, 4.0, 4.0]);
718        // Diameter is 4.0 (the max), radius is 1.0 (the min).
719        assert_eq!(radius_weighted(&g, &[1.0, 4.0]).unwrap(), Some(1.0));
720        assert_eq!(diameter_weighted(&g, &[1.0, 4.0]).unwrap(), Some(4.0));
721    }
722
723    #[test]
724    fn weighted_directed_in_mode_reverses() {
725        // Directed path 0→1→2 with weights (1, 2). OUT-mode ecc[2]=0
726        // (sink); IN-mode ecc[2]=3 (reaches both predecessors).
727        let mut g = Graph::new(3, true).unwrap();
728        g.add_edge(0, 1).unwrap();
729        g.add_edge(1, 2).unwrap();
730        let w = vec![1.0, 2.0];
731        assert_eq!(
732            eccentricity_weighted_with_mode(&g, &w, EccMode::Out).unwrap(),
733            vec![3.0, 2.0, 0.0]
734        );
735        assert_eq!(
736            eccentricity_weighted_with_mode(&g, &w, EccMode::In).unwrap(),
737            vec![0.0, 1.0, 3.0]
738        );
739        assert_eq!(
740            eccentricity_weighted_with_mode(&g, &w, EccMode::All).unwrap(),
741            vec![3.0, 2.0, 3.0]
742        );
743    }
744
745    #[test]
746    fn weighted_undirected_modes_agree() {
747        // Triangle 0-1, 1-2, 0-2 with weights 1, 2, 4. Min path 0→2 is
748        // via 1: cost 3 < direct 4. ecc[0] = 3, ecc[1] = 2, ecc[2] = 3.
749        let mut g = Graph::with_vertices(3);
750        g.add_edge(0, 1).unwrap();
751        g.add_edge(1, 2).unwrap();
752        g.add_edge(0, 2).unwrap();
753        let w = vec![1.0, 2.0, 4.0];
754        for m in [EccMode::Out, EccMode::In, EccMode::All] {
755            assert_eq!(
756                eccentricity_weighted_with_mode(&g, &w, m).unwrap(),
757                vec![3.0, 2.0, 3.0],
758                "mode {m:?}"
759            );
760        }
761    }
762
763    #[test]
764    fn weighted_negative_weight_errors() {
765        let mut g = Graph::with_vertices(2);
766        g.add_edge(0, 1).unwrap();
767        assert!(eccentricity_weighted(&g, &[-1.0]).is_err());
768    }
769
770    #[test]
771    fn weighted_empty_graph_radius_diameter_none() {
772        let g = Graph::with_vertices(0);
773        assert_eq!(radius_weighted(&g, &[]).unwrap(), None);
774        assert_eq!(diameter_weighted(&g, &[]).unwrap(), None);
775        assert!(eccentricity_weighted(&g, &[]).unwrap().is_empty());
776    }
777
778    #[test]
779    fn weighted_with_mode_default_matches_out() {
780        let mut g = Graph::with_vertices(3);
781        g.add_edge(0, 1).unwrap();
782        g.add_edge(1, 2).unwrap();
783        let w = vec![1.0, 2.5];
784        assert_eq!(
785            eccentricity_weighted(&g, &w).unwrap(),
786            eccentricity_weighted_with_mode(&g, &w, EccMode::Out).unwrap()
787        );
788    }
789}