Skip to main content

rust_igraph/algorithms/properties/
graph_density_profile.rs

1//! Graph density profile indices (ALGO-TR-094).
2//!
3//! Density-like measures that quantify the graph's connectivity
4//! from different perspectives:
5//!
6//! - **Triangle density** — fraction of possible triangles that exist
7//! - **Square density** — fraction of possible 4-cycles that exist
8//! - **Edge connectivity ratio** — 2m / (n·(n-1)) for undirected
9//! - **Degree density** — (Σd²/Σd - 1) / (n - 1) normalized second moment
10
11#![allow(
12    clippy::cast_possible_truncation,
13    clippy::cast_precision_loss,
14    clippy::many_single_char_names,
15    clippy::needless_range_loop,
16    clippy::similar_names,
17    clippy::too_many_lines
18)]
19
20use crate::core::{Graph, IgraphResult};
21
22/// Compute the triangle density of the graph.
23///
24/// `T_d = 6·triangles / (n·(n-1)·(n-2))` for n ≥ 3
25///
26/// Fraction of all possible vertex triples that form a triangle.
27/// Returns 0.0 for graphs with fewer than 3 vertices. Related to
28/// transitivity but normalized by the total number of triples, not
29/// just connected triples (paths of length 2).
30///
31/// # Examples
32///
33/// ```
34/// use rust_igraph::{Graph, triangle_density};
35///
36/// // K_3: 1 triangle, n=3, denom=6 → 6·1/6 = 1.0
37/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
38/// assert!((triangle_density(&g).unwrap() - 1.0).abs() < 1e-10);
39/// ```
40pub fn triangle_density(graph: &Graph) -> IgraphResult<f64> {
41    let n = u64::from(graph.vcount());
42    if n < 3 {
43        return Ok(0.0);
44    }
45
46    let mut tri_count = 0_u64;
47    let nv = n as usize;
48    for v in 0..nv {
49        let vid = v as u32;
50        let neighbors = graph.neighbors(vid)?;
51        for i in 0..neighbors.len() {
52            for j in (i + 1)..neighbors.len() {
53                let a = neighbors[i];
54                let b = neighbors[j];
55                if a > vid && b > vid && graph.has_edge(a, b) {
56                    tri_count += 1;
57                }
58            }
59        }
60    }
61
62    let denom = n * (n - 1) * (n - 2);
63    Ok(6.0 * tri_count as f64 / denom as f64)
64}
65
66/// Compute the square (4-cycle) density of the graph.
67///
68/// Number of distinct 4-cycles divided by `C(n, 4)` for n ≥ 4.
69///
70/// A 4-cycle is an induced cycle on 4 vertices (no chord). Counted
71/// via non-adjacent pairs sharing common neighbors: each pair of
72/// common neighbors of a non-edge (u,v) forms a 4-cycle u-w1-v-w2-u
73/// provided w1-w2 are not adjacent. Returns 0.0 for n < 4.
74///
75/// # Examples
76///
77/// ```
78/// use rust_igraph::{Graph, square_density};
79///
80/// // C_4: exactly 1 chordless 4-cycle, C(4,4)=1 → 1.0
81/// let g = Graph::from_edges(&[(0,1),(1,2),(2,3),(3,0)], false, Some(4)).unwrap();
82/// assert!((square_density(&g).unwrap() - 1.0).abs() < 1e-10);
83/// ```
84pub fn square_density(graph: &Graph) -> IgraphResult<f64> {
85    let n = u64::from(graph.vcount());
86    if n < 4 {
87        return Ok(0.0);
88    }
89
90    let nv = n as usize;
91    let mut count = 0_u64;
92
93    for u in 0..nv {
94        let uid = u as u32;
95        for v in (u + 1)..nv {
96            let vid = v as u32;
97            if graph.has_edge(uid, vid) {
98                continue;
99            }
100            let nu = graph.neighbors(uid)?;
101            let nv_list = graph.neighbors(vid)?;
102
103            let mut common = Vec::new();
104            for &w in &nu {
105                if w != vid {
106                    for &x in &nv_list {
107                        if x == w && x != uid {
108                            common.push(w);
109                            break;
110                        }
111                    }
112                }
113            }
114
115            for i in 0..common.len() {
116                for j in (i + 1)..common.len() {
117                    if !graph.has_edge(common[i], common[j]) {
118                        count += 1;
119                    }
120                }
121            }
122        }
123    }
124
125    let denom = n * (n - 1) * (n - 2) * (n - 3) / 24;
126    if denom == 0 {
127        return Ok(0.0);
128    }
129
130    // Each chordless 4-cycle is found twice (once per diagonal pair)
131    Ok(count as f64 / (2.0 * denom as f64))
132}
133
134/// Compute the edge connectivity ratio.
135///
136/// `r = 2m / (n·(n-1))` for undirected, `m / (n·(n-1))` for directed
137///
138/// Identical to `density()` for simple graphs but computed directly
139/// from edge and vertex counts without checking simplicity. Returns
140/// 0.0 for graphs with fewer than 2 vertices.
141///
142/// # Examples
143///
144/// ```
145/// use rust_igraph::{Graph, edge_connectivity_ratio};
146///
147/// // K_3: 3 edges, n=3 → 2·3/(3·2) = 1.0
148/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
149/// assert!((edge_connectivity_ratio(&g).unwrap() - 1.0).abs() < 1e-10);
150/// ```
151pub fn edge_connectivity_ratio(graph: &Graph) -> IgraphResult<f64> {
152    let n = u64::from(graph.vcount());
153    if n < 2 {
154        return Ok(0.0);
155    }
156
157    let m = graph.ecount() as f64;
158    let denom = n * (n - 1);
159
160    if graph.is_directed() {
161        Ok(m / denom as f64)
162    } else {
163        Ok(2.0 * m / denom as f64)
164    }
165}
166
167/// Compute the degree density (normalized second moment of degree).
168///
169/// `DD = (⟨d²⟩/⟨d⟩ - 1) / (n - 1)`
170///
171/// where `⟨d²⟩` and `⟨d⟩` are the mean of d² and d. For a random
172/// Erdős–Rényi graph, this equals the edge probability p. For
173/// regular graphs, equals (degree) / (n-1). Returns 0.0 for graphs
174/// with fewer than 2 vertices or zero total degree.
175///
176/// # Examples
177///
178/// ```
179/// use rust_igraph::{Graph, degree_density};
180///
181/// // K_3: d=2 for all, ⟨d²⟩=4, ⟨d⟩=2 → (4/2-1)/(3-1) = 1/2 = 0.5
182/// // But density(K_3) = 1.0, so this is different
183/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
184/// assert!((degree_density(&g).unwrap() - 0.5).abs() < 1e-10);
185/// ```
186pub fn degree_density(graph: &Graph) -> IgraphResult<f64> {
187    let n = graph.vcount() as usize;
188    if n < 2 {
189        return Ok(0.0);
190    }
191
192    let mut sum_d = 0.0_f64;
193    let mut sum_d2 = 0.0_f64;
194
195    for v in 0..n {
196        let d = graph.degree(v as u32)? as f64;
197        sum_d += d;
198        sum_d2 += d * d;
199    }
200
201    if sum_d < 1e-15 {
202        return Ok(0.0);
203    }
204
205    let mean_d = sum_d / n as f64;
206    let mean_d2 = sum_d2 / n as f64;
207
208    Ok((mean_d2 / mean_d - 1.0) / (n as f64 - 1.0))
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    fn single_edge() -> Graph {
216        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
217    }
218
219    fn path3() -> Graph {
220        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
221    }
222
223    fn k3() -> Graph {
224        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
225    }
226
227    fn k4() -> Graph {
228        Graph::from_edges(
229            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
230            false,
231            Some(4),
232        )
233        .unwrap()
234    }
235
236    fn cycle4() -> Graph {
237        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
238    }
239
240    fn star5() -> Graph {
241        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
242    }
243
244    fn paw() -> Graph {
245        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
246    }
247
248    // --- triangle_density ---
249
250    #[test]
251    fn tri_empty() {
252        let g = Graph::with_vertices(0);
253        assert!(triangle_density(&g).unwrap().abs() < 1e-10);
254    }
255
256    #[test]
257    fn tri_two() {
258        let g = Graph::with_vertices(2);
259        assert!(triangle_density(&g).unwrap().abs() < 1e-10);
260    }
261
262    #[test]
263    fn tri_k3() {
264        // 1 triangle, 6·1/(3·2·1) = 1.0
265        assert!((triangle_density(&k3()).unwrap() - 1.0).abs() < 1e-10);
266    }
267
268    #[test]
269    fn tri_k4() {
270        // 4 triangles, 6·4/(4·3·2) = 24/24 = 1.0
271        assert!((triangle_density(&k4()).unwrap() - 1.0).abs() < 1e-10);
272    }
273
274    #[test]
275    fn tri_path3() {
276        // 0 triangles
277        assert!(triangle_density(&path3()).unwrap().abs() < 1e-10);
278    }
279
280    #[test]
281    fn tri_cycle4() {
282        // 0 triangles on C_4
283        assert!(triangle_density(&cycle4()).unwrap().abs() < 1e-10);
284    }
285
286    #[test]
287    fn tri_star5() {
288        // 0 triangles
289        assert!(triangle_density(&star5()).unwrap().abs() < 1e-10);
290    }
291
292    #[test]
293    fn tri_paw() {
294        // 1 triangle (0,1,2), n=4
295        // 6·1/(4·3·2) = 6/24 = 0.25
296        assert!((triangle_density(&paw()).unwrap() - 0.25).abs() < 1e-10);
297    }
298
299    // --- square_density ---
300
301    #[test]
302    fn sq_empty() {
303        let g = Graph::with_vertices(0);
304        assert!(square_density(&g).unwrap().abs() < 1e-10);
305    }
306
307    #[test]
308    fn sq_three() {
309        assert!(square_density(&k3()).unwrap().abs() < 1e-10);
310    }
311
312    #[test]
313    fn sq_cycle4() {
314        // C_4 has exactly 1 distinct 4-cycle
315        // C(4,4) = 1, so density = 1/1 = 1.0
316        assert!((square_density(&cycle4()).unwrap() - 1.0).abs() < 1e-10);
317    }
318
319    #[test]
320    fn sq_k4() {
321        // K_4: every 4-cycle has a chord → 0 chordless 4-cycles
322        assert!(square_density(&k4()).unwrap().abs() < 1e-10);
323    }
324
325    #[test]
326    fn sq_star5() {
327        // No 4-cycles in a star
328        assert!(square_density(&star5()).unwrap().abs() < 1e-10);
329    }
330
331    #[test]
332    fn sq_path3() {
333        assert!(square_density(&path3()).unwrap().abs() < 1e-10);
334    }
335
336    // --- edge_connectivity_ratio ---
337
338    #[test]
339    fn ecr_empty() {
340        let g = Graph::with_vertices(0);
341        assert!(edge_connectivity_ratio(&g).unwrap().abs() < 1e-10);
342    }
343
344    #[test]
345    fn ecr_single() {
346        let g = Graph::with_vertices(1);
347        assert!(edge_connectivity_ratio(&g).unwrap().abs() < 1e-10);
348    }
349
350    #[test]
351    fn ecr_k3() {
352        // 2·3/(3·2) = 1.0
353        assert!((edge_connectivity_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
354    }
355
356    #[test]
357    fn ecr_k4() {
358        // 2·6/(4·3) = 1.0
359        assert!((edge_connectivity_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
360    }
361
362    #[test]
363    fn ecr_cycle4() {
364        // 2·4/(4·3) = 8/12 = 2/3
365        assert!((edge_connectivity_ratio(&cycle4()).unwrap() - 2.0 / 3.0).abs() < 1e-10);
366    }
367
368    #[test]
369    fn ecr_single_edge() {
370        // 2·1/(2·1) = 1.0
371        assert!((edge_connectivity_ratio(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
372    }
373
374    #[test]
375    fn ecr_star5() {
376        // 2·4/(5·4) = 8/20 = 0.4
377        assert!((edge_connectivity_ratio(&star5()).unwrap() - 0.4).abs() < 1e-10);
378    }
379
380    #[test]
381    fn ecr_path3() {
382        // 2·2/(3·2) = 4/6 = 2/3
383        assert!((edge_connectivity_ratio(&path3()).unwrap() - 2.0 / 3.0).abs() < 1e-10);
384    }
385
386    #[test]
387    fn ecr_paw() {
388        // 2·4/(4·3) = 8/12 = 2/3
389        assert!((edge_connectivity_ratio(&paw()).unwrap() - 2.0 / 3.0).abs() < 1e-10);
390    }
391
392    // --- degree_density ---
393
394    #[test]
395    fn dd_empty() {
396        let g = Graph::with_vertices(0);
397        assert!(degree_density(&g).unwrap().abs() < 1e-10);
398    }
399
400    #[test]
401    fn dd_single() {
402        let g = Graph::with_vertices(1);
403        assert!(degree_density(&g).unwrap().abs() < 1e-10);
404    }
405
406    #[test]
407    fn dd_isolated() {
408        let g = Graph::with_vertices(5);
409        assert!(degree_density(&g).unwrap().abs() < 1e-10);
410    }
411
412    #[test]
413    fn dd_k3() {
414        // d=2 for all: ⟨d²⟩=4, ⟨d⟩=2, (4/2-1)/(3-1) = 1/2
415        assert!((degree_density(&k3()).unwrap() - 0.5).abs() < 1e-10);
416    }
417
418    #[test]
419    fn dd_k4() {
420        // d=3 for all: ⟨d²⟩=9, ⟨d⟩=3, (9/3-1)/(4-1) = 2/3
421        assert!((degree_density(&k4()).unwrap() - 2.0 / 3.0).abs() < 1e-10);
422    }
423
424    #[test]
425    fn dd_single_edge() {
426        // d=1 for all: (1/1-1)/(2-1) = 0/1 = 0
427        assert!(degree_density(&single_edge()).unwrap().abs() < 1e-10);
428    }
429
430    #[test]
431    fn dd_star5() {
432        // degrees [4,1,1,1,1], n=5
433        // ⟨d⟩ = 8/5 = 1.6
434        // ⟨d²⟩ = (16+1+1+1+1)/5 = 20/5 = 4.0
435        // DD = (4.0/1.6 - 1) / (5-1) = (2.5-1)/4 = 1.5/4 = 0.375
436        assert!((degree_density(&star5()).unwrap() - 0.375).abs() < 1e-10);
437    }
438
439    #[test]
440    fn dd_path3() {
441        // degrees [1,2,1], n=3
442        // ⟨d⟩ = 4/3, ⟨d²⟩ = (1+4+1)/3 = 2
443        // DD = (2/(4/3) - 1) / (3-1) = (1.5-1)/2 = 0.5/2 = 0.25
444        assert!((degree_density(&path3()).unwrap() - 0.25).abs() < 1e-10);
445    }
446
447    #[test]
448    fn dd_paw() {
449        // degrees [2,2,3,1], n=4
450        // ⟨d⟩ = 8/4 = 2, ⟨d²⟩ = (4+4+9+1)/4 = 18/4 = 4.5
451        // DD = (4.5/2 - 1) / (4-1) = (2.25-1)/3 = 1.25/3
452        let expected = 1.25 / 3.0;
453        assert!((degree_density(&paw()).unwrap() - expected).abs() < 1e-10);
454    }
455
456    // --- cross-consistency ---
457
458    #[test]
459    fn tri_in_01() {
460        for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
461            let t = triangle_density(g).unwrap();
462            assert!(t >= -1e-10);
463            assert!(t <= 1.0 + 1e-10);
464        }
465    }
466
467    #[test]
468    fn ecr_in_01() {
469        for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
470            let r = edge_connectivity_ratio(g).unwrap();
471            assert!(r >= -1e-10);
472            assert!(r <= 1.0 + 1e-10);
473        }
474    }
475
476    #[test]
477    fn dd_nonneg() {
478        for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
479            assert!(degree_density(g).unwrap() >= -1e-10);
480        }
481    }
482
483    #[test]
484    fn complete_ecr_is_one() {
485        assert!((edge_connectivity_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
486        assert!((edge_connectivity_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
487    }
488
489    #[test]
490    fn complete_tri_is_one() {
491        assert!((triangle_density(&k3()).unwrap() - 1.0).abs() < 1e-10);
492        assert!((triangle_density(&k4()).unwrap() - 1.0).abs() < 1e-10);
493    }
494}