rust_igraph/algorithms/properties/
edge_degree_norm.rs1#![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
20pub fn edge_inverse_degree_sum(graph: &Graph) -> IgraphResult<f64> {
38 let mut result = 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 let s = du + dv;
47 if s == 0.0 {
48 continue;
49 }
50 result += 1.0 / s;
51 }
52
53 Ok(result)
54}
55
56pub fn edge_degree_diff_ratio(graph: &Graph) -> IgraphResult<f64> {
73 let mut result = 0.0_f64;
74
75 for (u, v) in graph.edges() {
76 if u == v {
77 continue;
78 }
79 let du = graph.degree(u)?;
80 let dv = graph.degree(v)?;
81 let s = du + dv;
82 if s == 0 {
83 continue;
84 }
85 result += du.abs_diff(dv) as f64 / s as f64;
86 }
87
88 Ok(result)
89}
90
91pub fn edge_degree_sorensen(graph: &Graph) -> IgraphResult<f64> {
110 let mut result = 0.0_f64;
111
112 for (u, v) in graph.edges() {
113 if u == v {
114 continue;
115 }
116 let du = graph.degree(u)?;
117 let dv = graph.degree(v)?;
118 let s = du + dv;
119 if s == 0 {
120 continue;
121 }
122 let min_d = du.min(dv);
123 result += 2.0 * min_d as f64 / s as f64;
124 }
125
126 Ok(result)
127}
128
129pub fn edge_degree_product_ratio(graph: &Graph) -> IgraphResult<f64> {
146 let mut result = 0.0_f64;
147
148 for (u, v) in graph.edges() {
149 if u == v {
150 continue;
151 }
152 let du = graph.degree(u)? as f64;
153 let dv = graph.degree(v)? as f64;
154 let s = du + dv;
155 if s == 0.0 {
156 continue;
157 }
158 result += du * dv / (s * s);
159 }
160
161 Ok(result)
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 fn single_edge() -> Graph {
169 Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
170 }
171
172 fn path3() -> Graph {
173 Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
174 }
175
176 fn k3() -> Graph {
177 Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
178 }
179
180 fn k4() -> Graph {
181 Graph::from_edges(
182 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
183 false,
184 Some(4),
185 )
186 .unwrap()
187 }
188
189 fn cycle4() -> Graph {
190 Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
191 }
192
193 fn star5() -> Graph {
194 Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
195 }
196
197 fn paw() -> Graph {
198 Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
199 }
200
201 #[test]
204 fn inv_empty() {
205 let g = Graph::with_vertices(0);
206 assert!(edge_inverse_degree_sum(&g).unwrap().abs() < 1e-10);
207 }
208
209 #[test]
210 fn inv_isolated() {
211 let g = Graph::with_vertices(5);
212 assert!(edge_inverse_degree_sum(&g).unwrap().abs() < 1e-10);
213 }
214
215 #[test]
216 fn inv_k3() {
217 assert!((edge_inverse_degree_sum(&k3()).unwrap() - 0.75).abs() < 1e-10);
219 }
220
221 #[test]
222 fn inv_k4() {
223 assert!((edge_inverse_degree_sum(&k4()).unwrap() - 1.0).abs() < 1e-10);
225 }
226
227 #[test]
228 fn inv_single_edge() {
229 assert!((edge_inverse_degree_sum(&single_edge()).unwrap() - 0.5).abs() < 1e-10);
231 }
232
233 #[test]
234 fn inv_star5() {
235 assert!((edge_inverse_degree_sum(&star5()).unwrap() - 0.8).abs() < 1e-10);
237 }
238
239 #[test]
240 fn inv_path3() {
241 let expected = 2.0 / 3.0;
243 assert!((edge_inverse_degree_sum(&path3()).unwrap() - expected).abs() < 1e-10);
244 }
245
246 #[test]
247 fn inv_paw() {
248 let expected = 0.25 + 0.2 + 0.2 + 0.25;
250 assert!((edge_inverse_degree_sum(&paw()).unwrap() - expected).abs() < 1e-10);
251 }
252
253 #[test]
256 fn diff_empty() {
257 let g = Graph::with_vertices(0);
258 assert!(edge_degree_diff_ratio(&g).unwrap().abs() < 1e-10);
259 }
260
261 #[test]
262 fn diff_regular() {
263 assert!(edge_degree_diff_ratio(&k3()).unwrap().abs() < 1e-10);
265 assert!(edge_degree_diff_ratio(&k4()).unwrap().abs() < 1e-10);
266 assert!(edge_degree_diff_ratio(&cycle4()).unwrap().abs() < 1e-10);
267 }
268
269 #[test]
270 fn diff_single_edge() {
271 assert!(edge_degree_diff_ratio(&single_edge()).unwrap().abs() < 1e-10);
273 }
274
275 #[test]
276 fn diff_star5() {
277 assert!((edge_degree_diff_ratio(&star5()).unwrap() - 2.4).abs() < 1e-10);
279 }
280
281 #[test]
282 fn diff_path3() {
283 let expected = 2.0 / 3.0;
285 assert!((edge_degree_diff_ratio(&path3()).unwrap() - expected).abs() < 1e-10);
286 }
287
288 #[test]
289 fn diff_paw() {
290 let expected = 0.0 + 0.2 + 0.2 + 0.5;
292 assert!((edge_degree_diff_ratio(&paw()).unwrap() - expected).abs() < 1e-10);
293 }
294
295 #[test]
298 fn sor_empty() {
299 let g = Graph::with_vertices(0);
300 assert!(edge_degree_sorensen(&g).unwrap().abs() < 1e-10);
301 }
302
303 #[test]
304 fn sor_regular() {
305 assert!((edge_degree_sorensen(&k3()).unwrap() - 3.0).abs() < 1e-10);
307 assert!((edge_degree_sorensen(&k4()).unwrap() - 6.0).abs() < 1e-10);
308 assert!((edge_degree_sorensen(&cycle4()).unwrap() - 4.0).abs() < 1e-10);
309 }
310
311 #[test]
312 fn sor_single_edge() {
313 assert!((edge_degree_sorensen(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
315 }
316
317 #[test]
318 fn sor_star5() {
319 assert!((edge_degree_sorensen(&star5()).unwrap() - 1.6).abs() < 1e-10);
321 }
322
323 #[test]
324 fn sor_path3() {
325 let expected = 4.0 / 3.0;
327 assert!((edge_degree_sorensen(&path3()).unwrap() - expected).abs() < 1e-10);
328 }
329
330 #[test]
331 fn sor_paw() {
332 let expected = 1.0 + 0.8 + 0.8 + 0.5;
334 assert!((edge_degree_sorensen(&paw()).unwrap() - expected).abs() < 1e-10);
335 }
336
337 #[test]
340 fn pr_empty() {
341 let g = Graph::with_vertices(0);
342 assert!(edge_degree_product_ratio(&g).unwrap().abs() < 1e-10);
343 }
344
345 #[test]
346 fn pr_regular() {
347 assert!((edge_degree_product_ratio(&k3()).unwrap() - 0.75).abs() < 1e-10);
349 assert!((edge_degree_product_ratio(&k4()).unwrap() - 1.5).abs() < 1e-10);
350 assert!((edge_degree_product_ratio(&cycle4()).unwrap() - 1.0).abs() < 1e-10);
351 }
352
353 #[test]
354 fn pr_single_edge() {
355 assert!((edge_degree_product_ratio(&single_edge()).unwrap() - 0.25).abs() < 1e-10);
357 }
358
359 #[test]
360 fn pr_star5() {
361 assert!((edge_degree_product_ratio(&star5()).unwrap() - 0.64).abs() < 1e-10);
363 }
364
365 #[test]
366 fn pr_path3() {
367 let expected = 4.0 / 9.0;
369 assert!((edge_degree_product_ratio(&path3()).unwrap() - expected).abs() < 1e-10);
370 }
371
372 #[test]
373 fn pr_paw() {
374 let expected = 0.25 + 6.0 / 25.0 + 6.0 / 25.0 + 3.0 / 16.0;
376 assert!((edge_degree_product_ratio(&paw()).unwrap() - expected).abs() < 1e-10);
377 }
378
379 #[test]
382 fn sorensen_plus_diff_equals_m() {
383 for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
387 let sor = edge_degree_sorensen(g).unwrap();
388 let diff = edge_degree_diff_ratio(g).unwrap();
389 let m = g.edges().filter(|&(u, v)| u != v).count() as f64;
390 assert!(
391 (sor + diff - m).abs() < 1e-10,
392 "sorensen({sor}) + diff({diff}) != m({m})"
393 );
394 }
395 }
396
397 #[test]
398 fn product_ratio_le_quarter_m() {
399 for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
401 let pr = edge_degree_product_ratio(g).unwrap();
402 let m = g.edges().filter(|&(u, v)| u != v).count() as f64;
403 assert!(pr <= m / 4.0 + 1e-10);
404 }
405 }
406
407 #[test]
408 fn diff_nonnegative() {
409 for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
410 assert!(edge_degree_diff_ratio(g).unwrap() >= -1e-10);
411 }
412 }
413
414 #[test]
415 fn inv_positive_for_nonempty() {
416 for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
417 assert!(edge_inverse_degree_sum(g).unwrap() > 0.0);
418 }
419 }
420}