rust_igraph/algorithms/properties/
graph_curvature.rs1#![allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
14
15use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
16
17pub fn forman_ricci_curvature(graph: &Graph) -> IgraphResult<Vec<f64>> {
38 if graph.is_directed() {
39 return Err(IgraphError::InvalidArgument(
40 "forman_ricci_curvature is defined for undirected graphs only".to_string(),
41 ));
42 }
43
44 let nv = graph.vcount() as usize;
45 let ne = graph.ecount();
46
47 let mut degrees = Vec::with_capacity(nv);
48 for v in 0..nv {
49 degrees.push(graph.degree(v as VertexId)?);
50 }
51
52 let mut curvatures = Vec::with_capacity(ne);
53
54 for (u, v) in graph.edges() {
55 let du = degrees[u as usize];
56 let dv = degrees[v as usize];
57 let tri = count_edge_triangles(graph, u, v)?;
58 let f = 4.0 - du as f64 - dv as f64 + 3.0 * tri as f64;
59 curvatures.push(f);
60 }
61
62 Ok(curvatures)
63}
64
65pub fn augmented_forman_ricci_curvature(graph: &Graph) -> IgraphResult<Vec<f64>> {
86 if graph.is_directed() {
87 return Err(IgraphError::InvalidArgument(
88 "augmented_forman_ricci_curvature is defined for undirected graphs only".to_string(),
89 ));
90 }
91
92 let nv = graph.vcount() as usize;
93 let ne = graph.ecount();
94
95 let mut degrees = Vec::with_capacity(nv);
96 for v in 0..nv {
97 degrees.push(graph.degree(v as VertexId)?);
98 }
99
100 let mut curvatures = Vec::with_capacity(ne);
101
102 for (u, v) in graph.edges() {
103 let du = degrees[u as usize];
104 let dv = degrees[v as usize];
105 let tri = count_edge_triangles(graph, u, v)?;
106 let quad = count_edge_quadrangles(graph, u, v)?;
107 let af = 4.0 - du as f64 - dv as f64 + 3.0 * tri as f64 + 2.0 * quad as f64;
108 curvatures.push(af);
109 }
110
111 Ok(curvatures)
112}
113
114pub fn ollivier_ricci_curvature(graph: &Graph, alpha: f64) -> IgraphResult<Vec<f64>> {
143 if graph.is_directed() {
144 return Err(IgraphError::InvalidArgument(
145 "ollivier_ricci_curvature is defined for undirected graphs only".to_string(),
146 ));
147 }
148
149 if !(0.0..1.0).contains(&alpha) {
150 return Err(IgraphError::InvalidArgument(format!(
151 "alpha must be in [0, 1), got {alpha}"
152 )));
153 }
154
155 let nv = graph.vcount() as usize;
156 let ne = graph.ecount();
157
158 let mut degrees = Vec::with_capacity(nv);
159 let mut neighbors_cache: Vec<Vec<VertexId>> = Vec::with_capacity(nv);
160 for v in 0..nv {
161 degrees.push(graph.degree(v as VertexId)?);
162 neighbors_cache.push(graph.neighbors(v as VertexId)?);
163 }
164
165 let mut curvatures = Vec::with_capacity(ne);
166
167 for (u, v) in graph.edges() {
168 let kappa = compute_ollivier_edge(&neighbors_cache, °rees, u, v, alpha);
169 curvatures.push(kappa);
170 }
171
172 Ok(curvatures)
173}
174
175pub fn mean_forman_ricci(graph: &Graph) -> IgraphResult<f64> {
190 let curvatures = forman_ricci_curvature(graph)?;
191 if curvatures.is_empty() {
192 return Ok(0.0);
193 }
194 let sum: f64 = curvatures.iter().sum();
195 Ok(sum / curvatures.len() as f64)
196}
197
198fn count_edge_triangles(graph: &Graph, u: VertexId, v: VertexId) -> IgraphResult<usize> {
201 let nu = graph.neighbors(u)?;
202 let nv = graph.neighbors(v)?;
203 let mut count = 0;
204 for &w in &nu {
205 if w != v && nv.contains(&w) {
206 count += 1;
207 }
208 }
209 Ok(count)
210}
211
212fn count_edge_quadrangles(graph: &Graph, u: VertexId, v: VertexId) -> IgraphResult<usize> {
213 let nu = graph.neighbors(u)?;
214 let nv = graph.neighbors(v)?;
215 let mut count = 0;
216 for &w1 in &nu {
225 if w1 == v {
226 continue;
227 }
228 let nw1 = graph.neighbors(w1)?;
229 for &w2 in &nv {
230 if w2 == u || w2 == w1 {
231 continue;
232 }
233 if nw1.contains(&w2) {
234 count += 1;
235 }
236 }
237 }
238 Ok(count)
239}
240
241fn compute_ollivier_edge(
242 neighbors_cache: &[Vec<VertexId>],
243 degrees: &[usize],
244 u: VertexId,
245 v: VertexId,
246 alpha: f64,
247) -> f64 {
248 let du = degrees[u as usize];
251 let dv = degrees[v as usize];
252
253 if du == 0 || dv == 0 {
254 return 0.0;
255 }
256
257 let neighbors_u = &neighbors_cache[u as usize];
258 let neighbors_v = &neighbors_cache[v as usize];
259
260 let mut triangles = 0usize;
261 for &w in neighbors_u {
262 if w != v && neighbors_v.contains(&w) {
263 triangles += 1;
264 }
265 }
266
267 let recip_u = 1.0 / du as f64;
268 let recip_v = 1.0 / dv as f64;
269 let recip_sum = recip_u + recip_v;
270
271 (1.0 - alpha) * (triangles as f64 * recip_sum + recip_sum) + alpha - 1.0
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 fn triangle() -> Graph {
279 Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
280 }
281
282 fn path4() -> Graph {
283 Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
284 }
285
286 fn square() -> Graph {
287 Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
288 }
289
290 fn diamond() -> Graph {
291 Graph::from_edges(&[(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)], false, Some(4)).unwrap()
293 }
294
295 #[test]
298 fn forman_triangle() {
299 let g = triangle();
300 let curv = forman_ricci_curvature(&g).unwrap();
301 assert_eq!(curv.len(), 3);
303 for &c in &curv {
304 assert!((c - 3.0).abs() < 1e-10);
305 }
306 }
307
308 #[test]
309 fn forman_path() {
310 let g = path4();
311 let curv = forman_ricci_curvature(&g).unwrap();
312 assert!((curv[0] - 1.0).abs() < 1e-10);
314 assert!(curv[1].abs() < 1e-10);
316 assert!((curv[2] - 1.0).abs() < 1e-10);
318 }
319
320 #[test]
321 fn forman_diamond() {
322 let g = diamond();
323 let curv = forman_ricci_curvature(&g).unwrap();
324 assert_eq!(curv.len(), 5);
325 assert!((curv[0] - 2.0).abs() < 1e-10);
328 }
329
330 #[test]
331 fn forman_star() {
332 let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false, Some(4)).unwrap();
334 let curv = forman_ricci_curvature(&g).unwrap();
335 for &c in &curv {
337 assert!(c.abs() < 1e-10);
338 }
339 }
340
341 #[test]
342 fn forman_empty_graph() {
343 let g = Graph::with_vertices(3);
344 let curv = forman_ricci_curvature(&g).unwrap();
345 assert!(curv.is_empty());
346 }
347
348 #[test]
349 fn forman_directed_error() {
350 let g = Graph::from_edges(&[(0, 1)], true, Some(2)).unwrap();
351 assert!(forman_ricci_curvature(&g).is_err());
352 }
353
354 #[test]
357 fn augmented_forman_square() {
358 let g = square();
359 let curv = augmented_forman_ricci_curvature(&g).unwrap();
360 for &c in &curv {
363 assert!((c - 2.0).abs() < 1e-10);
364 }
365 }
366
367 #[test]
368 fn augmented_forman_triangle() {
369 let g = triangle();
370 let curv = augmented_forman_ricci_curvature(&g).unwrap();
371 let regular = forman_ricci_curvature(&g).unwrap();
373 for (a, r) in curv.iter().zip(regular.iter()) {
374 assert!((a - r).abs() < 1e-10);
375 }
376 }
377
378 #[test]
379 fn augmented_forman_path() {
380 let g = path4();
381 let curv = augmented_forman_ricci_curvature(&g).unwrap();
382 let regular = forman_ricci_curvature(&g).unwrap();
384 for (a, r) in curv.iter().zip(regular.iter()) {
385 assert!((a - r).abs() < 1e-10);
386 }
387 }
388
389 #[test]
392 fn ollivier_triangle() {
393 let g = triangle();
394 let curv = ollivier_ricci_curvature(&g, 0.0).unwrap();
395 for &c in &curv {
399 assert!((c - 1.0).abs() < 1e-10);
400 }
401 }
402
403 #[test]
404 fn ollivier_path_bridge() {
405 let g = path4();
406 let curv = ollivier_ricci_curvature(&g, 0.0).unwrap();
407 assert!(curv[1].abs() < 1e-10);
410 }
411
412 #[test]
413 fn ollivier_positive_for_dense() {
414 let g = diamond();
415 let curv = ollivier_ricci_curvature(&g, 0.0).unwrap();
416 assert!((curv[2] - 1.0).abs() < 1e-10);
419 }
420
421 #[test]
422 fn ollivier_with_alpha() {
423 let g = triangle();
424 let curv = ollivier_ricci_curvature(&g, 0.5).unwrap();
425 for &c in &curv {
428 assert!((c - 0.5).abs() < 1e-10);
429 }
430 }
431
432 #[test]
433 fn ollivier_invalid_alpha() {
434 let g = triangle();
435 assert!(ollivier_ricci_curvature(&g, 1.0).is_err());
436 assert!(ollivier_ricci_curvature(&g, -0.1).is_err());
437 }
438
439 #[test]
440 fn ollivier_directed_error() {
441 let g = Graph::from_edges(&[(0, 1)], true, Some(2)).unwrap();
442 assert!(ollivier_ricci_curvature(&g, 0.0).is_err());
443 }
444
445 #[test]
446 fn ollivier_single_edge() {
447 let g = Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap();
449 let curv = ollivier_ricci_curvature(&g, 0.0).unwrap();
450 assert!((curv[0] - 1.0).abs() < 1e-10);
453 }
454
455 #[test]
458 fn mean_forman_triangle() {
459 let g = triangle();
460 let mean = mean_forman_ricci(&g).unwrap();
461 assert!((mean - 3.0).abs() < 1e-10);
462 }
463
464 #[test]
465 fn mean_forman_empty() {
466 let g = Graph::with_vertices(5);
467 let mean = mean_forman_ricci(&g).unwrap();
468 assert!(mean.abs() < 1e-10);
469 }
470}