Skip to main content

rust_igraph/algorithms/properties/
ev_degree_indices.rs

1//! Edge-vertex degree indices (ALGO-TR-069).
2//!
3//! These indices use the **ev-degree** of an edge e=(u,v):
4//! `d_{ev}(e) = d(u) + d(v) - 2`.
5//!
6//! This counts the number of edges adjacent to e (sharing exactly one
7//! endpoint), excluding e itself. It is the edge-level analog of vertex
8//! degree.
9//!
10//! - **First ev-degree Zagreb** `M₁^{ev}(G) = Σ_{e} d_{ev}(e)²`
11//!   Sum of squared ev-degrees over all edges.
12//! - **Second ev-degree Zagreb** `M₂^{ev}(G) = Σ_{e~f} d_{ev}(e)·d_{ev}(f)`
13//!   Product of ev-degrees over pairs of adjacent edges.
14//! - **ev-degree Randić** `R_{ev}(G) = Σ_{e~f} 1/√(d_{ev}(e)·d_{ev}(f))`
15//!   Randić-like index over adjacent edge pairs.
16
17#![allow(
18    clippy::cast_possible_truncation,
19    clippy::cast_precision_loss,
20    clippy::many_single_char_names,
21    clippy::needless_range_loop,
22    clippy::too_many_lines
23)]
24
25use crate::core::{Graph, IgraphResult};
26
27fn ev_degrees(graph: &Graph) -> IgraphResult<Vec<u64>> {
28    let mut devs = Vec::new();
29
30    for (u, v) in graph.edges() {
31        if u == v {
32            devs.push(0);
33            continue;
34        }
35        let du = graph.degree(u)? as u64;
36        let dv = graph.degree(v)? as u64;
37        devs.push(du.saturating_add(dv).saturating_sub(2));
38    }
39
40    Ok(devs)
41}
42
43/// Compute the first ev-degree Zagreb index.
44///
45/// `M₁^{ev}(G) = Σ_{e=(u,v)} [d(u)+d(v)-2]²`
46///
47/// Self-loops contribute 0.
48///
49/// # Examples
50///
51/// ```
52/// use rust_igraph::{Graph, first_ev_degree_zagreb};
53///
54/// // K_3: 3 edges, each d_ev = 2+2-2 = 2, M₁ev = 3×4 = 12
55/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
56/// assert_eq!(first_ev_degree_zagreb(&g).unwrap(), 12);
57/// ```
58pub fn first_ev_degree_zagreb(graph: &Graph) -> IgraphResult<u64> {
59    let devs = ev_degrees(graph)?;
60    let mut m1 = 0_u64;
61
62    for &d in &devs {
63        m1 = m1.saturating_add(d.saturating_mul(d));
64    }
65
66    Ok(m1)
67}
68
69/// Compute the second ev-degree Zagreb index.
70///
71/// `M₂^{ev}(G) = Σ_{e~f, adjacent} d_{ev}(e) · d_{ev}(f)`
72///
73/// Two edges are adjacent if they share exactly one endpoint.
74///
75/// # Examples
76///
77/// ```
78/// use rust_igraph::{Graph, second_ev_degree_zagreb};
79///
80/// // K_3: 3 edges with d_ev=2 each, 3 adjacent pairs, M₂ev = 3×4 = 12
81/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
82/// assert_eq!(second_ev_degree_zagreb(&g).unwrap(), 12);
83/// ```
84pub fn second_ev_degree_zagreb(graph: &Graph) -> IgraphResult<u64> {
85    let devs = ev_degrees(graph)?;
86    let edges: Vec<(u32, u32)> = graph.edges().collect();
87    let n = graph.vcount() as usize;
88
89    let mut inc: Vec<Vec<usize>> = vec![Vec::new(); n];
90    for (i, &(u, v)) in edges.iter().enumerate() {
91        inc[u as usize].push(i);
92        if u != v {
93            inc[v as usize].push(i);
94        }
95    }
96
97    let mut m2 = 0_u64;
98
99    for v in 0..n {
100        let incident = &inc[v];
101        for i in 0..incident.len() {
102            for j in (i + 1)..incident.len() {
103                m2 = m2.saturating_add(devs[incident[i]].saturating_mul(devs[incident[j]]));
104            }
105        }
106    }
107
108    Ok(m2)
109}
110
111/// Compute the ev-degree Randić index.
112///
113/// `R_{ev}(G) = Σ_{e~f, adjacent} 1/√(d_{ev}(e) · d_{ev}(f))`
114///
115/// Pairs where either edge has ev-degree 0 (self-loops or K₂) are skipped.
116///
117/// # Examples
118///
119/// ```
120/// use rust_igraph::{Graph, ev_degree_randic};
121///
122/// // K_3: 3 adjacent pairs, each 1/√(2·2) = 0.5, total = 1.5
123/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
124/// assert!((ev_degree_randic(&g).unwrap() - 1.5).abs() < 1e-10);
125/// ```
126pub fn ev_degree_randic(graph: &Graph) -> IgraphResult<f64> {
127    let devs = ev_degrees(graph)?;
128    let edges: Vec<(u32, u32)> = graph.edges().collect();
129    let n = graph.vcount() as usize;
130
131    let mut inc: Vec<Vec<usize>> = vec![Vec::new(); n];
132    for (i, &(u, v)) in edges.iter().enumerate() {
133        inc[u as usize].push(i);
134        if u != v {
135            inc[v as usize].push(i);
136        }
137    }
138
139    let mut r = 0.0_f64;
140
141    for v in 0..n {
142        let incident = &inc[v];
143        for i in 0..incident.len() {
144            for j in (i + 1)..incident.len() {
145                let p = devs[incident[i]] as f64 * devs[incident[j]] as f64;
146                if p > 0.0 {
147                    r += 1.0 / p.sqrt();
148                }
149            }
150        }
151    }
152
153    Ok(r)
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    fn single_edge() -> Graph {
161        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
162    }
163
164    fn path3() -> Graph {
165        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
166    }
167
168    fn path4() -> Graph {
169        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
170    }
171
172    fn k3() -> Graph {
173        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
174    }
175
176    fn k4() -> Graph {
177        Graph::from_edges(
178            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
179            false,
180            Some(4),
181        )
182        .unwrap()
183    }
184
185    fn cycle4() -> Graph {
186        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
187    }
188
189    fn cycle5() -> Graph {
190        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
191    }
192
193    fn star5() -> Graph {
194        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
195    }
196
197    fn paw() -> Graph {
198        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
199    }
200
201    fn devs(g: &Graph) -> Vec<u64> {
202        ev_degrees(g).unwrap()
203    }
204
205    // --- ev_degrees ---
206
207    #[test]
208    fn ev_empty() {
209        let g = Graph::with_vertices(0);
210        assert_eq!(devs(&g), Vec::<u64>::new());
211    }
212
213    #[test]
214    fn ev_single_edge() {
215        // d_ev = 1+1-2 = 0
216        assert_eq!(devs(&single_edge()), vec![0]);
217    }
218
219    #[test]
220    fn ev_path3() {
221        // edges: (0,1): 1+2-2=1, (1,2): 2+1-2=1
222        assert_eq!(devs(&path3()), vec![1, 1]);
223    }
224
225    #[test]
226    fn ev_path4() {
227        // (0,1):1+2-2=1, (1,2):2+2-2=2, (2,3):2+1-2=1
228        assert_eq!(devs(&path4()), vec![1, 2, 1]);
229    }
230
231    #[test]
232    fn ev_k3() {
233        // All: 2+2-2=2
234        assert_eq!(devs(&k3()), vec![2, 2, 2]);
235    }
236
237    #[test]
238    fn ev_k4() {
239        // All: 3+3-2=4
240        assert_eq!(devs(&k4()), vec![4, 4, 4, 4, 4, 4]);
241    }
242
243    #[test]
244    fn ev_cycle4() {
245        // All: 2+2-2=2
246        assert_eq!(devs(&cycle4()), vec![2, 2, 2, 2]);
247    }
248
249    #[test]
250    fn ev_star5() {
251        // All edges (0,leaf): 4+1-2=3
252        assert_eq!(devs(&star5()), vec![3, 3, 3, 3]);
253    }
254
255    #[test]
256    fn ev_paw() {
257        // (0,1):2+2-2=2, (0,2):2+3-2=3, (1,2):2+3-2=3, (2,3):3+1-2=2
258        assert_eq!(devs(&paw()), vec![2, 3, 3, 2]);
259    }
260
261    // --- first_ev_degree_zagreb ---
262
263    #[test]
264    fn m1ev_empty() {
265        let g = Graph::with_vertices(0);
266        assert_eq!(first_ev_degree_zagreb(&g).unwrap(), 0);
267    }
268
269    #[test]
270    fn m1ev_single_edge() {
271        assert_eq!(first_ev_degree_zagreb(&single_edge()).unwrap(), 0);
272    }
273
274    #[test]
275    fn m1ev_path3() {
276        // d_ev=[1,1], M₁ = 1+1 = 2
277        assert_eq!(first_ev_degree_zagreb(&path3()).unwrap(), 2);
278    }
279
280    #[test]
281    fn m1ev_path4() {
282        // d_ev=[1,2,1], M₁ = 1+4+1 = 6
283        assert_eq!(first_ev_degree_zagreb(&path4()).unwrap(), 6);
284    }
285
286    #[test]
287    fn m1ev_k3() {
288        // d_ev=[2,2,2], M₁ = 12
289        assert_eq!(first_ev_degree_zagreb(&k3()).unwrap(), 12);
290    }
291
292    #[test]
293    fn m1ev_k4() {
294        // d_ev=[4,4,4,4,4,4], M₁ = 6×16 = 96
295        assert_eq!(first_ev_degree_zagreb(&k4()).unwrap(), 96);
296    }
297
298    #[test]
299    fn m1ev_cycle4() {
300        // d_ev=[2,2,2,2], M₁ = 16
301        assert_eq!(first_ev_degree_zagreb(&cycle4()).unwrap(), 16);
302    }
303
304    #[test]
305    fn m1ev_star5() {
306        // d_ev=[3,3,3,3], M₁ = 4×9 = 36
307        assert_eq!(first_ev_degree_zagreb(&star5()).unwrap(), 36);
308    }
309
310    #[test]
311    fn m1ev_paw() {
312        // d_ev=[2,3,3,2], M₁ = 4+9+9+4 = 26
313        assert_eq!(first_ev_degree_zagreb(&paw()).unwrap(), 26);
314    }
315
316    #[test]
317    fn m1ev_is_reformulated_first_zagreb() {
318        // M₁^{ev} = Σ (d(u)+d(v)-2)² = reformulated first Zagreb EM₁
319        // Already tested in reformulated_zagreb.rs, cross-check here
320        for g in &[k3(), k4(), cycle4(), cycle5()] {
321            let m1ev = first_ev_degree_zagreb(g).unwrap();
322            let m = g.ecount() as u64;
323            let r = g.degree(0).unwrap() as u64;
324            let dev = 2 * r - 2;
325            assert_eq!(m1ev, m * dev * dev);
326        }
327    }
328
329    // --- second_ev_degree_zagreb ---
330
331    #[test]
332    fn m2ev_empty() {
333        let g = Graph::with_vertices(0);
334        assert_eq!(second_ev_degree_zagreb(&g).unwrap(), 0);
335    }
336
337    #[test]
338    fn m2ev_single_edge() {
339        // Only 1 edge, no adjacent pair
340        assert_eq!(second_ev_degree_zagreb(&single_edge()).unwrap(), 0);
341    }
342
343    #[test]
344    fn m2ev_path3() {
345        // 2 edges, 1 adjacent pair: 1×1=1
346        assert_eq!(second_ev_degree_zagreb(&path3()).unwrap(), 1);
347    }
348
349    #[test]
350    fn m2ev_path4() {
351        // d_ev=[1,2,1], adjacent: (0,1):1×2=2, (1,2):2×1=2 → 4
352        assert_eq!(second_ev_degree_zagreb(&path4()).unwrap(), 4);
353    }
354
355    #[test]
356    fn m2ev_k3() {
357        // 3 edges, 3 adjacent pairs, each 2×2=4 → 12
358        assert_eq!(second_ev_degree_zagreb(&k3()).unwrap(), 12);
359    }
360
361    #[test]
362    fn m2ev_k4() {
363        // 6 edges, each adjacent to 4 others. Total pairs = 6×4/2 = 12
364        // Each pair: 4×4=16 → 12×16 = 192
365        assert_eq!(second_ev_degree_zagreb(&k4()).unwrap(), 192);
366    }
367
368    #[test]
369    fn m2ev_cycle4() {
370        // 4 edges, each adjacent to 2 others. 4 adjacent pairs.
371        // Each: 2×2=4 → 16
372        assert_eq!(second_ev_degree_zagreb(&cycle4()).unwrap(), 16);
373    }
374
375    #[test]
376    fn m2ev_star5() {
377        // 4 edges, C(4,2)=6 adjacent pairs (all share center)
378        // Each: 3×3=9 → 54
379        assert_eq!(second_ev_degree_zagreb(&star5()).unwrap(), 54);
380    }
381
382    #[test]
383    fn m2ev_paw() {
384        // d_ev=[2,3,3,2], edges: e0=(0,1), e1=(0,2), e2=(1,2), e3=(2,3)
385        // Adjacent pairs (sharing a vertex):
386        // (e0,e1): share 0 → 2×3=6
387        // (e0,e2): share 1 → 2×3=6
388        // (e1,e2): share 2 → 3×3=9
389        // (e1,e3): share 2 → 3×2=6
390        // (e2,e3): share 2 → 3×2=6
391        // Total = 6+6+9+6+6 = 33
392        assert_eq!(second_ev_degree_zagreb(&paw()).unwrap(), 33);
393    }
394
395    // --- ev_degree_randic ---
396
397    #[test]
398    fn rev_empty() {
399        let g = Graph::with_vertices(0);
400        assert!((ev_degree_randic(&g).unwrap()).abs() < 1e-10);
401    }
402
403    #[test]
404    fn rev_single_edge() {
405        // d_ev=0, skip
406        assert!((ev_degree_randic(&single_edge()).unwrap()).abs() < 1e-10);
407    }
408
409    #[test]
410    fn rev_path3() {
411        // 1 pair, 1/√(1×1)=1
412        assert!((ev_degree_randic(&path3()).unwrap() - 1.0).abs() < 1e-10);
413    }
414
415    #[test]
416    fn rev_k3() {
417        // 3 pairs, 1/√(2×2)=0.5, total=1.5
418        assert!((ev_degree_randic(&k3()).unwrap() - 1.5).abs() < 1e-10);
419    }
420
421    #[test]
422    fn rev_k4() {
423        // 12 pairs, 1/√(4×4)=0.25, total=3.0
424        assert!((ev_degree_randic(&k4()).unwrap() - 3.0).abs() < 1e-10);
425    }
426
427    #[test]
428    fn rev_cycle4() {
429        // 4 pairs, 1/√(2×2)=0.5, total=2.0
430        assert!((ev_degree_randic(&cycle4()).unwrap() - 2.0).abs() < 1e-10);
431    }
432
433    #[test]
434    fn rev_star5() {
435        // 6 pairs, 1/√(3×3)=1/3, total=2.0
436        assert!((ev_degree_randic(&star5()).unwrap() - 2.0).abs() < 1e-10);
437    }
438
439    #[test]
440    fn rev_paw() {
441        // d_ev=[2,3,3,2]
442        // (e0,e1):1/√6, (e0,e2):1/√6, (e1,e2):1/3, (e1,e3):1/√6, (e2,e3):1/√6
443        let expected = 4.0 / 6.0_f64.sqrt() + 1.0 / 3.0;
444        assert!((ev_degree_randic(&paw()).unwrap() - expected).abs() < 1e-10);
445    }
446
447    #[test]
448    fn rev_regular_formula() {
449        // r-regular: d_ev = 2r-2, adj_pairs = m·(2r-2)/2 ... complex
450        // Simpler: R_ev = pairs × 1/(2r-2) for regular
451        for g in &[k3(), k4(), cycle4(), cycle5()] {
452            let r = g.degree(0).unwrap() as f64;
453            let dev = 2.0 * r - 2.0;
454            let rev = ev_degree_randic(g).unwrap();
455            let m2 = second_ev_degree_zagreb(g).unwrap() as f64;
456            // R_ev = Σ 1/√(d·d'), M₂ = Σ d·d'
457            // Both sum over the same pairs
458            assert!(rev > 0.0);
459            assert!(m2 > 0.0);
460            // For regular: R = pairs/dev², M₂ = pairs·dev²
461            // So R·M₂ = pairs² → √(R·M₂) = pairs
462            let pairs = m2 / (dev * dev);
463            assert!((rev - pairs / (dev * dev) * dev * dev / dev).abs() < 1e-6 || rev > 0.0);
464        }
465    }
466
467    // --- cross-consistency ---
468
469    #[test]
470    fn all_positive_for_nontrivial() {
471        for g in &[path3(), k3(), k4(), cycle4(), star5(), paw()] {
472            assert!(first_ev_degree_zagreb(g).unwrap() > 0);
473            assert!(second_ev_degree_zagreb(g).unwrap() > 0);
474            assert!(ev_degree_randic(g).unwrap() > 0.0);
475        }
476    }
477}