Skip to main content

rust_igraph/algorithms/properties/
multiplicative_connectivity.rs

1//! Multiplicative connectivity indices (ALGO-TR-074).
2//!
3//! Product-based (multiplicative) versions of classical additive
4//! bond-additive indices. Uses logarithmic accumulation to avoid
5//! floating-point overflow for large graphs:
6//!
7//! - **Multiplicative sum-connectivity** `Πsc(G) = Π_{(u,v)∈E} 1/√(du+dv)`
8//! - **Multiplicative Randić** `Πχ(G) = Π_{(u,v)∈E} 1/√(du·dv)`
9//! - **Multiplicative ABC** `Πabc(G) = Π_{(u,v)∈E} √((du+dv-2)/(du·dv))`
10//! - **Multiplicative GA** `Πga(G) = Π_{(u,v)∈E} 2√(du·dv)/(du+dv)`
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 multiplicative sum-connectivity index.
23///
24/// `Πsc(G) = Π_{(u,v)∈E} 1/√(du+dv)`
25///
26/// Edges with `du+dv == 0` are skipped. Self-loops are skipped.
27/// Returns 1.0 for edgeless graphs (empty product).
28///
29/// Uses log-sum to avoid overflow: `Πsc = exp(-½ Σ ln(du+dv))`.
30///
31/// # Examples
32///
33/// ```
34/// use rust_igraph::{Graph, multiplicative_sum_connectivity};
35///
36/// // K_3: 3 edges, d=(2,2), each 1/√4 = 1/2 → (1/2)³ = 1/8
37/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
38/// assert!((multiplicative_sum_connectivity(&g).unwrap() - 0.125).abs() < 1e-10);
39/// ```
40pub fn multiplicative_sum_connectivity(graph: &Graph) -> IgraphResult<f64> {
41    let mut log_sum = 0.0_f64;
42    let mut count = 0_usize;
43
44    for (u, v) in graph.edges() {
45        if u == v {
46            continue;
47        }
48        let du = graph.degree(u)? as f64;
49        let dv = graph.degree(v)? as f64;
50        let s = du + dv;
51        if s > 0.0 {
52            log_sum -= 0.5 * s.ln();
53            count += 1;
54        }
55    }
56
57    if count == 0 {
58        return Ok(1.0);
59    }
60
61    Ok(log_sum.exp())
62}
63
64/// Compute the multiplicative Randić index.
65///
66/// `Πχ(G) = Π_{(u,v)∈E} 1/√(du·dv)`
67///
68/// Edges with a degree-0 endpoint are skipped. Self-loops are skipped.
69/// Returns 1.0 for edgeless graphs (empty product).
70///
71/// # Examples
72///
73/// ```
74/// use rust_igraph::{Graph, multiplicative_randic};
75///
76/// // K_3: 3 edges, d=(2,2), each 1/2 → (1/2)³ = 1/8
77/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
78/// assert!((multiplicative_randic(&g).unwrap() - 0.125).abs() < 1e-10);
79/// ```
80pub fn multiplicative_randic(graph: &Graph) -> IgraphResult<f64> {
81    let mut log_sum = 0.0_f64;
82    let mut count = 0_usize;
83
84    for (u, v) in graph.edges() {
85        if u == v {
86            continue;
87        }
88        let du = graph.degree(u)? as f64;
89        let dv = graph.degree(v)? as f64;
90        let product = du * dv;
91        if product > 0.0 {
92            log_sum -= 0.5 * product.ln();
93            count += 1;
94        }
95    }
96
97    if count == 0 {
98        return Ok(1.0);
99    }
100
101    Ok(log_sum.exp())
102}
103
104/// Compute the multiplicative atom-bond connectivity index.
105///
106/// `Πabc(G) = Π_{(u,v)∈E} √((du+dv-2)/(du·dv))`
107///
108/// Edges where `du+dv-2 ≤ 0` or `du·dv == 0` are skipped.
109/// Self-loops are skipped. Returns 1.0 for edgeless graphs.
110///
111/// # Examples
112///
113/// ```
114/// use rust_igraph::{Graph, multiplicative_abc};
115///
116/// // K_3: 3 edges, d=(2,2), each √(2/4) = 1/√2 → (1/√2)³
117/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
118/// let expected = (1.0 / 2.0_f64.sqrt()).powi(3);
119/// assert!((multiplicative_abc(&g).unwrap() - expected).abs() < 1e-10);
120/// ```
121pub fn multiplicative_abc(graph: &Graph) -> IgraphResult<f64> {
122    let mut log_sum = 0.0_f64;
123    let mut count = 0_usize;
124
125    for (u, v) in graph.edges() {
126        if u == v {
127            continue;
128        }
129        let du = graph.degree(u)? as f64;
130        let dv = graph.degree(v)? as f64;
131        let product = du * dv;
132        let numer = du + dv - 2.0;
133        if product > 0.0 && numer > 0.0 {
134            log_sum += 0.5 * (numer / product).ln();
135            count += 1;
136        }
137    }
138
139    if count == 0 {
140        return Ok(1.0);
141    }
142
143    Ok(log_sum.exp())
144}
145
146/// Compute the multiplicative geometric-arithmetic index.
147///
148/// `Πga(G) = Π_{(u,v)∈E} 2√(du·dv)/(du+dv)`
149///
150/// Edges with `du+dv == 0` are skipped. Self-loops are skipped.
151/// Returns 1.0 for edgeless graphs.
152///
153/// For regular graphs, each factor is 1, so `Πga = 1`.
154///
155/// # Examples
156///
157/// ```
158/// use rust_igraph::{Graph, multiplicative_ga};
159///
160/// // K_3: regular → each factor = 1 → product = 1
161/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
162/// assert!((multiplicative_ga(&g).unwrap() - 1.0).abs() < 1e-10);
163/// ```
164pub fn multiplicative_ga(graph: &Graph) -> IgraphResult<f64> {
165    let mut log_sum = 0.0_f64;
166    let mut count = 0_usize;
167
168    for (u, v) in graph.edges() {
169        if u == v {
170            continue;
171        }
172        let du = graph.degree(u)? as f64;
173        let dv = graph.degree(v)? as f64;
174        let s = du + dv;
175        if s > 0.0 {
176            let factor = 2.0 * (du * dv).sqrt() / s;
177            if factor > 0.0 {
178                log_sum += factor.ln();
179                count += 1;
180            }
181        }
182    }
183
184    if count == 0 {
185        return Ok(1.0);
186    }
187
188    Ok(log_sum.exp())
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    fn single_edge() -> Graph {
196        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
197    }
198
199    fn path3() -> Graph {
200        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
201    }
202
203    fn k3() -> Graph {
204        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
205    }
206
207    fn k4() -> Graph {
208        Graph::from_edges(
209            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
210            false,
211            Some(4),
212        )
213        .unwrap()
214    }
215
216    fn cycle4() -> Graph {
217        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
218    }
219
220    fn cycle5() -> Graph {
221        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
222    }
223
224    fn star5() -> Graph {
225        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
226    }
227
228    fn paw() -> Graph {
229        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
230    }
231
232    // --- multiplicative_sum_connectivity ---
233
234    #[test]
235    fn msc_empty() {
236        let g = Graph::with_vertices(0);
237        assert!((multiplicative_sum_connectivity(&g).unwrap() - 1.0).abs() < 1e-10);
238    }
239
240    #[test]
241    fn msc_isolated() {
242        let g = Graph::with_vertices(5);
243        assert!((multiplicative_sum_connectivity(&g).unwrap() - 1.0).abs() < 1e-10);
244    }
245
246    #[test]
247    fn msc_single_edge() {
248        // d=(1,1), 1/√2
249        let expected = 1.0 / 2.0_f64.sqrt();
250        assert!(
251            (multiplicative_sum_connectivity(&single_edge()).unwrap() - expected).abs() < 1e-10
252        );
253    }
254
255    #[test]
256    fn msc_k3() {
257        // 3 edges, d=(2,2), each 1/√4=1/2 → (1/2)³=1/8
258        assert!((multiplicative_sum_connectivity(&k3()).unwrap() - 0.125).abs() < 1e-10);
259    }
260
261    #[test]
262    fn msc_k4() {
263        // 6 edges, d=(3,3), each 1/√6 → (1/√6)^6 = 1/216
264        let expected = (1.0 / 6.0_f64.sqrt()).powi(6);
265        assert!((multiplicative_sum_connectivity(&k4()).unwrap() - expected).abs() < 1e-10);
266    }
267
268    #[test]
269    fn msc_path3() {
270        // 2 edges, d=(1,2): 1/√3 each → (1/√3)² = 1/3
271        let expected = 1.0 / 3.0;
272        assert!((multiplicative_sum_connectivity(&path3()).unwrap() - expected).abs() < 1e-10);
273    }
274
275    #[test]
276    fn msc_cycle4() {
277        // 4 edges, d=(2,2): 1/2 each → (1/2)⁴ = 1/16
278        let expected = 1.0 / 16.0;
279        assert!((multiplicative_sum_connectivity(&cycle4()).unwrap() - expected).abs() < 1e-10);
280    }
281
282    #[test]
283    fn msc_star5() {
284        // 4 edges, d=(4,1): 1/√5 each → (1/√5)⁴ = 1/25
285        let expected = 1.0 / 25.0;
286        assert!((multiplicative_sum_connectivity(&star5()).unwrap() - expected).abs() < 1e-10);
287    }
288
289    #[test]
290    fn msc_paw() {
291        // (0,1) d=(2,2): 1/√4=1/2
292        // (0,2) d=(2,3): 1/√5
293        // (1,2) d=(2,3): 1/√5
294        // (2,3) d=(3,1): 1/√4=1/2
295        let expected = 0.5 * (1.0 / 5.0_f64.sqrt()).powi(2) * 0.5;
296        assert!((multiplicative_sum_connectivity(&paw()).unwrap() - expected).abs() < 1e-10);
297    }
298
299    // --- multiplicative_randic ---
300
301    #[test]
302    fn mr_empty() {
303        let g = Graph::with_vertices(0);
304        assert!((multiplicative_randic(&g).unwrap() - 1.0).abs() < 1e-10);
305    }
306
307    #[test]
308    fn mr_isolated() {
309        let g = Graph::with_vertices(5);
310        assert!((multiplicative_randic(&g).unwrap() - 1.0).abs() < 1e-10);
311    }
312
313    #[test]
314    fn mr_single_edge() {
315        // d=(1,1), 1/1=1
316        assert!((multiplicative_randic(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
317    }
318
319    #[test]
320    fn mr_k3() {
321        // d=(2,2), 1/2 per edge → (1/2)³ = 1/8
322        assert!((multiplicative_randic(&k3()).unwrap() - 0.125).abs() < 1e-10);
323    }
324
325    #[test]
326    fn mr_k4() {
327        // d=(3,3), 1/3 per edge → (1/3)^6
328        let expected = (1.0_f64 / 3.0).powi(6);
329        assert!((multiplicative_randic(&k4()).unwrap() - expected).abs() < 1e-10);
330    }
331
332    #[test]
333    fn mr_path3() {
334        // d=(1,2): 1/√2 each → (1/√2)² = 1/2
335        assert!((multiplicative_randic(&path3()).unwrap() - 0.5).abs() < 1e-10);
336    }
337
338    #[test]
339    fn mr_cycle4() {
340        // d=(2,2), 1/2 → (1/2)⁴ = 1/16
341        let expected = 1.0 / 16.0;
342        assert!((multiplicative_randic(&cycle4()).unwrap() - expected).abs() < 1e-10);
343    }
344
345    #[test]
346    fn mr_star5() {
347        // d=(4,1): 1/2 each → (1/2)⁴ = 1/16
348        let expected = 1.0 / 16.0;
349        assert!((multiplicative_randic(&star5()).unwrap() - expected).abs() < 1e-10);
350    }
351
352    #[test]
353    fn mr_paw() {
354        // (0,1) d=(2,2): 1/2
355        // (0,2) d=(2,3): 1/√6
356        // (1,2) d=(2,3): 1/√6
357        // (2,3) d=(3,1): 1/√3
358        let expected = 0.5 * (1.0 / 6.0_f64.sqrt()).powi(2) * (1.0 / 3.0_f64.sqrt());
359        assert!((multiplicative_randic(&paw()).unwrap() - expected).abs() < 1e-10);
360    }
361
362    // --- multiplicative_abc ---
363
364    #[test]
365    fn mabc_empty() {
366        let g = Graph::with_vertices(0);
367        assert!((multiplicative_abc(&g).unwrap() - 1.0).abs() < 1e-10);
368    }
369
370    #[test]
371    fn mabc_isolated() {
372        let g = Graph::with_vertices(5);
373        assert!((multiplicative_abc(&g).unwrap() - 1.0).abs() < 1e-10);
374    }
375
376    #[test]
377    fn mabc_single_edge() {
378        // d=(1,1), numer=0 → skipped → empty product = 1
379        assert!((multiplicative_abc(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
380    }
381
382    #[test]
383    fn mabc_k3() {
384        // d=(2,2), √(2/4)=1/√2 → (1/√2)³
385        let expected = (1.0 / 2.0_f64.sqrt()).powi(3);
386        assert!((multiplicative_abc(&k3()).unwrap() - expected).abs() < 1e-10);
387    }
388
389    #[test]
390    fn mabc_k4() {
391        // d=(3,3), √(4/9)=2/3 → (2/3)^6
392        let expected = (2.0_f64 / 3.0).powi(6);
393        assert!((multiplicative_abc(&k4()).unwrap() - expected).abs() < 1e-10);
394    }
395
396    #[test]
397    fn mabc_path3() {
398        // d=(1,2): √(1/2)=1/√2 → (1/√2)²=1/2
399        assert!((multiplicative_abc(&path3()).unwrap() - 0.5).abs() < 1e-10);
400    }
401
402    #[test]
403    fn mabc_cycle4() {
404        // d=(2,2): 1/√2 → (1/√2)⁴ = 1/4
405        assert!((multiplicative_abc(&cycle4()).unwrap() - 0.25).abs() < 1e-10);
406    }
407
408    #[test]
409    fn mabc_star5() {
410        // d=(4,1): √(3/4)=√3/2 → (√3/2)⁴ = 9/16
411        let expected = (3.0_f64.sqrt() / 2.0).powi(4);
412        assert!((multiplicative_abc(&star5()).unwrap() - expected).abs() < 1e-10);
413    }
414
415    #[test]
416    fn mabc_paw() {
417        // (0,1) d=(2,2): 1/√2
418        // (0,2) d=(2,3): √(3/6)=1/√2
419        // (1,2) d=(2,3): 1/√2
420        // (2,3) d=(3,1): √(2/3)
421        let expected = (1.0 / 2.0_f64.sqrt()).powi(3) * (2.0_f64 / 3.0).sqrt();
422        assert!((multiplicative_abc(&paw()).unwrap() - expected).abs() < 1e-10);
423    }
424
425    // --- multiplicative_ga ---
426
427    #[test]
428    fn mga_empty() {
429        let g = Graph::with_vertices(0);
430        assert!((multiplicative_ga(&g).unwrap() - 1.0).abs() < 1e-10);
431    }
432
433    #[test]
434    fn mga_isolated() {
435        let g = Graph::with_vertices(5);
436        assert!((multiplicative_ga(&g).unwrap() - 1.0).abs() < 1e-10);
437    }
438
439    #[test]
440    fn mga_single_edge() {
441        // d=(1,1), 2·1/2=1 → product=1
442        assert!((multiplicative_ga(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
443    }
444
445    #[test]
446    fn mga_k3() {
447        // regular → all factors = 1 → product = 1
448        assert!((multiplicative_ga(&k3()).unwrap() - 1.0).abs() < 1e-10);
449    }
450
451    #[test]
452    fn mga_k4() {
453        assert!((multiplicative_ga(&k4()).unwrap() - 1.0).abs() < 1e-10);
454    }
455
456    #[test]
457    fn mga_cycle4() {
458        assert!((multiplicative_ga(&cycle4()).unwrap() - 1.0).abs() < 1e-10);
459    }
460
461    #[test]
462    fn mga_cycle5() {
463        assert!((multiplicative_ga(&cycle5()).unwrap() - 1.0).abs() < 1e-10);
464    }
465
466    #[test]
467    fn mga_path3() {
468        // d=(1,2): 2√2/3 → (2√2/3)²
469        let factor = 2.0 * 2.0_f64.sqrt() / 3.0;
470        let expected = factor * factor;
471        assert!((multiplicative_ga(&path3()).unwrap() - expected).abs() < 1e-10);
472    }
473
474    #[test]
475    fn mga_star5() {
476        // d=(4,1): 2·2/5=4/5 → (4/5)⁴
477        let expected = (4.0_f64 / 5.0).powi(4);
478        assert!((multiplicative_ga(&star5()).unwrap() - expected).abs() < 1e-10);
479    }
480
481    #[test]
482    fn mga_paw() {
483        // (0,1) d=(2,2): 1
484        // (0,2) d=(2,3): 2√6/5
485        // (1,2) d=(2,3): 2√6/5
486        // (2,3) d=(3,1): 2√3/4=√3/2
487        let expected = (2.0 * 6.0_f64.sqrt() / 5.0).powi(2) * (3.0_f64.sqrt() / 2.0);
488        assert!((multiplicative_ga(&paw()).unwrap() - expected).abs() < 1e-10);
489    }
490
491    // --- cross-consistency ---
492
493    #[test]
494    fn regular_ga_is_one() {
495        for g in &[k3(), k4(), cycle4(), cycle5()] {
496            assert!((multiplicative_ga(g).unwrap() - 1.0).abs() < 1e-10);
497        }
498    }
499
500    #[test]
501    fn ga_le_one_for_simple() {
502        // AM-GM: 2√(du·dv)/(du+dv) ≤ 1, so product ≤ 1
503        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
504            assert!(multiplicative_ga(g).unwrap() <= 1.0 + 1e-10);
505        }
506    }
507
508    #[test]
509    fn all_positive() {
510        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
511            assert!(multiplicative_sum_connectivity(g).unwrap() > 0.0);
512            assert!(multiplicative_randic(g).unwrap() > 0.0);
513            assert!(multiplicative_abc(g).unwrap() > 0.0);
514            assert!(multiplicative_ga(g).unwrap() > 0.0);
515        }
516    }
517}