Skip to main content

rust_igraph/algorithms/properties/
edge_degree_pair.rs

1//! Edge degree-pair aggregates (ALGO-TR-086).
2//!
3//! Simple edge-additive indices that aggregate min, max, and product
4//! functions of endpoint degrees:
5//!
6//! - **Edge degree min sum** `EDmin(G) = Σ_{(u,v)∈E} min(d(u),d(v))`
7//! - **Edge degree max sum** `EDmax(G) = Σ_{(u,v)∈E} max(d(u),d(v))`
8//! - **Edge degree log-product sum** `EDlp(G) = Σ_{(u,v)∈E} ln(d(u)·d(v))`
9//!   (skips edges with a degree-0 endpoint)
10//! - **Edge degree mean sum** `EDμ(G) = Σ_{(u,v)∈E} (d(u)+d(v))/2`
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 edge degree min sum.
23///
24/// `EDmin(G) = Σ_{(u,v)∈E} min(d(u), d(v))`
25///
26/// Self-loops are skipped. For regular graphs with degree r:
27/// `EDmin = m·r`.
28///
29/// # Examples
30///
31/// ```
32/// use rust_igraph::{Graph, edge_degree_min_sum};
33///
34/// // K_3: 3 edges, all (2,2) → 3·2 = 6
35/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
36/// assert_eq!(edge_degree_min_sum(&g).unwrap(), 6);
37/// ```
38pub fn edge_degree_min_sum(graph: &Graph) -> IgraphResult<u64> {
39    let mut result = 0_u64;
40
41    for (u, v) in graph.edges() {
42        if u == v {
43            continue;
44        }
45        let du = graph.degree(u)?;
46        let dv = graph.degree(v)?;
47        result += du.min(dv) as u64;
48    }
49
50    Ok(result)
51}
52
53/// Compute the edge degree max sum.
54///
55/// `EDmax(G) = Σ_{(u,v)∈E} max(d(u), d(v))`
56///
57/// Self-loops are skipped. For regular graphs with degree r:
58/// `EDmax = m·r`. Note that `EDmin + EDmax = M₁` (first Zagreb).
59///
60/// # Examples
61///
62/// ```
63/// use rust_igraph::{Graph, edge_degree_max_sum};
64///
65/// // K_3: 3 edges, all (2,2) → 3·2 = 6
66/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
67/// assert_eq!(edge_degree_max_sum(&g).unwrap(), 6);
68/// ```
69pub fn edge_degree_max_sum(graph: &Graph) -> IgraphResult<u64> {
70    let mut result = 0_u64;
71
72    for (u, v) in graph.edges() {
73        if u == v {
74            continue;
75        }
76        let du = graph.degree(u)?;
77        let dv = graph.degree(v)?;
78        result += du.max(dv) as u64;
79    }
80
81    Ok(result)
82}
83
84/// Compute the edge degree log-product sum.
85///
86/// `EDlp(G) = Σ_{(u,v)∈E} ln(d(u) · d(v))`
87///
88/// Equivalent to `Σ ln(d(u)) + ln(d(v))`. Self-loops and edges
89/// with a degree-0 endpoint are skipped. Returns 0.0 for edgeless
90/// graphs.
91///
92/// # Examples
93///
94/// ```
95/// use rust_igraph::{Graph, edge_degree_log_product};
96///
97/// // K_3: 3 edges, all (2,2) → 3·ln(4)
98/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
99/// assert!((edge_degree_log_product(&g).unwrap() - 3.0 * 4.0_f64.ln()).abs() < 1e-10);
100/// ```
101pub fn edge_degree_log_product(graph: &Graph) -> IgraphResult<f64> {
102    let mut result = 0.0_f64;
103
104    for (u, v) in graph.edges() {
105        if u == v {
106            continue;
107        }
108        let du = graph.degree(u)?;
109        let dv = graph.degree(v)?;
110        if du == 0 || dv == 0 {
111            continue;
112        }
113        result += ((du * dv) as f64).ln();
114    }
115
116    Ok(result)
117}
118
119/// Compute the edge degree mean sum.
120///
121/// `EDμ(G) = Σ_{(u,v)∈E} (d(u) + d(v)) / 2`
122///
123/// This equals `M₁/2` (half the first Zagreb index). Self-loops are
124/// skipped. For regular graphs with degree r: `EDμ = m·r`.
125///
126/// # Examples
127///
128/// ```
129/// use rust_igraph::{Graph, edge_degree_mean_sum};
130///
131/// // K_3: 3 edges, all (2,2) → 3·2 = 6
132/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
133/// assert!((edge_degree_mean_sum(&g).unwrap() - 6.0).abs() < 1e-10);
134/// ```
135pub fn edge_degree_mean_sum(graph: &Graph) -> IgraphResult<f64> {
136    let mut result = 0.0_f64;
137
138    for (u, v) in graph.edges() {
139        if u == v {
140            continue;
141        }
142        let du = graph.degree(u)? as f64;
143        let dv = graph.degree(v)? as f64;
144        result += f64::midpoint(du, dv);
145    }
146
147    Ok(result)
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    fn single_edge() -> Graph {
155        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
156    }
157
158    fn path3() -> Graph {
159        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
160    }
161
162    fn k3() -> Graph {
163        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
164    }
165
166    fn k4() -> Graph {
167        Graph::from_edges(
168            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
169            false,
170            Some(4),
171        )
172        .unwrap()
173    }
174
175    fn cycle4() -> Graph {
176        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
177    }
178
179    fn star5() -> Graph {
180        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
181    }
182
183    fn paw() -> Graph {
184        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
185    }
186
187    // --- edge_degree_min_sum ---
188
189    #[test]
190    fn edmin_empty() {
191        let g = Graph::with_vertices(0);
192        assert_eq!(edge_degree_min_sum(&g).unwrap(), 0);
193    }
194
195    #[test]
196    fn edmin_isolated() {
197        let g = Graph::with_vertices(5);
198        assert_eq!(edge_degree_min_sum(&g).unwrap(), 0);
199    }
200
201    #[test]
202    fn edmin_regular() {
203        // Regular degree r: each edge contributes r → m·r
204        assert_eq!(edge_degree_min_sum(&k3()).unwrap(), 6); // 3·2
205        assert_eq!(edge_degree_min_sum(&k4()).unwrap(), 18); // 6·3
206        assert_eq!(edge_degree_min_sum(&cycle4()).unwrap(), 8); // 4·2
207    }
208
209    #[test]
210    fn edmin_single_edge() {
211        assert_eq!(edge_degree_min_sum(&single_edge()).unwrap(), 1);
212    }
213
214    #[test]
215    fn edmin_star5() {
216        // 4 edges (4,1): min=1 each → 4
217        assert_eq!(edge_degree_min_sum(&star5()).unwrap(), 4);
218    }
219
220    #[test]
221    fn edmin_path3() {
222        // 2 edges (1,2): min=1 each → 2
223        assert_eq!(edge_degree_min_sum(&path3()).unwrap(), 2);
224    }
225
226    #[test]
227    fn edmin_paw() {
228        // (0,1)d=(2,2):2  (0,2)d=(2,3):2  (1,2)d=(2,3):2  (2,3)d=(3,1):1
229        assert_eq!(edge_degree_min_sum(&paw()).unwrap(), 7);
230    }
231
232    // --- edge_degree_max_sum ---
233
234    #[test]
235    fn edmax_empty() {
236        let g = Graph::with_vertices(0);
237        assert_eq!(edge_degree_max_sum(&g).unwrap(), 0);
238    }
239
240    #[test]
241    fn edmax_isolated() {
242        let g = Graph::with_vertices(5);
243        assert_eq!(edge_degree_max_sum(&g).unwrap(), 0);
244    }
245
246    #[test]
247    fn edmax_regular() {
248        assert_eq!(edge_degree_max_sum(&k3()).unwrap(), 6);
249        assert_eq!(edge_degree_max_sum(&k4()).unwrap(), 18);
250        assert_eq!(edge_degree_max_sum(&cycle4()).unwrap(), 8);
251    }
252
253    #[test]
254    fn edmax_single_edge() {
255        assert_eq!(edge_degree_max_sum(&single_edge()).unwrap(), 1);
256    }
257
258    #[test]
259    fn edmax_star5() {
260        // 4 edges (4,1): max=4 each → 16
261        assert_eq!(edge_degree_max_sum(&star5()).unwrap(), 16);
262    }
263
264    #[test]
265    fn edmax_path3() {
266        // 2 edges (1,2): max=2 each → 4
267        assert_eq!(edge_degree_max_sum(&path3()).unwrap(), 4);
268    }
269
270    #[test]
271    fn edmax_paw() {
272        // (0,1):2  (0,2):3  (1,2):3  (2,3):3 → 11
273        assert_eq!(edge_degree_max_sum(&paw()).unwrap(), 11);
274    }
275
276    // --- min + max = M1 ---
277
278    #[test]
279    fn min_plus_max_equals_m1() {
280        // min(d(u),d(v)) + max(d(u),d(v)) = d(u)+d(v)
281        // So EDmin + EDmax = Σ (d(u)+d(v)) = M₁ (first Zagreb)
282        for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
283            let min_val = edge_degree_min_sum(g).unwrap();
284            let max_val = edge_degree_max_sum(g).unwrap();
285            let m1: u64 = g
286                .edges()
287                .filter(|&(u, v)| u != v)
288                .map(|(u, v)| (g.degree(u).unwrap() + g.degree(v).unwrap()) as u64)
289                .sum();
290            assert_eq!(min_val + max_val, m1);
291        }
292    }
293
294    // --- edge_degree_log_product ---
295
296    #[test]
297    fn edlp_empty() {
298        let g = Graph::with_vertices(0);
299        assert!(edge_degree_log_product(&g).unwrap().abs() < 1e-10);
300    }
301
302    #[test]
303    fn edlp_isolated() {
304        let g = Graph::with_vertices(5);
305        assert!(edge_degree_log_product(&g).unwrap().abs() < 1e-10);
306    }
307
308    #[test]
309    fn edlp_k3() {
310        // 3 edges (2,2): 3·ln(4)
311        let expected = 3.0 * 4.0_f64.ln();
312        assert!((edge_degree_log_product(&k3()).unwrap() - expected).abs() < 1e-10);
313    }
314
315    #[test]
316    fn edlp_single_edge() {
317        // 1 edge (1,1): ln(1)=0
318        assert!(edge_degree_log_product(&single_edge()).unwrap().abs() < 1e-10);
319    }
320
321    #[test]
322    fn edlp_star5() {
323        // 4 edges (4,1): 4·ln(4)
324        let expected = 4.0 * 4.0_f64.ln();
325        assert!((edge_degree_log_product(&star5()).unwrap() - expected).abs() < 1e-10);
326    }
327
328    #[test]
329    fn edlp_path3() {
330        // 2 edges (1,2): 2·ln(2)
331        let expected = 2.0 * 2.0_f64.ln();
332        assert!((edge_degree_log_product(&path3()).unwrap() - expected).abs() < 1e-10);
333    }
334
335    #[test]
336    fn edlp_paw() {
337        // (0,1):ln(4)  (0,2):ln(6)  (1,2):ln(6)  (2,3):ln(3)
338        let expected = 4.0_f64.ln() + 2.0 * 6.0_f64.ln() + 3.0_f64.ln();
339        assert!((edge_degree_log_product(&paw()).unwrap() - expected).abs() < 1e-10);
340    }
341
342    // --- edge_degree_mean_sum ---
343
344    #[test]
345    fn edmean_empty() {
346        let g = Graph::with_vertices(0);
347        assert!(edge_degree_mean_sum(&g).unwrap().abs() < 1e-10);
348    }
349
350    #[test]
351    fn edmean_isolated() {
352        let g = Graph::with_vertices(5);
353        assert!(edge_degree_mean_sum(&g).unwrap().abs() < 1e-10);
354    }
355
356    #[test]
357    fn edmean_regular() {
358        // Regular degree r: each edge mean = r → m·r
359        assert!((edge_degree_mean_sum(&k3()).unwrap() - 6.0).abs() < 1e-10);
360        assert!((edge_degree_mean_sum(&k4()).unwrap() - 18.0).abs() < 1e-10);
361        assert!((edge_degree_mean_sum(&cycle4()).unwrap() - 8.0).abs() < 1e-10);
362    }
363
364    #[test]
365    fn edmean_single_edge() {
366        assert!((edge_degree_mean_sum(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
367    }
368
369    #[test]
370    fn edmean_star5() {
371        // 4 edges (4,1): mean=2.5 each → 10
372        assert!((edge_degree_mean_sum(&star5()).unwrap() - 10.0).abs() < 1e-10);
373    }
374
375    #[test]
376    fn edmean_paw() {
377        // (0,1):2  (0,2):2.5  (1,2):2.5  (2,3):2 → 9.0
378        assert!((edge_degree_mean_sum(&paw()).unwrap() - 9.0).abs() < 1e-10);
379    }
380
381    // --- cross-consistency ---
382
383    #[test]
384    fn edmin_le_edmean_le_edmax() {
385        for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
386            let min_val = edge_degree_min_sum(g).unwrap() as f64;
387            let mean_val = edge_degree_mean_sum(g).unwrap();
388            let max_val = edge_degree_max_sum(g).unwrap() as f64;
389            assert!(min_val <= mean_val + 1e-10);
390            assert!(mean_val <= max_val + 1e-10);
391        }
392    }
393
394    #[test]
395    fn edmean_is_half_m1() {
396        // EDmean = M₁/2
397        for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
398            let min_val = edge_degree_min_sum(g).unwrap();
399            let max_val = edge_degree_max_sum(g).unwrap();
400            let m1 = (min_val + max_val) as f64;
401            let mean_val = edge_degree_mean_sum(g).unwrap();
402            assert!((mean_val - m1 / 2.0).abs() < 1e-10);
403        }
404    }
405}