Skip to main content

rust_igraph/algorithms/properties/
forgotten_zagreb.rs

1//! Forgotten topological index and reduced second Zagreb (ALGO-TR-049).
2//!
3//! - **Forgotten topological index** `F(G) = Σ_{(u,v)∈E} (d_u² + d_v²)`
4//!   Also written as `Σ_{v∈V} d_v³`. Introduced by Furtula & Gutman
5//!   (2015), rediscovered from a 1972 paper. Sums the cubes of degrees,
6//!   or equivalently sums squared degrees over edge endpoints.
7//! - **Reduced second Zagreb index** `RM₂(G) = Σ_{(u,v)∈E} (d_u-1)(d_v-1)`
8//!   Counts "reduced" degree products. Useful in QSPR for modelling
9//!   molecular properties. For a tree on n vertices, `RM₂ = M₁ - (n-1)`
10//!   where `M₁` is the first Zagreb index.
11//! - **Modified first Zagreb index** `^mM₁(G) = Σ_{v∈V} 1/d_v²`
12//!   The sum of reciprocal squared degrees (excludes isolated vertices).
13//!   Used in QSPR studies of graph invariants.
14
15#![allow(
16    clippy::cast_possible_truncation,
17    clippy::cast_precision_loss,
18    clippy::many_single_char_names,
19    clippy::needless_range_loop,
20    clippy::too_many_lines
21)]
22
23use crate::core::{Graph, IgraphResult};
24
25/// Compute the forgotten topological index.
26///
27/// `F(G) = Σ_{(u,v)∈E} (d_u² + d_v²)`
28///
29/// Equivalently, `F(G) = Σ_{v∈V} d_v³` (each vertex's cube-degree
30/// summed). Self-loops are skipped in the edge formulation.
31///
32/// # Examples
33///
34/// ```
35/// use rust_igraph::{Graph, forgotten_index};
36///
37/// // K_3: all degrees 2 → F = 3·(4+4) = 24
38/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
39/// assert!((forgotten_index(&g).unwrap() - 24.0).abs() < 1e-10);
40/// ```
41pub fn forgotten_index(graph: &Graph) -> IgraphResult<f64> {
42    let n = graph.vcount();
43    if n == 0 {
44        return Ok(0.0);
45    }
46
47    let mut f = 0.0_f64;
48
49    for (u, v) in graph.edges() {
50        if u == v {
51            continue;
52        }
53        let du = graph.degree(u)? as f64;
54        let dv = graph.degree(v)? as f64;
55        f += du * du + dv * dv;
56    }
57
58    Ok(f)
59}
60
61/// Compute the reduced second Zagreb index.
62///
63/// `RM₂(G) = Σ_{(u,v)∈E} (d_u − 1)(d_v − 1)`
64///
65/// Self-loops are skipped. Pendant edges (where one endpoint has
66/// degree 1) contribute 0.
67///
68/// # Examples
69///
70/// ```
71/// use rust_igraph::{Graph, reduced_second_zagreb};
72///
73/// // K_3: all degrees 2 → each edge: (2-1)(2-1)=1, 3 edges → RM₂=3
74/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
75/// assert!((reduced_second_zagreb(&g).unwrap() - 3.0).abs() < 1e-10);
76/// ```
77pub fn reduced_second_zagreb(graph: &Graph) -> IgraphResult<f64> {
78    let n = graph.vcount();
79    if n == 0 {
80        return Ok(0.0);
81    }
82
83    let mut rm2 = 0.0_f64;
84
85    for (u, v) in graph.edges() {
86        if u == v {
87            continue;
88        }
89        let du = graph.degree(u)? as f64;
90        let dv = graph.degree(v)? as f64;
91        rm2 += (du - 1.0) * (dv - 1.0);
92    }
93
94    Ok(rm2)
95}
96
97/// Compute the modified first Zagreb index.
98///
99/// `^mM₁(G) = Σ_{v∈V, d_v>0} 1/d_v²`
100///
101/// Sums the reciprocal of squared degree over all non-isolated vertices.
102///
103/// # Examples
104///
105/// ```
106/// use rust_igraph::{Graph, modified_first_zagreb};
107///
108/// // K_3: all degrees 2 → 3 · (1/4) = 3/4
109/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
110/// assert!((modified_first_zagreb(&g).unwrap() - 0.75).abs() < 1e-10);
111/// ```
112pub fn modified_first_zagreb(graph: &Graph) -> IgraphResult<f64> {
113    let n = graph.vcount();
114    if n == 0 {
115        return Ok(0.0);
116    }
117
118    let mut mm1 = 0.0_f64;
119
120    for v in 0..n {
121        let d = graph.degree(v)? as f64;
122        if d > 0.0 {
123            mm1 += 1.0 / (d * d);
124        }
125    }
126
127    Ok(mm1)
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    fn single_edge() -> Graph {
135        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
136    }
137
138    fn path3() -> Graph {
139        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
140    }
141
142    fn path4() -> Graph {
143        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
144    }
145
146    fn k3() -> Graph {
147        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
148    }
149
150    fn k4() -> Graph {
151        Graph::from_edges(
152            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
153            false,
154            Some(4),
155        )
156        .unwrap()
157    }
158
159    fn cycle4() -> Graph {
160        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
161    }
162
163    fn cycle5() -> Graph {
164        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
165    }
166
167    fn star5() -> Graph {
168        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
169    }
170
171    fn paw() -> Graph {
172        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
173    }
174
175    fn diamond() -> Graph {
176        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)], false, Some(4)).unwrap()
177    }
178
179    // --- forgotten_index ---
180
181    #[test]
182    fn fi_empty() {
183        let g = Graph::with_vertices(0);
184        assert!((forgotten_index(&g).unwrap()).abs() < 1e-10);
185    }
186
187    #[test]
188    fn fi_single_vertex() {
189        let g = Graph::with_vertices(1);
190        assert!((forgotten_index(&g).unwrap()).abs() < 1e-10);
191    }
192
193    #[test]
194    fn fi_no_edges() {
195        let g = Graph::with_vertices(3);
196        assert!((forgotten_index(&g).unwrap()).abs() < 1e-10);
197    }
198
199    #[test]
200    fn fi_single_edge() {
201        // d_u=d_v=1: 1²+1² = 2
202        assert!((forgotten_index(&single_edge()).unwrap() - 2.0).abs() < 1e-10);
203    }
204
205    #[test]
206    fn fi_path3() {
207        // degrees [1,2,1]
208        // (0,1): 1+4=5, (1,2): 4+1=5 → F=10
209        assert!((forgotten_index(&path3()).unwrap() - 10.0).abs() < 1e-10);
210    }
211
212    #[test]
213    fn fi_path4() {
214        // degrees [1,2,2,1]
215        // (0,1): 1+4=5, (1,2): 4+4=8, (2,3): 4+1=5 → F=18
216        assert!((forgotten_index(&path4()).unwrap() - 18.0).abs() < 1e-10);
217    }
218
219    #[test]
220    fn fi_k3() {
221        // all degrees 2: 3·(4+4) = 24
222        assert!((forgotten_index(&k3()).unwrap() - 24.0).abs() < 1e-10);
223    }
224
225    #[test]
226    fn fi_k4() {
227        // all degrees 3: 6·(9+9) = 108
228        assert!((forgotten_index(&k4()).unwrap() - 108.0).abs() < 1e-10);
229    }
230
231    #[test]
232    fn fi_cycle4() {
233        // all degrees 2: 4·8 = 32
234        assert!((forgotten_index(&cycle4()).unwrap() - 32.0).abs() < 1e-10);
235    }
236
237    #[test]
238    fn fi_cycle5() {
239        // all degrees 2: 5·8 = 40
240        assert!((forgotten_index(&cycle5()).unwrap() - 40.0).abs() < 1e-10);
241    }
242
243    #[test]
244    fn fi_star5() {
245        // center deg 4, leaf deg 1: 4·(16+1) = 68
246        assert!((forgotten_index(&star5()).unwrap() - 68.0).abs() < 1e-10);
247    }
248
249    #[test]
250    fn fi_paw() {
251        // degrees [2,2,3,1]
252        // (0,1): 4+4=8, (0,2): 4+9=13, (1,2): 4+9=13, (2,3): 9+1=10
253        // F = 8+13+13+10 = 44
254        assert!((forgotten_index(&paw()).unwrap() - 44.0).abs() < 1e-10);
255    }
256
257    #[test]
258    fn fi_diamond() {
259        // degrees [3,3,2,2]
260        // (0,1): 9+9=18, (0,2): 9+4=13, (0,3): 9+4=13, (1,2): 9+4=13, (1,3): 9+4=13
261        // F = 18+13+13+13+13 = 70
262        assert!((forgotten_index(&diamond()).unwrap() - 70.0).abs() < 1e-10);
263    }
264
265    #[test]
266    fn fi_equals_sum_cubes() {
267        // F(G) = Σ d_v³ (vertex sum form)
268        for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
269            let f_edge = forgotten_index(g).unwrap();
270            let mut f_vertex = 0.0_f64;
271            for v in 0..g.vcount() {
272                let d = g.degree(v).unwrap() as f64;
273                f_vertex += d * d * d;
274            }
275            assert!((f_edge - f_vertex).abs() < 1e-8);
276        }
277    }
278
279    #[test]
280    fn fi_regular_formula() {
281        // For r-regular: F = m·2r² = 2m·r²
282        for g in &[k3(), k4(), cycle4(), cycle5()] {
283            let m = g.ecount() as f64;
284            let r = g.degree(0).unwrap() as f64;
285            let expected = 2.0 * m * r * r;
286            assert!((forgotten_index(g).unwrap() - expected).abs() < 1e-8);
287        }
288    }
289
290    // --- reduced_second_zagreb ---
291
292    #[test]
293    fn rm2_empty() {
294        let g = Graph::with_vertices(0);
295        assert!((reduced_second_zagreb(&g).unwrap()).abs() < 1e-10);
296    }
297
298    #[test]
299    fn rm2_single_vertex() {
300        let g = Graph::with_vertices(1);
301        assert!((reduced_second_zagreb(&g).unwrap()).abs() < 1e-10);
302    }
303
304    #[test]
305    fn rm2_no_edges() {
306        let g = Graph::with_vertices(3);
307        assert!((reduced_second_zagreb(&g).unwrap()).abs() < 1e-10);
308    }
309
310    #[test]
311    fn rm2_single_edge() {
312        // (1-1)(1-1) = 0
313        assert!((reduced_second_zagreb(&single_edge()).unwrap()).abs() < 1e-10);
314    }
315
316    #[test]
317    fn rm2_path3() {
318        // (0,1): (1-1)(2-1)=0, (1,2): (2-1)(1-1)=0 → RM₂=0
319        assert!((reduced_second_zagreb(&path3()).unwrap()).abs() < 1e-10);
320    }
321
322    #[test]
323    fn rm2_path4() {
324        // (0,1): (0)(1)=0, (1,2): (1)(1)=1, (2,3): (1)(0)=0 → RM₂=1
325        assert!((reduced_second_zagreb(&path4()).unwrap() - 1.0).abs() < 1e-10);
326    }
327
328    #[test]
329    fn rm2_k3() {
330        // each: (2-1)(2-1)=1, 3 edges → 3
331        assert!((reduced_second_zagreb(&k3()).unwrap() - 3.0).abs() < 1e-10);
332    }
333
334    #[test]
335    fn rm2_k4() {
336        // each: (3-1)(3-1)=4, 6 edges → 24
337        assert!((reduced_second_zagreb(&k4()).unwrap() - 24.0).abs() < 1e-10);
338    }
339
340    #[test]
341    fn rm2_cycle4() {
342        // each: (1)(1)=1, 4 edges → 4
343        assert!((reduced_second_zagreb(&cycle4()).unwrap() - 4.0).abs() < 1e-10);
344    }
345
346    #[test]
347    fn rm2_cycle5() {
348        // each: 1, 5 edges → 5
349        assert!((reduced_second_zagreb(&cycle5()).unwrap() - 5.0).abs() < 1e-10);
350    }
351
352    #[test]
353    fn rm2_star5() {
354        // center deg 4, leaf deg 1: (4-1)(1-1)=0, 4 edges → 0
355        assert!((reduced_second_zagreb(&star5()).unwrap()).abs() < 1e-10);
356    }
357
358    #[test]
359    fn rm2_paw() {
360        // degrees [2,2,3,1]
361        // (0,1): (1)(1)=1, (0,2): (1)(2)=2, (1,2): (1)(2)=2, (2,3): (2)(0)=0
362        // RM₂ = 1+2+2+0 = 5
363        assert!((reduced_second_zagreb(&paw()).unwrap() - 5.0).abs() < 1e-10);
364    }
365
366    #[test]
367    fn rm2_diamond() {
368        // degrees [3,3,2,2]
369        // (0,1): (2)(2)=4, (0,2): (2)(1)=2, (0,3): (2)(1)=2, (1,2): (2)(1)=2, (1,3): (2)(1)=2
370        // RM₂ = 4+2+2+2+2 = 12
371        assert!((reduced_second_zagreb(&diamond()).unwrap() - 12.0).abs() < 1e-10);
372    }
373
374    #[test]
375    fn rm2_nonneg_for_connected() {
376        for g in &[
377            single_edge(),
378            path3(),
379            k3(),
380            k4(),
381            star5(),
382            paw(),
383            diamond(),
384        ] {
385            assert!(reduced_second_zagreb(g).unwrap() >= -1e-10);
386        }
387    }
388
389    #[test]
390    fn rm2_regular_formula() {
391        // r-regular: RM₂ = m·(r-1)²
392        for g in &[k3(), k4(), cycle4(), cycle5()] {
393            let m = g.ecount() as f64;
394            let r = g.degree(0).unwrap() as f64;
395            let expected = m * (r - 1.0) * (r - 1.0);
396            assert!((reduced_second_zagreb(g).unwrap() - expected).abs() < 1e-8);
397        }
398    }
399
400    // --- modified_first_zagreb ---
401
402    #[test]
403    fn mm1_empty() {
404        let g = Graph::with_vertices(0);
405        assert!((modified_first_zagreb(&g).unwrap()).abs() < 1e-10);
406    }
407
408    #[test]
409    fn mm1_single_vertex() {
410        let g = Graph::with_vertices(1);
411        assert!((modified_first_zagreb(&g).unwrap()).abs() < 1e-10);
412    }
413
414    #[test]
415    fn mm1_no_edges() {
416        let g = Graph::with_vertices(3);
417        assert!((modified_first_zagreb(&g).unwrap()).abs() < 1e-10);
418    }
419
420    #[test]
421    fn mm1_single_edge() {
422        // 2 · 1/1² = 2
423        assert!((modified_first_zagreb(&single_edge()).unwrap() - 2.0).abs() < 1e-10);
424    }
425
426    #[test]
427    fn mm1_path3() {
428        // degrees [1,2,1]: 1/1 + 1/4 + 1/1 = 2.25
429        assert!((modified_first_zagreb(&path3()).unwrap() - 2.25).abs() < 1e-10);
430    }
431
432    #[test]
433    fn mm1_k3() {
434        // 3 · 1/4 = 0.75
435        assert!((modified_first_zagreb(&k3()).unwrap() - 0.75).abs() < 1e-10);
436    }
437
438    #[test]
439    fn mm1_k4() {
440        // 4 · 1/9 = 4/9
441        assert!((modified_first_zagreb(&k4()).unwrap() - 4.0 / 9.0).abs() < 1e-10);
442    }
443
444    #[test]
445    fn mm1_cycle4() {
446        // 4 · 1/4 = 1
447        assert!((modified_first_zagreb(&cycle4()).unwrap() - 1.0).abs() < 1e-10);
448    }
449
450    #[test]
451    fn mm1_cycle5() {
452        // 5 · 1/4 = 1.25
453        assert!((modified_first_zagreb(&cycle5()).unwrap() - 1.25).abs() < 1e-10);
454    }
455
456    #[test]
457    fn mm1_star5() {
458        // 1/16 + 4·(1/1) = 1/16 + 4 = 65/16
459        let expected = 1.0 / 16.0 + 4.0;
460        assert!((modified_first_zagreb(&star5()).unwrap() - expected).abs() < 1e-10);
461    }
462
463    #[test]
464    fn mm1_paw() {
465        // degrees [2,2,3,1]: 1/4 + 1/4 + 1/9 + 1/1 = 0.5 + 1/9 + 1 = 1.5 + 1/9
466        let expected = 0.25 + 0.25 + 1.0 / 9.0 + 1.0;
467        assert!((modified_first_zagreb(&paw()).unwrap() - expected).abs() < 1e-10);
468    }
469
470    #[test]
471    fn mm1_with_isolated() {
472        // 0-1 plus isolated 2: degrees [1,1,0], skip vertex 2
473        // 1/1 + 1/1 = 2
474        let g = Graph::from_edges(&[(0, 1)], false, Some(3)).unwrap();
475        assert!((modified_first_zagreb(&g).unwrap() - 2.0).abs() < 1e-10);
476    }
477
478    #[test]
479    fn mm1_regular_formula() {
480        // r-regular: n · 1/r² = n/r²
481        for g in &[k3(), k4(), cycle4(), cycle5()] {
482            let n = f64::from(g.vcount());
483            let r = g.degree(0).unwrap() as f64;
484            let expected = n / (r * r);
485            assert!((modified_first_zagreb(g).unwrap() - expected).abs() < 1e-8);
486        }
487    }
488
489    // --- cross-consistency ---
490
491    #[test]
492    fn all_positive_for_connected() {
493        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5()] {
494            assert!(forgotten_index(g).unwrap() > 0.0);
495            assert!(modified_first_zagreb(g).unwrap() > 0.0);
496        }
497    }
498
499    #[test]
500    fn fi_geq_2m() {
501        // F(G) = Σ(d_u²+d_v²) ≥ 2m since d_u≥1, d_v≥1
502        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5()] {
503            let f = forgotten_index(g).unwrap();
504            assert!(f >= 2.0 * g.ecount() as f64 - 1e-10);
505        }
506    }
507
508    #[test]
509    fn fi_relationship_to_m1_sigma() {
510        // F(G) = M₁(G) + σ(G) where M₁ = Σ(d_u+d_v) = first Zagreb, σ = sigma index
511        // Because (d_u²+d_v²) = (d_u+d_v)² - 2d_u·d_v, and
512        // (d_u-d_v)² = d_u²+d_v² - 2d_u·d_v, actually:
513        // F = Σ(d_u²+d_v²), σ = Σ(d_u-d_v)², M₁ = Σ(d_u+d_v)
514        // Note: F + 2·M₂ = Σ(d_u²+d_v²+2d_u·d_v) = Σ(d_u+d_v)² not M₁
515        // F = M₁ + σ is NOT correct in general.
516        // But we can verify: F ≥ σ (since d_u²+d_v² ≥ (d_u-d_v)²)
517        for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
518            let f = forgotten_index(g).unwrap();
519            let mut sigma = 0.0_f64;
520            for (u, v) in g.edges() {
521                if u == v {
522                    continue;
523                }
524                let du = g.degree(u).unwrap() as f64;
525                let dv = g.degree(v).unwrap() as f64;
526                sigma += (du - dv) * (du - dv);
527            }
528            assert!(f >= sigma - 1e-8);
529        }
530    }
531}