Skip to main content

rust_igraph/algorithms/properties/
irregularity.rs

1//! Graph irregularity indices (ALGO-TR-048).
2//!
3//! - **Albertson index** `ALB(G) = Σ_{(u,v)∈E} |d_u − d_v|`
4//!   The sum of absolute degree differences over all edges. Introduced by
5//!   Albertson (1997). Zero iff the graph is regular.
6//! - **Sigma index** `σ(G) = Σ_{(u,v)∈E} (d_u − d_v)²`
7//!   The Gutman irregularity index. Sums the squared degree differences.
8//!   Always ≥ ALB(G)² / m. Zero iff regular.
9//! - **Total irregularity** `irr_t(G) = ½ Σ_{u,v∈V} |d_u − d_v|`
10//!   The sum over all vertex pairs, not just edges. Introduced by
11//!   Abdo, Brandt & Dimitrov (2014). Zero iff regular.
12
13#![allow(
14    clippy::cast_possible_truncation,
15    clippy::cast_precision_loss,
16    clippy::many_single_char_names,
17    clippy::needless_range_loop,
18    clippy::too_many_lines
19)]
20
21use crate::core::{Graph, IgraphResult};
22
23/// Compute the Albertson index (edge irregularity).
24///
25/// `ALB(G) = Σ_{(u,v)∈E} |d_u − d_v|`
26///
27/// Self-loops contribute 0 (both endpoints have the same vertex).
28/// For regular graphs the result is 0.
29///
30/// # Examples
31///
32/// ```
33/// use rust_igraph::{Graph, albertson_index};
34///
35/// // Star S_4 (center degree 4, leaves degree 1): |4-1| × 4 = 12
36/// let g = Graph::from_edges(&[(0,1),(0,2),(0,3),(0,4)], false, Some(5)).unwrap();
37/// assert!((albertson_index(&g).unwrap() - 12.0).abs() < 1e-10);
38/// ```
39pub fn albertson_index(graph: &Graph) -> IgraphResult<f64> {
40    let n = graph.vcount();
41    if n == 0 {
42        return Ok(0.0);
43    }
44
45    let mut alb = 0.0_f64;
46
47    for (u, v) in graph.edges() {
48        if u == v {
49            continue;
50        }
51        let du = graph.degree(u)? as f64;
52        let dv = graph.degree(v)? as f64;
53        alb += (du - dv).abs();
54    }
55
56    Ok(alb)
57}
58
59/// Compute the sigma index (Gutman irregularity).
60///
61/// `σ(G) = Σ_{(u,v)∈E} (d_u − d_v)²`
62///
63/// Self-loops contribute 0.  For regular graphs the result is 0.
64///
65/// # Examples
66///
67/// ```
68/// use rust_igraph::{Graph, sigma_index};
69///
70/// // Path 0-1-2: degrees [1,2,1]
71/// // edge(0,1): (1-2)²=1, edge(1,2): (2-1)²=1
72/// // σ = 2
73/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
74/// assert!((sigma_index(&g).unwrap() - 2.0).abs() < 1e-10);
75/// ```
76pub fn sigma_index(graph: &Graph) -> IgraphResult<f64> {
77    let n = graph.vcount();
78    if n == 0 {
79        return Ok(0.0);
80    }
81
82    let mut sigma = 0.0_f64;
83
84    for (u, v) in graph.edges() {
85        if u == v {
86            continue;
87        }
88        let du = graph.degree(u)? as f64;
89        let dv = graph.degree(v)? as f64;
90        let diff = du - dv;
91        sigma += diff * diff;
92    }
93
94    Ok(sigma)
95}
96
97/// Compute the total irregularity.
98///
99/// `irr_t(G) = ½ Σ_{u≠v} |d_u − d_v|`
100///
101/// Sums over *all* unordered vertex pairs, not just edges. For
102/// regular graphs the result is 0.
103///
104/// # Examples
105///
106/// ```
107/// use rust_igraph::{Graph, total_irregularity};
108///
109/// // Path 0-1-2: degrees [1,2,1]
110/// // pairs: (0,1)→|1-2|=1, (0,2)→|1-1|=0, (1,2)→|2-1|=1
111/// // irr_t = ½(1+0+1) = 1
112/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
113/// assert!((total_irregularity(&g).unwrap() - 1.0).abs() < 1e-10);
114/// ```
115pub fn total_irregularity(graph: &Graph) -> IgraphResult<f64> {
116    let n = graph.vcount();
117    if n < 2 {
118        return Ok(0.0);
119    }
120
121    let mut degrees = Vec::with_capacity(n as usize);
122    for v in 0..n {
123        degrees.push(graph.degree(v)? as f64);
124    }
125
126    let mut total = 0.0_f64;
127    for i in 0..degrees.len() {
128        for j in (i + 1)..degrees.len() {
129            total += (degrees[i] - degrees[j]).abs();
130        }
131    }
132
133    Ok(total / 2.0)
134}
135
136/// Compute the variance of the degree sequence.
137///
138/// `Var(G) = (1/n) Σ_{v∈V} (d_v − d̄)²`
139///
140/// Where `d̄ = 2m/n` is the mean degree. Zero iff regular.
141/// This is a normalised irregularity measure.
142///
143/// # Examples
144///
145/// ```
146/// use rust_igraph::{Graph, degree_variance};
147///
148/// // K_3: all degrees 2 → Var = 0
149/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
150/// assert!((degree_variance(&g).unwrap()).abs() < 1e-10);
151/// ```
152pub fn degree_variance(graph: &Graph) -> IgraphResult<f64> {
153    let n = graph.vcount();
154    if n == 0 {
155        return Ok(0.0);
156    }
157
158    let nf = f64::from(n);
159    let m = graph.ecount() as f64;
160    let mean_deg = 2.0 * m / nf;
161
162    let mut var = 0.0_f64;
163    for v in 0..n {
164        let d = graph.degree(v)? as f64;
165        let diff = d - mean_deg;
166        var += diff * diff;
167    }
168
169    Ok(var / nf)
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    fn single_edge() -> Graph {
177        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
178    }
179
180    fn path3() -> Graph {
181        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
182    }
183
184    fn path4() -> Graph {
185        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
186    }
187
188    fn k3() -> Graph {
189        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
190    }
191
192    fn k4() -> Graph {
193        Graph::from_edges(
194            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
195            false,
196            Some(4),
197        )
198        .unwrap()
199    }
200
201    fn cycle4() -> Graph {
202        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
203    }
204
205    fn cycle5() -> Graph {
206        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
207    }
208
209    fn star5() -> Graph {
210        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
211    }
212
213    fn paw() -> Graph {
214        // Triangle 0-1-2 plus pendant 2-3
215        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
216    }
217
218    fn diamond() -> Graph {
219        // K4 minus one edge
220        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)], false, Some(4)).unwrap()
221    }
222
223    // --- albertson_index ---
224
225    #[test]
226    fn alb_empty() {
227        let g = Graph::with_vertices(0);
228        assert!((albertson_index(&g).unwrap()).abs() < 1e-10);
229    }
230
231    #[test]
232    fn alb_single_vertex() {
233        let g = Graph::with_vertices(1);
234        assert!((albertson_index(&g).unwrap()).abs() < 1e-10);
235    }
236
237    #[test]
238    fn alb_no_edges() {
239        let g = Graph::with_vertices(3);
240        assert!((albertson_index(&g).unwrap()).abs() < 1e-10);
241    }
242
243    #[test]
244    fn alb_single_edge() {
245        // both degree 1 → |1-1| = 0
246        assert!((albertson_index(&single_edge()).unwrap()).abs() < 1e-10);
247    }
248
249    #[test]
250    fn alb_path3() {
251        // degrees [1,2,1]: edges (0,1)→|1-2|=1, (1,2)→|2-1|=1
252        assert!((albertson_index(&path3()).unwrap() - 2.0).abs() < 1e-10);
253    }
254
255    #[test]
256    fn alb_path4() {
257        // degrees [1,2,2,1]: (0,1)→1, (1,2)→0, (2,3)→1 → ALB=2
258        assert!((albertson_index(&path4()).unwrap() - 2.0).abs() < 1e-10);
259    }
260
261    #[test]
262    fn alb_k3() {
263        // regular → 0
264        assert!((albertson_index(&k3()).unwrap()).abs() < 1e-10);
265    }
266
267    #[test]
268    fn alb_k4() {
269        assert!((albertson_index(&k4()).unwrap()).abs() < 1e-10);
270    }
271
272    #[test]
273    fn alb_cycle4() {
274        assert!((albertson_index(&cycle4()).unwrap()).abs() < 1e-10);
275    }
276
277    #[test]
278    fn alb_cycle5() {
279        assert!((albertson_index(&cycle5()).unwrap()).abs() < 1e-10);
280    }
281
282    #[test]
283    fn alb_star5() {
284        // center=4, leaf=1: |4-1|=3, 4 edges → ALB=12
285        assert!((albertson_index(&star5()).unwrap() - 12.0).abs() < 1e-10);
286    }
287
288    #[test]
289    fn alb_paw() {
290        // degrees [2,2,3,1]
291        // (0,1): |2-2|=0, (0,2): |2-3|=1, (1,2): |2-3|=1, (2,3): |3-1|=2
292        // ALB = 0+1+1+2 = 4
293        assert!((albertson_index(&paw()).unwrap() - 4.0).abs() < 1e-10);
294    }
295
296    #[test]
297    fn alb_diamond() {
298        // degrees [3,3,2,2]
299        // (0,1):0, (0,2):1, (0,3):1, (1,2):1, (1,3):1 → ALB=4
300        assert!((albertson_index(&diamond()).unwrap() - 4.0).abs() < 1e-10);
301    }
302
303    #[test]
304    fn alb_nonnegative() {
305        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
306            assert!(albertson_index(g).unwrap() >= -1e-10);
307        }
308    }
309
310    #[test]
311    fn alb_zero_iff_regular() {
312        for g in &[k3(), k4(), cycle4(), cycle5()] {
313            assert!((albertson_index(g).unwrap()).abs() < 1e-10);
314        }
315        for g in &[path3(), star5(), paw(), diamond()] {
316            assert!(albertson_index(g).unwrap() > 1e-10);
317        }
318    }
319
320    // --- sigma_index ---
321
322    #[test]
323    fn sig_empty() {
324        let g = Graph::with_vertices(0);
325        assert!((sigma_index(&g).unwrap()).abs() < 1e-10);
326    }
327
328    #[test]
329    fn sig_single_vertex() {
330        let g = Graph::with_vertices(1);
331        assert!((sigma_index(&g).unwrap()).abs() < 1e-10);
332    }
333
334    #[test]
335    fn sig_single_edge() {
336        assert!((sigma_index(&single_edge()).unwrap()).abs() < 1e-10);
337    }
338
339    #[test]
340    fn sig_path3() {
341        // (0,1):(1-2)²=1, (1,2):(2-1)²=1 → σ=2
342        assert!((sigma_index(&path3()).unwrap() - 2.0).abs() < 1e-10);
343    }
344
345    #[test]
346    fn sig_path4() {
347        // (0,1):(1-2)²=1, (1,2):0, (2,3):(2-1)²=1 → σ=2
348        assert!((sigma_index(&path4()).unwrap() - 2.0).abs() < 1e-10);
349    }
350
351    #[test]
352    fn sig_k3() {
353        assert!((sigma_index(&k3()).unwrap()).abs() < 1e-10);
354    }
355
356    #[test]
357    fn sig_k4() {
358        assert!((sigma_index(&k4()).unwrap()).abs() < 1e-10);
359    }
360
361    #[test]
362    fn sig_cycle4() {
363        assert!((sigma_index(&cycle4()).unwrap()).abs() < 1e-10);
364    }
365
366    #[test]
367    fn sig_star5() {
368        // (4-1)²=9, 4 edges → σ=36
369        assert!((sigma_index(&star5()).unwrap() - 36.0).abs() < 1e-10);
370    }
371
372    #[test]
373    fn sig_paw() {
374        // (0,1):0, (0,2):(2-3)²=1, (1,2):1, (2,3):(3-1)²=4
375        // σ = 0+1+1+4 = 6
376        assert!((sigma_index(&paw()).unwrap() - 6.0).abs() < 1e-10);
377    }
378
379    #[test]
380    fn sig_diamond() {
381        // (0,1):0, (0,2):(3-2)²=1, (0,3):1, (1,2):1, (1,3):1 → σ=4
382        assert!((sigma_index(&diamond()).unwrap() - 4.0).abs() < 1e-10);
383    }
384
385    #[test]
386    fn sig_zero_iff_regular() {
387        for g in &[k3(), k4(), cycle4(), cycle5()] {
388            assert!((sigma_index(g).unwrap()).abs() < 1e-10);
389        }
390        for g in &[path3(), star5(), paw()] {
391            assert!(sigma_index(g).unwrap() > 1e-10);
392        }
393    }
394
395    #[test]
396    fn sig_geq_alb_squared_over_m() {
397        // By Cauchy-Schwarz: σ ≥ ALB² / m
398        for g in &[
399            single_edge(),
400            path3(),
401            path4(),
402            k3(),
403            star5(),
404            paw(),
405            diamond(),
406        ] {
407            let sig = sigma_index(g).unwrap();
408            let alb = albertson_index(g).unwrap();
409            let m = g.ecount() as f64;
410            if m > 0.0 {
411                assert!(sig >= alb * alb / m - 1e-8);
412            }
413        }
414    }
415
416    // --- total_irregularity ---
417
418    #[test]
419    fn irrt_empty() {
420        let g = Graph::with_vertices(0);
421        assert!((total_irregularity(&g).unwrap()).abs() < 1e-10);
422    }
423
424    #[test]
425    fn irrt_single_vertex() {
426        let g = Graph::with_vertices(1);
427        assert!((total_irregularity(&g).unwrap()).abs() < 1e-10);
428    }
429
430    #[test]
431    fn irrt_single_edge() {
432        // degrees [1,1] → 0
433        assert!((total_irregularity(&single_edge()).unwrap()).abs() < 1e-10);
434    }
435
436    #[test]
437    fn irrt_path3() {
438        // degrees [1,2,1]
439        // pairs: (0,1)→1, (0,2)→0, (1,2)→1
440        // irr_t = (1+0+1)/2 = 1
441        assert!((total_irregularity(&path3()).unwrap() - 1.0).abs() < 1e-10);
442    }
443
444    #[test]
445    fn irrt_path4() {
446        // degrees [1,2,2,1]
447        // pairs: (0,1)→1,(0,2)→1,(0,3)→0,(1,2)→0,(1,3)→1,(2,3)→1
448        // irr_t = (1+1+0+0+1+1)/2 = 2
449        assert!((total_irregularity(&path4()).unwrap() - 2.0).abs() < 1e-10);
450    }
451
452    #[test]
453    fn irrt_k3() {
454        assert!((total_irregularity(&k3()).unwrap()).abs() < 1e-10);
455    }
456
457    #[test]
458    fn irrt_k4() {
459        assert!((total_irregularity(&k4()).unwrap()).abs() < 1e-10);
460    }
461
462    #[test]
463    fn irrt_cycle4() {
464        assert!((total_irregularity(&cycle4()).unwrap()).abs() < 1e-10);
465    }
466
467    #[test]
468    fn irrt_star5() {
469        // degrees [4,1,1,1,1]
470        // (0,1):3, (0,2):3, (0,3):3, (0,4):3, (1,2):0,(1,3):0,(1,4):0,(2,3):0,(2,4):0,(3,4):0
471        // irr_t = (4·3)/2 = 6
472        assert!((total_irregularity(&star5()).unwrap() - 6.0).abs() < 1e-10);
473    }
474
475    #[test]
476    fn irrt_paw() {
477        // degrees [2,2,3,1]
478        // (0,1):0,(0,2):1,(0,3):1,(1,2):1,(1,3):1,(2,3):2
479        // irr_t = (0+1+1+1+1+2)/2 = 3
480        assert!((total_irregularity(&paw()).unwrap() - 3.0).abs() < 1e-10);
481    }
482
483    #[test]
484    fn irrt_diamond() {
485        // degrees [3,3,2,2]
486        // (0,1):0,(0,2):1,(0,3):1,(1,2):1,(1,3):1,(2,3):0
487        // irr_t = (0+1+1+1+1+0)/2 = 2
488        assert!((total_irregularity(&diamond()).unwrap() - 2.0).abs() < 1e-10);
489    }
490
491    #[test]
492    fn irrt_zero_iff_regular() {
493        for g in &[k3(), k4(), cycle4(), cycle5()] {
494            assert!((total_irregularity(g).unwrap()).abs() < 1e-10);
495        }
496        for g in &[path3(), star5(), paw()] {
497            assert!(total_irregularity(g).unwrap() > 1e-10);
498        }
499    }
500
501    #[test]
502    fn irrt_geq_alb() {
503        // ALB counts only edge pairs; irr_t counts all pairs → irr_t ≥ ALB/2
504        // Actually irr_t = ½ Σ_{all pairs} and ALB = Σ_{edges}, so irr_t ≥ ½ ALB
505        for g in &[
506            single_edge(),
507            path3(),
508            path4(),
509            k3(),
510            star5(),
511            paw(),
512            diamond(),
513        ] {
514            let irrt = total_irregularity(g).unwrap();
515            let alb = albertson_index(g).unwrap();
516            assert!(irrt >= alb / 2.0 - 1e-8);
517        }
518    }
519
520    #[test]
521    fn irrt_with_isolated() {
522        // 0-1 plus isolated 2: degrees [1,1,0]
523        // (0,1):0, (0,2):1, (1,2):1
524        // irr_t = (0+1+1)/2 = 1
525        let g = Graph::from_edges(&[(0, 1)], false, Some(3)).unwrap();
526        assert!((total_irregularity(&g).unwrap() - 1.0).abs() < 1e-10);
527    }
528
529    // --- degree_variance ---
530
531    #[test]
532    fn dv_empty() {
533        let g = Graph::with_vertices(0);
534        assert!((degree_variance(&g).unwrap()).abs() < 1e-10);
535    }
536
537    #[test]
538    fn dv_single_vertex() {
539        let g = Graph::with_vertices(1);
540        assert!((degree_variance(&g).unwrap()).abs() < 1e-10);
541    }
542
543    #[test]
544    fn dv_no_edges() {
545        let g = Graph::with_vertices(3);
546        assert!((degree_variance(&g).unwrap()).abs() < 1e-10);
547    }
548
549    #[test]
550    fn dv_single_edge() {
551        // degrees [1,1], mean=1, var=0
552        assert!((degree_variance(&single_edge()).unwrap()).abs() < 1e-10);
553    }
554
555    #[test]
556    fn dv_k3() {
557        assert!((degree_variance(&k3()).unwrap()).abs() < 1e-10);
558    }
559
560    #[test]
561    fn dv_k4() {
562        assert!((degree_variance(&k4()).unwrap()).abs() < 1e-10);
563    }
564
565    #[test]
566    fn dv_cycle4() {
567        assert!((degree_variance(&cycle4()).unwrap()).abs() < 1e-10);
568    }
569
570    #[test]
571    fn dv_path3() {
572        // degrees [1,2,1], mean=4/3
573        // var = ((1-4/3)²+(2-4/3)²+(1-4/3)²)/3
574        //     = (1/9 + 4/9 + 1/9)/3 = (6/9)/3 = 2/9
575        assert!((degree_variance(&path3()).unwrap() - 2.0 / 9.0).abs() < 1e-10);
576    }
577
578    #[test]
579    fn dv_star5() {
580        // degrees [4,1,1,1,1], mean=8/5=1.6
581        // var = ((4-1.6)²+4·(1-1.6)²)/5 = (5.76+4·0.36)/5 = (5.76+1.44)/5 = 7.2/5 = 1.44
582        assert!((degree_variance(&star5()).unwrap() - 1.44).abs() < 1e-10);
583    }
584
585    #[test]
586    fn dv_paw() {
587        // degrees [2,2,3,1], mean=8/4=2
588        // var = ((0)²+(0)²+(1)²+(-1)²)/4 = 2/4 = 0.5
589        assert!((degree_variance(&paw()).unwrap() - 0.5).abs() < 1e-10);
590    }
591
592    #[test]
593    fn dv_zero_iff_regular() {
594        for g in &[k3(), k4(), cycle4(), cycle5()] {
595            assert!((degree_variance(g).unwrap()).abs() < 1e-10);
596        }
597        for g in &[path3(), star5(), paw()] {
598            assert!(degree_variance(g).unwrap() > 1e-10);
599        }
600    }
601
602    #[test]
603    fn dv_with_isolated() {
604        // 0-1 plus isolated 2: degrees [1,1,0], mean=2/3
605        // var = ((1-2/3)²+(1-2/3)²+(0-2/3)²)/3
606        //     = (1/9 + 1/9 + 4/9)/3 = (6/9)/3 = 2/9
607        let g = Graph::from_edges(&[(0, 1)], false, Some(3)).unwrap();
608        assert!((degree_variance(&g).unwrap() - 2.0 / 9.0).abs() < 1e-10);
609    }
610
611    // --- cross-consistency ---
612
613    #[test]
614    fn all_nonneg_for_connected() {
615        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
616            assert!(albertson_index(g).unwrap() >= -1e-10);
617            assert!(sigma_index(g).unwrap() >= -1e-10);
618            assert!(total_irregularity(g).unwrap() >= -1e-10);
619            assert!(degree_variance(g).unwrap() >= -1e-10);
620        }
621    }
622
623    #[test]
624    fn all_zero_for_regular() {
625        for g in &[k3(), k4(), cycle4(), cycle5()] {
626            assert!((albertson_index(g).unwrap()).abs() < 1e-10);
627            assert!((sigma_index(g).unwrap()).abs() < 1e-10);
628            assert!((total_irregularity(g).unwrap()).abs() < 1e-10);
629            assert!((degree_variance(g).unwrap()).abs() < 1e-10);
630        }
631    }
632
633    #[test]
634    fn sigma_geq_albertson() {
635        // σ(G) ≥ ALB(G) for edges where |d_u-d_v|≥1
636        // (because x² ≥ |x| for |x|≥1, and |x|²≥0 always)
637        // Actually σ = Σ(d_u-d_v)² can be < ALB = Σ|d_u-d_v| only if diffs are <1 (impossible for integers)
638        // For integer degrees: (d_u-d_v)² ≥ |d_u-d_v| when |d_u-d_v|≥1, and =0 when equal.
639        // So σ ≥ ALB for integer degrees.
640        for g in &[
641            single_edge(),
642            path3(),
643            k3(),
644            k4(),
645            star5(),
646            paw(),
647            diamond(),
648        ] {
649            let sig = sigma_index(g).unwrap();
650            let alb = albertson_index(g).unwrap();
651            assert!(sig >= alb - 1e-8);
652        }
653    }
654}