Skip to main content

rust_igraph/algorithms/properties/
edge_degree_indices.rs

1//! Edge-degree based topological indices (ALGO-TR-061).
2//!
3//! - **Platt index** `F(G) = Σ_{(u,v)∈E} (d(u) + d(v) - 2)`
4//!   Sum of edge degrees (each edge's degree = d(u)+d(v)-2).
5//!   Introduced by Platt (1947).
6//! - **Gordon-Scantlebury index** `GS(G) = Platt(G) / 2`
7//!   Counts the number of paths of length 2 in G.
8//!   Gordon & Scantlebury (1964).
9//! - **Bertz complexity index** `B(G) = Σ_{(u,v)∈E} C(d(u)+d(v)-2, 2)`
10//!   where `C(n,2) = n·(n-1)/2`. Combinatorial edge complexity.
11//!   Bertz (1981).
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 Platt index (sum of edge degrees).
24///
25/// `F(G) = Σ_{(u,v)∈E} (d(u) + d(v) - 2)`
26///
27/// Each edge (u,v) has edge-degree = d(u) + d(v) - 2 (the number
28/// of edges adjacent to it, not counting itself). Self-loops are skipped.
29///
30/// # Examples
31///
32/// ```
33/// use rust_igraph::{Graph, platt_index};
34///
35/// // K_3: each edge (2+2-2)=2, 3 edges → 6
36/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
37/// assert_eq!(platt_index(&g).unwrap(), 6);
38/// ```
39pub fn platt_index(graph: &Graph) -> IgraphResult<u64> {
40    let mut f = 0_u64;
41
42    for (u, v) in graph.edges() {
43        if u == v {
44            continue;
45        }
46        let du = graph.degree(u)? as u64;
47        let dv = graph.degree(v)? as u64;
48        f = f.saturating_add(du + dv - 2);
49    }
50
51    Ok(f)
52}
53
54/// Compute the Gordon-Scantlebury index (path-of-length-2 count).
55///
56/// `GS(G) = Platt(G) / 2 = Σ_{(u,v)∈E} (d(u) + d(v) - 2) / 2`
57///
58/// Counts the number of paths of length 2 (P₂) in the graph.
59/// Equivalently, `GS = Σ_v C(d(v), 2) = Σ_v d(v)·(d(v)-1)/2`.
60///
61/// Self-loops are skipped.
62///
63/// # Examples
64///
65/// ```
66/// use rust_igraph::{Graph, gordon_scantlebury_index};
67///
68/// // K_3: Platt=6, GS=3 (three paths of length 2)
69/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
70/// assert_eq!(gordon_scantlebury_index(&g).unwrap(), 3);
71/// ```
72pub fn gordon_scantlebury_index(graph: &Graph) -> IgraphResult<u64> {
73    let mut gs = 0_u64;
74
75    let n = graph.vcount();
76    for v in 0..n {
77        let d = graph.degree(v)? as u64;
78        if d >= 2 {
79            gs = gs.saturating_add(d * (d - 1) / 2);
80        }
81    }
82
83    Ok(gs)
84}
85
86/// Compute the Bertz complexity index.
87///
88/// `B(G) = Σ_{(u,v)∈E} C(d(u)+d(v)-2, 2)`
89///
90/// where `C(n,2) = n·(n-1)/2`. Self-loops are skipped.
91/// Edges where `d(u)+d(v) < 4` contribute 0.
92///
93/// # Examples
94///
95/// ```
96/// use rust_igraph::{Graph, bertz_complexity_index};
97///
98/// // K_3: each edge d(u)+d(v)-2=2, C(2,2)=1, 3 edges → 3
99/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
100/// assert_eq!(bertz_complexity_index(&g).unwrap(), 3);
101/// ```
102pub fn bertz_complexity_index(graph: &Graph) -> IgraphResult<u64> {
103    let mut b = 0_u64;
104
105    for (u, v) in graph.edges() {
106        if u == v {
107            continue;
108        }
109        let du = graph.degree(u)? as u64;
110        let dv = graph.degree(v)? as u64;
111        let edge_deg = du + dv - 2;
112        if edge_deg >= 2 {
113            b = b.saturating_add(edge_deg * (edge_deg - 1) / 2);
114        }
115    }
116
117    Ok(b)
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    fn single_edge() -> Graph {
125        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
126    }
127
128    fn path3() -> Graph {
129        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
130    }
131
132    fn path4() -> Graph {
133        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
134    }
135
136    fn path5() -> Graph {
137        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)], false, Some(5)).unwrap()
138    }
139
140    fn k3() -> Graph {
141        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
142    }
143
144    fn k4() -> Graph {
145        Graph::from_edges(
146            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
147            false,
148            Some(4),
149        )
150        .unwrap()
151    }
152
153    fn cycle4() -> Graph {
154        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
155    }
156
157    fn cycle5() -> Graph {
158        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
159    }
160
161    fn star5() -> Graph {
162        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
163    }
164
165    fn paw() -> Graph {
166        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
167    }
168
169    fn diamond() -> Graph {
170        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)], false, Some(4)).unwrap()
171    }
172
173    // --- platt_index ---
174
175    #[test]
176    fn platt_empty() {
177        let g = Graph::with_vertices(0);
178        assert_eq!(platt_index(&g).unwrap(), 0);
179    }
180
181    #[test]
182    fn platt_isolated() {
183        let g = Graph::with_vertices(5);
184        assert_eq!(platt_index(&g).unwrap(), 0);
185    }
186
187    #[test]
188    fn platt_single_edge() {
189        // (1+1-2) = 0
190        assert_eq!(platt_index(&single_edge()).unwrap(), 0);
191    }
192
193    #[test]
194    fn platt_path3() {
195        // (0,1): 1+2-2=1, (1,2): 2+1-2=1 → 2
196        assert_eq!(platt_index(&path3()).unwrap(), 2);
197    }
198
199    #[test]
200    fn platt_path4() {
201        // (0,1):1+2-2=1, (1,2):2+2-2=2, (2,3):2+1-2=1 → 4
202        assert_eq!(platt_index(&path4()).unwrap(), 4);
203    }
204
205    #[test]
206    fn platt_path5() {
207        // (0,1):1, (1,2):2, (2,3):2, (3,4):1 → 6
208        assert_eq!(platt_index(&path5()).unwrap(), 6);
209    }
210
211    #[test]
212    fn platt_k3() {
213        // 3 × (2+2-2) = 3×2 = 6
214        assert_eq!(platt_index(&k3()).unwrap(), 6);
215    }
216
217    #[test]
218    fn platt_k4() {
219        // 6 × (3+3-2) = 6×4 = 24
220        assert_eq!(platt_index(&k4()).unwrap(), 24);
221    }
222
223    #[test]
224    fn platt_cycle4() {
225        // 4 × (2+2-2) = 4×2 = 8
226        assert_eq!(platt_index(&cycle4()).unwrap(), 8);
227    }
228
229    #[test]
230    fn platt_cycle5() {
231        // 5 × (2+2-2) = 5×2 = 10
232        assert_eq!(platt_index(&cycle5()).unwrap(), 10);
233    }
234
235    #[test]
236    fn platt_star5() {
237        // 4 × (4+1-2) = 4×3 = 12
238        assert_eq!(platt_index(&star5()).unwrap(), 12);
239    }
240
241    #[test]
242    fn platt_paw() {
243        // degrees [2,2,3,1]
244        // (0,1):2+2-2=2, (0,2):2+3-2=3, (1,2):2+3-2=3, (2,3):3+1-2=2
245        // F = 2+3+3+2 = 10
246        assert_eq!(platt_index(&paw()).unwrap(), 10);
247    }
248
249    #[test]
250    fn platt_diamond() {
251        // degrees [3,3,2,2]
252        // (0,1):3+3-2=4, (0,2):3+2-2=3, (0,3):3+2-2=3, (1,2):3+2-2=3, (1,3):3+2-2=3
253        // F = 4+3+3+3+3 = 16
254        assert_eq!(platt_index(&diamond()).unwrap(), 16);
255    }
256
257    #[test]
258    fn platt_equals_first_zagreb_minus_2m() {
259        // Platt = M₁ - 2m where M₁ = first Zagreb index = Σ d²
260        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
261            let m = g.ecount() as u64;
262            let mut m1 = 0_u64;
263            for v in 0..g.vcount() {
264                let d = g.degree(v).unwrap() as u64;
265                m1 += d * d;
266            }
267            assert_eq!(platt_index(g).unwrap(), m1 - 2 * m);
268        }
269    }
270
271    #[test]
272    fn platt_regular_formula() {
273        // r-regular: Platt = m·(2r-2) = 2m(r-1)
274        for g in &[k3(), k4(), cycle4(), cycle5()] {
275            let m = g.ecount() as u64;
276            let r = g.degree(0).unwrap() as u64;
277            assert_eq!(platt_index(g).unwrap(), 2 * m * (r - 1));
278        }
279    }
280
281    // --- gordon_scantlebury_index ---
282
283    #[test]
284    fn gs_empty() {
285        let g = Graph::with_vertices(0);
286        assert_eq!(gordon_scantlebury_index(&g).unwrap(), 0);
287    }
288
289    #[test]
290    fn gs_isolated() {
291        let g = Graph::with_vertices(5);
292        assert_eq!(gordon_scantlebury_index(&g).unwrap(), 0);
293    }
294
295    #[test]
296    fn gs_single_edge() {
297        // No vertex has d≥2 → 0
298        assert_eq!(gordon_scantlebury_index(&single_edge()).unwrap(), 0);
299    }
300
301    #[test]
302    fn gs_path3() {
303        // Only vertex 1 has d=2: C(2,2)=1 → GS=1
304        assert_eq!(gordon_scantlebury_index(&path3()).unwrap(), 1);
305    }
306
307    #[test]
308    fn gs_path4() {
309        // vertices 1,2 have d=2: 2×C(2,2)=2 → GS=2
310        assert_eq!(gordon_scantlebury_index(&path4()).unwrap(), 2);
311    }
312
313    #[test]
314    fn gs_path5() {
315        // vertices 1,2,3 have d=2: 3×1=3
316        assert_eq!(gordon_scantlebury_index(&path5()).unwrap(), 3);
317    }
318
319    #[test]
320    fn gs_k3() {
321        // 3 vertices d=2: 3×C(2,2)=3
322        assert_eq!(gordon_scantlebury_index(&k3()).unwrap(), 3);
323    }
324
325    #[test]
326    fn gs_k4() {
327        // 4 vertices d=3: 4×C(3,2)=4×3=12
328        assert_eq!(gordon_scantlebury_index(&k4()).unwrap(), 12);
329    }
330
331    #[test]
332    fn gs_cycle4() {
333        // 4 × C(2,2) = 4
334        assert_eq!(gordon_scantlebury_index(&cycle4()).unwrap(), 4);
335    }
336
337    #[test]
338    fn gs_cycle5() {
339        // 5 × C(2,2) = 5
340        assert_eq!(gordon_scantlebury_index(&cycle5()).unwrap(), 5);
341    }
342
343    #[test]
344    fn gs_star5() {
345        // center d=4: C(4,2)=6; leaves d=1: 0 → GS=6
346        assert_eq!(gordon_scantlebury_index(&star5()).unwrap(), 6);
347    }
348
349    #[test]
350    fn gs_paw() {
351        // degrees [2,2,3,1]: C(2,2)+C(2,2)+C(3,2)+0 = 1+1+3 = 5
352        assert_eq!(gordon_scantlebury_index(&paw()).unwrap(), 5);
353    }
354
355    #[test]
356    fn gs_diamond() {
357        // degrees [3,3,2,2]: C(3,2)+C(3,2)+C(2,2)+C(2,2) = 3+3+1+1 = 8
358        assert_eq!(gordon_scantlebury_index(&diamond()).unwrap(), 8);
359    }
360
361    #[test]
362    fn gs_equals_platt_half() {
363        // GS = Platt / 2
364        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
365            assert_eq!(
366                gordon_scantlebury_index(g).unwrap(),
367                platt_index(g).unwrap() / 2
368            );
369        }
370    }
371
372    #[test]
373    fn gs_regular_formula() {
374        // r-regular: GS = n·C(r,2) = n·r(r-1)/2
375        for g in &[k3(), k4(), cycle4(), cycle5()] {
376            let n = u64::from(g.vcount());
377            let r = g.degree(0).unwrap() as u64;
378            assert_eq!(gordon_scantlebury_index(g).unwrap(), n * r * (r - 1) / 2);
379        }
380    }
381
382    // --- bertz_complexity_index ---
383
384    #[test]
385    fn bertz_empty() {
386        let g = Graph::with_vertices(0);
387        assert_eq!(bertz_complexity_index(&g).unwrap(), 0);
388    }
389
390    #[test]
391    fn bertz_isolated() {
392        let g = Graph::with_vertices(5);
393        assert_eq!(bertz_complexity_index(&g).unwrap(), 0);
394    }
395
396    #[test]
397    fn bertz_single_edge() {
398        // edge-deg = 0, C(0,2)=0
399        assert_eq!(bertz_complexity_index(&single_edge()).unwrap(), 0);
400    }
401
402    #[test]
403    fn bertz_path3() {
404        // edges: (0,1): ed=1→C(1,2)=0, (1,2): ed=1→0 → B=0
405        assert_eq!(bertz_complexity_index(&path3()).unwrap(), 0);
406    }
407
408    #[test]
409    fn bertz_path4() {
410        // (0,1):ed=1→0, (1,2):ed=2→C(2,2)=1, (2,3):ed=1→0 → B=1
411        assert_eq!(bertz_complexity_index(&path4()).unwrap(), 1);
412    }
413
414    #[test]
415    fn bertz_path5() {
416        // (0,1):1→0, (1,2):2→1, (2,3):2→1, (3,4):1→0 → B=2
417        assert_eq!(bertz_complexity_index(&path5()).unwrap(), 2);
418    }
419
420    #[test]
421    fn bertz_k3() {
422        // 3 edges, each ed=2, C(2,2)=1 → B=3
423        assert_eq!(bertz_complexity_index(&k3()).unwrap(), 3);
424    }
425
426    #[test]
427    fn bertz_k4() {
428        // 6 edges, each ed=4, C(4,2)=6 → B=36
429        assert_eq!(bertz_complexity_index(&k4()).unwrap(), 36);
430    }
431
432    #[test]
433    fn bertz_cycle4() {
434        // 4 edges, each ed=2, C(2,2)=1 → B=4
435        assert_eq!(bertz_complexity_index(&cycle4()).unwrap(), 4);
436    }
437
438    #[test]
439    fn bertz_cycle5() {
440        // 5 × C(2,2) = 5
441        assert_eq!(bertz_complexity_index(&cycle5()).unwrap(), 5);
442    }
443
444    #[test]
445    fn bertz_star5() {
446        // 4 edges, each ed=3, C(3,2)=3 → B=12
447        assert_eq!(bertz_complexity_index(&star5()).unwrap(), 12);
448    }
449
450    #[test]
451    fn bertz_paw() {
452        // (0,1):ed=2→1, (0,2):ed=3→3, (1,2):ed=3→3, (2,3):ed=2→1 → B=8
453        assert_eq!(bertz_complexity_index(&paw()).unwrap(), 8);
454    }
455
456    #[test]
457    fn bertz_diamond() {
458        // (0,1):ed=4→6, (0,2):ed=3→3, (0,3):ed=3→3, (1,2):ed=3→3, (1,3):ed=3→3
459        // B = 6+3+3+3+3 = 18
460        assert_eq!(bertz_complexity_index(&diamond()).unwrap(), 18);
461    }
462
463    #[test]
464    fn bertz_regular_formula() {
465        // r-regular: B = m·C(2r-2,2) = m·(2r-2)(2r-3)/2
466        for g in &[k3(), k4(), cycle4(), cycle5()] {
467            let m = g.ecount() as u64;
468            let r = g.degree(0).unwrap() as u64;
469            let ed = 2 * r - 2;
470            assert_eq!(bertz_complexity_index(g).unwrap(), m * ed * (ed - 1) / 2);
471        }
472    }
473
474    // --- cross-consistency ---
475
476    #[test]
477    fn platt_geq_zero() {
478        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
479            let _ = platt_index(g).unwrap();
480        }
481    }
482
483    #[test]
484    fn gs_leq_platt() {
485        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
486            assert!(gordon_scantlebury_index(g).unwrap() <= platt_index(g).unwrap());
487        }
488    }
489
490    #[test]
491    fn bertz_leq_gs_squared_approx() {
492        // Just sanity-check: Bertz is always computable
493        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
494            let _ = bertz_complexity_index(g).unwrap();
495        }
496    }
497}