Skip to main content

rust_igraph/algorithms/properties/
degree_correlation.rs

1//! Degree correlation function `k_nn(k)` (ALGO-PR-039).
2//!
3//! Computes the mean degree of neighbors at distance 1, grouped by
4//! the source vertex degree. Supports directed mode selection for
5//! both source and target degrees, optional weights, and the
6//! `directed_neighbors` flag.
7//!
8//! Counterpart of `igraph_degree_correlation_vector` from
9//! `references/igraph/src/properties/degrees.c`.
10
11use crate::algorithms::properties::degree::DegreeMode;
12use crate::core::{Graph, IgraphResult};
13
14/// Compute the degree correlation function `k_nn(k)`.
15///
16/// For each degree value `d`, computes the weighted average degree of
17/// vertices connected to by vertices of degree `d`. The result vector
18/// is indexed by degree: `result[d]` = mean neighbor degree for source
19/// degree `d`.
20///
21/// - `from_mode`: how to measure source vertex degree (Out/In/All).
22/// - `to_mode`: how to measure target vertex degree (Out/In/All).
23/// - `directed_neighbors`: if true, only follow edge direction (u→v).
24///   If false, treat edges as reciprocal (both u→v and v→u counted).
25///   Ignored for undirected graphs.
26/// - `weights`: optional edge weights. If `None`, all weights are 1.
27///
28/// Returns a vector of length `max_from_degree + 1`. Entries for
29/// degrees with no edges will be `f64::NAN`.
30///
31/// # Examples
32///
33/// ```
34/// use rust_igraph::{Graph, DegreeMode, degree_correlation_vector};
35///
36/// // Star graph: center has degree 4, leaves have degree 1
37/// let mut g = Graph::with_vertices(5);
38/// g.add_edge(0, 1).unwrap();
39/// g.add_edge(0, 2).unwrap();
40/// g.add_edge(0, 3).unwrap();
41/// g.add_edge(0, 4).unwrap();
42/// let knnk = degree_correlation_vector(
43///     &g, DegreeMode::All, DegreeMode::All, false, None,
44/// ).unwrap();
45/// // k_nn(1) = 4.0 (leaves connect to center with deg 4)
46/// assert!((knnk[1] - 4.0).abs() < 1e-10);
47/// // k_nn(4) = 1.0 (center connects to leaves with deg 1)
48/// assert!((knnk[4] - 1.0).abs() < 1e-10);
49/// ```
50pub fn degree_correlation_vector(
51    graph: &Graph,
52    from_mode: DegreeMode,
53    to_mode: DegreeMode,
54    directed_neighbors: bool,
55    weights: Option<&[f64]>,
56) -> IgraphResult<Vec<f64>> {
57    let ecount = graph.ecount();
58
59    if graph.vcount() == 0 {
60        return Ok(Vec::new());
61    }
62
63    if let Some(w) = weights {
64        if w.len() != ecount {
65            return Err(crate::core::IgraphError::InvalidArgument(format!(
66                "degree_correlation_vector: weight vector length ({}) does not match edge count ({ecount})",
67                w.len()
68            )));
69        }
70    }
71
72    let (from_mode_eff, to_mode_eff, directed_eff) = if graph.is_directed() {
73        (from_mode, to_mode, directed_neighbors)
74    } else {
75        (DegreeMode::All, DegreeMode::All, false)
76    };
77
78    let deg_from = compute_degrees(graph, from_mode_eff)?;
79    let deg_to = compute_degrees(graph, to_mode_eff)?;
80
81    let max_from_deg = if ecount > 0 {
82        deg_from.iter().copied().max().unwrap_or(0) as usize
83    } else {
84        0
85    };
86
87    let mut knnk = vec![0.0_f64; max_from_deg + 1];
88    let mut weight_sums = vec![0.0_f64; max_from_deg + 1];
89
90    for eid in 0..ecount {
91        #[allow(clippy::cast_possible_truncation)]
92        let (from, to) = graph.edge(eid as u32)?;
93        let from_deg = deg_from[from as usize] as usize;
94        let to_deg = f64::from(deg_to[to as usize]);
95        let w = weights.map_or(1.0, |ws| ws[eid]);
96
97        weight_sums[from_deg] += w;
98        knnk[from_deg] += w * to_deg;
99
100        if !directed_eff {
101            let to_from_deg = deg_from[to as usize] as usize;
102            let from_to_deg = f64::from(deg_to[from as usize]);
103            weight_sums[to_from_deg] += w;
104            knnk[to_from_deg] += w * from_to_deg;
105        }
106    }
107
108    for i in 0..knnk.len() {
109        if weight_sums[i] > 0.0 {
110            knnk[i] /= weight_sums[i];
111        } else {
112            knnk[i] = f64::NAN;
113        }
114    }
115
116    Ok(knnk)
117}
118
119fn compute_degrees(graph: &Graph, mode: DegreeMode) -> IgraphResult<Vec<u32>> {
120    let n = graph.vcount() as usize;
121    let ecount = graph.ecount();
122    let mut deg = vec![0u32; n];
123
124    for eid in 0..ecount {
125        #[allow(clippy::cast_possible_truncation)]
126        let (from, to) = graph.edge(eid as u32)?;
127
128        match mode {
129            DegreeMode::Out => {
130                deg[from as usize] += 1;
131            }
132            DegreeMode::In => {
133                deg[to as usize] += 1;
134            }
135            DegreeMode::All => {
136                deg[from as usize] += 1;
137                if from == to {
138                    deg[from as usize] += 1;
139                } else {
140                    deg[to as usize] += 1;
141                }
142            }
143        }
144    }
145
146    Ok(deg)
147}
148
149#[cfg(test)]
150#[allow(clippy::float_cmp)]
151mod tests {
152    use super::*;
153
154    fn approx_eq(a: f64, b: f64) -> bool {
155        (a - b).abs() < 1e-10
156    }
157
158    #[test]
159    fn empty_graph() {
160        let g = Graph::with_vertices(0);
161        let knnk =
162            degree_correlation_vector(&g, DegreeMode::All, DegreeMode::All, false, None).unwrap();
163        assert!(knnk.is_empty());
164    }
165
166    #[test]
167    fn isolated_vertices() {
168        let g = Graph::with_vertices(5);
169        let knnk =
170            degree_correlation_vector(&g, DegreeMode::All, DegreeMode::All, false, None).unwrap();
171        assert_eq!(knnk.len(), 1); // max_deg = 0
172        assert!(knnk[0].is_nan());
173    }
174
175    #[test]
176    fn single_edge() {
177        let mut g = Graph::with_vertices(2);
178        g.add_edge(0, 1).unwrap();
179        let knnk =
180            degree_correlation_vector(&g, DegreeMode::All, DegreeMode::All, false, None).unwrap();
181        // Both vertices have degree 1, connected to degree 1
182        assert_eq!(knnk.len(), 2);
183        assert!(knnk[0].is_nan());
184        assert!(approx_eq(knnk[1], 1.0));
185    }
186
187    #[test]
188    fn star_graph() {
189        let mut g = Graph::with_vertices(5);
190        g.add_edge(0, 1).unwrap();
191        g.add_edge(0, 2).unwrap();
192        g.add_edge(0, 3).unwrap();
193        g.add_edge(0, 4).unwrap();
194        let knnk =
195            degree_correlation_vector(&g, DegreeMode::All, DegreeMode::All, false, None).unwrap();
196        assert_eq!(knnk.len(), 5); // max_deg = 4
197        assert!(knnk[0].is_nan());
198        assert!(approx_eq(knnk[1], 4.0)); // leaves → center (deg 4)
199        assert!(knnk[2].is_nan());
200        assert!(knnk[3].is_nan());
201        assert!(approx_eq(knnk[4], 1.0)); // center → leaves (deg 1)
202    }
203
204    #[test]
205    fn triangle() {
206        let mut g = Graph::with_vertices(3);
207        g.add_edge(0, 1).unwrap();
208        g.add_edge(1, 2).unwrap();
209        g.add_edge(2, 0).unwrap();
210        let knnk =
211            degree_correlation_vector(&g, DegreeMode::All, DegreeMode::All, false, None).unwrap();
212        // All vertices deg 2, all neighbors deg 2
213        assert!(approx_eq(knnk[2], 2.0));
214    }
215
216    #[test]
217    fn path_3() {
218        let mut g = Graph::with_vertices(3);
219        g.add_edge(0, 1).unwrap();
220        g.add_edge(1, 2).unwrap();
221        let knnk =
222            degree_correlation_vector(&g, DegreeMode::All, DegreeMode::All, false, None).unwrap();
223        // deg 1 vertices (0, 2) connect to deg 2 vertex (1): knnk[1] = 2.0
224        // deg 2 vertex (1) connects to deg 1 vertices: knnk[2] = 1.0
225        assert!(approx_eq(knnk[1], 2.0));
226        assert!(approx_eq(knnk[2], 1.0));
227    }
228
229    #[test]
230    fn directed_out_in() {
231        let mut g = Graph::new(3, true).unwrap();
232        g.add_edge(0, 1).unwrap(); // 0 out=1, 1 in=1
233        g.add_edge(0, 2).unwrap(); // 0 out=2, 2 in=1
234        g.add_edge(1, 2).unwrap(); // 1 out=1, 2 in=2
235        // from_mode=Out, to_mode=In, directed=true
236        let knnk =
237            degree_correlation_vector(&g, DegreeMode::Out, DegreeMode::In, true, None).unwrap();
238        // from out-degrees: 0→2, 1→1
239        // Edge 0→1: from_deg[0]=2 (out), to_deg[1]=1 (in)
240        // Edge 0→2: from_deg[0]=2 (out), to_deg[2]=2 (in)
241        // Edge 1→2: from_deg[1]=1 (out), to_deg[2]=2 (in)
242        // knnk[2] = (1*1 + 1*2) / (1+1) = 3/2 = 1.5
243        // knnk[1] = (1*2) / 1 = 2.0
244        assert_eq!(knnk.len(), 3);
245        assert!(knnk[0].is_nan());
246        assert!(approx_eq(knnk[1], 2.0));
247        assert!(approx_eq(knnk[2], 1.5));
248    }
249
250    #[test]
251    fn directed_undirected_neighbors() {
252        let mut g = Graph::new(3, true).unwrap();
253        g.add_edge(0, 1).unwrap();
254        g.add_edge(1, 2).unwrap();
255        // directed_neighbors=false: treat each edge as reciprocal
256        let knnk =
257            degree_correlation_vector(&g, DegreeMode::Out, DegreeMode::Out, false, None).unwrap();
258        // out-degrees: 0→1, 1→1, 2→0
259        // Edge 0→1: forward: from_deg[0]=1, to_deg[1]=1; reverse: from_deg[1]=1, to_deg[0]=1
260        // Edge 1→2: forward: from_deg[1]=1, to_deg[2]=0; reverse: from_deg[2]=0, to_deg[1]=1
261        // For deg 1: contributions = (1*1) + (1*1) + (1*0) = 2; weight_sum = 3
262        // knnk[1] = 2/3
263        // For deg 0: contributions = (1*1); weight_sum = 1; knnk[0] = 1.0
264        assert!(approx_eq(knnk[0], 1.0));
265        assert!(approx_eq(knnk[1], 2.0 / 3.0));
266    }
267
268    #[test]
269    fn weighted() {
270        let mut g = Graph::with_vertices(3);
271        g.add_edge(0, 1).unwrap();
272        g.add_edge(1, 2).unwrap();
273        let weights = vec![2.0, 1.0];
274        let knnk =
275            degree_correlation_vector(&g, DegreeMode::All, DegreeMode::All, false, Some(&weights))
276                .unwrap();
277        // deg 1: vertices 0 and 2
278        //   Edge 0-1 (w=2): 0(deg1)→1(deg2) contributes 2*2=4 / ws+=2
279        //                  + 1(deg2)→0(deg1) contributes 2*1=2 / ws+=2
280        //   Edge 1-2 (w=1): 2(deg1)→1(deg2) contributes 1*2=2 / ws+=1
281        //                  + 1(deg2)→2(deg1) contributes 1*1=1 / ws+=1
282        // knnk[1] = (4 + 2) / (2 + 1) = 6/3 = 2.0
283        // knnk[2] = (2 + 1) / (2 + 1) = 3/3 = 1.0
284        assert!(approx_eq(knnk[1], 2.0));
285        assert!(approx_eq(knnk[2], 1.0));
286    }
287
288    #[test]
289    fn weight_length_mismatch() {
290        let mut g = Graph::with_vertices(2);
291        g.add_edge(0, 1).unwrap();
292        let bad_weights = vec![1.0, 2.0, 3.0];
293        let result = degree_correlation_vector(
294            &g,
295            DegreeMode::All,
296            DegreeMode::All,
297            false,
298            Some(&bad_weights),
299        );
300        assert!(result.is_err());
301    }
302
303    #[test]
304    fn self_loop_counted() {
305        let mut g = Graph::with_vertices(2);
306        g.add_edge(0, 0).unwrap(); // self-loop: adds 2 to deg(0)
307        g.add_edge(0, 1).unwrap();
308        let knnk =
309            degree_correlation_vector(&g, DegreeMode::All, DegreeMode::All, false, None).unwrap();
310        // deg(0) = 3 (self-loop counts 2 + edge to 1), deg(1) = 1
311        // Edge 0-0: from_deg[0]=3, to_deg[0]=3
312        //   forward: ws[3] += 1, knnk[3] += 3
313        //   reverse: ws[3] += 1, knnk[3] += 3
314        // Edge 0-1: from_deg[0]=3, to_deg[1]=1
315        //   forward: ws[3] += 1, knnk[3] += 1
316        //   reverse: ws[1] += 1, knnk[1] += 3
317        // knnk[3] = (3+3+1) / (1+1+1) = 7/3
318        // knnk[1] = 3 / 1 = 3.0
319        assert!(approx_eq(knnk[1], 3.0));
320        assert!(approx_eq(knnk[3], 7.0 / 3.0));
321    }
322
323    #[test]
324    fn complete_k4() {
325        let mut g = Graph::with_vertices(4);
326        g.add_edge(0, 1).unwrap();
327        g.add_edge(0, 2).unwrap();
328        g.add_edge(0, 3).unwrap();
329        g.add_edge(1, 2).unwrap();
330        g.add_edge(1, 3).unwrap();
331        g.add_edge(2, 3).unwrap();
332        let knnk =
333            degree_correlation_vector(&g, DegreeMode::All, DegreeMode::All, false, None).unwrap();
334        // All deg 3, all neighbors deg 3
335        assert!(approx_eq(knnk[3], 3.0));
336    }
337}