Skip to main content

rust_igraph/algorithms/properties/
degree_neighbor_stats.rs

1//! Vertex-level neighbor degree statistics (ALGO-TR-089).
2//!
3//! Aggregates over the degree of each vertex's neighborhood:
4//!
5//! - **Neighbor max sum** `Σ_v max_{u∈N(v)} d(u)`
6//! - **Neighbor min sum** `Σ_v min_{u∈N(v)} d(u)`
7//! - **Neighbor range sum** `Σ_v (max - min)` of neighbor degrees
8//! - **Neighbor variance sum** `Σ_v Var(d(N(v)))` — total neighbor-degree variance
9
10#![allow(
11    clippy::cast_possible_truncation,
12    clippy::cast_precision_loss,
13    clippy::many_single_char_names,
14    clippy::needless_range_loop,
15    clippy::too_many_lines
16)]
17
18use crate::core::{Graph, IgraphResult};
19
20/// Compute the sum of maximum neighbor degrees.
21///
22/// `Σ_{v: d(v)>0} max_{u ∈ N(v)} d(u)`
23///
24/// For each non-isolated vertex, finds the highest degree among its
25/// neighbors and sums these maxima. Returns 0 for edgeless graphs.
26///
27/// # Examples
28///
29/// ```
30/// use rust_igraph::{Graph, degree_neighbor_max_sum};
31///
32/// // Star S_5: center max=1, 4 leaves max=4 → 1 + 4·4 = 17
33/// let g = Graph::from_edges(&[(0,1),(0,2),(0,3),(0,4)], false, Some(5)).unwrap();
34/// assert_eq!(degree_neighbor_max_sum(&g).unwrap(), 17);
35/// ```
36pub fn degree_neighbor_max_sum(graph: &Graph) -> IgraphResult<u64> {
37    let n = graph.vcount() as usize;
38    let mut result = 0_u64;
39
40    for v in 0..n {
41        let v_id = v as u32;
42        let neighbors = graph.neighbors(v_id)?;
43        let mut max_d = None;
44        for &u in &neighbors {
45            let d = graph.degree(u)?;
46            max_d = Some(match max_d {
47                None => d,
48                Some(prev) if d > prev => d,
49                Some(prev) => prev,
50            });
51        }
52        if let Some(m) = max_d {
53            result += m as u64;
54        }
55    }
56
57    Ok(result)
58}
59
60/// Compute the sum of minimum neighbor degrees.
61///
62/// `Σ_{v: d(v)>0} min_{u ∈ N(v)} d(u)`
63///
64/// For each non-isolated vertex, finds the lowest degree among its
65/// neighbors and sums these minima. Returns 0 for edgeless graphs.
66///
67/// # Examples
68///
69/// ```
70/// use rust_igraph::{Graph, degree_neighbor_min_sum};
71///
72/// // Star S_5: center min=1, 4 leaves min=4 → 1 + 4·4 = 17
73/// let g = Graph::from_edges(&[(0,1),(0,2),(0,3),(0,4)], false, Some(5)).unwrap();
74/// assert_eq!(degree_neighbor_min_sum(&g).unwrap(), 17);
75/// ```
76pub fn degree_neighbor_min_sum(graph: &Graph) -> IgraphResult<u64> {
77    let n = graph.vcount() as usize;
78    let mut result = 0_u64;
79
80    for v in 0..n {
81        let v_id = v as u32;
82        let neighbors = graph.neighbors(v_id)?;
83        let mut min_d = None;
84        for &u in &neighbors {
85            let d = graph.degree(u)?;
86            min_d = Some(match min_d {
87                None => d,
88                Some(prev) if d < prev => d,
89                Some(prev) => prev,
90            });
91        }
92        if let Some(m) = min_d {
93            result += m as u64;
94        }
95    }
96
97    Ok(result)
98}
99
100/// Compute the sum of neighbor degree ranges.
101///
102/// `Σ_{v: d(v)>0} (max_{u ∈ N(v)} d(u) - min_{u ∈ N(v)} d(u))`
103///
104/// For each non-isolated vertex, computes the range (max - min) of
105/// neighbor degrees and sums them. Returns 0 for edgeless or regular
106/// graphs.
107///
108/// # Examples
109///
110/// ```
111/// use rust_igraph::{Graph, degree_neighbor_range_sum};
112///
113/// // K_3: each vertex sees [2,2] → range 0 → sum 0
114/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
115/// assert_eq!(degree_neighbor_range_sum(&g).unwrap(), 0);
116/// ```
117pub fn degree_neighbor_range_sum(graph: &Graph) -> IgraphResult<u64> {
118    let n = graph.vcount() as usize;
119    let mut result = 0_u64;
120
121    for v in 0..n {
122        let v_id = v as u32;
123        let neighbors = graph.neighbors(v_id)?;
124        let mut max_d: Option<usize> = None;
125        let mut min_d: Option<usize> = None;
126        for &u in &neighbors {
127            let d = graph.degree(u)?;
128            max_d = Some(match max_d {
129                None => d,
130                Some(prev) if d > prev => d,
131                Some(prev) => prev,
132            });
133            min_d = Some(match min_d {
134                None => d,
135                Some(prev) if d < prev => d,
136                Some(prev) => prev,
137            });
138        }
139        if let (Some(mx), Some(mn)) = (max_d, min_d) {
140            result += (mx - mn) as u64;
141        }
142    }
143
144    Ok(result)
145}
146
147/// Compute the sum of neighbor degree variances.
148///
149/// `Σ_{v: d(v)>0} Var(d(N(v)))`
150///
151/// For each non-isolated vertex, computes the population variance of
152/// its neighbors' degrees and sums them. Returns 0.0 for edgeless or
153/// regular graphs.
154///
155/// # Examples
156///
157/// ```
158/// use rust_igraph::{Graph, degree_neighbor_variance_sum};
159///
160/// // K_3: each vertex sees [2,2] → Var=0 → sum 0
161/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
162/// assert!(degree_neighbor_variance_sum(&g).unwrap().abs() < 1e-10);
163/// ```
164pub fn degree_neighbor_variance_sum(graph: &Graph) -> IgraphResult<f64> {
165    let n = graph.vcount() as usize;
166    let mut result = 0.0_f64;
167
168    for v in 0..n {
169        let v_id = v as u32;
170        let neighbors = graph.neighbors(v_id)?;
171        if neighbors.is_empty() {
172            continue;
173        }
174        let k = neighbors.len() as f64;
175        let mut sum = 0.0_f64;
176        let mut sum_sq = 0.0_f64;
177        for &u in &neighbors {
178            let d = graph.degree(u)? as f64;
179            sum += d;
180            sum_sq += d * d;
181        }
182        let mean = sum / k;
183        let var = sum_sq / k - mean * mean;
184        result += var;
185    }
186
187    Ok(result)
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    fn single_edge() -> Graph {
195        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
196    }
197
198    fn path3() -> Graph {
199        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
200    }
201
202    fn k3() -> Graph {
203        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
204    }
205
206    fn k4() -> Graph {
207        Graph::from_edges(
208            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
209            false,
210            Some(4),
211        )
212        .unwrap()
213    }
214
215    fn cycle4() -> Graph {
216        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
217    }
218
219    fn star5() -> Graph {
220        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
221    }
222
223    fn paw() -> Graph {
224        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
225    }
226
227    // --- degree_neighbor_max_sum ---
228
229    #[test]
230    fn nmax_empty() {
231        let g = Graph::with_vertices(0);
232        assert_eq!(degree_neighbor_max_sum(&g).unwrap(), 0);
233    }
234
235    #[test]
236    fn nmax_isolated() {
237        let g = Graph::with_vertices(5);
238        assert_eq!(degree_neighbor_max_sum(&g).unwrap(), 0);
239    }
240
241    #[test]
242    fn nmax_single_edge() {
243        // Both see neighbor of degree 1 → 1+1 = 2
244        assert_eq!(degree_neighbor_max_sum(&single_edge()).unwrap(), 2);
245    }
246
247    #[test]
248    fn nmax_k3() {
249        // Each vertex sees [2,2] → max=2, 3·2 = 6
250        assert_eq!(degree_neighbor_max_sum(&k3()).unwrap(), 6);
251    }
252
253    #[test]
254    fn nmax_k4() {
255        // Each vertex sees [3,3,3] → max=3, 4·3 = 12
256        assert_eq!(degree_neighbor_max_sum(&k4()).unwrap(), 12);
257    }
258
259    #[test]
260    fn nmax_star5() {
261        // Center(d=4) neighbors=[1,1,1,1] max=1
262        // 4 leaves: neighbor=[4] max=4
263        // 1 + 4·4 = 17
264        assert_eq!(degree_neighbor_max_sum(&star5()).unwrap(), 17);
265    }
266
267    #[test]
268    fn nmax_path3() {
269        // v0(d=1) N=[v1(d=2)] max=2
270        // v1(d=2) N=[v0(1),v2(1)] max=1
271        // v2(d=1) N=[v1(d=2)] max=2
272        // 2 + 1 + 2 = 5
273        assert_eq!(degree_neighbor_max_sum(&path3()).unwrap(), 5);
274    }
275
276    #[test]
277    fn nmax_paw() {
278        // v0(d=2) N=[v1(2),v2(3)] max=3
279        // v1(d=2) N=[v0(2),v2(3)] max=3
280        // v2(d=3) N=[v0(2),v1(2),v3(1)] max=2
281        // v3(d=1) N=[v2(3)] max=3
282        // 3+3+2+3 = 11
283        assert_eq!(degree_neighbor_max_sum(&paw()).unwrap(), 11);
284    }
285
286    // --- degree_neighbor_min_sum ---
287
288    #[test]
289    fn nmin_empty() {
290        let g = Graph::with_vertices(0);
291        assert_eq!(degree_neighbor_min_sum(&g).unwrap(), 0);
292    }
293
294    #[test]
295    fn nmin_isolated() {
296        let g = Graph::with_vertices(5);
297        assert_eq!(degree_neighbor_min_sum(&g).unwrap(), 0);
298    }
299
300    #[test]
301    fn nmin_single_edge() {
302        assert_eq!(degree_neighbor_min_sum(&single_edge()).unwrap(), 2);
303    }
304
305    #[test]
306    fn nmin_k3() {
307        // Each sees [2,2] → min=2, 3·2=6
308        assert_eq!(degree_neighbor_min_sum(&k3()).unwrap(), 6);
309    }
310
311    #[test]
312    fn nmin_star5() {
313        // Center: min=1, leaves: min=4 → 1 + 4·4 = 17
314        assert_eq!(degree_neighbor_min_sum(&star5()).unwrap(), 17);
315    }
316
317    #[test]
318    fn nmin_path3() {
319        // v0:min=2, v1:min=1, v2:min=2 → 5
320        assert_eq!(degree_neighbor_min_sum(&path3()).unwrap(), 5);
321    }
322
323    #[test]
324    fn nmin_paw() {
325        // v0:min=2, v1:min=2, v2:min=1, v3:min=3
326        // 2+2+1+3 = 8
327        assert_eq!(degree_neighbor_min_sum(&paw()).unwrap(), 8);
328    }
329
330    // --- degree_neighbor_range_sum ---
331
332    #[test]
333    fn nrange_empty() {
334        let g = Graph::with_vertices(0);
335        assert_eq!(degree_neighbor_range_sum(&g).unwrap(), 0);
336    }
337
338    #[test]
339    fn nrange_isolated() {
340        let g = Graph::with_vertices(5);
341        assert_eq!(degree_neighbor_range_sum(&g).unwrap(), 0);
342    }
343
344    #[test]
345    fn nrange_regular() {
346        // Regular: all neighbors same degree → range 0
347        assert_eq!(degree_neighbor_range_sum(&k3()).unwrap(), 0);
348        assert_eq!(degree_neighbor_range_sum(&k4()).unwrap(), 0);
349        assert_eq!(degree_neighbor_range_sum(&cycle4()).unwrap(), 0);
350    }
351
352    #[test]
353    fn nrange_single_edge() {
354        // Each has 1 neighbor → range 0
355        assert_eq!(degree_neighbor_range_sum(&single_edge()).unwrap(), 0);
356    }
357
358    #[test]
359    fn nrange_star5() {
360        // Center: all neighbors d=1 → range=0
361        // Leaves: 1 neighbor → range=0
362        assert_eq!(degree_neighbor_range_sum(&star5()).unwrap(), 0);
363    }
364
365    #[test]
366    fn nrange_path3() {
367        // v0: [2] range=0; v1: [1,1] range=0; v2: [2] range=0
368        assert_eq!(degree_neighbor_range_sum(&path3()).unwrap(), 0);
369    }
370
371    #[test]
372    fn nrange_paw() {
373        // v0: N=[2,3] range=1; v1: N=[2,3] range=1
374        // v2: N=[2,2,1] range=1; v3: N=[3] range=0
375        // 1+1+1+0 = 3
376        assert_eq!(degree_neighbor_range_sum(&paw()).unwrap(), 3);
377    }
378
379    // --- degree_neighbor_variance_sum ---
380
381    #[test]
382    fn nvar_empty() {
383        let g = Graph::with_vertices(0);
384        assert!(degree_neighbor_variance_sum(&g).unwrap().abs() < 1e-10);
385    }
386
387    #[test]
388    fn nvar_isolated() {
389        let g = Graph::with_vertices(5);
390        assert!(degree_neighbor_variance_sum(&g).unwrap().abs() < 1e-10);
391    }
392
393    #[test]
394    fn nvar_regular() {
395        // All neighbors have same degree → variance 0
396        assert!(degree_neighbor_variance_sum(&k3()).unwrap().abs() < 1e-10);
397        assert!(degree_neighbor_variance_sum(&k4()).unwrap().abs() < 1e-10);
398        assert!(degree_neighbor_variance_sum(&cycle4()).unwrap().abs() < 1e-10);
399    }
400
401    #[test]
402    fn nvar_single_edge() {
403        // 1 neighbor each → var=0
404        assert!(degree_neighbor_variance_sum(&single_edge()).unwrap().abs() < 1e-10);
405    }
406
407    #[test]
408    fn nvar_star5() {
409        // Center: [1,1,1,1] var=0; leaves: [4] var=0
410        assert!(degree_neighbor_variance_sum(&star5()).unwrap().abs() < 1e-10);
411    }
412
413    #[test]
414    fn nvar_path3() {
415        // v0:[2] var=0; v1:[1,1] var=0; v2:[2] var=0
416        assert!(degree_neighbor_variance_sum(&path3()).unwrap().abs() < 1e-10);
417    }
418
419    #[test]
420    fn nvar_paw() {
421        // v0: N=[2,3] mean=2.5, var=((2-2.5)²+(3-2.5)²)/2 = 0.25
422        // v1: N=[2,3] same → 0.25
423        // v2: N=[2,2,1] mean=5/3, var=((2-5/3)²+(2-5/3)²+(1-5/3)²)/3
424        //     = ((1/3)²+(1/3)²+(-2/3)²)/3 = (1/9+1/9+4/9)/3 = (6/9)/3 = 2/9
425        // v3: N=[3] var=0
426        let expected = 0.25 + 0.25 + 2.0 / 9.0;
427        assert!((degree_neighbor_variance_sum(&paw()).unwrap() - expected).abs() < 1e-10);
428    }
429
430    // --- cross-consistency ---
431
432    #[test]
433    fn max_ge_min() {
434        for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
435            assert!(degree_neighbor_max_sum(g).unwrap() >= degree_neighbor_min_sum(g).unwrap());
436        }
437    }
438
439    #[test]
440    fn range_equals_max_minus_min() {
441        for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
442            let mx = degree_neighbor_max_sum(g).unwrap();
443            let mn = degree_neighbor_min_sum(g).unwrap();
444            let rng = degree_neighbor_range_sum(g).unwrap();
445            assert!(rng <= mx - mn);
446        }
447    }
448
449    #[test]
450    fn variance_nonnegative() {
451        for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
452            assert!(degree_neighbor_variance_sum(g).unwrap() >= -1e-10);
453        }
454    }
455}