Skip to main content

rust_igraph/algorithms/properties/
sombor_variants.rs

1//! Sombor index variants (ALGO-TR-070).
2//!
3//! Extensions of the Sombor index family introduced by Gutman (2021):
4//!
5//! - **Elliptic Sombor** `ESO(G) = Σ_{(u,v)∈E} (d(u)+d(v)) √(d(u)²+d(v)²)`
6//! - **Modified Sombor** `mSO(G) = Σ_{(u,v)∈E} 1/√(d(u)²+d(v)²)`
7//! - **Sombor coindex** `\overline{SO}(G) = Σ_{u≠v, (u,v)∉E} √(d(u)²+d(v)²)`
8//!   Sum over non-adjacent vertex pairs.
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 elliptic Sombor index.
21///
22/// `ESO(G) = Σ_{(u,v)∈E} (d(u)+d(v)) √(d(u)²+d(v)²)`
23///
24/// Self-loops are skipped.
25///
26/// # Examples
27///
28/// ```
29/// use rust_igraph::{Graph, elliptic_sombor_index};
30///
31/// // K_3: 3 edges, d=2 for all. Each term: (2+2)√(4+4) = 4√8
32/// // ESO = 3 × 4√8 = 12√8 ≈ 33.941
33/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
34/// let eso = elliptic_sombor_index(&g).unwrap();
35/// assert!((eso - 12.0 * 8.0_f64.sqrt()).abs() < 1e-10);
36/// ```
37pub fn elliptic_sombor_index(graph: &Graph) -> IgraphResult<f64> {
38    let mut eso = 0.0_f64;
39
40    for (u, v) in graph.edges() {
41        if u == v {
42            continue;
43        }
44        let du = graph.degree(u)? as f64;
45        let dv = graph.degree(v)? as f64;
46        eso += (du + dv) * (du * du + dv * dv).sqrt();
47    }
48
49    Ok(eso)
50}
51
52/// Compute the modified Sombor index.
53///
54/// `mSO(G) = Σ_{(u,v)∈E} 1/√(d(u)²+d(v)²)`
55///
56/// Self-loops are skipped.
57///
58/// # Examples
59///
60/// ```
61/// use rust_igraph::{Graph, modified_sombor_index};
62///
63/// // K_3: 3 edges, each 1/√(4+4) = 1/√8
64/// // mSO = 3/√8 ≈ 1.0607
65/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
66/// let mso = modified_sombor_index(&g).unwrap();
67/// assert!((mso - 3.0 / 8.0_f64.sqrt()).abs() < 1e-10);
68/// ```
69pub fn modified_sombor_index(graph: &Graph) -> IgraphResult<f64> {
70    let mut mso = 0.0_f64;
71
72    for (u, v) in graph.edges() {
73        if u == v {
74            continue;
75        }
76        let du = graph.degree(u)? as f64;
77        let dv = graph.degree(v)? as f64;
78        let s = du * du + dv * dv;
79        if s > 0.0 {
80            mso += 1.0 / s.sqrt();
81        }
82    }
83
84    Ok(mso)
85}
86
87/// Compute the Sombor coindex.
88///
89/// `\overline{SO}(G) = Σ_{u<v, (u,v)∉E} √(d(u)²+d(v)²)`
90///
91/// Sum over non-adjacent distinct vertex pairs.
92///
93/// # Examples
94///
95/// ```
96/// use rust_igraph::{Graph, sombor_coindex};
97///
98/// // K_3: no non-adjacent pairs → 0
99/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
100/// assert!((sombor_coindex(&g).unwrap()).abs() < 1e-10);
101///
102/// // Path 0-1-2: non-adjacent pair (0,2), d=(1,1), √(1+1)=√2
103/// let p = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
104/// assert!((sombor_coindex(&p).unwrap() - 2.0_f64.sqrt()).abs() < 1e-10);
105/// ```
106pub fn sombor_coindex(graph: &Graph) -> IgraphResult<f64> {
107    let n = graph.vcount();
108    let mut degrees = Vec::with_capacity(n as usize);
109    for v in 0..n {
110        degrees.push(graph.degree(v)? as f64);
111    }
112
113    let mut total_sum = 0.0_f64;
114    for u in 0..n {
115        for v in (u + 1)..n {
116            let du = degrees[u as usize];
117            let dv = degrees[v as usize];
118            total_sum += (du * du + dv * dv).sqrt();
119        }
120    }
121
122    let mut edge_sum = 0.0_f64;
123    for (u, v) in graph.edges() {
124        if u == v {
125            continue;
126        }
127        let du = degrees[u as usize];
128        let dv = degrees[v as usize];
129        edge_sum += (du * du + dv * dv).sqrt();
130    }
131
132    Ok(total_sum - edge_sum)
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    fn single_edge() -> Graph {
140        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
141    }
142
143    fn path3() -> Graph {
144        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
145    }
146
147    fn path4() -> Graph {
148        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
149    }
150
151    fn k3() -> Graph {
152        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
153    }
154
155    fn k4() -> Graph {
156        Graph::from_edges(
157            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
158            false,
159            Some(4),
160        )
161        .unwrap()
162    }
163
164    fn cycle4() -> Graph {
165        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
166    }
167
168    fn cycle5() -> Graph {
169        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
170    }
171
172    fn star5() -> Graph {
173        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
174    }
175
176    fn paw() -> Graph {
177        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
178    }
179
180    // --- elliptic_sombor_index ---
181
182    #[test]
183    fn eso_empty() {
184        let g = Graph::with_vertices(0);
185        assert!(elliptic_sombor_index(&g).unwrap().abs() < 1e-10);
186    }
187
188    #[test]
189    fn eso_isolated() {
190        let g = Graph::with_vertices(5);
191        assert!(elliptic_sombor_index(&g).unwrap().abs() < 1e-10);
192    }
193
194    #[test]
195    fn eso_single_edge() {
196        // (1+1)√(1+1) = 2√2
197        let expected = 2.0 * 2.0_f64.sqrt();
198        assert!((elliptic_sombor_index(&single_edge()).unwrap() - expected).abs() < 1e-10);
199    }
200
201    #[test]
202    fn eso_path3() {
203        // e(0,1): d=(1,2), (3)√(1+4) = 3√5
204        // e(1,2): d=(2,1), same → 3√5
205        let expected = 6.0 * 5.0_f64.sqrt();
206        assert!((elliptic_sombor_index(&path3()).unwrap() - expected).abs() < 1e-10);
207    }
208
209    #[test]
210    fn eso_k3() {
211        // 3 edges, d=(2,2), (4)√(4+4) = 4√8
212        let expected = 12.0 * 8.0_f64.sqrt();
213        assert!((elliptic_sombor_index(&k3()).unwrap() - expected).abs() < 1e-10);
214    }
215
216    #[test]
217    fn eso_k4() {
218        // 6 edges, d=(3,3), (6)√(9+9) = 6√18
219        let expected = 36.0 * 18.0_f64.sqrt();
220        assert!((elliptic_sombor_index(&k4()).unwrap() - expected).abs() < 1e-10);
221    }
222
223    #[test]
224    fn eso_cycle4() {
225        // 4 edges, d=(2,2), (4)√8
226        let expected = 16.0 * 8.0_f64.sqrt();
227        assert!((elliptic_sombor_index(&cycle4()).unwrap() - expected).abs() < 1e-10);
228    }
229
230    #[test]
231    fn eso_cycle5() {
232        // 5 edges, d=(2,2), (4)√8
233        let expected = 20.0 * 8.0_f64.sqrt();
234        assert!((elliptic_sombor_index(&cycle5()).unwrap() - expected).abs() < 1e-10);
235    }
236
237    #[test]
238    fn eso_star5() {
239        // 4 edges, d=(4,1), (5)√(16+1) = 5√17
240        let expected = 20.0 * 17.0_f64.sqrt();
241        assert!((elliptic_sombor_index(&star5()).unwrap() - expected).abs() < 1e-10);
242    }
243
244    #[test]
245    fn eso_paw() {
246        // e(0,1): d=(2,2), (4)√8
247        // e(0,2): d=(2,3), (5)√(4+9) = 5√13
248        // e(1,2): d=(2,3), (5)√13
249        // e(2,3): d=(3,1), (4)√(9+1) = 4√10
250        let expected = 4.0 * 8.0_f64.sqrt() + 10.0 * 13.0_f64.sqrt() + 4.0 * 10.0_f64.sqrt();
251        assert!((elliptic_sombor_index(&paw()).unwrap() - expected).abs() < 1e-10);
252    }
253
254    #[test]
255    fn eso_regular_formula() {
256        // r-regular: ESO = m × 2r × √(2r²) = m × 2r × r√2 = 2mr²√2
257        for g in &[k3(), k4(), cycle4(), cycle5()] {
258            let m = g.ecount() as f64;
259            let r = g.degree(0).unwrap() as f64;
260            let expected = 2.0 * m * r * r * 2.0_f64.sqrt();
261            assert!((elliptic_sombor_index(g).unwrap() - expected).abs() < 1e-8);
262        }
263    }
264
265    // --- modified_sombor_index ---
266
267    #[test]
268    fn mso_empty() {
269        let g = Graph::with_vertices(0);
270        assert!(modified_sombor_index(&g).unwrap().abs() < 1e-10);
271    }
272
273    #[test]
274    fn mso_isolated() {
275        let g = Graph::with_vertices(5);
276        assert!(modified_sombor_index(&g).unwrap().abs() < 1e-10);
277    }
278
279    #[test]
280    fn mso_single_edge() {
281        // 1/√(1+1) = 1/√2
282        let expected = 1.0 / 2.0_f64.sqrt();
283        assert!((modified_sombor_index(&single_edge()).unwrap() - expected).abs() < 1e-10);
284    }
285
286    #[test]
287    fn mso_path3() {
288        // 2 edges, each 1/√(1+4) = 1/√5
289        let expected = 2.0 / 5.0_f64.sqrt();
290        assert!((modified_sombor_index(&path3()).unwrap() - expected).abs() < 1e-10);
291    }
292
293    #[test]
294    fn mso_k3() {
295        // 3 × 1/√8
296        let expected = 3.0 / 8.0_f64.sqrt();
297        assert!((modified_sombor_index(&k3()).unwrap() - expected).abs() < 1e-10);
298    }
299
300    #[test]
301    fn mso_k4() {
302        // 6 × 1/√18
303        let expected = 6.0 / 18.0_f64.sqrt();
304        assert!((modified_sombor_index(&k4()).unwrap() - expected).abs() < 1e-10);
305    }
306
307    #[test]
308    fn mso_cycle4() {
309        // 4 × 1/√8
310        let expected = 4.0 / 8.0_f64.sqrt();
311        assert!((modified_sombor_index(&cycle4()).unwrap() - expected).abs() < 1e-10);
312    }
313
314    #[test]
315    fn mso_star5() {
316        // 4 × 1/√(16+1) = 4/√17
317        let expected = 4.0 / 17.0_f64.sqrt();
318        assert!((modified_sombor_index(&star5()).unwrap() - expected).abs() < 1e-10);
319    }
320
321    #[test]
322    fn mso_paw() {
323        // 1/√8 + 2/√13 + 1/√10
324        let expected = 1.0 / 8.0_f64.sqrt() + 2.0 / 13.0_f64.sqrt() + 1.0 / 10.0_f64.sqrt();
325        assert!((modified_sombor_index(&paw()).unwrap() - expected).abs() < 1e-10);
326    }
327
328    #[test]
329    fn mso_regular_formula() {
330        // r-regular: mSO = m/(r√2)
331        for g in &[k3(), k4(), cycle4(), cycle5()] {
332            let m = g.ecount() as f64;
333            let r = g.degree(0).unwrap() as f64;
334            let expected = m / (r * 2.0_f64.sqrt());
335            assert!((modified_sombor_index(g).unwrap() - expected).abs() < 1e-8);
336        }
337    }
338
339    #[test]
340    fn mso_reciprocal_of_sombor() {
341        // mSO × SO/m² is a useful ratio for regular graphs
342        // For r-regular: SO = m·r√2, mSO = m/(r√2)
343        // SO × mSO = m²
344        for g in &[k3(), k4(), cycle4(), cycle5()] {
345            let m = g.ecount() as f64;
346            let so = crate::sombor_index(g).unwrap();
347            let mso = modified_sombor_index(g).unwrap();
348            assert!((so * mso - m * m).abs() < 1e-6);
349        }
350    }
351
352    // --- sombor_coindex ---
353
354    #[test]
355    fn sco_empty() {
356        let g = Graph::with_vertices(0);
357        assert!(sombor_coindex(&g).unwrap().abs() < 1e-10);
358    }
359
360    #[test]
361    fn sco_isolated() {
362        // All pairs have d=0 → √0 = 0
363        let g = Graph::with_vertices(5);
364        assert!(sombor_coindex(&g).unwrap().abs() < 1e-10);
365    }
366
367    #[test]
368    fn sco_single_edge() {
369        // No non-adjacent pairs (only 2 vertices)
370        assert!(sombor_coindex(&single_edge()).unwrap().abs() < 1e-10);
371    }
372
373    #[test]
374    fn sco_k3() {
375        // Complete graph: no non-adjacent pairs → 0
376        assert!(sombor_coindex(&k3()).unwrap().abs() < 1e-10);
377    }
378
379    #[test]
380    fn sco_k4() {
381        assert!(sombor_coindex(&k4()).unwrap().abs() < 1e-10);
382    }
383
384    #[test]
385    fn sco_path3() {
386        // Non-adj: (0,2), d=(1,1), √(1+1)=√2
387        let expected = 2.0_f64.sqrt();
388        assert!((sombor_coindex(&path3()).unwrap() - expected).abs() < 1e-10);
389    }
390
391    #[test]
392    fn sco_path4() {
393        // Non-adj pairs: (0,2) d=(1,2), (0,3) d=(1,1), (1,3) d=(2,1)
394        // √(1+4) + √(1+1) + √(4+1) = 2√5 + √2
395        let expected = 2.0 * 5.0_f64.sqrt() + 2.0_f64.sqrt();
396        assert!((sombor_coindex(&path4()).unwrap() - expected).abs() < 1e-10);
397    }
398
399    #[test]
400    fn sco_cycle4() {
401        // C_4: edges (0,1),(1,2),(2,3),(3,0). Non-adj: (0,2),(1,3)
402        // d=(2,2), √(4+4) = √8. Two pairs → 2√8
403        let expected = 2.0 * 8.0_f64.sqrt();
404        assert!((sombor_coindex(&cycle4()).unwrap() - expected).abs() < 1e-10);
405    }
406
407    #[test]
408    fn sco_star5() {
409        // S_5: edges (0,1),(0,2),(0,3),(0,4). Non-adj: C(4,2)=6 leaf pairs
410        // Each leaf pair d=(1,1), √(1+1)=√2. Total = 6√2
411        let expected = 6.0 * 2.0_f64.sqrt();
412        assert!((sombor_coindex(&star5()).unwrap() - expected).abs() < 1e-10);
413    }
414
415    #[test]
416    fn sco_paw() {
417        // Paw: edges (0,1),(0,2),(1,2),(2,3). 4 vertices, C(4,2)=6 pairs, 4 edges → 2 non-adj
418        // Non-adj: (0,3) d=(2,1), (1,3) d=(2,1)
419        // √(4+1) + √(4+1) = 2√5
420        let expected = 2.0 * 5.0_f64.sqrt();
421        assert!((sombor_coindex(&paw()).unwrap() - expected).abs() < 1e-10);
422    }
423
424    #[test]
425    fn sco_complement_relation() {
426        // SO(G) + \bar{SO}(G) = Σ_{u<v} √(d(u)²+d(v)²) (all pairs)
427        for g in &[path3(), k3(), cycle4(), star5(), paw()] {
428            let so = crate::sombor_index(g).unwrap();
429            let sco = sombor_coindex(g).unwrap();
430            let n = g.vcount();
431            let mut total = 0.0_f64;
432            for u in 0..n {
433                for v in (u + 1)..n {
434                    let du = g.degree(u).unwrap() as f64;
435                    let dv = g.degree(v).unwrap() as f64;
436                    total += (du * du + dv * dv).sqrt();
437                }
438            }
439            assert!((so + sco - total).abs() < 1e-8);
440        }
441    }
442
443    // --- cross-consistency ---
444
445    #[test]
446    fn all_positive_for_nontrivial() {
447        for g in &[path3(), k3(), k4(), cycle4(), star5(), paw()] {
448            assert!(elliptic_sombor_index(g).unwrap() > 0.0);
449            assert!(modified_sombor_index(g).unwrap() > 0.0);
450        }
451    }
452
453    #[test]
454    fn eso_ge_sombor() {
455        // ESO(G) ≥ SO(G) because (d(u)+d(v)) ≥ 1 for edges with d≥1
456        // Actually ESO = (du+dv)·√(du²+dv²) and SO = √(du²+dv²)
457        // So ESO = (du+dv)·SO_term ≥ 2·SO_term for du,dv≥1
458        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
459            let eso = elliptic_sombor_index(g).unwrap();
460            let so = crate::sombor_index(g).unwrap();
461            assert!(eso >= so - 1e-10);
462        }
463    }
464}