Skip to main content

rust_igraph/algorithms/properties/
forgotten_coindex.rs

1//! Forgotten (F-index) coindex and related coindices (ALGO-TR-071).
2//!
3//! Coindices are computed over non-adjacent vertex pairs (complement edges):
4//!
5//! - **Forgotten coindex** `\bar{F}(G) = Σ_{u≠v, (u,v)∉E} [d(u)²+d(v)²]`
6//! - **First hyper-Zagreb coindex** `\bar{HM₁}(G) = Σ_{u≠v, (u,v)∉E} [d(u)+d(v)]²`
7//! - **Second hyper-Zagreb coindex** `\bar{HM₂}(G) = Σ_{u≠v, (u,v)∉E} [d(u)·d(v)]²`
8
9#![allow(
10    clippy::cast_possible_truncation,
11    clippy::cast_precision_loss,
12    clippy::many_single_char_names,
13    clippy::needless_range_loop,
14    clippy::too_many_lines
15)]
16
17use crate::core::{Graph, IgraphResult};
18
19/// Compute the forgotten coindex (F-coindex).
20///
21/// `\bar{F}(G) = Σ_{u<v, (u,v)∉E} [d(u)²+d(v)²]`
22///
23/// Uses the identity: `\bar{F} = (n-1)·Σd² - F(G)` where F(G) is the
24/// forgotten index.
25///
26/// # Examples
27///
28/// ```
29/// use rust_igraph::{Graph, forgotten_coindex};
30///
31/// // K_3: no non-adjacent pairs → 0
32/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
33/// assert_eq!(forgotten_coindex(&g).unwrap(), 0);
34///
35/// // Path 0-1-2: non-adj (0,2), d=(1,1) → 1+1 = 2
36/// let p = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
37/// assert_eq!(forgotten_coindex(&p).unwrap(), 2);
38/// ```
39pub fn forgotten_coindex(graph: &Graph) -> IgraphResult<u64> {
40    let n = graph.vcount() as usize;
41    if n < 2 {
42        return Ok(0);
43    }
44
45    let mut sum_d2 = 0_u64;
46    let mut f_index = 0_u64;
47
48    for v in 0..n {
49        let d = graph.degree(v as u32)? as u64;
50        sum_d2 = sum_d2.saturating_add(d.saturating_mul(d));
51    }
52
53    for (u, v) in graph.edges() {
54        if u == v {
55            continue;
56        }
57        let du = graph.degree(u)? as u64;
58        let dv = graph.degree(v)? as u64;
59        f_index =
60            f_index.saturating_add(du.saturating_mul(du).saturating_add(dv.saturating_mul(dv)));
61    }
62
63    let n_minus_1 = (n as u64).saturating_sub(1);
64    Ok(n_minus_1.saturating_mul(sum_d2).saturating_sub(f_index))
65}
66
67/// Compute the first hyper-Zagreb coindex.
68///
69/// `\bar{HM₁}(G) = Σ_{u<v, (u,v)∉E} [d(u)+d(v)]²`
70///
71/// Uses the identity: `\bar{HM₁} = 4m²·(n-1) + (n-1)·Σd² - HM₁(G)`
72/// where HM₁ is the first hyper-Zagreb index (sum of (du+dv)² over edges),
73/// m is edge count, and Σd² = M₁ (first Zagreb index).
74///
75/// Derivation: `Σ_all(du+dv)² = Σ_all(du²+2du·dv+dv²)`
76///           `= (n-1)Σd² + 2·(Σd)² - 2·Σd²`
77/// but simpler: just compute directly for correctness.
78///
79/// # Examples
80///
81/// ```
82/// use rust_igraph::{Graph, first_hyper_zagreb_coindex};
83///
84/// // K_3: no non-adjacent pairs → 0
85/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
86/// assert_eq!(first_hyper_zagreb_coindex(&g).unwrap(), 0);
87///
88/// // Path 0-1-2: non-adj (0,2), (1+1)² = 4
89/// let p = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
90/// assert_eq!(first_hyper_zagreb_coindex(&p).unwrap(), 4);
91/// ```
92pub fn first_hyper_zagreb_coindex(graph: &Graph) -> IgraphResult<u64> {
93    let n = graph.vcount() as usize;
94    if n < 2 {
95        return Ok(0);
96    }
97
98    let mut degrees = Vec::with_capacity(n);
99    for v in 0..n {
100        degrees.push(graph.degree(v as u32)? as u64);
101    }
102
103    let mut total = 0_u64;
104    for u in 0..n {
105        for v in (u + 1)..n {
106            let s = degrees[u].saturating_add(degrees[v]);
107            total = total.saturating_add(s.saturating_mul(s));
108        }
109    }
110
111    let mut edge_sum = 0_u64;
112    for (u, v) in graph.edges() {
113        if u == v {
114            continue;
115        }
116        let s = degrees[u as usize].saturating_add(degrees[v as usize]);
117        edge_sum = edge_sum.saturating_add(s.saturating_mul(s));
118    }
119
120    Ok(total.saturating_sub(edge_sum))
121}
122
123/// Compute the second hyper-Zagreb coindex.
124///
125/// `\bar{HM₂}(G) = Σ_{u<v, (u,v)∉E} [d(u)·d(v)]²`
126///
127/// # Examples
128///
129/// ```
130/// use rust_igraph::{Graph, second_hyper_zagreb_coindex};
131///
132/// // K_3: no non-adjacent pairs → 0
133/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
134/// assert_eq!(second_hyper_zagreb_coindex(&g).unwrap(), 0);
135///
136/// // Path 0-1-2: non-adj (0,2), (1·1)² = 1
137/// let p = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
138/// assert_eq!(second_hyper_zagreb_coindex(&p).unwrap(), 1);
139/// ```
140pub fn second_hyper_zagreb_coindex(graph: &Graph) -> IgraphResult<u64> {
141    let n = graph.vcount() as usize;
142    if n < 2 {
143        return Ok(0);
144    }
145
146    let mut degrees = Vec::with_capacity(n);
147    for v in 0..n {
148        degrees.push(graph.degree(v as u32)? as u64);
149    }
150
151    let mut total = 0_u64;
152    for u in 0..n {
153        for v in (u + 1)..n {
154            let p = degrees[u].saturating_mul(degrees[v]);
155            total = total.saturating_add(p.saturating_mul(p));
156        }
157    }
158
159    let mut edge_sum = 0_u64;
160    for (u, v) in graph.edges() {
161        if u == v {
162            continue;
163        }
164        let p = degrees[u as usize].saturating_mul(degrees[v as usize]);
165        edge_sum = edge_sum.saturating_add(p.saturating_mul(p));
166    }
167
168    Ok(total.saturating_sub(edge_sum))
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    fn single_edge() -> Graph {
176        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
177    }
178
179    fn path3() -> Graph {
180        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
181    }
182
183    fn path4() -> Graph {
184        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
185    }
186
187    fn k3() -> Graph {
188        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
189    }
190
191    fn k4() -> Graph {
192        Graph::from_edges(
193            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
194            false,
195            Some(4),
196        )
197        .unwrap()
198    }
199
200    fn cycle4() -> Graph {
201        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
202    }
203
204    fn cycle5() -> Graph {
205        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
206    }
207
208    fn star5() -> Graph {
209        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
210    }
211
212    fn paw() -> Graph {
213        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
214    }
215
216    // --- forgotten_coindex ---
217
218    #[test]
219    fn fco_empty() {
220        let g = Graph::with_vertices(0);
221        assert_eq!(forgotten_coindex(&g).unwrap(), 0);
222    }
223
224    #[test]
225    fn fco_isolated() {
226        let g = Graph::with_vertices(5);
227        assert_eq!(forgotten_coindex(&g).unwrap(), 0);
228    }
229
230    #[test]
231    fn fco_single_edge() {
232        // Only 2 vertices, both adjacent → 0
233        assert_eq!(forgotten_coindex(&single_edge()).unwrap(), 0);
234    }
235
236    #[test]
237    fn fco_k3() {
238        assert_eq!(forgotten_coindex(&k3()).unwrap(), 0);
239    }
240
241    #[test]
242    fn fco_k4() {
243        assert_eq!(forgotten_coindex(&k4()).unwrap(), 0);
244    }
245
246    #[test]
247    fn fco_path3() {
248        // Non-adj: (0,2), d=(1,1), 1+1=2
249        assert_eq!(forgotten_coindex(&path3()).unwrap(), 2);
250    }
251
252    #[test]
253    fn fco_path4() {
254        // Non-adj: (0,2) d=(1,2):1+4=5, (0,3) d=(1,1):1+1=2, (1,3) d=(2,1):4+1=5
255        assert_eq!(forgotten_coindex(&path4()).unwrap(), 12);
256    }
257
258    #[test]
259    fn fco_cycle4() {
260        // Non-adj: (0,2),(1,3), d=(2,2) each → 4+4=8 each → 16
261        assert_eq!(forgotten_coindex(&cycle4()).unwrap(), 16);
262    }
263
264    #[test]
265    fn fco_cycle5() {
266        // Non-adj: 5 pairs (each vertex non-adj to 2 others, 5×2/2=5)
267        // All d=2, each pair: 4+4=8 → 5×8=40
268        assert_eq!(forgotten_coindex(&cycle5()).unwrap(), 40);
269    }
270
271    #[test]
272    fn fco_star5() {
273        // Non-adj: C(4,2)=6 leaf pairs, d=(1,1), 1+1=2 → 6×2=12
274        assert_eq!(forgotten_coindex(&star5()).unwrap(), 12);
275    }
276
277    #[test]
278    fn fco_paw() {
279        // Non-adj: (0,3) d=(2,1):4+1=5, (1,3) d=(2,1):4+1=5 → 10
280        assert_eq!(forgotten_coindex(&paw()).unwrap(), 10);
281    }
282
283    // --- first_hyper_zagreb_coindex ---
284
285    #[test]
286    fn hm1co_empty() {
287        let g = Graph::with_vertices(0);
288        assert_eq!(first_hyper_zagreb_coindex(&g).unwrap(), 0);
289    }
290
291    #[test]
292    fn hm1co_isolated() {
293        let g = Graph::with_vertices(5);
294        assert_eq!(first_hyper_zagreb_coindex(&g).unwrap(), 0);
295    }
296
297    #[test]
298    fn hm1co_single_edge() {
299        assert_eq!(first_hyper_zagreb_coindex(&single_edge()).unwrap(), 0);
300    }
301
302    #[test]
303    fn hm1co_k3() {
304        assert_eq!(first_hyper_zagreb_coindex(&k3()).unwrap(), 0);
305    }
306
307    #[test]
308    fn hm1co_k4() {
309        assert_eq!(first_hyper_zagreb_coindex(&k4()).unwrap(), 0);
310    }
311
312    #[test]
313    fn hm1co_path3() {
314        // (0,2): (1+1)²=4
315        assert_eq!(first_hyper_zagreb_coindex(&path3()).unwrap(), 4);
316    }
317
318    #[test]
319    fn hm1co_path4() {
320        // (0,2):(1+2)²=9, (0,3):(1+1)²=4, (1,3):(2+1)²=9 → 22
321        assert_eq!(first_hyper_zagreb_coindex(&path4()).unwrap(), 22);
322    }
323
324    #[test]
325    fn hm1co_cycle4() {
326        // (0,2),(1,3): (2+2)²=16 each → 32
327        assert_eq!(first_hyper_zagreb_coindex(&cycle4()).unwrap(), 32);
328    }
329
330    #[test]
331    fn hm1co_cycle5() {
332        // 5 non-adj pairs, d=2: (2+2)²=16 each → 80
333        assert_eq!(first_hyper_zagreb_coindex(&cycle5()).unwrap(), 80);
334    }
335
336    #[test]
337    fn hm1co_star5() {
338        // 6 leaf pairs: (1+1)²=4 each → 24
339        assert_eq!(first_hyper_zagreb_coindex(&star5()).unwrap(), 24);
340    }
341
342    #[test]
343    fn hm1co_paw() {
344        // (0,3):(2+1)²=9, (1,3):(2+1)²=9 → 18
345        assert_eq!(first_hyper_zagreb_coindex(&paw()).unwrap(), 18);
346    }
347
348    // --- second_hyper_zagreb_coindex ---
349
350    #[test]
351    fn hm2co_empty() {
352        let g = Graph::with_vertices(0);
353        assert_eq!(second_hyper_zagreb_coindex(&g).unwrap(), 0);
354    }
355
356    #[test]
357    fn hm2co_isolated() {
358        let g = Graph::with_vertices(5);
359        assert_eq!(second_hyper_zagreb_coindex(&g).unwrap(), 0);
360    }
361
362    #[test]
363    fn hm2co_single_edge() {
364        assert_eq!(second_hyper_zagreb_coindex(&single_edge()).unwrap(), 0);
365    }
366
367    #[test]
368    fn hm2co_k3() {
369        assert_eq!(second_hyper_zagreb_coindex(&k3()).unwrap(), 0);
370    }
371
372    #[test]
373    fn hm2co_k4() {
374        assert_eq!(second_hyper_zagreb_coindex(&k4()).unwrap(), 0);
375    }
376
377    #[test]
378    fn hm2co_path3() {
379        // (0,2): (1·1)²=1
380        assert_eq!(second_hyper_zagreb_coindex(&path3()).unwrap(), 1);
381    }
382
383    #[test]
384    fn hm2co_path4() {
385        // (0,2):(1·2)²=4, (0,3):(1·1)²=1, (1,3):(2·1)²=4 → 9
386        assert_eq!(second_hyper_zagreb_coindex(&path4()).unwrap(), 9);
387    }
388
389    #[test]
390    fn hm2co_cycle4() {
391        // (0,2),(1,3): (2·2)²=16 each → 32
392        assert_eq!(second_hyper_zagreb_coindex(&cycle4()).unwrap(), 32);
393    }
394
395    #[test]
396    fn hm2co_cycle5() {
397        // 5 pairs, (2·2)²=16 each → 80
398        assert_eq!(second_hyper_zagreb_coindex(&cycle5()).unwrap(), 80);
399    }
400
401    #[test]
402    fn hm2co_star5() {
403        // 6 leaf pairs: (1·1)²=1 each → 6
404        assert_eq!(second_hyper_zagreb_coindex(&star5()).unwrap(), 6);
405    }
406
407    #[test]
408    fn hm2co_paw() {
409        // (0,3):(2·1)²=4, (1,3):(2·1)²=4 → 8
410        assert_eq!(second_hyper_zagreb_coindex(&paw()).unwrap(), 8);
411    }
412
413    // --- cross-consistency ---
414
415    #[test]
416    fn complete_graphs_all_zero() {
417        for g in &[k3(), k4()] {
418            assert_eq!(forgotten_coindex(g).unwrap(), 0);
419            assert_eq!(first_hyper_zagreb_coindex(g).unwrap(), 0);
420            assert_eq!(second_hyper_zagreb_coindex(g).unwrap(), 0);
421        }
422    }
423
424    #[test]
425    fn all_positive_for_incomplete() {
426        for g in &[path3(), path4(), cycle4(), star5(), paw()] {
427            assert!(forgotten_coindex(g).unwrap() > 0);
428            assert!(first_hyper_zagreb_coindex(g).unwrap() > 0);
429            assert!(second_hyper_zagreb_coindex(g).unwrap() > 0);
430        }
431    }
432
433    #[test]
434    fn hm1co_ge_fco() {
435        // (du+dv)² = du²+2du·dv+dv² ≥ du²+dv² when du,dv≥0 (since 2du·dv≥0)
436        for g in &[path3(), path4(), cycle4(), cycle5(), star5(), paw()] {
437            let fco = forgotten_coindex(g).unwrap();
438            let hm1co = first_hyper_zagreb_coindex(g).unwrap();
439            assert!(hm1co >= fco);
440        }
441    }
442
443    #[test]
444    fn fco_via_direct_sum() {
445        // Verify forgotten_coindex by direct computation
446        for g in &[path3(), path4(), cycle4(), star5(), paw()] {
447            let n = g.vcount() as usize;
448            let mut degrees = vec![0_u64; n];
449            for v in 0..n {
450                degrees[v] = g.degree(v as u32).unwrap() as u64;
451            }
452
453            let adj: std::collections::HashSet<(usize, usize)> = g
454                .edges()
455                .flat_map(|(u, v)| {
456                    let a = u as usize;
457                    let b = v as usize;
458                    vec![(a, b), (b, a)]
459                })
460                .collect();
461
462            let mut direct = 0_u64;
463            for u in 0..n {
464                for v in (u + 1)..n {
465                    if !adj.contains(&(u, v)) {
466                        direct += degrees[u] * degrees[u] + degrees[v] * degrees[v];
467                    }
468                }
469            }
470
471            assert_eq!(forgotten_coindex(g).unwrap(), direct);
472        }
473    }
474}