Skip to main content

rust_igraph/algorithms/properties/
knn.rs

1//! Average nearest-neighbour degree (ALGO-PR-005 + ALGO-PR-005b).
2//!
3//! For each vertex `v`, the average degree over `v`'s neighbours.
4//! Counterpart of `igraph_avg_nearest_neighbor_degree(_, vss_all(),
5//! IGRAPH_ALL, IGRAPH_ALL, &knn, &knnk, weights)` from
6//! `references/igraph/src/properties/degrees.c:263`.
7//!
8//! Phase-1 minimal slice: undirected (or `IGRAPH_ALL` mode for
9//! directed input). Three exposed entry points:
10//!
11//! - [`avg_nearest_neighbor_degree`] — unweighted per-vertex (PR-005)
12//! - [`avg_nearest_neighbor_degree_weighted`] — weighted per-vertex
13//!   (PR-005b)
14//! - [`knnk`] / [`knnk_weighted`] — per-degree aggregate `k_nn(k)`
15//!   (PR-005b)
16//!
17//! All variants return `Vec<Option<f64>>` where `None` corresponds to
18//! upstream's `IGRAPH_NAN` (no neighbours / no vertices of that degree).
19
20use crate::core::{Graph, IgraphError, IgraphResult};
21
22/// Average nearest-neighbour degree, per vertex.
23///
24/// `result[v] = Some(d)` where `d` is the mean degree over `v`'s
25/// neighbours; `None` if `v` has no neighbours. Self-loops are
26/// counted under upstream's `IGRAPH_LOOPS` convention (each loop
27/// counts twice for undirected degree).
28///
29/// # Examples
30///
31/// ```
32/// use rust_igraph::{Graph, avg_nearest_neighbor_degree};
33///
34/// // Star with centre 0 and leaves 1-2-3:
35/// // Centre's neighbours have degree 1 each → knn[0] = 1.
36/// // Leaves' single neighbour (centre) has degree 3 → knn[leaf] = 3.
37/// let mut g = Graph::with_vertices(4);
38/// g.add_edge(0, 1).unwrap();
39/// g.add_edge(0, 2).unwrap();
40/// g.add_edge(0, 3).unwrap();
41/// let knn = avg_nearest_neighbor_degree(&g).unwrap();
42/// assert_eq!(knn, vec![Some(1.0), Some(3.0), Some(3.0), Some(3.0)]);
43/// ```
44pub 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    // Pre-cache per-vertex degree (LOOPS-counted; matches upstream's
49    // IGRAPH_LOOPS default).
50    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        // sum / nv. nv ≤ |E|·2 ≤ 2 * 2^31 — fits f64.
70        #[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
77/// Weighted average nearest-neighbour degree (Barrat formula).
78///
79/// `result[v] = Some( (1/s_v) Σ_{u ∼ v} w(v,u) · deg(u) )` where
80/// `s_v = Σ_{u ∼ v} w(v,u)` is `v`'s strength (sum of incident edge
81/// weights, with self-loops counted twice for undirected graphs to
82/// match upstream's `IGRAPH_LOOPS` convention).
83///
84/// Returns `None` for vertices with strength 0 (no neighbours, or all
85/// incident weights are 0). Weights must have length `graph.ecount()`
86/// and contain only finite, non-negative values; otherwise this returns
87/// `IgraphError`. Reference: Barrat et al., PNAS 101 3747 (2004),
88/// equation (6).
89///
90/// # Examples
91///
92/// ```
93/// use rust_igraph::{Graph, avg_nearest_neighbor_degree_weighted};
94///
95/// // Triangle 0-1-2 with weights (1,2,4) on edges (0,1),(1,2),(2,0).
96/// // Vertex 0 incident to e0=1.0 (→1) and e2=4.0 (→2). deg[1]=deg[2]=2.
97/// // s_0 = 1+4 = 5; sum = 1*2 + 4*2 = 10; knn[0] = 10/5 = 2.0.
98/// let mut g = Graph::with_vertices(3);
99/// g.add_edge(0, 1).unwrap();
100/// g.add_edge(1, 2).unwrap();
101/// g.add_edge(2, 0).unwrap();
102/// let r = avg_nearest_neighbor_degree_weighted(&g, &[1.0, 2.0, 4.0]).unwrap();
103/// assert_eq!(r, vec![Some(2.0); 3]);
104/// ```
105pub 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        // Iterate over incident edges directly — `neighbors()` merges
136        // out/in lists in sorted order while `incident()` concatenates
137        // them, so the two are NOT positionally aligned. Use
138        // `edge_other()` to get the corresponding neighbour vertex.
139        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
157/// Per-degree average nearest-neighbour degree, `k_nn(k)`.
158///
159/// Counterpart of `igraph_avg_nearest_neighbor_degree(_, _, _, _,
160/// NULL, &knnk, NULL)`. The output has length `maxdeg(graph)`; entry
161/// `result[k-1]` is the mean of `knn[v]` over all vertices with
162/// degree `k`. Returns `None` for degrees that no vertex has
163/// (matches upstream's `IGRAPH_NAN`).
164///
165/// # Examples
166///
167/// ```
168/// use rust_igraph::{Graph, knnk};
169///
170/// // Star K_{1,3}: leaves (deg 1) all have knn=3, centre (deg 3) has knn=1.
171/// let mut g = Graph::with_vertices(4);
172/// g.add_edge(0, 1).unwrap();
173/// g.add_edge(0, 2).unwrap();
174/// g.add_edge(0, 3).unwrap();
175/// let r = knnk(&g).unwrap();
176/// assert_eq!(r, vec![Some(3.0), None, Some(1.0)]);
177/// ```
178pub 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
209/// Per-degree weighted average nearest-neighbour degree.
210///
211/// Counterpart of `igraph_avg_nearest_neighbor_degree(_, _, _, _,
212/// NULL, &knnk, weights)`. Output indexed by *degree* (not strength),
213/// `result[k-1]` is `Σ_{deg(v)=k} sum_v / Σ_{deg(v)=k} strength_v`,
214/// matching upstream's pooling formula. Same input requirements as
215/// [`avg_nearest_neighbor_degree_weighted`].
216///
217/// # Examples
218///
219/// ```
220/// use rust_igraph::{Graph, knnk_weighted};
221///
222/// let mut g = Graph::with_vertices(3);
223/// g.add_edge(0, 1).unwrap();
224/// g.add_edge(1, 2).unwrap();
225/// let k = knnk_weighted(&g, &[1.0, 1.0]).unwrap();
226/// assert!(!k.is_empty());
227/// ```
228pub 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        // 0-1-2-3-4. Degrees: 1,2,2,2,1.
319        // knn[0] = deg[1] / 1 = 2.
320        // knn[1] = (deg[0] + deg[2]) / 2 = (1 + 2)/2 = 1.5.
321        // knn[2] = (deg[1] + deg[3]) / 2 = (2+2)/2 = 2.
322        // knn[3] = (deg[2] + deg[4]) / 2 = (2+1)/2 = 1.5.
323        // knn[4] = deg[3] / 1 = 2.
324        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        // Path 0-1-2-3-4 with all unit weights — must match unweighted knn.
348        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        // Triangle 0-1-2: edges (0,1)=1, (1,2)=2, (2,0)=4.
368        // deg[0]=deg[1]=deg[2]=2.
369        // Vertex 0: incident to e0=1.0 (→1) + e2=4.0 (→2). s=5; sum=2+8=10. knn=2.
370        // Vertex 1: incident to e0=1.0 (→0) + e1=2.0 (→2). s=3; sum=2+4=6. knn=2.
371        // Vertex 2: incident to e1=2.0 (→1) + e2=4.0 (→0). s=6; sum=4+8=12. knn=2.
372        // All vertices have neighbours of identical degree (2), so weighted
373        // average must equal 2.0 regardless of weighting.
374        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        // Vertex 0 has only one incident edge, weight 0 → strength 0 → None.
392        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        // Star K_{1,3}: degrees [3, 1, 1, 1]. Centre's knn=1, leaves' knn=3.
415        // knnk[0] (deg 1) = avg of three 3.0 = 3.0.
416        // knnk[1] (deg 2) = None (no deg-2 vertex).
417        // knnk[2] (deg 3) = 1.0.
418        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        // Path 0-1-2-3-4. Degrees: 1,2,2,2,1.
429        // knn = [2.0, 1.5, 2.0, 1.5, 2.0].
430        // knnk[0] (deg 1) = (2.0 + 2.0) / 2 = 2.0 (vertices 0, 4).
431        // knnk[1] (deg 2) = (1.5 + 2.0 + 1.5) / 3 = 5.0/3 (vertices 1, 2, 3).
432        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        // maxdeg = 0 → empty.
452        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        // Vertex 0 has a self-loop and an edge to 1: degree 0 = 3
476        // (LOOPS_TWICE), degree 1 = 1.
477        // 0's neighbours via `neighbors()`: [0, 0, 1] (self-loop reported twice
478        // + 1 once). knn[0] = (deg[0] + deg[0] + deg[1]) / 3 = (3+3+1)/3 = 7/3.
479        // 1's neighbours: [0]; knn[1] = deg[0] = 3.
480        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}