Skip to main content

rust_igraph/algorithms/properties/
graph_periphery.rs

1//! Graph periphery and pseudo-peripheral vertices (ALGO-TR-038).
2//!
3//! The **periphery** of a graph is the set of vertices whose
4//! eccentricity equals the diameter (maximum eccentricity).
5//! A vertex is **pseudo-peripheral** if its eccentricity equals
6//! the eccentricity of every vertex at maximum distance from it.
7
8#![allow(
9    clippy::cast_possible_truncation,
10    clippy::cast_precision_loss,
11    clippy::many_single_char_names,
12    clippy::needless_range_loop,
13    clippy::too_many_lines
14)]
15
16use crate::algorithms::paths::radii::{EccMode, eccentricity_with_mode};
17use crate::core::{Graph, IgraphResult, VertexId};
18
19/// Return the periphery vertices of a graph.
20///
21/// A vertex belongs to the periphery if its eccentricity equals the
22/// graph diameter (maximum eccentricity). For directed graphs, `mode`
23/// controls BFS direction.
24///
25/// Returns an empty vector for an empty graph.
26///
27/// # Examples
28///
29/// ```
30/// use rust_igraph::{Graph, graph_periphery, EccMode};
31///
32/// // Path 0-1-2-3-4: periphery is {0, 4} (eccentricity 4 = diameter)
33/// let g = Graph::from_edges(&[(0,1),(1,2),(2,3),(3,4)], false, Some(5)).unwrap();
34/// let peri = graph_periphery(&g, EccMode::All).unwrap();
35/// assert_eq!(peri, vec![0, 4]);
36/// ```
37pub fn graph_periphery(graph: &Graph, mode: EccMode) -> IgraphResult<Vec<VertexId>> {
38    let n = graph.vcount();
39    if n == 0 {
40        return Ok(Vec::new());
41    }
42
43    let ecc = eccentricity_with_mode(graph, mode)?;
44
45    let max_ecc = ecc.iter().copied().max().unwrap_or(0);
46
47    let periphery: Vec<VertexId> = ecc
48        .iter()
49        .enumerate()
50        .filter(|(_, e)| **e == max_ecc)
51        .map(|(i, _)| i as VertexId)
52        .collect();
53
54    Ok(periphery)
55}
56
57/// Classify each vertex by its eccentricity class.
58///
59/// Returns a struct with center, periphery, and the eccentricity
60/// vector for further analysis.
61///
62/// # Examples
63///
64/// ```
65/// use rust_igraph::{Graph, eccentricity_classes, EccMode};
66///
67/// let g = Graph::from_edges(&[(0,1),(1,2),(2,3),(3,4)], false, Some(5)).unwrap();
68/// let classes = eccentricity_classes(&g, EccMode::All).unwrap();
69/// assert_eq!(classes.center, vec![2]);
70/// assert_eq!(classes.periphery, vec![0, 4]);
71/// assert_eq!(classes.radius, 2);
72/// assert_eq!(classes.diameter, 4);
73/// ```
74pub fn eccentricity_classes(graph: &Graph, mode: EccMode) -> IgraphResult<EccentricityClasses> {
75    let n = graph.vcount();
76    if n == 0 {
77        return Ok(EccentricityClasses {
78            eccentricities: Vec::new(),
79            center: Vec::new(),
80            periphery: Vec::new(),
81            radius: 0,
82            diameter: 0,
83        });
84    }
85
86    let ecc = eccentricity_with_mode(graph, mode)?;
87
88    let min_ecc = ecc.iter().copied().min().unwrap_or(0);
89    let max_ecc = ecc.iter().copied().max().unwrap_or(0);
90
91    let center: Vec<VertexId> = ecc
92        .iter()
93        .enumerate()
94        .filter(|(_, e)| **e == min_ecc)
95        .map(|(i, _)| i as VertexId)
96        .collect();
97
98    let periphery: Vec<VertexId> = ecc
99        .iter()
100        .enumerate()
101        .filter(|(_, e)| **e == max_ecc)
102        .map(|(i, _)| i as VertexId)
103        .collect();
104
105    Ok(EccentricityClasses {
106        eccentricities: ecc,
107        center,
108        periphery,
109        radius: min_ecc,
110        diameter: max_ecc,
111    })
112}
113
114/// Result of [`eccentricity_classes`].
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct EccentricityClasses {
117    /// Eccentricity of each vertex.
118    pub eccentricities: Vec<u32>,
119    /// Center vertices (minimum eccentricity).
120    pub center: Vec<VertexId>,
121    /// Periphery vertices (maximum eccentricity).
122    pub periphery: Vec<VertexId>,
123    /// Radius (minimum eccentricity).
124    pub radius: u32,
125    /// Diameter (maximum eccentricity).
126    pub diameter: u32,
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    fn path5() -> Graph {
134        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)], false, Some(5)).unwrap()
135    }
136
137    fn path6() -> Graph {
138        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)], false, Some(6)).unwrap()
139    }
140
141    fn k4() -> Graph {
142        Graph::from_edges(
143            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
144            false,
145            Some(4),
146        )
147        .unwrap()
148    }
149
150    fn cycle5() -> Graph {
151        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
152    }
153
154    fn star5() -> Graph {
155        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
156    }
157
158    // --- graph_periphery ---
159
160    #[test]
161    fn gp_empty() {
162        let g = Graph::with_vertices(0);
163        assert!(graph_periphery(&g, EccMode::All).unwrap().is_empty());
164    }
165
166    #[test]
167    fn gp_single() {
168        let g = Graph::with_vertices(1);
169        assert_eq!(graph_periphery(&g, EccMode::All).unwrap(), vec![0]);
170    }
171
172    #[test]
173    fn gp_path5() {
174        // Endpoints have eccentricity 4 = diameter
175        let peri = graph_periphery(&path5(), EccMode::All).unwrap();
176        assert_eq!(peri, vec![0, 4]);
177    }
178
179    #[test]
180    fn gp_path6() {
181        let peri = graph_periphery(&path6(), EccMode::All).unwrap();
182        assert_eq!(peri, vec![0, 5]);
183    }
184
185    #[test]
186    fn gp_k4() {
187        // All vertices have eccentricity 1 → all are periphery (and center)
188        let peri = graph_periphery(&k4(), EccMode::All).unwrap();
189        assert_eq!(peri, vec![0, 1, 2, 3]);
190    }
191
192    #[test]
193    fn gp_cycle5() {
194        // All vertices have eccentricity 2 → all periphery
195        let peri = graph_periphery(&cycle5(), EccMode::All).unwrap();
196        assert_eq!(peri, vec![0, 1, 2, 3, 4]);
197    }
198
199    #[test]
200    fn gp_star5() {
201        // Leaves have eccentricity 2, center has 1 → periphery = leaves
202        let peri = graph_periphery(&star5(), EccMode::All).unwrap();
203        assert_eq!(peri, vec![1, 2, 3, 4]);
204    }
205
206    #[test]
207    fn gp_no_edges() {
208        // All vertices eccentricity 0 → all periphery
209        let g = Graph::with_vertices(3);
210        let peri = graph_periphery(&g, EccMode::All).unwrap();
211        assert_eq!(peri, vec![0, 1, 2]);
212    }
213
214    // --- eccentricity_classes ---
215
216    #[test]
217    fn ec_path5() {
218        let ec = eccentricity_classes(&path5(), EccMode::All).unwrap();
219        assert_eq!(ec.radius, 2);
220        assert_eq!(ec.diameter, 4);
221        assert_eq!(ec.center, vec![2]);
222        assert_eq!(ec.periphery, vec![0, 4]);
223        assert_eq!(ec.eccentricities, vec![4, 3, 2, 3, 4]);
224    }
225
226    #[test]
227    fn ec_k4() {
228        let ec = eccentricity_classes(&k4(), EccMode::All).unwrap();
229        assert_eq!(ec.radius, 1);
230        assert_eq!(ec.diameter, 1);
231        assert_eq!(ec.center, vec![0, 1, 2, 3]);
232        assert_eq!(ec.periphery, vec![0, 1, 2, 3]);
233    }
234
235    #[test]
236    fn ec_star5() {
237        let ec = eccentricity_classes(&star5(), EccMode::All).unwrap();
238        assert_eq!(ec.radius, 1);
239        assert_eq!(ec.diameter, 2);
240        assert_eq!(ec.center, vec![0]);
241        assert_eq!(ec.periphery, vec![1, 2, 3, 4]);
242    }
243
244    #[test]
245    fn ec_empty() {
246        let g = Graph::with_vertices(0);
247        let ec = eccentricity_classes(&g, EccMode::All).unwrap();
248        assert_eq!(ec.radius, 0);
249        assert_eq!(ec.diameter, 0);
250        assert!(ec.center.is_empty());
251        assert!(ec.periphery.is_empty());
252    }
253
254    #[test]
255    fn ec_single() {
256        let g = Graph::with_vertices(1);
257        let ec = eccentricity_classes(&g, EccMode::All).unwrap();
258        assert_eq!(ec.radius, 0);
259        assert_eq!(ec.diameter, 0);
260        assert_eq!(ec.center, vec![0]);
261        assert_eq!(ec.periphery, vec![0]);
262    }
263
264    // --- cross-consistency ---
265
266    #[test]
267    fn center_and_periphery_cover_self_regular() {
268        // In a vertex-transitive graph (cycle, complete), center == periphery
269        let ec = eccentricity_classes(&cycle5(), EccMode::All).unwrap();
270        assert_eq!(ec.center, ec.periphery);
271    }
272
273    #[test]
274    fn radius_leq_diameter() {
275        for g in &[path5(), path6(), k4(), cycle5(), star5()] {
276            let ec = eccentricity_classes(g, EccMode::All).unwrap();
277            assert!(ec.radius <= ec.diameter);
278        }
279    }
280
281    #[test]
282    fn diameter_leq_2_radius() {
283        // For connected graphs: diameter <= 2 * radius
284        for g in &[path5(), path6(), k4(), cycle5(), star5()] {
285            let ec = eccentricity_classes(g, EccMode::All).unwrap();
286            assert!(ec.diameter <= 2 * ec.radius);
287        }
288    }
289
290    #[test]
291    fn center_periphery_partition() {
292        // Center ∩ periphery should be non-empty only when radius == diameter
293        let ec = eccentricity_classes(&path5(), EccMode::All).unwrap();
294        let center_set: std::collections::HashSet<_> = ec.center.iter().collect();
295        let peri_set: std::collections::HashSet<_> = ec.periphery.iter().collect();
296        let overlap: Vec<_> = center_set.intersection(&peri_set).collect();
297        if ec.radius == ec.diameter {
298            assert!(!overlap.is_empty());
299        } else {
300            assert!(overlap.is_empty());
301        }
302    }
303}