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