rust_igraph/algorithms/properties/
knn.rs1use crate::core::{Graph, IgraphError, IgraphResult};
21
22pub fn avg_nearest_neighbor_degree(graph: &Graph) -> IgraphResult<Vec<Option<f64>>> {
45 let n = graph.vcount();
46 let n_us = n as usize;
47
48 let mut deg: Vec<u32> = Vec::with_capacity(n_us);
51 for v in 0..n {
52 deg.push(
53 u32::try_from(graph.degree(v)?)
54 .map_err(|_| crate::core::IgraphError::Internal("degree exceeds u32 in knn"))?,
55 );
56 }
57
58 let mut out: Vec<Option<f64>> = Vec::with_capacity(n_us);
59 for v in 0..n {
60 let neis = graph.neighbors(v)?;
61 if neis.is_empty() {
62 out.push(None);
63 continue;
64 }
65 let mut sum: u64 = 0;
66 for &w in &neis {
67 sum += u64::from(deg[w as usize]);
68 }
69 #[allow(clippy::cast_precision_loss)]
71 let avg = (sum as f64) / (neis.len() as f64);
72 out.push(Some(avg));
73 }
74 Ok(out)
75}
76
77pub fn avg_nearest_neighbor_degree_weighted(
106 graph: &Graph,
107 weights: &[f64],
108) -> IgraphResult<Vec<Option<f64>>> {
109 let n = graph.vcount();
110 let n_us = n as usize;
111 let m = graph.ecount();
112 if weights.len() != m {
113 return Err(IgraphError::Internal(
114 "weights length does not match ecount in knn_weighted",
115 ));
116 }
117 for &w in weights {
118 if !w.is_finite() || w < 0.0 {
119 return Err(IgraphError::Internal(
120 "weights must be finite and non-negative in knn_weighted",
121 ));
122 }
123 }
124
125 let mut deg: Vec<u32> = Vec::with_capacity(n_us);
126 for v in 0..n {
127 deg.push(
128 u32::try_from(graph.degree(v)?)
129 .map_err(|_| IgraphError::Internal("degree exceeds u32 in knn_weighted"))?,
130 );
131 }
132
133 let mut out: Vec<Option<f64>> = Vec::with_capacity(n_us);
134 for v in 0..n {
135 let inc = graph.incident(v)?;
140 let mut sum = 0.0_f64;
141 let mut strength = 0.0_f64;
142 for &e in &inc {
143 let u = graph.edge_other(e, v)?;
144 let w = weights[e as usize];
145 strength += w;
146 sum += w * f64::from(deg[u as usize]);
147 }
148 if strength > 0.0 {
149 out.push(Some(sum / strength));
150 } else {
151 out.push(None);
152 }
153 }
154 Ok(out)
155}
156
157pub fn knnk(graph: &Graph) -> IgraphResult<Vec<Option<f64>>> {
179 let n = graph.vcount();
180 let knn = avg_nearest_neighbor_degree(graph)?;
181 let mut max_deg: u32 = 0;
182 for v in 0..n {
183 let d = u32::try_from(graph.degree(v)?)
184 .map_err(|_| IgraphError::Internal("degree exceeds u32 in knnk"))?;
185 if d > max_deg {
186 max_deg = d;
187 }
188 }
189 if max_deg == 0 {
190 return Ok(Vec::new());
191 }
192 let max_deg_us = max_deg as usize;
193 let mut sums: Vec<f64> = vec![0.0; max_deg_us];
194 let mut counts: Vec<u32> = vec![0; max_deg_us];
195 for v in 0..n {
196 if let Some(k) = knn[v as usize] {
197 let d = graph.degree(v)?;
198 sums[d - 1] += k;
199 counts[d - 1] += 1;
200 }
201 }
202 Ok(sums
203 .iter()
204 .zip(counts.iter())
205 .map(|(&s, &c)| if c == 0 { None } else { Some(s / f64::from(c)) })
206 .collect())
207}
208
209pub fn knnk_weighted(graph: &Graph, weights: &[f64]) -> IgraphResult<Vec<Option<f64>>> {
229 let n = graph.vcount();
230 let m = graph.ecount();
231 if weights.len() != m {
232 return Err(IgraphError::Internal(
233 "weights length does not match ecount in knnk_weighted",
234 ));
235 }
236 for &w in weights {
237 if !w.is_finite() || w < 0.0 {
238 return Err(IgraphError::Internal(
239 "weights must be finite and non-negative in knnk_weighted",
240 ));
241 }
242 }
243 let mut max_deg: u32 = 0;
244 let mut deg: Vec<u32> = Vec::with_capacity(n as usize);
245 for v in 0..n {
246 let d = u32::try_from(graph.degree(v)?)
247 .map_err(|_| IgraphError::Internal("degree exceeds u32 in knnk_weighted"))?;
248 deg.push(d);
249 if d > max_deg {
250 max_deg = d;
251 }
252 }
253 if max_deg == 0 {
254 return Ok(Vec::new());
255 }
256 let max_deg_us = max_deg as usize;
257 let mut sums: Vec<f64> = vec![0.0; max_deg_us];
258 let mut strs: Vec<f64> = vec![0.0; max_deg_us];
259
260 for v in 0..n {
261 let inc = graph.incident(v)?;
262 if inc.is_empty() {
263 continue;
264 }
265 let mut sum_v = 0.0_f64;
266 let mut str_v = 0.0_f64;
267 for &e in &inc {
268 let u = graph.edge_other(e, v)?;
269 let w = weights[e as usize];
270 str_v += w;
271 sum_v += w * f64::from(deg[u as usize]);
272 }
273 let bucket = deg[v as usize] as usize - 1;
274 sums[bucket] += sum_v;
275 strs[bucket] += str_v;
276 }
277
278 Ok(sums
279 .iter()
280 .zip(strs.iter())
281 .map(|(&s, &t)| if t > 0.0 { Some(s / t) } else { None })
282 .collect())
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn empty_graph_yields_empty_vec() {
291 let g = Graph::with_vertices(0);
292 assert!(avg_nearest_neighbor_degree(&g).unwrap().is_empty());
293 }
294
295 #[test]
296 fn isolated_vertices_have_none() {
297 let g = Graph::with_vertices(3);
298 assert_eq!(
299 avg_nearest_neighbor_degree(&g).unwrap(),
300 vec![None, None, None]
301 );
302 }
303
304 #[test]
305 fn star_centre_has_avg_1_leaves_have_3() {
306 let mut g = Graph::with_vertices(4);
307 g.add_edge(0, 1).unwrap();
308 g.add_edge(0, 2).unwrap();
309 g.add_edge(0, 3).unwrap();
310 assert_eq!(
311 avg_nearest_neighbor_degree(&g).unwrap(),
312 vec![Some(1.0), Some(3.0), Some(3.0), Some(3.0)]
313 );
314 }
315
316 #[test]
317 fn path_5_endpoints_see_internal_neighbours() {
318 let mut g = Graph::with_vertices(5);
325 for i in 0..4 {
326 g.add_edge(i, i + 1).unwrap();
327 }
328 assert_eq!(
329 avg_nearest_neighbor_degree(&g).unwrap(),
330 vec![Some(2.0), Some(1.5), Some(2.0), Some(1.5), Some(2.0)]
331 );
332 }
333
334 #[test]
335 fn k4_uniform_degree_3() {
336 let mut g = Graph::with_vertices(4);
337 for u in 0..4u32 {
338 for v in (u + 1)..4 {
339 g.add_edge(u, v).unwrap();
340 }
341 }
342 assert_eq!(avg_nearest_neighbor_degree(&g).unwrap(), vec![Some(3.0); 4]);
343 }
344
345 #[test]
346 fn weighted_uniform_weights_match_unweighted() {
347 let mut g = Graph::with_vertices(5);
349 for i in 0..4u32 {
350 g.add_edge(i, i + 1).unwrap();
351 }
352 let weights = vec![1.0; 4];
353 let unweighted = avg_nearest_neighbor_degree(&g).unwrap();
354 let weighted = avg_nearest_neighbor_degree_weighted(&g, &weights).unwrap();
355 assert_eq!(unweighted.len(), weighted.len());
356 for i in 0..unweighted.len() {
357 match (unweighted[i], weighted[i]) {
358 (Some(a), Some(b)) => assert!((a - b).abs() < 1e-12),
359 (None, None) => {}
360 _ => panic!("uniform weights diverged at vertex {i}"),
361 }
362 }
363 }
364
365 #[test]
366 fn weighted_triangle_unequal_weights() {
367 let mut g = Graph::with_vertices(3);
375 g.add_edge(0, 1).unwrap();
376 g.add_edge(1, 2).unwrap();
377 g.add_edge(2, 0).unwrap();
378 let r = avg_nearest_neighbor_degree_weighted(&g, &[1.0, 2.0, 4.0]).unwrap();
379 assert_eq!(r, vec![Some(2.0); 3]);
380 }
381
382 #[test]
383 fn weighted_isolated_vertex_yields_none() {
384 let g = Graph::with_vertices(2);
385 let r = avg_nearest_neighbor_degree_weighted(&g, &[]).unwrap();
386 assert_eq!(r, vec![None, None]);
387 }
388
389 #[test]
390 fn weighted_zero_weight_edge_yields_none() {
391 let mut g = Graph::with_vertices(2);
393 g.add_edge(0, 1).unwrap();
394 let r = avg_nearest_neighbor_degree_weighted(&g, &[0.0]).unwrap();
395 assert_eq!(r, vec![None, None]);
396 }
397
398 #[test]
399 fn weighted_invalid_weights_length_errors() {
400 let mut g = Graph::with_vertices(2);
401 g.add_edge(0, 1).unwrap();
402 assert!(avg_nearest_neighbor_degree_weighted(&g, &[]).is_err());
403 }
404
405 #[test]
406 fn weighted_negative_weights_error() {
407 let mut g = Graph::with_vertices(2);
408 g.add_edge(0, 1).unwrap();
409 assert!(avg_nearest_neighbor_degree_weighted(&g, &[-1.0]).is_err());
410 }
411
412 #[test]
413 fn knnk_star_distinct_degrees() {
414 let mut g = Graph::with_vertices(4);
419 g.add_edge(0, 1).unwrap();
420 g.add_edge(0, 2).unwrap();
421 g.add_edge(0, 3).unwrap();
422 let r = knnk(&g).unwrap();
423 assert_eq!(r, vec![Some(3.0), None, Some(1.0)]);
424 }
425
426 #[test]
427 fn knnk_path_5() {
428 let mut g = Graph::with_vertices(5);
433 for i in 0..4u32 {
434 g.add_edge(i, i + 1).unwrap();
435 }
436 let r = knnk(&g).unwrap();
437 assert_eq!(r.len(), 2);
438 assert_eq!(r[0], Some(2.0));
439 assert!((r[1].unwrap() - 5.0_f64 / 3.0).abs() < 1e-12);
440 }
441
442 #[test]
443 fn knnk_empty_graph_yields_empty() {
444 let g = Graph::with_vertices(0);
445 assert!(knnk(&g).unwrap().is_empty());
446 }
447
448 #[test]
449 fn knnk_no_edges_yields_empty() {
450 let g = Graph::with_vertices(5);
451 assert!(knnk(&g).unwrap().is_empty());
453 }
454
455 #[test]
456 fn knnk_weighted_uniform_matches_knnk() {
457 let mut g = Graph::with_vertices(5);
458 for i in 0..4u32 {
459 g.add_edge(i, i + 1).unwrap();
460 }
461 let unw = knnk(&g).unwrap();
462 let w = knnk_weighted(&g, &[1.0; 4]).unwrap();
463 assert_eq!(unw.len(), w.len());
464 for i in 0..unw.len() {
465 match (unw[i], w[i]) {
466 (Some(a), Some(b)) => assert!((a - b).abs() < 1e-12),
467 (None, None) => {}
468 _ => panic!("knnk_weighted uniform diverged at idx {i}"),
469 }
470 }
471 }
472
473 #[test]
474 fn self_loop_inflates_neighbour_degree() {
475 let mut g = Graph::with_vertices(2);
481 g.add_edge(0, 0).unwrap();
482 g.add_edge(0, 1).unwrap();
483 let r = avg_nearest_neighbor_degree(&g).unwrap();
484 let seven_thirds = 7.0_f64 / 3.0;
485 assert_eq!(r[0], Some(seven_thirds));
486 assert_eq!(r[1], Some(3.0));
487 }
488}