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}