Skip to main content

rust_igraph/algorithms/properties/
reduced_indices.rs

1//! Reduced (decremented) degree-based indices (ALGO-TR-075).
2//!
3//! Indices using `(d(v)-1)` ("reduced degree") instead of `d(v)`:
4//!
5//! - **Reduced reciprocal Randić** `RRR(G) = Σ_{(u,v)∈E} 1/√((du-1)(dv-1))`
6//! - **Reduced sum-connectivity** `χ_red(G) = Σ_{(u,v)∈E} 1/√((du-1)+(dv-1))`
7//! - **Reduced first Zagreb** `M₁_red(G) = Σ_v (d(v)-1)²`
8//! - **Reduced second Zagreb** already exists as `reduced_second_zagreb`
9//!   in `forgotten_zagreb.rs`; this module adds the remaining family.
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 reduced reciprocal Randić index.
22///
23/// `RRR(G) = Σ_{(u,v)∈E} 1/√((du-1)·(dv-1))`
24///
25/// Edges where either endpoint has degree ≤ 1 are skipped (the
26/// reduced degree would be 0, making the denominator zero).
27/// Self-loops are skipped.
28///
29/// # Examples
30///
31/// ```
32/// use rust_igraph::{Graph, reduced_reciprocal_randic};
33///
34/// // K_3: 3 edges, d=(2,2), each 1/√(1·1)=1 → 3
35/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
36/// assert!((reduced_reciprocal_randic(&g).unwrap() - 3.0).abs() < 1e-10);
37/// ```
38pub fn reduced_reciprocal_randic(graph: &Graph) -> IgraphResult<f64> {
39    let mut result = 0.0_f64;
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        if du >= 2 && dv >= 2 {
48            let product = ((du - 1) * (dv - 1)) as f64;
49            result += 1.0 / product.sqrt();
50        }
51    }
52
53    Ok(result)
54}
55
56/// Compute the reduced sum-connectivity index.
57///
58/// `χ_red(G) = Σ_{(u,v)∈E} 1/√((du-1)+(dv-1))`
59///
60/// Edges where `du + dv - 2 == 0` (both endpoints degree 1) are
61/// skipped. Self-loops are skipped.
62///
63/// # Examples
64///
65/// ```
66/// use rust_igraph::{Graph, reduced_sum_connectivity};
67///
68/// // K_3: 3 edges, d=(2,2), each 1/√(1+1)=1/√2 → 3/√2
69/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
70/// let expected = 3.0 / 2.0_f64.sqrt();
71/// assert!((reduced_sum_connectivity(&g).unwrap() - expected).abs() < 1e-10);
72/// ```
73pub fn reduced_sum_connectivity(graph: &Graph) -> IgraphResult<f64> {
74    let mut result = 0.0_f64;
75
76    for (u, v) in graph.edges() {
77        if u == v {
78            continue;
79        }
80        let du = graph.degree(u)?;
81        let dv = graph.degree(v)?;
82        let s = du + dv;
83        if s > 2 {
84            result += 1.0 / ((s - 2) as f64).sqrt();
85        }
86    }
87
88    Ok(result)
89}
90
91/// Compute the reduced first Zagreb index.
92///
93/// `M₁_red(G) = Σ_v (d(v)-1)²`
94///
95/// For isolated vertices (degree 0), the term is `(-1)² = 1`, but
96/// conventionally these are treated as having reduced degree 0, so
97/// we skip vertices with degree 0.
98///
99/// # Examples
100///
101/// ```
102/// use rust_igraph::{Graph, reduced_first_zagreb};
103///
104/// // K_3: d=(2,2,2), each (2-1)²=1 → 3
105/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
106/// assert_eq!(reduced_first_zagreb(&g).unwrap(), 3);
107///
108/// // Path 0-1-2: d=(1,2,1), (0)²+(1)²+(0)²=1
109/// let p = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
110/// assert_eq!(reduced_first_zagreb(&p).unwrap(), 1);
111/// ```
112pub fn reduced_first_zagreb(graph: &Graph) -> IgraphResult<u64> {
113    let n = graph.vcount() as usize;
114    let mut result = 0_u64;
115
116    for v in 0..n {
117        let d = graph.degree(v as u32)?;
118        if d >= 1 {
119            let rd = (d - 1) as u64;
120            result = result.saturating_add(rd.saturating_mul(rd));
121        }
122    }
123
124    Ok(result)
125}
126
127/// Compute the reduced forgotten index.
128///
129/// `F_red(G) = Σ_v (d(v)-1)³`
130///
131/// Vertices with degree 0 are skipped.
132///
133/// # Examples
134///
135/// ```
136/// use rust_igraph::{Graph, reduced_forgotten_index};
137///
138/// // K_4: d=(3,3,3,3), each (3-1)³=8 → 32
139/// let g = Graph::from_edges(
140///     &[(0,1),(0,2),(0,3),(1,2),(1,3),(2,3)], false, Some(4)
141/// ).unwrap();
142/// assert_eq!(reduced_forgotten_index(&g).unwrap(), 32);
143/// ```
144pub fn reduced_forgotten_index(graph: &Graph) -> IgraphResult<u64> {
145    let n = graph.vcount() as usize;
146    let mut result = 0_u64;
147
148    for v in 0..n {
149        let d = graph.degree(v as u32)?;
150        if d >= 1 {
151            let rd = (d - 1) as u64;
152            result = result.saturating_add(rd.saturating_mul(rd).saturating_mul(rd));
153        }
154    }
155
156    Ok(result)
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    fn single_edge() -> Graph {
164        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
165    }
166
167    fn path3() -> Graph {
168        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
169    }
170
171    fn path4() -> Graph {
172        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
173    }
174
175    fn k3() -> Graph {
176        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
177    }
178
179    fn k4() -> Graph {
180        Graph::from_edges(
181            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
182            false,
183            Some(4),
184        )
185        .unwrap()
186    }
187
188    fn cycle4() -> Graph {
189        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
190    }
191
192    fn cycle5() -> Graph {
193        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
194    }
195
196    fn star5() -> Graph {
197        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
198    }
199
200    fn paw() -> Graph {
201        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
202    }
203
204    // --- reduced_reciprocal_randic ---
205
206    #[test]
207    fn rrr_empty() {
208        let g = Graph::with_vertices(0);
209        assert!(reduced_reciprocal_randic(&g).unwrap().abs() < 1e-10);
210    }
211
212    #[test]
213    fn rrr_isolated() {
214        let g = Graph::with_vertices(5);
215        assert!(reduced_reciprocal_randic(&g).unwrap().abs() < 1e-10);
216    }
217
218    #[test]
219    fn rrr_single_edge() {
220        // d=(1,1), both degree 1 → skipped → 0
221        assert!(reduced_reciprocal_randic(&single_edge()).unwrap().abs() < 1e-10);
222    }
223
224    #[test]
225    fn rrr_path3() {
226        // 2 edges d=(1,2): degree 1 endpoint → skipped → 0
227        assert!(reduced_reciprocal_randic(&path3()).unwrap().abs() < 1e-10);
228    }
229
230    #[test]
231    fn rrr_k3() {
232        // d=(2,2), 1/√(1·1)=1 per edge → 3
233        assert!((reduced_reciprocal_randic(&k3()).unwrap() - 3.0).abs() < 1e-10);
234    }
235
236    #[test]
237    fn rrr_k4() {
238        // d=(3,3), 1/√(2·2)=1/2 per edge, 6 edges → 3
239        assert!((reduced_reciprocal_randic(&k4()).unwrap() - 3.0).abs() < 1e-10);
240    }
241
242    #[test]
243    fn rrr_cycle4() {
244        // d=(2,2), 1/1=1 per edge, 4 edges → 4
245        assert!((reduced_reciprocal_randic(&cycle4()).unwrap() - 4.0).abs() < 1e-10);
246    }
247
248    #[test]
249    fn rrr_cycle5() {
250        // d=(2,2), 1/1 per edge, 5 edges → 5
251        assert!((reduced_reciprocal_randic(&cycle5()).unwrap() - 5.0).abs() < 1e-10);
252    }
253
254    #[test]
255    fn rrr_star5() {
256        // d=(4,1): leaf deg=1 → all skipped → 0
257        assert!(reduced_reciprocal_randic(&star5()).unwrap().abs() < 1e-10);
258    }
259
260    #[test]
261    fn rrr_paw() {
262        // (0,1) d=(2,2): 1/1=1
263        // (0,2) d=(2,3): 1/√(1·2)=1/√2
264        // (1,2) d=(2,3): 1/√2
265        // (2,3) d=(3,1): leaf → skipped
266        let expected = 1.0 + 2.0 / 2.0_f64.sqrt();
267        assert!((reduced_reciprocal_randic(&paw()).unwrap() - expected).abs() < 1e-10);
268    }
269
270    // --- reduced_sum_connectivity ---
271
272    #[test]
273    fn rsc_empty() {
274        let g = Graph::with_vertices(0);
275        assert!(reduced_sum_connectivity(&g).unwrap().abs() < 1e-10);
276    }
277
278    #[test]
279    fn rsc_isolated() {
280        let g = Graph::with_vertices(5);
281        assert!(reduced_sum_connectivity(&g).unwrap().abs() < 1e-10);
282    }
283
284    #[test]
285    fn rsc_single_edge() {
286        // d=(1,1), du+dv-2=0 → skipped → 0
287        assert!(reduced_sum_connectivity(&single_edge()).unwrap().abs() < 1e-10);
288    }
289
290    #[test]
291    fn rsc_path3() {
292        // d=(1,2): 1+2-2=1, 1/√1=1 per edge → 2
293        assert!((reduced_sum_connectivity(&path3()).unwrap() - 2.0).abs() < 1e-10);
294    }
295
296    #[test]
297    fn rsc_k3() {
298        // d=(2,2): 2+2-2=2, 1/√2 per edge → 3/√2
299        let expected = 3.0 / 2.0_f64.sqrt();
300        assert!((reduced_sum_connectivity(&k3()).unwrap() - expected).abs() < 1e-10);
301    }
302
303    #[test]
304    fn rsc_k4() {
305        // d=(3,3): 3+3-2=4, 1/√4=1/2 per edge, 6 edges → 3
306        assert!((reduced_sum_connectivity(&k4()).unwrap() - 3.0).abs() < 1e-10);
307    }
308
309    #[test]
310    fn rsc_cycle4() {
311        // d=(2,2): 2, 1/√2 per edge → 4/√2 = 2√2
312        let expected = 4.0 / 2.0_f64.sqrt();
313        assert!((reduced_sum_connectivity(&cycle4()).unwrap() - expected).abs() < 1e-10);
314    }
315
316    #[test]
317    fn rsc_star5() {
318        // d=(4,1): 4+1-2=3, 1/√3 per edge → 4/√3
319        let expected = 4.0 / 3.0_f64.sqrt();
320        assert!((reduced_sum_connectivity(&star5()).unwrap() - expected).abs() < 1e-10);
321    }
322
323    #[test]
324    fn rsc_paw() {
325        // (0,1) d=(2,2): 1/√2
326        // (0,2) d=(2,3): 1/√3
327        // (1,2) d=(2,3): 1/√3
328        // (2,3) d=(3,1): 1/√2
329        let expected = 2.0 / 2.0_f64.sqrt() + 2.0 / 3.0_f64.sqrt();
330        assert!((reduced_sum_connectivity(&paw()).unwrap() - expected).abs() < 1e-10);
331    }
332
333    // --- reduced_first_zagreb ---
334
335    #[test]
336    fn rfz_empty() {
337        let g = Graph::with_vertices(0);
338        assert_eq!(reduced_first_zagreb(&g).unwrap(), 0);
339    }
340
341    #[test]
342    fn rfz_isolated() {
343        let g = Graph::with_vertices(5);
344        assert_eq!(reduced_first_zagreb(&g).unwrap(), 0);
345    }
346
347    #[test]
348    fn rfz_single_edge() {
349        // d=(1,1), (1-1)²=0 each → 0
350        assert_eq!(reduced_first_zagreb(&single_edge()).unwrap(), 0);
351    }
352
353    #[test]
354    fn rfz_path3() {
355        // d=(1,2,1): 0+1+0=1
356        assert_eq!(reduced_first_zagreb(&path3()).unwrap(), 1);
357    }
358
359    #[test]
360    fn rfz_path4() {
361        // d=(1,2,2,1): 0+1+1+0=2
362        assert_eq!(reduced_first_zagreb(&path4()).unwrap(), 2);
363    }
364
365    #[test]
366    fn rfz_k3() {
367        // d=(2,2,2): 3×1=3
368        assert_eq!(reduced_first_zagreb(&k3()).unwrap(), 3);
369    }
370
371    #[test]
372    fn rfz_k4() {
373        // d=(3,3,3,3): 4×4=16
374        assert_eq!(reduced_first_zagreb(&k4()).unwrap(), 16);
375    }
376
377    #[test]
378    fn rfz_cycle4() {
379        // d=(2,2,2,2): 4×1=4
380        assert_eq!(reduced_first_zagreb(&cycle4()).unwrap(), 4);
381    }
382
383    #[test]
384    fn rfz_cycle5() {
385        // d=(2,2,2,2,2): 5×1=5
386        assert_eq!(reduced_first_zagreb(&cycle5()).unwrap(), 5);
387    }
388
389    #[test]
390    fn rfz_star5() {
391        // d=(4,1,1,1,1): (3)²+0+0+0+0=9
392        assert_eq!(reduced_first_zagreb(&star5()).unwrap(), 9);
393    }
394
395    #[test]
396    fn rfz_paw() {
397        // d=(2,2,3,1): 1+1+4+0=6
398        assert_eq!(reduced_first_zagreb(&paw()).unwrap(), 6);
399    }
400
401    // --- reduced_forgotten_index ---
402
403    #[test]
404    fn rfi_empty() {
405        let g = Graph::with_vertices(0);
406        assert_eq!(reduced_forgotten_index(&g).unwrap(), 0);
407    }
408
409    #[test]
410    fn rfi_isolated() {
411        let g = Graph::with_vertices(5);
412        assert_eq!(reduced_forgotten_index(&g).unwrap(), 0);
413    }
414
415    #[test]
416    fn rfi_single_edge() {
417        // d=(1,1), (0)³=0 each → 0
418        assert_eq!(reduced_forgotten_index(&single_edge()).unwrap(), 0);
419    }
420
421    #[test]
422    fn rfi_path3() {
423        // d=(1,2,1): 0+1+0=1
424        assert_eq!(reduced_forgotten_index(&path3()).unwrap(), 1);
425    }
426
427    #[test]
428    fn rfi_k3() {
429        // d=(2,2,2): 3×1³=3
430        assert_eq!(reduced_forgotten_index(&k3()).unwrap(), 3);
431    }
432
433    #[test]
434    fn rfi_k4() {
435        // d=(3,3,3,3): 4×2³=32
436        assert_eq!(reduced_forgotten_index(&k4()).unwrap(), 32);
437    }
438
439    #[test]
440    fn rfi_cycle4() {
441        // d=(2,2,2,2): 4×1=4
442        assert_eq!(reduced_forgotten_index(&cycle4()).unwrap(), 4);
443    }
444
445    #[test]
446    fn rfi_star5() {
447        // d=(4,1,1,1,1): 3³+0+0+0+0=27
448        assert_eq!(reduced_forgotten_index(&star5()).unwrap(), 27);
449    }
450
451    #[test]
452    fn rfi_paw() {
453        // d=(2,2,3,1): 1+1+8+0=10
454        assert_eq!(reduced_forgotten_index(&paw()).unwrap(), 10);
455    }
456
457    // --- cross-consistency ---
458
459    #[test]
460    fn rfz_le_rfi_for_nontrivial() {
461        // (d-1)² ≤ (d-1)³ when d-1 ≥ 1, i.e. d ≥ 2
462        // Not strictly true for d=2 where equality holds, but sum-wise:
463        // rfz ≤ rfi when max degree ≥ 3 (otherwise equal)
464        for g in &[k4(), star5()] {
465            assert!(reduced_first_zagreb(g).unwrap() <= reduced_forgotten_index(g).unwrap());
466        }
467    }
468
469    #[test]
470    fn regular_rrr_equals_edge_count() {
471        // For r-regular (r≥2): each edge 1/√((r-1)²) = 1/(r-1) → m/(r-1)
472        // K_3: r=2 → m/1 = 3
473        assert!((reduced_reciprocal_randic(&k3()).unwrap() - 3.0).abs() < 1e-10);
474        // C_4: r=2 → 4/1 = 4
475        assert!((reduced_reciprocal_randic(&cycle4()).unwrap() - 4.0).abs() < 1e-10);
476        // K_4: r=3 → 6/2 = 3
477        assert!((reduced_reciprocal_randic(&k4()).unwrap() - 3.0).abs() < 1e-10);
478    }
479}