Skip to main content

rust_igraph/algorithms/properties/
edge_degree_mean.rs

1//! Edge degree mean-type indices (ALGO-TR-087).
2//!
3//! Edge-level aggregates using harmonic, geometric, ratio, and RMS
4//! functions of endpoint degrees:
5//!
6//! - **Harmonic sum** `Σ 2·d(u)·d(v) / (d(u)+d(v))` — harmonic mean per edge
7//! - **Geometric sum** `Σ √(d(u)·d(v))` — geometric mean per edge
8//! - **Ratio sum** `Σ min(d(u),d(v)) / max(d(u),d(v))` — edge regularity
9//! - **Endpoint RMS** `√(Σ (d(u)²+d(v)²) / (2m))` — root-mean-square
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::too_many_lines
17)]
18
19use crate::core::{Graph, IgraphResult};
20
21/// Compute the edge degree harmonic sum.
22///
23/// `Σ_{(u,v)∈E} 2·d(u)·d(v) / (d(u)+d(v))`
24///
25/// The harmonic mean of endpoint degrees, summed over all edges.
26/// Self-loops and edges with a degree-0 endpoint are skipped.
27/// Returns 0.0 for edgeless graphs. For regular graphs with
28/// degree r: `H = m·r`.
29///
30/// # Examples
31///
32/// ```
33/// use rust_igraph::{Graph, edge_degree_harmonic_sum};
34///
35/// // K_3: 3 edges, all (2,2) → 3·2 = 6.0
36/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
37/// assert!((edge_degree_harmonic_sum(&g).unwrap() - 6.0).abs() < 1e-10);
38/// ```
39pub fn edge_degree_harmonic_sum(graph: &Graph) -> IgraphResult<f64> {
40    let mut result = 0.0_f64;
41
42    for (u, v) in graph.edges() {
43        if u == v {
44            continue;
45        }
46        let du = graph.degree(u)? as f64;
47        let dv = graph.degree(v)? as f64;
48        let sum = du + dv;
49        if sum == 0.0 {
50            continue;
51        }
52        result += 2.0 * du * dv / sum;
53    }
54
55    Ok(result)
56}
57
58/// Compute the edge degree geometric sum.
59///
60/// `Σ_{(u,v)∈E} √(d(u)·d(v))`
61///
62/// The geometric mean of endpoint degrees, summed over all edges.
63/// Self-loops are skipped. Related to the Randić index (which sums
64/// the *reciprocal* √(d(u)·d(v))). For regular graphs with
65/// degree r: `G = m·r`.
66///
67/// # Examples
68///
69/// ```
70/// use rust_igraph::{Graph, edge_degree_geometric_sum};
71///
72/// // K_3: 3 edges, all (2,2) → 3·2 = 6.0
73/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
74/// assert!((edge_degree_geometric_sum(&g).unwrap() - 6.0).abs() < 1e-10);
75/// ```
76pub fn edge_degree_geometric_sum(graph: &Graph) -> IgraphResult<f64> {
77    let mut result = 0.0_f64;
78
79    for (u, v) in graph.edges() {
80        if u == v {
81            continue;
82        }
83        let du = graph.degree(u)? as f64;
84        let dv = graph.degree(v)? as f64;
85        result += (du * dv).sqrt();
86    }
87
88    Ok(result)
89}
90
91/// Compute the edge degree ratio sum.
92///
93/// `Σ_{(u,v)∈E} min(d(u),d(v)) / max(d(u),d(v))`
94///
95/// Each edge contributes a value in (0, 1]. Equals m for regular
96/// graphs. Self-loops and edges with a degree-0 endpoint are
97/// skipped. Returns 0.0 for edgeless graphs.
98///
99/// # Examples
100///
101/// ```
102/// use rust_igraph::{Graph, edge_degree_ratio_sum};
103///
104/// // K_3: 3 edges, all (2,2) → 3·1.0 = 3.0
105/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
106/// assert!((edge_degree_ratio_sum(&g).unwrap() - 3.0).abs() < 1e-10);
107/// ```
108pub fn edge_degree_ratio_sum(graph: &Graph) -> IgraphResult<f64> {
109    let mut result = 0.0_f64;
110
111    for (u, v) in graph.edges() {
112        if u == v {
113            continue;
114        }
115        let du = graph.degree(u)?;
116        let dv = graph.degree(v)?;
117        if du == 0 || dv == 0 {
118            continue;
119        }
120        let min_d = du.min(dv) as f64;
121        let max_d = du.max(dv) as f64;
122        result += min_d / max_d;
123    }
124
125    Ok(result)
126}
127
128/// Compute the edge endpoint degree RMS.
129///
130/// `√( Σ_{(u,v)∈E} (d(u)² + d(v)²) / (2m) )`
131///
132/// Root-mean-square of endpoint degrees over all edge endpoints.
133/// Self-loops are skipped (m = non-loop edge count). Returns 0.0
134/// for edgeless graphs. For regular graphs with degree r: `RMS = r`.
135///
136/// # Examples
137///
138/// ```
139/// use rust_igraph::{Graph, edge_degree_rms};
140///
141/// // K_3: 3 edges, all (2,2) → √((3·(4+4))/(2·3)) = √(4) = 2.0
142/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
143/// assert!((edge_degree_rms(&g).unwrap() - 2.0).abs() < 1e-10);
144/// ```
145pub fn edge_degree_rms(graph: &Graph) -> IgraphResult<f64> {
146    let mut sum_sq = 0.0_f64;
147    let mut edge_count = 0_u64;
148
149    for (u, v) in graph.edges() {
150        if u == v {
151            continue;
152        }
153        let du = graph.degree(u)? as f64;
154        let dv = graph.degree(v)? as f64;
155        sum_sq += du * du + dv * dv;
156        edge_count += 1;
157    }
158
159    if edge_count == 0 {
160        return Ok(0.0);
161    }
162
163    Ok((sum_sq / (2.0 * edge_count as f64)).sqrt())
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    fn single_edge() -> Graph {
171        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
172    }
173
174    fn path3() -> Graph {
175        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
176    }
177
178    fn k3() -> Graph {
179        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
180    }
181
182    fn k4() -> Graph {
183        Graph::from_edges(
184            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
185            false,
186            Some(4),
187        )
188        .unwrap()
189    }
190
191    fn cycle4() -> Graph {
192        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).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    // --- edge_degree_harmonic_sum ---
204
205    #[test]
206    fn harm_empty() {
207        let g = Graph::with_vertices(0);
208        assert!(edge_degree_harmonic_sum(&g).unwrap().abs() < 1e-10);
209    }
210
211    #[test]
212    fn harm_isolated() {
213        let g = Graph::with_vertices(5);
214        assert!(edge_degree_harmonic_sum(&g).unwrap().abs() < 1e-10);
215    }
216
217    #[test]
218    fn harm_regular() {
219        // Regular degree r: harmonic mean = r → m·r
220        assert!((edge_degree_harmonic_sum(&k3()).unwrap() - 6.0).abs() < 1e-10);
221        assert!((edge_degree_harmonic_sum(&k4()).unwrap() - 18.0).abs() < 1e-10);
222        assert!((edge_degree_harmonic_sum(&cycle4()).unwrap() - 8.0).abs() < 1e-10);
223    }
224
225    #[test]
226    fn harm_single_edge() {
227        // (1,1): harmonic = 1
228        assert!((edge_degree_harmonic_sum(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
229    }
230
231    #[test]
232    fn harm_star5() {
233        // 4 edges (4,1): harmonic = 2·4·1/(4+1) = 8/5 = 1.6 each → 6.4
234        assert!((edge_degree_harmonic_sum(&star5()).unwrap() - 6.4).abs() < 1e-10);
235    }
236
237    #[test]
238    fn harm_path3() {
239        // 2 edges (1,2): harmonic = 2·1·2/(1+2) = 4/3 each → 8/3
240        let expected = 8.0 / 3.0;
241        assert!((edge_degree_harmonic_sum(&path3()).unwrap() - expected).abs() < 1e-10);
242    }
243
244    #[test]
245    fn harm_paw() {
246        // (0,1)d=(2,2):2  (0,2)d=(2,3):2·6/5=2.4  (1,2)d=(2,3):2.4  (2,3)d=(3,1):2·3/4=1.5
247        let expected = 2.0 + 2.4 + 2.4 + 1.5;
248        assert!((edge_degree_harmonic_sum(&paw()).unwrap() - expected).abs() < 1e-10);
249    }
250
251    // --- edge_degree_geometric_sum ---
252
253    #[test]
254    fn geom_empty() {
255        let g = Graph::with_vertices(0);
256        assert!(edge_degree_geometric_sum(&g).unwrap().abs() < 1e-10);
257    }
258
259    #[test]
260    fn geom_isolated() {
261        let g = Graph::with_vertices(5);
262        assert!(edge_degree_geometric_sum(&g).unwrap().abs() < 1e-10);
263    }
264
265    #[test]
266    fn geom_regular() {
267        // Regular degree r: geometric mean = r → m·r
268        assert!((edge_degree_geometric_sum(&k3()).unwrap() - 6.0).abs() < 1e-10);
269        assert!((edge_degree_geometric_sum(&k4()).unwrap() - 18.0).abs() < 1e-10);
270        assert!((edge_degree_geometric_sum(&cycle4()).unwrap() - 8.0).abs() < 1e-10);
271    }
272
273    #[test]
274    fn geom_single_edge() {
275        // (1,1): √1 = 1
276        assert!((edge_degree_geometric_sum(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
277    }
278
279    #[test]
280    fn geom_star5() {
281        // 4 edges (4,1): √4 = 2 each → 8
282        assert!((edge_degree_geometric_sum(&star5()).unwrap() - 8.0).abs() < 1e-10);
283    }
284
285    #[test]
286    fn geom_path3() {
287        // 2 edges (1,2): √2 each → 2√2
288        let expected = 2.0 * 2.0_f64.sqrt();
289        assert!((edge_degree_geometric_sum(&path3()).unwrap() - expected).abs() < 1e-10);
290    }
291
292    #[test]
293    fn geom_paw() {
294        // (0,1):√4=2  (0,2):√6  (1,2):√6  (2,3):√3
295        let expected = 2.0 + 2.0 * 6.0_f64.sqrt() + 3.0_f64.sqrt();
296        assert!((edge_degree_geometric_sum(&paw()).unwrap() - expected).abs() < 1e-10);
297    }
298
299    // --- edge_degree_ratio_sum ---
300
301    #[test]
302    fn ratio_empty() {
303        let g = Graph::with_vertices(0);
304        assert!(edge_degree_ratio_sum(&g).unwrap().abs() < 1e-10);
305    }
306
307    #[test]
308    fn ratio_isolated() {
309        let g = Graph::with_vertices(5);
310        assert!(edge_degree_ratio_sum(&g).unwrap().abs() < 1e-10);
311    }
312
313    #[test]
314    fn ratio_regular() {
315        // Regular: ratio = 1 → sum = m
316        assert!((edge_degree_ratio_sum(&k3()).unwrap() - 3.0).abs() < 1e-10);
317        assert!((edge_degree_ratio_sum(&k4()).unwrap() - 6.0).abs() < 1e-10);
318        assert!((edge_degree_ratio_sum(&cycle4()).unwrap() - 4.0).abs() < 1e-10);
319    }
320
321    #[test]
322    fn ratio_single_edge() {
323        // (1,1): ratio = 1
324        assert!((edge_degree_ratio_sum(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
325    }
326
327    #[test]
328    fn ratio_star5() {
329        // 4 edges (4,1): ratio = 1/4 = 0.25 each → 1.0
330        assert!((edge_degree_ratio_sum(&star5()).unwrap() - 1.0).abs() < 1e-10);
331    }
332
333    #[test]
334    fn ratio_path3() {
335        // 2 edges (1,2): ratio = 1/2 each → 1.0
336        assert!((edge_degree_ratio_sum(&path3()).unwrap() - 1.0).abs() < 1e-10);
337    }
338
339    #[test]
340    fn ratio_paw() {
341        // (0,1):2/2=1  (0,2):2/3  (1,2):2/3  (2,3):1/3
342        let expected = 1.0 + 2.0 / 3.0 + 2.0 / 3.0 + 1.0 / 3.0;
343        assert!((edge_degree_ratio_sum(&paw()).unwrap() - expected).abs() < 1e-10);
344    }
345
346    // --- edge_degree_rms ---
347
348    #[test]
349    fn rms_empty() {
350        let g = Graph::with_vertices(0);
351        assert!(edge_degree_rms(&g).unwrap().abs() < 1e-10);
352    }
353
354    #[test]
355    fn rms_isolated() {
356        let g = Graph::with_vertices(5);
357        assert!(edge_degree_rms(&g).unwrap().abs() < 1e-10);
358    }
359
360    #[test]
361    fn rms_regular() {
362        // Regular degree r: √((m·2r²)/(2m)) = r
363        assert!((edge_degree_rms(&k3()).unwrap() - 2.0).abs() < 1e-10);
364        assert!((edge_degree_rms(&k4()).unwrap() - 3.0).abs() < 1e-10);
365        assert!((edge_degree_rms(&cycle4()).unwrap() - 2.0).abs() < 1e-10);
366    }
367
368    #[test]
369    fn rms_single_edge() {
370        // (1,1): √((1+1)/2) = 1
371        assert!((edge_degree_rms(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
372    }
373
374    #[test]
375    fn rms_star5() {
376        // 4 edges (4,1): sum_sq = 4·(16+1) = 68, 2m=8 → √(68/8) = √8.5
377        let expected = (68.0 / 8.0_f64).sqrt();
378        assert!((edge_degree_rms(&star5()).unwrap() - expected).abs() < 1e-10);
379    }
380
381    #[test]
382    fn rms_path3() {
383        // 2 edges (1,2): sum_sq = 2·(1+4) = 10, 2m=4 → √(10/4) = √2.5
384        let expected = (10.0 / 4.0_f64).sqrt();
385        assert!((edge_degree_rms(&path3()).unwrap() - expected).abs() < 1e-10);
386    }
387
388    #[test]
389    fn rms_paw() {
390        // (0,1):(4+4)=8  (0,2):(4+9)=13  (1,2):(4+9)=13  (2,3):(9+1)=10
391        // sum_sq = 44, 2m = 8 → √(44/8) = √5.5
392        let expected = (44.0 / 8.0_f64).sqrt();
393        assert!((edge_degree_rms(&paw()).unwrap() - expected).abs() < 1e-10);
394    }
395
396    // --- cross-consistency ---
397
398    #[test]
399    fn harmonic_le_geometric_le_arithmetic() {
400        // AM-GM-HM inequality: harmonic ≤ geometric ≤ arithmetic (per edge)
401        // So sums preserve the inequality
402        for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
403            let h = edge_degree_harmonic_sum(g).unwrap();
404            let geo = edge_degree_geometric_sum(g).unwrap();
405            assert!(h <= geo + 1e-10);
406        }
407    }
408
409    #[test]
410    fn ratio_in_0_m() {
411        for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
412            let val = edge_degree_ratio_sum(g).unwrap();
413            let m = g.edges().filter(|&(u, v)| u != v).count() as f64;
414            assert!(val >= -1e-10);
415            assert!(val <= m + 1e-10);
416        }
417    }
418
419    #[test]
420    fn ratio_equals_m_for_regular() {
421        // Regular graphs: all ratios = 1 → sum = m
422        for g in &[k3(), k4(), cycle4()] {
423            let val = edge_degree_ratio_sum(g).unwrap();
424            let m = g.edges().filter(|&(u, v)| u != v).count() as f64;
425            assert!((val - m).abs() < 1e-10);
426        }
427    }
428
429    #[test]
430    fn rms_ge_mean() {
431        // RMS ≥ arithmetic mean of endpoint degrees
432        for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
433            let rms = edge_degree_rms(g).unwrap();
434            let edges: Vec<_> = g.edges().filter(|&(u, v)| u != v).collect();
435            if edges.is_empty() {
436                continue;
437            }
438            let mean: f64 = edges
439                .iter()
440                .map(|&(u, v)| {
441                    let du = g.degree(u).unwrap() as f64;
442                    let dv = g.degree(v).unwrap() as f64;
443                    f64::midpoint(du, dv)
444                })
445                .sum::<f64>()
446                / edges.len() as f64;
447            assert!(rms >= mean - 1e-10);
448        }
449    }
450}