Skip to main content

rust_igraph/algorithms/properties/
narumi_katayama.rs

1//! Narumi-Katayama index and multiplicative variants (ALGO-TR-052).
2//!
3//! - **Narumi-Katayama index** `NK(G) = Π_{v∈V, d(v)≥1} d(v)`
4//!   Product of all non-zero degrees. Introduced by Narumi & Katayama (1984).
5//! - **First multiplicative Zagreb index** `Π₁(G) = Π_{v∈V} d(v)²`
6//!   Introduced by Todeschini & Consonni (2010).
7//! - **Second multiplicative Zagreb index** `Π₂(G) = Π_{(u,v)∈E} d(u)·d(v)`
8//!   Introduced by Todeschini & Consonni (2010).
9
10#![allow(
11    clippy::cast_possible_truncation,
12    clippy::cast_precision_loss,
13    clippy::many_single_char_names,
14    clippy::needless_range_loop,
15    clippy::too_many_lines
16)]
17
18use crate::core::{Graph, IgraphResult};
19
20/// Compute the Narumi-Katayama index.
21///
22/// `NK(G) = Π_{v∈V, d(v)≥1} d(v)`
23///
24/// The product of all non-zero vertex degrees. Isolated vertices
25/// (degree 0) are excluded from the product. Returns 1.0 for the
26/// empty graph or an edgeless graph (empty product).
27///
28/// # Examples
29///
30/// ```
31/// use rust_igraph::{Graph, narumi_katayama_index};
32///
33/// // K_3: degrees [2,2,2] → NK = 2·2·2 = 8
34/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
35/// assert!((narumi_katayama_index(&g).unwrap() - 8.0).abs() < 1e-10);
36/// ```
37pub fn narumi_katayama_index(graph: &Graph) -> IgraphResult<f64> {
38    let mut nk = 1.0_f64;
39
40    for v in 0..graph.vcount() {
41        let d = graph.degree(v)?;
42        if d > 0 {
43            nk *= d as f64;
44        }
45    }
46
47    Ok(nk)
48}
49
50/// Compute the first multiplicative Zagreb index.
51///
52/// `Π₁(G) = Π_{v∈V, d(v)≥1} d(v)²`
53///
54/// The product of squared degrees over vertices with non-zero degree.
55/// Returns 1.0 for the empty graph or edgeless graph.
56///
57/// # Examples
58///
59/// ```
60/// use rust_igraph::{Graph, first_multiplicative_zagreb};
61///
62/// // K_3: degrees [2,2,2] → Π₁ = 4·4·4 = 64
63/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
64/// assert!((first_multiplicative_zagreb(&g).unwrap() - 64.0).abs() < 1e-10);
65/// ```
66pub fn first_multiplicative_zagreb(graph: &Graph) -> IgraphResult<f64> {
67    let mut pi1 = 1.0_f64;
68
69    for v in 0..graph.vcount() {
70        let d = graph.degree(v)?;
71        if d > 0 {
72            let df = d as f64;
73            pi1 *= df * df;
74        }
75    }
76
77    Ok(pi1)
78}
79
80/// Compute the second multiplicative Zagreb index.
81///
82/// `Π₂(G) = Π_{(u,v)∈E} d(u) · d(v)`
83///
84/// The product of degree products over all edges. Self-loops are
85/// skipped. Returns 1.0 if there are no (non-loop) edges.
86///
87/// # Examples
88///
89/// ```
90/// use rust_igraph::{Graph, second_multiplicative_zagreb};
91///
92/// // K_3: 3 edges, each (2·2)=4 → Π₂ = 4³ = 64
93/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
94/// assert!((second_multiplicative_zagreb(&g).unwrap() - 64.0).abs() < 1e-10);
95/// ```
96pub fn second_multiplicative_zagreb(graph: &Graph) -> IgraphResult<f64> {
97    let mut pi2 = 1.0_f64;
98
99    for (u, v) in graph.edges() {
100        if u == v {
101            continue;
102        }
103        let du = graph.degree(u)? as f64;
104        let dv = graph.degree(v)? as f64;
105        pi2 *= du * dv;
106    }
107
108    Ok(pi2)
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    fn single_edge() -> Graph {
116        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
117    }
118
119    fn path3() -> Graph {
120        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
121    }
122
123    fn path4() -> Graph {
124        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
125    }
126
127    fn path5() -> Graph {
128        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)], false, Some(5)).unwrap()
129    }
130
131    fn k3() -> Graph {
132        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
133    }
134
135    fn k4() -> Graph {
136        Graph::from_edges(
137            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
138            false,
139            Some(4),
140        )
141        .unwrap()
142    }
143
144    fn cycle4() -> Graph {
145        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
146    }
147
148    fn cycle5() -> Graph {
149        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
150    }
151
152    fn star5() -> Graph {
153        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
154    }
155
156    fn paw() -> Graph {
157        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
158    }
159
160    // --- narumi_katayama_index ---
161
162    #[test]
163    fn nk_empty() {
164        let g = Graph::with_vertices(0);
165        assert!((narumi_katayama_index(&g).unwrap() - 1.0).abs() < 1e-10);
166    }
167
168    #[test]
169    fn nk_single_vertex() {
170        let g = Graph::with_vertices(1);
171        assert!((narumi_katayama_index(&g).unwrap() - 1.0).abs() < 1e-10);
172    }
173
174    #[test]
175    fn nk_isolated_vertices() {
176        let g = Graph::with_vertices(5);
177        assert!((narumi_katayama_index(&g).unwrap() - 1.0).abs() < 1e-10);
178    }
179
180    #[test]
181    fn nk_single_edge() {
182        // degrees [1,1] → NK = 1
183        assert!((narumi_katayama_index(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
184    }
185
186    #[test]
187    fn nk_path3() {
188        // degrees [1,2,1] → NK = 1·2·1 = 2
189        assert!((narumi_katayama_index(&path3()).unwrap() - 2.0).abs() < 1e-10);
190    }
191
192    #[test]
193    fn nk_path4() {
194        // degrees [1,2,2,1] → NK = 1·2·2·1 = 4
195        assert!((narumi_katayama_index(&path4()).unwrap() - 4.0).abs() < 1e-10);
196    }
197
198    #[test]
199    fn nk_path5() {
200        // degrees [1,2,2,2,1] → NK = 1·2·2·2·1 = 8
201        assert!((narumi_katayama_index(&path5()).unwrap() - 8.0).abs() < 1e-10);
202    }
203
204    #[test]
205    fn nk_k3() {
206        // degrees [2,2,2] → NK = 8
207        assert!((narumi_katayama_index(&k3()).unwrap() - 8.0).abs() < 1e-10);
208    }
209
210    #[test]
211    fn nk_k4() {
212        // degrees [3,3,3,3] → NK = 81
213        assert!((narumi_katayama_index(&k4()).unwrap() - 81.0).abs() < 1e-10);
214    }
215
216    #[test]
217    fn nk_cycle4() {
218        // degrees [2,2,2,2] → NK = 16
219        assert!((narumi_katayama_index(&cycle4()).unwrap() - 16.0).abs() < 1e-10);
220    }
221
222    #[test]
223    fn nk_cycle5() {
224        // degrees [2,2,2,2,2] → NK = 32
225        assert!((narumi_katayama_index(&cycle5()).unwrap() - 32.0).abs() < 1e-10);
226    }
227
228    #[test]
229    fn nk_star5() {
230        // degrees [4,1,1,1,1] → NK = 4
231        assert!((narumi_katayama_index(&star5()).unwrap() - 4.0).abs() < 1e-10);
232    }
233
234    #[test]
235    fn nk_paw() {
236        // degrees [2,2,3,1] → NK = 2·2·3·1 = 12
237        assert!((narumi_katayama_index(&paw()).unwrap() - 12.0).abs() < 1e-10);
238    }
239
240    #[test]
241    fn nk_regular_is_r_pow_n() {
242        // r-regular graph: NK = r^n
243        for g in &[k3(), k4(), cycle4(), cycle5()] {
244            let n = f64::from(g.vcount());
245            let r = g.degree(0).unwrap() as f64;
246            let expected = r.powf(n);
247            assert!((narumi_katayama_index(g).unwrap() - expected).abs() < 1e-6);
248        }
249    }
250
251    #[test]
252    fn nk_with_isolated() {
253        // 0-1 with isolated vertex 2
254        let g = Graph::from_edges(&[(0, 1)], false, Some(3)).unwrap();
255        // degrees [1,1,0] → NK = 1·1 = 1 (isolated vertex excluded)
256        assert!((narumi_katayama_index(&g).unwrap() - 1.0).abs() < 1e-10);
257    }
258
259    // --- first_multiplicative_zagreb ---
260
261    #[test]
262    fn pi1_empty() {
263        let g = Graph::with_vertices(0);
264        assert!((first_multiplicative_zagreb(&g).unwrap() - 1.0).abs() < 1e-10);
265    }
266
267    #[test]
268    fn pi1_single_vertex() {
269        let g = Graph::with_vertices(1);
270        assert!((first_multiplicative_zagreb(&g).unwrap() - 1.0).abs() < 1e-10);
271    }
272
273    #[test]
274    fn pi1_single_edge() {
275        // degrees [1,1] → Π₁ = 1²·1² = 1
276        assert!((first_multiplicative_zagreb(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
277    }
278
279    #[test]
280    fn pi1_path3() {
281        // degrees [1,2,1] → Π₁ = 1·4·1 = 4
282        assert!((first_multiplicative_zagreb(&path3()).unwrap() - 4.0).abs() < 1e-10);
283    }
284
285    #[test]
286    fn pi1_path4() {
287        // degrees [1,2,2,1] → Π₁ = 1·4·4·1 = 16
288        assert!((first_multiplicative_zagreb(&path4()).unwrap() - 16.0).abs() < 1e-10);
289    }
290
291    #[test]
292    fn pi1_k3() {
293        // degrees [2,2,2] → Π₁ = 4³ = 64
294        assert!((first_multiplicative_zagreb(&k3()).unwrap() - 64.0).abs() < 1e-10);
295    }
296
297    #[test]
298    fn pi1_k4() {
299        // degrees [3,3,3,3] → Π₁ = 9⁴ = 6561
300        assert!((first_multiplicative_zagreb(&k4()).unwrap() - 6561.0).abs() < 1e-10);
301    }
302
303    #[test]
304    fn pi1_cycle4() {
305        // degrees [2,2,2,2] → Π₁ = 4⁴ = 256
306        assert!((first_multiplicative_zagreb(&cycle4()).unwrap() - 256.0).abs() < 1e-10);
307    }
308
309    #[test]
310    fn pi1_star5() {
311        // degrees [4,1,1,1,1] → Π₁ = 16·1·1·1·1 = 16
312        assert!((first_multiplicative_zagreb(&star5()).unwrap() - 16.0).abs() < 1e-10);
313    }
314
315    #[test]
316    fn pi1_paw() {
317        // degrees [2,2,3,1] → Π₁ = 4·4·9·1 = 144
318        assert!((first_multiplicative_zagreb(&paw()).unwrap() - 144.0).abs() < 1e-10);
319    }
320
321    #[test]
322    fn pi1_is_nk_squared() {
323        // Π₁ = Π d(v)² = (Π d(v))² = NK² for graphs without isolated vertices
324        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
325            let nk = narumi_katayama_index(g).unwrap();
326            let pi1 = first_multiplicative_zagreb(g).unwrap();
327            assert!((pi1 - nk * nk).abs() < 1e-6);
328        }
329    }
330
331    #[test]
332    fn pi1_regular_formula() {
333        // r-regular: Π₁ = (r²)^n = r^(2n)
334        for g in &[k3(), k4(), cycle4(), cycle5()] {
335            let n = f64::from(g.vcount());
336            let r = g.degree(0).unwrap() as f64;
337            let expected = r.powf(2.0 * n);
338            assert!((first_multiplicative_zagreb(g).unwrap() - expected).abs() < 1e-4);
339        }
340    }
341
342    // --- second_multiplicative_zagreb ---
343
344    #[test]
345    fn pi2_empty() {
346        let g = Graph::with_vertices(0);
347        assert!((second_multiplicative_zagreb(&g).unwrap() - 1.0).abs() < 1e-10);
348    }
349
350    #[test]
351    fn pi2_single_vertex() {
352        let g = Graph::with_vertices(1);
353        assert!((second_multiplicative_zagreb(&g).unwrap() - 1.0).abs() < 1e-10);
354    }
355
356    #[test]
357    fn pi2_single_edge() {
358        // (1·1) = 1 → Π₂ = 1
359        assert!((second_multiplicative_zagreb(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
360    }
361
362    #[test]
363    fn pi2_path3() {
364        // (0,1): 1·2=2, (1,2): 2·1=2 → Π₂ = 4
365        assert!((second_multiplicative_zagreb(&path3()).unwrap() - 4.0).abs() < 1e-10);
366    }
367
368    #[test]
369    fn pi2_path4() {
370        // (0,1):1·2=2, (1,2):2·2=4, (2,3):2·1=2 → Π₂ = 2·4·2 = 16
371        assert!((second_multiplicative_zagreb(&path4()).unwrap() - 16.0).abs() < 1e-10);
372    }
373
374    #[test]
375    fn pi2_k3() {
376        // 3 edges, each 2·2=4 → Π₂ = 4³ = 64
377        assert!((second_multiplicative_zagreb(&k3()).unwrap() - 64.0).abs() < 1e-10);
378    }
379
380    #[test]
381    fn pi2_k4() {
382        // 6 edges, each 3·3=9 → Π₂ = 9⁶ = 531441
383        assert!((second_multiplicative_zagreb(&k4()).unwrap() - 531_441.0).abs() < 1e-6);
384    }
385
386    #[test]
387    fn pi2_cycle4() {
388        // 4 edges, each 2·2=4 → Π₂ = 4⁴ = 256
389        assert!((second_multiplicative_zagreb(&cycle4()).unwrap() - 256.0).abs() < 1e-10);
390    }
391
392    #[test]
393    fn pi2_cycle5() {
394        // 5 edges, each 2·2=4 → Π₂ = 4⁵ = 1024
395        assert!((second_multiplicative_zagreb(&cycle5()).unwrap() - 1024.0).abs() < 1e-10);
396    }
397
398    #[test]
399    fn pi2_star5() {
400        // 4 edges, each 4·1=4 → Π₂ = 4⁴ = 256
401        assert!((second_multiplicative_zagreb(&star5()).unwrap() - 256.0).abs() < 1e-10);
402    }
403
404    #[test]
405    fn pi2_paw() {
406        // degrees [2,2,3,1]
407        // (0,1):4, (0,2):6, (1,2):6, (2,3):3 → Π₂ = 4·6·6·3 = 432
408        assert!((second_multiplicative_zagreb(&paw()).unwrap() - 432.0).abs() < 1e-10);
409    }
410
411    #[test]
412    fn pi2_regular_formula() {
413        // r-regular: Π₂ = (r²)^m = r^(2m)
414        for g in &[k3(), k4(), cycle4(), cycle5()] {
415            let m = g.ecount() as f64;
416            let r = g.degree(0).unwrap() as f64;
417            let expected = r.powf(2.0 * m);
418            assert!((second_multiplicative_zagreb(g).unwrap() - expected).abs() < 1e-4);
419        }
420    }
421
422    // --- cross-consistency ---
423
424    #[test]
425    fn all_positive() {
426        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
427            assert!(narumi_katayama_index(g).unwrap() > 0.0);
428            assert!(first_multiplicative_zagreb(g).unwrap() > 0.0);
429            assert!(second_multiplicative_zagreb(g).unwrap() > 0.0);
430        }
431    }
432
433    #[test]
434    fn nk_leq_pi1() {
435        // NK = Π d(v) ≤ Π d(v)² = Π₁ for d(v)≥1
436        for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
437            let nk = narumi_katayama_index(g).unwrap();
438            let pi1 = first_multiplicative_zagreb(g).unwrap();
439            assert!(nk <= pi1 + 1e-8);
440        }
441    }
442
443    #[test]
444    fn log_pi2_equals_sum_log_deg_products() {
445        // ln(Π₂) = Σ_{(u,v)∈E} ln(d_u·d_v) = second Zagreb using logs
446        for g in &[path3(), k3(), cycle4(), star5(), paw()] {
447            let pi2 = second_multiplicative_zagreb(g).unwrap();
448            let mut log_sum = 0.0_f64;
449            for (u, v) in g.edges() {
450                if u == v {
451                    continue;
452                }
453                let du = g.degree(u).unwrap() as f64;
454                let dv = g.degree(v).unwrap() as f64;
455                log_sum += (du * dv).ln();
456            }
457            assert!((pi2.ln() - log_sum).abs() < 1e-8);
458        }
459    }
460}