Skip to main content

rust_igraph/algorithms/properties/
sombor_index.rs

1//! Sombor index variants (ALGO-TR-057).
2//!
3//! - **Sombor index** `SO(G) = Σ_{(u,v)∈E} √(d(u)² + d(v)²)`
4//!   Introduced by Gutman (2021). Degree-based geometric topological index.
5//! - **Reduced Sombor index** `SO_red(G) = Σ_{(u,v)∈E} √((d(u)-1)² + (d(v)-1)²)`
6//!   Uses reduced degrees (d-1). Zero contribution from pendant edges.
7//! - **Average Sombor index** `SO_avg(G) = SO(G) / m`
8//!   Sombor index normalized by edge count.
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 Sombor index.
21///
22/// `SO(G) = Σ_{(u,v)∈E} √(d(u)² + d(v)²)`
23///
24/// Self-loops are skipped.
25///
26/// # Examples
27///
28/// ```
29/// use rust_igraph::{Graph, sombor_index};
30///
31/// // K_3: each edge √(4+4) = 2√2, 3 edges → 6√2
32/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
33/// assert!((sombor_index(&g).unwrap() - 6.0 * std::f64::consts::SQRT_2).abs() < 1e-10);
34/// ```
35pub fn sombor_index(graph: &Graph) -> IgraphResult<f64> {
36    let mut so = 0.0_f64;
37
38    for (u, v) in graph.edges() {
39        if u == v {
40            continue;
41        }
42        let du = graph.degree(u)? as f64;
43        let dv = graph.degree(v)? as f64;
44        so += (du * du + dv * dv).sqrt();
45    }
46
47    Ok(so)
48}
49
50/// Compute the reduced Sombor index.
51///
52/// `SO_red(G) = Σ_{(u,v)∈E} √((d(u)-1)² + (d(v)-1)²)`
53///
54/// Uses reduced degrees `d(v)-1`. Self-loops are skipped.
55/// Pendant edges (where one endpoint has degree 1) contribute
56/// `√(0² + (d-1)²) = d-1`.
57///
58/// # Examples
59///
60/// ```
61/// use rust_igraph::{Graph, reduced_sombor_index};
62///
63/// // K_3: each edge √((2-1)²+(2-1)²) = √2, 3 edges → 3√2
64/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
65/// assert!((reduced_sombor_index(&g).unwrap() - 3.0 * std::f64::consts::SQRT_2).abs() < 1e-10);
66/// ```
67pub fn reduced_sombor_index(graph: &Graph) -> IgraphResult<f64> {
68    let mut so_red = 0.0_f64;
69
70    for (u, v) in graph.edges() {
71        if u == v {
72            continue;
73        }
74        let du = graph.degree(u)?;
75        let dv = graph.degree(v)?;
76        let a = if du > 0 { (du - 1) as f64 } else { 0.0 };
77        let b = if dv > 0 { (dv - 1) as f64 } else { 0.0 };
78        so_red += (a * a + b * b).sqrt();
79    }
80
81    Ok(so_red)
82}
83
84/// Compute the average Sombor index.
85///
86/// `SO_avg(G) = SO(G) / m`
87///
88/// Returns 0.0 for graphs with no edges.
89///
90/// # Examples
91///
92/// ```
93/// use rust_igraph::{Graph, average_sombor_index};
94///
95/// // K_3: SO = 6√2, m = 3 → SO_avg = 2√2
96/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
97/// assert!((average_sombor_index(&g).unwrap() - 2.0 * std::f64::consts::SQRT_2).abs() < 1e-10);
98/// ```
99pub fn average_sombor_index(graph: &Graph) -> IgraphResult<f64> {
100    let m = graph.ecount();
101    if m == 0 {
102        return Ok(0.0);
103    }
104    let so = sombor_index(graph)?;
105    Ok(so / m as f64)
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    fn single_edge() -> Graph {
113        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
114    }
115
116    fn path3() -> Graph {
117        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
118    }
119
120    fn path4() -> Graph {
121        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
122    }
123
124    fn k3() -> Graph {
125        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
126    }
127
128    fn k4() -> Graph {
129        Graph::from_edges(
130            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
131            false,
132            Some(4),
133        )
134        .unwrap()
135    }
136
137    fn cycle4() -> Graph {
138        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
139    }
140
141    fn cycle5() -> Graph {
142        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
143    }
144
145    fn star5() -> Graph {
146        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
147    }
148
149    fn paw() -> Graph {
150        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
151    }
152
153    // --- sombor_index ---
154
155    #[test]
156    fn so_empty() {
157        let g = Graph::with_vertices(0);
158        assert!((sombor_index(&g).unwrap()).abs() < 1e-10);
159    }
160
161    #[test]
162    fn so_isolated() {
163        let g = Graph::with_vertices(5);
164        assert!((sombor_index(&g).unwrap()).abs() < 1e-10);
165    }
166
167    #[test]
168    fn so_single_edge() {
169        // √(1+1) = √2
170        let expected = std::f64::consts::SQRT_2;
171        assert!((sombor_index(&single_edge()).unwrap() - expected).abs() < 1e-10);
172    }
173
174    #[test]
175    fn so_path3() {
176        // (0,1): √(1+4)=√5, (1,2): √(4+1)=√5
177        // SO = 2√5
178        let expected = 2.0 * 5.0_f64.sqrt();
179        assert!((sombor_index(&path3()).unwrap() - expected).abs() < 1e-10);
180    }
181
182    #[test]
183    fn so_path4() {
184        // (0,1): √(1+4)=√5, (1,2): √(4+4)=2√2, (2,3): √(4+1)=√5
185        // SO = 2√5 + 2√2
186        let expected = 2.0 * 5.0_f64.sqrt() + 2.0 * std::f64::consts::SQRT_2;
187        assert!((sombor_index(&path4()).unwrap() - expected).abs() < 1e-10);
188    }
189
190    #[test]
191    fn so_k3() {
192        // each edge √(4+4) = 2√2, 3 edges → 6√2
193        let expected = 6.0 * std::f64::consts::SQRT_2;
194        assert!((sombor_index(&k3()).unwrap() - expected).abs() < 1e-10);
195    }
196
197    #[test]
198    fn so_k4() {
199        // each edge √(9+9) = 3√2, 6 edges → 18√2
200        let expected = 18.0 * std::f64::consts::SQRT_2;
201        assert!((sombor_index(&k4()).unwrap() - expected).abs() < 1e-10);
202    }
203
204    #[test]
205    fn so_cycle4() {
206        // each edge √(4+4) = 2√2, 4 edges → 8√2
207        let expected = 8.0 * std::f64::consts::SQRT_2;
208        assert!((sombor_index(&cycle4()).unwrap() - expected).abs() < 1e-10);
209    }
210
211    #[test]
212    fn so_cycle5() {
213        // each edge √(4+4) = 2√2, 5 edges → 10√2
214        let expected = 10.0 * std::f64::consts::SQRT_2;
215        assert!((sombor_index(&cycle5()).unwrap() - expected).abs() < 1e-10);
216    }
217
218    #[test]
219    fn so_star5() {
220        // each edge √(16+1) = √17, 4 edges → 4√17
221        let expected = 4.0 * 17.0_f64.sqrt();
222        assert!((sombor_index(&star5()).unwrap() - expected).abs() < 1e-10);
223    }
224
225    #[test]
226    fn so_paw() {
227        // degrees [2,2,3,1]
228        // (0,1): √(4+4)=2√2, (0,2): √(4+9)=√13, (1,2): √(4+9)=√13, (2,3): √(9+1)=√10
229        let expected = 2.0 * std::f64::consts::SQRT_2 + 2.0 * 13.0_f64.sqrt() + 10.0_f64.sqrt();
230        assert!((sombor_index(&paw()).unwrap() - expected).abs() < 1e-10);
231    }
232
233    #[test]
234    fn so_regular_formula() {
235        // r-regular: SO = m · √(2r²) = m · r√2
236        for g in &[k3(), k4(), cycle4(), cycle5()] {
237            let m = g.ecount() as f64;
238            let r = g.degree(0).unwrap() as f64;
239            let expected = m * r * std::f64::consts::SQRT_2;
240            assert!((sombor_index(g).unwrap() - expected).abs() < 1e-8);
241        }
242    }
243
244    // --- reduced_sombor_index ---
245
246    #[test]
247    fn so_red_empty() {
248        let g = Graph::with_vertices(0);
249        assert!((reduced_sombor_index(&g).unwrap()).abs() < 1e-10);
250    }
251
252    #[test]
253    fn so_red_single_edge() {
254        // √((1-1)²+(1-1)²) = 0
255        assert!((reduced_sombor_index(&single_edge()).unwrap()).abs() < 1e-10);
256    }
257
258    #[test]
259    fn so_red_path3() {
260        // (0,1): √(0²+1²)=1, (1,2): √(1²+0²)=1
261        // SO_red = 2
262        assert!((reduced_sombor_index(&path3()).unwrap() - 2.0).abs() < 1e-10);
263    }
264
265    #[test]
266    fn so_red_path4() {
267        // (0,1): √(0+1)=1, (1,2): √(1+1)=√2, (2,3): √(1+0)=1
268        // SO_red = 2 + √2
269        let expected = 2.0 + std::f64::consts::SQRT_2;
270        assert!((reduced_sombor_index(&path4()).unwrap() - expected).abs() < 1e-10);
271    }
272
273    #[test]
274    fn so_red_k3() {
275        // each edge √(1+1)=√2, 3 edges → 3√2
276        let expected = 3.0 * std::f64::consts::SQRT_2;
277        assert!((reduced_sombor_index(&k3()).unwrap() - expected).abs() < 1e-10);
278    }
279
280    #[test]
281    fn so_red_k4() {
282        // each edge √(4+4)=2√2, 6 edges → 12√2
283        let expected = 12.0 * std::f64::consts::SQRT_2;
284        assert!((reduced_sombor_index(&k4()).unwrap() - expected).abs() < 1e-10);
285    }
286
287    #[test]
288    fn so_red_cycle4() {
289        // each edge √(1+1)=√2, 4 edges → 4√2
290        let expected = 4.0 * std::f64::consts::SQRT_2;
291        assert!((reduced_sombor_index(&cycle4()).unwrap() - expected).abs() < 1e-10);
292    }
293
294    #[test]
295    fn so_red_cycle5() {
296        // each edge √(1+1)=√2, 5 edges → 5√2
297        let expected = 5.0 * std::f64::consts::SQRT_2;
298        assert!((reduced_sombor_index(&cycle5()).unwrap() - expected).abs() < 1e-10);
299    }
300
301    #[test]
302    fn so_red_star5() {
303        // (0,leaf): √((4-1)²+(1-1)²) = √(9+0) = 3, 4 edges → 12
304        assert!((reduced_sombor_index(&star5()).unwrap() - 12.0).abs() < 1e-10);
305    }
306
307    #[test]
308    fn so_red_paw() {
309        // degrees [2,2,3,1]
310        // (0,1): √(1+1)=√2, (0,2): √(1+4)=√5, (1,2): √(1+4)=√5, (2,3): √(4+0)=2
311        let expected = std::f64::consts::SQRT_2 + 2.0 * 5.0_f64.sqrt() + 2.0;
312        assert!((reduced_sombor_index(&paw()).unwrap() - expected).abs() < 1e-10);
313    }
314
315    #[test]
316    fn so_red_regular_formula() {
317        // r-regular: SO_red = m · √(2(r-1)²) = m · (r-1)·√2
318        for g in &[k3(), k4(), cycle4(), cycle5()] {
319            let m = g.ecount() as f64;
320            let r = g.degree(0).unwrap() as f64;
321            let expected = m * (r - 1.0) * std::f64::consts::SQRT_2;
322            assert!((reduced_sombor_index(g).unwrap() - expected).abs() < 1e-8);
323        }
324    }
325
326    #[test]
327    fn so_red_leq_so() {
328        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
329            assert!(reduced_sombor_index(g).unwrap() <= sombor_index(g).unwrap() + 1e-10);
330        }
331    }
332
333    // --- average_sombor_index ---
334
335    #[test]
336    fn so_avg_empty() {
337        let g = Graph::with_vertices(0);
338        assert!((average_sombor_index(&g).unwrap()).abs() < 1e-10);
339    }
340
341    #[test]
342    fn so_avg_isolated() {
343        let g = Graph::with_vertices(5);
344        assert!((average_sombor_index(&g).unwrap()).abs() < 1e-10);
345    }
346
347    #[test]
348    fn so_avg_single_edge() {
349        // SO/m = √2/1 = √2
350        let expected = std::f64::consts::SQRT_2;
351        assert!((average_sombor_index(&single_edge()).unwrap() - expected).abs() < 1e-10);
352    }
353
354    #[test]
355    fn so_avg_k3() {
356        // SO = 6√2, m = 3 → 2√2
357        let expected = 2.0 * std::f64::consts::SQRT_2;
358        assert!((average_sombor_index(&k3()).unwrap() - expected).abs() < 1e-10);
359    }
360
361    #[test]
362    fn so_avg_k4() {
363        // SO = 18√2, m = 6 → 3√2
364        let expected = 3.0 * std::f64::consts::SQRT_2;
365        assert!((average_sombor_index(&k4()).unwrap() - expected).abs() < 1e-10);
366    }
367
368    #[test]
369    fn so_avg_regular_is_r_sqrt2() {
370        // r-regular: SO_avg = SO/m = r√2
371        for g in &[k3(), k4(), cycle4(), cycle5()] {
372            let r = g.degree(0).unwrap() as f64;
373            let expected = r * std::f64::consts::SQRT_2;
374            assert!((average_sombor_index(g).unwrap() - expected).abs() < 1e-8);
375        }
376    }
377
378    #[test]
379    fn so_avg_consistency() {
380        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
381            let m = g.ecount();
382            if m > 0 {
383                let so = sombor_index(g).unwrap();
384                let so_avg = average_sombor_index(g).unwrap();
385                assert!((so_avg - so / m as f64).abs() < 1e-10);
386            }
387        }
388    }
389
390    // --- cross-consistency ---
391
392    #[test]
393    fn all_nonneg() {
394        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
395            assert!(sombor_index(g).unwrap() >= -1e-10);
396            assert!(reduced_sombor_index(g).unwrap() >= -1e-10);
397            assert!(average_sombor_index(g).unwrap() >= -1e-10);
398        }
399    }
400
401    #[test]
402    fn so_geq_degree_sum() {
403        // SO(G) ≥ DS(G) since √(a²+b²) ≥ √(a+b) for a,b ≥ 1 is not always true,
404        // but SO(G) ≥ m·√2 for non-empty graphs (minimum when d_u=d_v=1)
405        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5()] {
406            let m = g.ecount() as f64;
407            assert!(sombor_index(g).unwrap() >= m * std::f64::consts::SQRT_2 - 1e-10);
408        }
409    }
410}