rust_igraph/algorithms/paths/
histogram.rs1use std::collections::VecDeque;
13
14use crate::core::{Graph, IgraphResult, VertexId};
15
16#[derive(Debug, Clone)]
26pub struct PathLengthHistResult {
27 pub hist: Vec<f64>,
29 pub unconnected: f64,
31}
32
33pub fn path_length_hist(graph: &Graph, directed: bool) -> IgraphResult<PathLengthHistResult> {
61 let n = graph.vcount();
62 let n_us = n as usize;
63
64 let effective_directed = directed && graph.is_directed();
65
66 if n == 0 {
67 return Ok(PathLengthHistResult {
68 hist: Vec::new(),
69 unconnected: 0.0,
70 });
71 }
72
73 let mut hist: Vec<f64> = Vec::new();
74 let mut unconnected: f64 = 0.0;
75
76 let mut visited: Vec<u32> = vec![0; n_us];
77 let mut queue: VecDeque<(VertexId, u32)> = VecDeque::new();
78
79 for i in 0..n {
80 let marker = i
81 .checked_add(1)
82 .ok_or(crate::core::IgraphError::Internal("vertex marker overflow"))?;
83 let mut nodes_reached: u32 = 1;
84
85 queue.clear();
86 queue.push_back((i, 0));
87 visited[i as usize] = marker;
88
89 while let Some((v, dist)) = queue.pop_front() {
90 let neis = neighbors_for_mode(graph, v, effective_directed)?;
91 for nb in neis {
92 if visited[nb as usize] == marker {
93 continue;
94 }
95 visited[nb as usize] = marker;
96 nodes_reached = nodes_reached
97 .checked_add(1)
98 .ok_or(crate::core::IgraphError::Internal("nodes_reached overflow"))?;
99
100 let idx = dist as usize;
101 if idx >= hist.len() {
102 hist.resize(idx + 1, 0.0);
103 }
104 hist[idx] += 1.0;
105
106 queue.push_back((
107 nb,
108 dist.checked_add(1)
109 .ok_or(crate::core::IgraphError::Internal("BFS distance overflow"))?,
110 ));
111 }
112 }
113
114 unconnected += f64::from(n - nodes_reached);
115 }
116
117 if !effective_directed {
118 for h in &mut hist {
119 *h /= 2.0;
120 }
121 unconnected /= 2.0;
122 }
123
124 Ok(PathLengthHistResult { hist, unconnected })
125}
126
127fn neighbors_for_mode(graph: &Graph, v: VertexId, directed: bool) -> IgraphResult<Vec<VertexId>> {
128 if directed {
129 graph.out_neighbors_vec(v)
130 } else if graph.is_directed() {
131 let mut out = graph.out_neighbors_vec(v)?;
132 let in_neis = graph.in_neighbors_vec(v)?;
133 out.extend(in_neis);
134 Ok(out)
135 } else {
136 graph.neighbors(v)
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::create;
144
145 fn approx_eq(a: f64, b: f64) -> bool {
146 (a - b).abs() < 1e-9
147 }
148
149 #[test]
150 fn empty_graph() {
151 let g = Graph::with_vertices(0);
152 let r = path_length_hist(&g, false).expect("ok");
153 assert!(r.hist.is_empty());
154 assert!(approx_eq(r.unconnected, 0.0));
155 }
156
157 #[test]
158 fn single_vertex() {
159 let g = Graph::with_vertices(1);
160 let r = path_length_hist(&g, false).expect("ok");
161 assert!(r.hist.is_empty());
162 assert!(approx_eq(r.unconnected, 0.0));
163 }
164
165 #[test]
166 fn two_isolated_vertices() {
167 let g = Graph::with_vertices(2);
168 let r = path_length_hist(&g, false).expect("ok");
169 assert!(r.hist.is_empty());
170 assert!(approx_eq(r.unconnected, 1.0));
171 }
172
173 #[test]
174 fn single_edge_undirected() {
175 let g = create(&[(0, 1)], 2, false).expect("ok");
176 let r = path_length_hist(&g, false).expect("ok");
177 assert_eq!(r.hist, vec![1.0]);
178 assert!(approx_eq(r.unconnected, 0.0));
179 }
180
181 #[test]
182 fn path_3_undirected() {
183 let g = create(&[(0, 1), (1, 2)], 3, false).expect("ok");
185 let r = path_length_hist(&g, false).expect("ok");
186 assert_eq!(r.hist, vec![2.0, 1.0]);
187 assert!(approx_eq(r.unconnected, 0.0));
188 }
189
190 #[test]
191 fn triangle_undirected() {
192 let g = create(&[(0, 1), (1, 2), (0, 2)], 3, false).expect("ok");
194 let r = path_length_hist(&g, false).expect("ok");
195 assert_eq!(r.hist, vec![3.0]);
196 assert!(approx_eq(r.unconnected, 0.0));
197 }
198
199 #[test]
200 fn star_4_undirected() {
201 let g = create(&[(0, 1), (0, 2), (0, 3)], 4, false).expect("ok");
204 let r = path_length_hist(&g, false).expect("ok");
205 assert_eq!(r.hist, vec![3.0, 3.0]);
206 assert!(approx_eq(r.unconnected, 0.0));
207 }
208
209 #[test]
210 fn disconnected_components_undirected() {
211 let g = create(&[(0, 1), (2, 3)], 4, false).expect("ok");
213 let r = path_length_hist(&g, false).expect("ok");
214 assert_eq!(r.hist, vec![2.0]);
215 assert!(approx_eq(r.unconnected, 4.0));
216 }
217
218 #[test]
219 fn directed_path_out_mode() {
220 let g = create(&[(0, 1), (1, 2)], 3, true).expect("ok");
222 let r = path_length_hist(&g, true).expect("ok");
223 assert_eq!(r.hist, vec![2.0, 1.0]);
226 assert!(approx_eq(r.unconnected, 3.0));
228 }
229
230 #[test]
231 fn directed_path_undirected_mode() {
232 let g = create(&[(0, 1), (1, 2)], 3, true).expect("ok");
234 let r = path_length_hist(&g, false).expect("ok");
235 assert_eq!(r.hist, vec![2.0, 1.0]);
237 assert!(approx_eq(r.unconnected, 0.0));
238 }
239
240 #[test]
241 fn directed_cycle() {
242 let g = create(&[(0, 1), (1, 2), (2, 0)], 3, true).expect("ok");
244 let r = path_length_hist(&g, true).expect("ok");
245 assert_eq!(r.hist, vec![3.0, 3.0]);
249 assert!(approx_eq(r.unconnected, 0.0));
250 }
251
252 #[test]
253 fn self_loop_ignored_in_counting() {
254 let g = create(&[(0, 0), (0, 1)], 2, false).expect("ok");
255 let r = path_length_hist(&g, false).expect("ok");
256 assert_eq!(r.hist, vec![1.0]);
257 assert!(approx_eq(r.unconnected, 0.0));
258 }
259
260 #[test]
261 fn multi_edge_does_not_double_count() {
262 let g = create(&[(0, 1), (0, 1), (1, 2)], 3, false).expect("ok");
263 let r = path_length_hist(&g, false).expect("ok");
264 assert_eq!(r.hist, vec![2.0, 1.0]);
265 assert!(approx_eq(r.unconnected, 0.0));
266 }
267
268 #[test]
269 fn k4_undirected() {
270 let g = create(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], 4, false).expect("ok");
272 let r = path_length_hist(&g, false).expect("ok");
273 assert_eq!(r.hist, vec![6.0]);
274 assert!(approx_eq(r.unconnected, 0.0));
275 }
276}
277
278#[cfg(all(test, feature = "proptest-harness"))]
279mod proptests {
280 use super::*;
281 use crate::create;
282 use proptest::prelude::*;
283
284 fn arb_graph(max_v: u32) -> impl Strategy<Value = (Graph, bool)> {
285 (1..=max_v).prop_flat_map(|n| {
286 let max_edges = (n as usize).saturating_mul(n.saturating_sub(1) as usize);
287 let max_e = max_edges.min(20);
288 (
289 proptest::collection::vec((0..n, 0..n), 0..=max_e),
290 Just(n),
291 proptest::bool::ANY,
292 )
293 .prop_map(|(edges, n, directed)| {
294 let edge_tuples: Vec<(u32, u32)> = edges.into_iter().collect();
295 let g = create(&edge_tuples, n, directed).expect("arb graph creation");
296 (g, directed)
297 })
298 })
299 }
300
301 proptest! {
302 #[test]
303 fn hist_counts_are_non_negative((g, directed) in arb_graph(10)) {
304 let r = path_length_hist(&g, directed).expect("ok");
305 for &h in &r.hist {
306 prop_assert!(h >= 0.0, "negative histogram count: {h}");
307 }
308 prop_assert!(r.unconnected >= 0.0);
309 }
310
311 #[test]
312 fn total_pairs_equals_expected((g, directed) in arb_graph(10)) {
313 let r = path_length_hist(&g, directed).expect("ok");
314 let n = g.vcount() as f64;
315 let total: f64 = r.hist.iter().sum::<f64>() + r.unconnected;
316 let effective_directed = directed && g.is_directed();
317 let expected = if effective_directed {
318 n * (n - 1.0)
319 } else {
320 n * (n - 1.0) / 2.0
321 };
322 prop_assert!(
323 (total - expected).abs() < 1e-9,
324 "total {total} != expected {expected}"
325 );
326 }
327
328 #[test]
329 fn undirected_mode_on_undirected_graph_halves_directed(
330 edges in proptest::collection::vec((0u32..8, 0u32..8), 0..=15)
331 ) {
332 let g = create(&edges, 8, false).expect("ok");
333 let r_undir = path_length_hist(&g, false).expect("ok");
334 let r_dir = path_length_hist(&g, true).expect("ok");
335 prop_assert_eq!(r_undir.hist.len(), r_dir.hist.len());
338 for (a, b) in r_undir.hist.iter().zip(r_dir.hist.iter()) {
339 prop_assert!((a - b).abs() < 1e-9);
340 }
341 prop_assert!((r_undir.unconnected - r_dir.unconnected).abs() < 1e-9);
342 }
343 }
344}