rust_igraph/algorithms/properties/
closeness_cutoff.rs1use crate::core::{Graph, IgraphResult, VertexId};
11use std::collections::VecDeque;
12
13#[derive(Debug, Clone)]
15pub struct ClosenessCutoffResult {
16 pub scores: Vec<Option<f64>>,
19 pub reachable_count: Vec<u32>,
21 pub all_reachable: bool,
23}
24
25pub fn closeness_cutoff(
60 graph: &Graph,
61 cutoff: u32,
62 normalized: bool,
63) -> IgraphResult<ClosenessCutoffResult> {
64 let n = graph.vcount();
65 let n_us = n as usize;
66 let mut scores = Vec::with_capacity(n_us);
67 let mut reachable_count = Vec::with_capacity(n_us);
68 let mut all_reachable = true;
69
70 for v in 0..n {
71 let (sum_dist, reach) = bfs_cutoff(graph, v, cutoff)?;
72
73 reachable_count.push(reach);
74
75 if reach == 0 || sum_dist == 0 {
76 scores.push(None);
77 } else if normalized {
78 #[allow(clippy::cast_precision_loss)]
79 let val = f64::from(reach) / f64::from(sum_dist);
80 scores.push(Some(val));
81 } else {
82 #[allow(clippy::cast_precision_loss)]
83 let val = 1.0 / f64::from(sum_dist);
84 scores.push(Some(val));
85 }
86
87 if reach < n.saturating_sub(1) {
88 all_reachable = false;
89 }
90 }
91
92 Ok(ClosenessCutoffResult {
93 scores,
94 reachable_count,
95 all_reachable,
96 })
97}
98
99fn bfs_cutoff(graph: &Graph, source: VertexId, cutoff: u32) -> IgraphResult<(u32, u32)> {
102 let n = graph.vcount() as usize;
103 let mut visited = vec![false; n];
104 visited[source as usize] = true;
105
106 let mut queue: VecDeque<(VertexId, u32)> = VecDeque::new();
107 queue.push_back((source, 0));
108
109 let mut sum_dist: u32 = 0;
110 let mut reach: u32 = 0;
111
112 while let Some((node, dist)) = queue.pop_front() {
113 if dist > cutoff {
114 continue;
115 }
116
117 if node != source {
118 sum_dist = sum_dist.saturating_add(dist);
119 reach = reach.saturating_add(1);
120 }
121
122 if dist == cutoff {
123 continue;
124 }
125
126 for &neighbor in &graph.neighbors(node)? {
127 if !visited[neighbor as usize] {
128 visited[neighbor as usize] = true;
129 queue.push_back((neighbor, dist.saturating_add(1)));
130 }
131 }
132 }
133
134 Ok((sum_dist, reach))
135}
136
137#[cfg(test)]
138#[allow(clippy::float_cmp)]
139mod tests {
140 use super::*;
141 use crate::core::Graph;
142
143 #[test]
144 fn empty_graph() {
145 let g = Graph::new(0, false).unwrap();
146 let r = closeness_cutoff(&g, 1, true).unwrap();
147 assert!(r.scores.is_empty());
148 assert!(r.all_reachable);
149 }
150
151 #[test]
152 fn single_vertex() {
153 let g = Graph::new(1, false).unwrap();
154 let r = closeness_cutoff(&g, 1, true).unwrap();
155 assert_eq!(r.scores[0], None);
156 assert_eq!(r.reachable_count[0], 0);
157 assert!(r.all_reachable);
158 }
159
160 #[test]
161 fn disconnected() {
162 let g = Graph::new(3, false).unwrap();
163 let r = closeness_cutoff(&g, 5, true).unwrap();
164 for s in &r.scores {
165 assert_eq!(*s, None);
166 }
167 assert!(!r.all_reachable);
168 }
169
170 #[test]
171 fn complete_graph_cutoff_1() {
172 let mut g = Graph::new(4, false).unwrap();
173 g.add_edges(vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)])
174 .unwrap();
175 let r = closeness_cutoff(&g, 1, true).unwrap();
176 for s in &r.scores {
179 assert_eq!(*s, Some(1.0));
180 }
181 assert!(r.all_reachable);
182 }
183
184 #[test]
185 fn path_cutoff_1() {
186 let mut g = Graph::new(4, false).unwrap();
188 g.add_edges(vec![(0, 1), (1, 2), (2, 3)]).unwrap();
189 let r = closeness_cutoff(&g, 1, true).unwrap();
190 assert_eq!(r.scores[0], Some(1.0));
192 assert_eq!(r.reachable_count[0], 1);
193 assert_eq!(r.scores[1], Some(1.0));
195 assert_eq!(r.reachable_count[1], 2);
196 assert!(!r.all_reachable);
197 }
198
199 #[test]
200 fn path_cutoff_2() {
201 let mut g = Graph::new(5, false).unwrap();
203 g.add_edges(vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap();
204 let r = closeness_cutoff(&g, 2, true).unwrap();
205 let expected = 2.0 / 3.0;
207 assert!((r.scores[0].unwrap() - expected).abs() < 1e-12);
208 assert_eq!(r.reachable_count[0], 2);
209 assert!(!r.all_reachable);
210 }
211
212 #[test]
213 fn large_cutoff_equals_closeness() {
214 let mut g = Graph::new(4, false).unwrap();
216 g.add_edges(vec![(0, 1), (1, 2), (2, 3)]).unwrap();
217 let r = closeness_cutoff(&g, 100, true).unwrap();
218 assert!(r.all_reachable);
219 assert!((r.scores[0].unwrap() - 0.5).abs() < 1e-12);
221 assert!((r.scores[1].unwrap() - 0.75).abs() < 1e-12);
223 }
224
225 #[test]
226 fn normalized_false() {
227 let mut g = Graph::new(3, false).unwrap();
229 g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
230 let r = closeness_cutoff(&g, 10, false).unwrap();
231 let expected = 1.0 / 3.0;
233 assert!((r.scores[0].unwrap() - expected).abs() < 1e-12);
234 assert!((r.scores[1].unwrap() - 0.5).abs() < 1e-12);
236 }
237
238 #[test]
239 fn cutoff_zero() {
240 let mut g = Graph::new(3, false).unwrap();
241 g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
242 let r = closeness_cutoff(&g, 0, true).unwrap();
243 for s in &r.scores {
245 assert_eq!(*s, None);
246 }
247 }
248
249 #[test]
250 fn directed_graph() {
251 let mut g = Graph::new(3, true).unwrap();
253 g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
254 let r = closeness_cutoff(&g, 10, true).unwrap();
255 let expected = 2.0 / 3.0;
257 assert!((r.scores[0].unwrap() - expected).abs() < 1e-12);
258 assert_eq!(r.scores[2], None);
260 }
261
262 #[test]
263 fn star_cutoff_1() {
264 let mut g = Graph::new(5, false).unwrap();
266 g.add_edges(vec![(0, 1), (0, 2), (0, 3), (0, 4)]).unwrap();
267 let r = closeness_cutoff(&g, 1, true).unwrap();
268 assert_eq!(r.scores[0], Some(1.0));
270 assert_eq!(r.reachable_count[0], 4);
271 assert_eq!(r.scores[1], Some(1.0));
273 assert_eq!(r.reachable_count[1], 1);
274 assert!(!r.all_reachable);
275 }
276}