Skip to main content

rust_igraph/algorithms/properties/
degree_sum_variants.rs

1//! Degree-sum bond-additive variants (ALGO-TR-072).
2//!
3//! Bond-additive indices using degree-sum functions over edges:
4//!
5//! - **Arithmetic-geometric index** `AG(G) = Σ_{(u,v)∈E} (d(u)+d(v))/(2√(d(u)·d(v)))`
6//!   Ratio of arithmetic to geometric mean of endpoint degrees.
7//! - **Sigma coindex** `\bar{σ}(G) = Σ_{u<v,(u,v)∉E} (d(u)-d(v))²`
8//!   Irregularity measure over complement edges.
9//! - **Albertson coindex** `\bar{Alb}(G) = Σ_{u<v,(u,v)∉E} |d(u)-d(v)|`
10//!   Albertson-type measure over complement edges.
11
12#![allow(
13    clippy::cast_possible_truncation,
14    clippy::cast_precision_loss,
15    clippy::many_single_char_names,
16    clippy::needless_range_loop,
17    clippy::too_many_lines
18)]
19
20use crate::core::{Graph, IgraphResult};
21
22/// Compute the arithmetic-geometric index.
23///
24/// `AG(G) = Σ_{(u,v)∈E} (d(u)+d(v)) / (2√(d(u)·d(v)))`
25///
26/// Self-loops and edges with a degree-0 endpoint are skipped.
27///
28/// # Examples
29///
30/// ```
31/// use rust_igraph::{Graph, arithmetic_geometric_index};
32///
33/// // K_3: 3 edges, d=(2,2), each: (4)/(2·2) = 1 → total = 3
34/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
35/// assert!((arithmetic_geometric_index(&g).unwrap() - 3.0).abs() < 1e-10);
36/// ```
37pub fn arithmetic_geometric_index(graph: &Graph) -> IgraphResult<f64> {
38    let mut ag = 0.0_f64;
39
40    for (u, v) in graph.edges() {
41        if u == v {
42            continue;
43        }
44        let du = graph.degree(u)? as f64;
45        let dv = graph.degree(v)? as f64;
46        let product = du * dv;
47        if product > 0.0 {
48            ag += (du + dv) / (2.0 * product.sqrt());
49        }
50    }
51
52    Ok(ag)
53}
54
55/// Compute the sigma coindex.
56///
57/// `\bar{σ}(G) = Σ_{u<v, (u,v)∉E} (d(u)-d(v))²`
58///
59/// Uses: `\bar{σ} = (n-1)·Σd² - 2m·Σd² / n ... ` — simpler to compute
60/// directly as total minus edge contribution.
61///
62/// # Examples
63///
64/// ```
65/// use rust_igraph::{Graph, sigma_coindex};
66///
67/// // K_3: no non-adjacent pairs → 0
68/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
69/// assert_eq!(sigma_coindex(&g).unwrap(), 0);
70///
71/// // Path 0-1-2: non-adj (0,2), (1-1)²=0
72/// let p = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
73/// assert_eq!(sigma_coindex(&p).unwrap(), 0);
74/// ```
75pub fn sigma_coindex(graph: &Graph) -> IgraphResult<u64> {
76    let n = graph.vcount() as usize;
77    if n < 2 {
78        return Ok(0);
79    }
80
81    let mut degrees = Vec::with_capacity(n);
82    for v in 0..n {
83        degrees.push(graph.degree(v as u32)? as u64);
84    }
85
86    let mut total = 0_u64;
87    for u in 0..n {
88        for v in (u + 1)..n {
89            let diff = degrees[u].abs_diff(degrees[v]);
90            total = total.saturating_add(diff.saturating_mul(diff));
91        }
92    }
93
94    let mut edge_sum = 0_u64;
95    for (u, v) in graph.edges() {
96        if u == v {
97            continue;
98        }
99        let du = degrees[u as usize];
100        let dv = degrees[v as usize];
101        let diff = du.abs_diff(dv);
102        edge_sum = edge_sum.saturating_add(diff.saturating_mul(diff));
103    }
104
105    Ok(total.saturating_sub(edge_sum))
106}
107
108/// Compute the Albertson coindex.
109///
110/// `\bar{Alb}(G) = Σ_{u<v, (u,v)∉E} |d(u)-d(v)|`
111///
112/// # Examples
113///
114/// ```
115/// use rust_igraph::{Graph, albertson_coindex};
116///
117/// // K_3: no non-adjacent pairs → 0
118/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
119/// assert_eq!(albertson_coindex(&g).unwrap(), 0);
120///
121/// // Star S_5: 6 leaf pairs, |1-1|=0 → 0
122/// let s = Graph::from_edges(&[(0,1),(0,2),(0,3),(0,4)], false, Some(5)).unwrap();
123/// assert_eq!(albertson_coindex(&s).unwrap(), 0);
124/// ```
125pub fn albertson_coindex(graph: &Graph) -> IgraphResult<u64> {
126    let n = graph.vcount() as usize;
127    if n < 2 {
128        return Ok(0);
129    }
130
131    let mut degrees = Vec::with_capacity(n);
132    for v in 0..n {
133        degrees.push(graph.degree(v as u32)? as u64);
134    }
135
136    let mut total = 0_u64;
137    for u in 0..n {
138        for v in (u + 1)..n {
139            let diff = degrees[u].abs_diff(degrees[v]);
140            total = total.saturating_add(diff);
141        }
142    }
143
144    let mut edge_sum = 0_u64;
145    for (u, v) in graph.edges() {
146        if u == v {
147            continue;
148        }
149        let du = degrees[u as usize];
150        let dv = degrees[v as usize];
151        let diff = du.abs_diff(dv);
152        edge_sum = edge_sum.saturating_add(diff);
153    }
154
155    Ok(total.saturating_sub(edge_sum))
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    fn single_edge() -> Graph {
163        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
164    }
165
166    fn path3() -> Graph {
167        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
168    }
169
170    fn path4() -> Graph {
171        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
172    }
173
174    fn k3() -> Graph {
175        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
176    }
177
178    fn k4() -> Graph {
179        Graph::from_edges(
180            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
181            false,
182            Some(4),
183        )
184        .unwrap()
185    }
186
187    fn cycle4() -> Graph {
188        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
189    }
190
191    fn cycle5() -> Graph {
192        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
193    }
194
195    fn star5() -> Graph {
196        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
197    }
198
199    fn paw() -> Graph {
200        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
201    }
202
203    // --- arithmetic_geometric_index ---
204
205    #[test]
206    fn ag_empty() {
207        let g = Graph::with_vertices(0);
208        assert!(arithmetic_geometric_index(&g).unwrap().abs() < 1e-10);
209    }
210
211    #[test]
212    fn ag_isolated() {
213        let g = Graph::with_vertices(5);
214        assert!(arithmetic_geometric_index(&g).unwrap().abs() < 1e-10);
215    }
216
217    #[test]
218    fn ag_single_edge() {
219        // (1+1)/(2√1) = 2/2 = 1
220        assert!((arithmetic_geometric_index(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
221    }
222
223    #[test]
224    fn ag_path3() {
225        // 2 edges, d=(1,2): (3)/(2√2). Total = 3/√2 ≈ 2.121
226        let expected = 3.0 / 2.0_f64.sqrt();
227        assert!((arithmetic_geometric_index(&path3()).unwrap() - expected).abs() < 1e-10);
228    }
229
230    #[test]
231    fn ag_k3() {
232        // 3 edges, (2+2)/(2√4)=4/4=1 each → 3
233        assert!((arithmetic_geometric_index(&k3()).unwrap() - 3.0).abs() < 1e-10);
234    }
235
236    #[test]
237    fn ag_k4() {
238        // 6 edges, (3+3)/(2√9)=6/6=1 each → 6
239        assert!((arithmetic_geometric_index(&k4()).unwrap() - 6.0).abs() < 1e-10);
240    }
241
242    #[test]
243    fn ag_cycle4() {
244        // 4 edges, d=(2,2), (4)/(2·2)=1 each → 4
245        assert!((arithmetic_geometric_index(&cycle4()).unwrap() - 4.0).abs() < 1e-10);
246    }
247
248    #[test]
249    fn ag_cycle5() {
250        // 5 edges, each 1 → 5
251        assert!((arithmetic_geometric_index(&cycle5()).unwrap() - 5.0).abs() < 1e-10);
252    }
253
254    #[test]
255    fn ag_star5() {
256        // 4 edges, d=(4,1): (5)/(2√4) = 5/4 each → 5
257        let expected = 4.0 * 5.0 / 4.0;
258        assert!((arithmetic_geometric_index(&star5()).unwrap() - expected).abs() < 1e-10);
259    }
260
261    #[test]
262    fn ag_paw() {
263        // (0,1) d=(2,2): 4/4=1
264        // (0,2) d=(2,3): 5/(2√6)
265        // (1,2) d=(2,3): 5/(2√6)
266        // (2,3) d=(3,1): 4/(2√3)
267        let expected = 1.0 + 5.0 / 6.0_f64.sqrt() + 4.0 / (2.0 * 3.0_f64.sqrt());
268        assert!((arithmetic_geometric_index(&paw()).unwrap() - expected).abs() < 1e-10);
269    }
270
271    #[test]
272    fn ag_regular_equals_edge_count() {
273        // r-regular: AG = m × (2r)/(2r) = m
274        for g in &[k3(), k4(), cycle4(), cycle5()] {
275            let m = g.ecount() as f64;
276            assert!((arithmetic_geometric_index(g).unwrap() - m).abs() < 1e-8);
277        }
278    }
279
280    #[test]
281    fn ag_ge_edge_count_for_simple() {
282        // AM-GM: (du+dv)/(2√(du·dv)) ≥ 1, so AG ≥ m
283        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
284            let m = g.ecount() as f64;
285            assert!(arithmetic_geometric_index(g).unwrap() >= m - 1e-10);
286        }
287    }
288
289    // --- sigma_coindex ---
290
291    #[test]
292    fn sco_empty() {
293        let g = Graph::with_vertices(0);
294        assert_eq!(sigma_coindex(&g).unwrap(), 0);
295    }
296
297    #[test]
298    fn sco_isolated() {
299        let g = Graph::with_vertices(5);
300        assert_eq!(sigma_coindex(&g).unwrap(), 0);
301    }
302
303    #[test]
304    fn sco_single_edge() {
305        assert_eq!(sigma_coindex(&single_edge()).unwrap(), 0);
306    }
307
308    #[test]
309    fn sco_k3() {
310        assert_eq!(sigma_coindex(&k3()).unwrap(), 0);
311    }
312
313    #[test]
314    fn sco_k4() {
315        assert_eq!(sigma_coindex(&k4()).unwrap(), 0);
316    }
317
318    #[test]
319    fn sco_path3() {
320        // Non-adj (0,2): (1-1)²=0
321        assert_eq!(sigma_coindex(&path3()).unwrap(), 0);
322    }
323
324    #[test]
325    fn sco_path4() {
326        // Non-adj: (0,2):(1-2)²=1, (0,3):(1-1)²=0, (1,3):(2-1)²=1 → 2
327        assert_eq!(sigma_coindex(&path4()).unwrap(), 2);
328    }
329
330    #[test]
331    fn sco_cycle4() {
332        // Non-adj: (0,2),(1,3), d=2 each → 0
333        assert_eq!(sigma_coindex(&cycle4()).unwrap(), 0);
334    }
335
336    #[test]
337    fn sco_cycle5() {
338        // Regular → 0
339        assert_eq!(sigma_coindex(&cycle5()).unwrap(), 0);
340    }
341
342    #[test]
343    fn sco_star5() {
344        // Non-adj: 6 leaf pairs (1,1) → all 0
345        assert_eq!(sigma_coindex(&star5()).unwrap(), 0);
346    }
347
348    #[test]
349    fn sco_paw() {
350        // Non-adj: (0,3):(2-1)²=1, (1,3):(2-1)²=1 → 2
351        assert_eq!(sigma_coindex(&paw()).unwrap(), 2);
352    }
353
354    #[test]
355    fn sco_regular_zero() {
356        // Regular graphs: all differences are 0
357        for g in &[k3(), k4(), cycle4(), cycle5()] {
358            assert_eq!(sigma_coindex(g).unwrap(), 0);
359        }
360    }
361
362    // --- albertson_coindex ---
363
364    #[test]
365    fn aco_empty() {
366        let g = Graph::with_vertices(0);
367        assert_eq!(albertson_coindex(&g).unwrap(), 0);
368    }
369
370    #[test]
371    fn aco_isolated() {
372        let g = Graph::with_vertices(5);
373        assert_eq!(albertson_coindex(&g).unwrap(), 0);
374    }
375
376    #[test]
377    fn aco_single_edge() {
378        assert_eq!(albertson_coindex(&single_edge()).unwrap(), 0);
379    }
380
381    #[test]
382    fn aco_k3() {
383        assert_eq!(albertson_coindex(&k3()).unwrap(), 0);
384    }
385
386    #[test]
387    fn aco_k4() {
388        assert_eq!(albertson_coindex(&k4()).unwrap(), 0);
389    }
390
391    #[test]
392    fn aco_path3() {
393        // (0,2): |1-1|=0
394        assert_eq!(albertson_coindex(&path3()).unwrap(), 0);
395    }
396
397    #[test]
398    fn aco_path4() {
399        // (0,2):|1-2|=1, (0,3):|1-1|=0, (1,3):|2-1|=1 → 2
400        assert_eq!(albertson_coindex(&path4()).unwrap(), 2);
401    }
402
403    #[test]
404    fn aco_cycle4() {
405        assert_eq!(albertson_coindex(&cycle4()).unwrap(), 0);
406    }
407
408    #[test]
409    fn aco_star5() {
410        // 6 leaf pairs, |1-1|=0
411        assert_eq!(albertson_coindex(&star5()).unwrap(), 0);
412    }
413
414    #[test]
415    fn aco_paw() {
416        // (0,3):|2-1|=1, (1,3):|2-1|=1 → 2
417        assert_eq!(albertson_coindex(&paw()).unwrap(), 2);
418    }
419
420    #[test]
421    fn aco_regular_zero() {
422        for g in &[k3(), k4(), cycle4(), cycle5()] {
423            assert_eq!(albertson_coindex(g).unwrap(), 0);
424        }
425    }
426
427    // --- cross-consistency ---
428
429    #[test]
430    fn sigma_ge_albertson_for_nontrivial() {
431        // σ ≥ a (since (d-d')² ≥ |d-d'| when |d-d'|≥1, and = when |d-d'|∈{0,1})
432        for g in &[path4(), paw()] {
433            let s = sigma_coindex(g).unwrap();
434            let a = albertson_coindex(g).unwrap();
435            assert!(s >= a);
436        }
437    }
438}