1#![allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
13
14use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
15
16pub fn heat_kernel_diffuse(
52 graph: &Graph,
53 signal: &[f64],
54 t: f64,
55 max_terms: usize,
56) -> IgraphResult<Vec<f64>> {
57 let nv = graph.vcount() as usize;
58
59 if signal.len() != nv {
60 return Err(IgraphError::InvalidArgument(format!(
61 "signal length {} does not match vcount {}",
62 signal.len(),
63 nv
64 )));
65 }
66
67 if t <= 0.0 {
68 return Err(IgraphError::InvalidArgument(
69 "diffusion time t must be positive".to_string(),
70 ));
71 }
72
73 if graph.is_directed() {
74 return Err(IgraphError::InvalidArgument(
75 "heat_kernel_diffuse is defined for undirected graphs only".to_string(),
76 ));
77 }
78
79 let degrees = compute_degrees(graph, nv)?;
80
81 let mut current = signal.to_vec();
84 let mut result = vec![0.0_f64; nv];
85
86 let mut poisson_weight = (-t).exp(); add_scaled(&mut result, ¤t, poisson_weight);
88
89 for k in 1..=max_terms {
90 current = apply_transition(graph, ¤t, °rees, nv)?;
91 poisson_weight *= t / k as f64;
92 add_scaled(&mut result, ¤t, poisson_weight);
93
94 if poisson_weight.abs() < 1e-15 {
95 break;
96 }
97 }
98
99 Ok(result)
100}
101
102pub fn ppr_diffuse(
138 graph: &Graph,
139 signal: &[f64],
140 alpha: f64,
141 max_iter: usize,
142 tol: f64,
143) -> IgraphResult<Vec<f64>> {
144 let nv = graph.vcount() as usize;
145
146 if signal.len() != nv {
147 return Err(IgraphError::InvalidArgument(format!(
148 "signal length {} does not match vcount {}",
149 signal.len(),
150 nv
151 )));
152 }
153
154 if alpha <= 0.0 || alpha > 1.0 {
155 return Err(IgraphError::InvalidArgument(format!(
156 "alpha must be in (0.0, 1.0], got {alpha}"
157 )));
158 }
159
160 if graph.is_directed() {
161 return Err(IgraphError::InvalidArgument(
162 "ppr_diffuse is defined for undirected graphs only".to_string(),
163 ));
164 }
165
166 let degrees = compute_degrees(graph, nv)?;
167 let one_minus_alpha = 1.0 - alpha;
168
169 let mut z = signal.to_vec();
171
172 for _ in 0..max_iter {
173 let propagated = apply_transition(graph, &z, °rees, nv)?;
174
175 let mut max_diff = 0.0_f64;
176 for i in 0..nv {
177 let new_val = alpha * signal[i] + one_minus_alpha * propagated[i];
178 let diff = (new_val - z[i]).abs();
179 if diff > max_diff {
180 max_diff = diff;
181 }
182 z[i] = new_val;
183 }
184
185 if max_diff < tol {
186 break;
187 }
188 }
189
190 Ok(z)
191}
192
193pub fn symmetric_diffuse(graph: &Graph, signal: &[f64], k: usize) -> IgraphResult<Vec<f64>> {
223 let nv = graph.vcount() as usize;
224
225 if signal.len() != nv {
226 return Err(IgraphError::InvalidArgument(format!(
227 "signal length {} does not match vcount {}",
228 signal.len(),
229 nv
230 )));
231 }
232
233 if graph.is_directed() {
234 return Err(IgraphError::InvalidArgument(
235 "symmetric_diffuse is defined for undirected graphs only".to_string(),
236 ));
237 }
238
239 let degrees = compute_degrees(graph, nv)?;
240
241 let inv_sqrt_deg: Vec<f64> = degrees
243 .iter()
244 .map(|&d| if d == 0 { 0.0 } else { 1.0 / (d as f64).sqrt() })
245 .collect();
246
247 let mut current = signal.to_vec();
248
249 for _ in 0..k {
250 let mut next = vec![0.0_f64; nv];
253
254 for v in 0..nv {
255 if degrees[v] == 0 {
256 continue;
257 }
258 let neighbors = graph.neighbors(v as VertexId)?;
259 let mut sum = 0.0;
260 for &u in &neighbors {
261 let u_idx = u as usize;
262 sum += inv_sqrt_deg[u_idx] * current[u_idx];
263 }
264 next[v] = inv_sqrt_deg[v] * sum;
265 }
266
267 current = next;
268 }
269
270 Ok(current)
271}
272
273fn compute_degrees(graph: &Graph, nv: usize) -> IgraphResult<Vec<usize>> {
276 let mut degrees = Vec::with_capacity(nv);
277 for v in 0..nv {
278 degrees.push(graph.degree(v as VertexId)?);
279 }
280 Ok(degrees)
281}
282
283fn apply_transition(
285 graph: &Graph,
286 signal: &[f64],
287 degrees: &[usize],
288 nv: usize,
289) -> IgraphResult<Vec<f64>> {
290 let mut result = vec![0.0_f64; nv];
291
292 for v in 0..nv {
293 if signal[v] == 0.0 {
294 continue;
295 }
296 let deg = degrees[v];
297 if deg == 0 {
298 result[v] += signal[v];
299 continue;
300 }
301 let weight = signal[v] / deg as f64;
302 let neighbors = graph.neighbors(v as VertexId)?;
303 for &u in &neighbors {
304 result[u as usize] += weight;
305 }
306 }
307
308 Ok(result)
309}
310
311fn add_scaled(target: &mut [f64], source: &[f64], scale: f64) {
312 for (t, &s) in target.iter_mut().zip(source.iter()) {
313 *t += s * scale;
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 fn path4() -> Graph {
322 Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
323 }
324
325 fn triangle() -> Graph {
326 Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
327 }
328
329 fn complete4() -> Graph {
330 Graph::from_edges(
331 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
332 false,
333 Some(4),
334 )
335 .unwrap()
336 }
337
338 #[test]
341 fn heat_zero_signal() {
342 let g = path4();
343 let signal = vec![0.0; 4];
344 let result = heat_kernel_diffuse(&g, &signal, 1.0, 20).unwrap();
345 for &v in &result {
346 assert!((v).abs() < 1e-15);
347 }
348 }
349
350 #[test]
351 fn heat_uniform_signal_regular() {
352 let g = triangle();
353 let signal = vec![1.0, 1.0, 1.0];
354 let result = heat_kernel_diffuse(&g, &signal, 2.0, 30).unwrap();
355 for &v in &result {
356 assert!((v - 1.0).abs() < 1e-10);
357 }
358 }
359
360 #[test]
361 fn heat_signal_spreads() {
362 let g = path4();
363 let signal = vec![1.0, 0.0, 0.0, 0.0];
364 let result = heat_kernel_diffuse(&g, &signal, 1.0, 20).unwrap();
365 assert!(result[0] < 1.0);
366 assert!(result[1] > 0.0);
367 assert!(result[2] > 0.0);
368 }
369
370 #[test]
371 fn heat_nonnegative() {
372 let g = path4();
373 let signal = vec![1.0, 0.0, 0.0, 0.0];
374 let result = heat_kernel_diffuse(&g, &signal, 0.5, 20).unwrap();
375 for &v in &result {
376 assert!(v >= -1e-15);
377 }
378 }
379
380 #[test]
381 fn heat_invalid_signal_length() {
382 let g = path4();
383 assert!(heat_kernel_diffuse(&g, &[1.0, 2.0], 1.0, 20).is_err());
384 }
385
386 #[test]
387 fn heat_invalid_t() {
388 let g = path4();
389 assert!(heat_kernel_diffuse(&g, &[0.0; 4], 0.0, 20).is_err());
390 assert!(heat_kernel_diffuse(&g, &[0.0; 4], -1.0, 20).is_err());
391 }
392
393 #[test]
394 fn heat_directed_error() {
395 let g = Graph::from_edges(&[(0, 1), (1, 2)], true, Some(3)).unwrap();
396 assert!(heat_kernel_diffuse(&g, &[1.0; 3], 1.0, 20).is_err());
397 }
398
399 #[test]
400 fn heat_isolated_vertex() {
401 let g = Graph::with_vertices(3);
402 let signal = vec![1.0, 2.0, 3.0];
403 let result = heat_kernel_diffuse(&g, &signal, 1.0, 20).unwrap();
404 for i in 0..3 {
405 assert!((result[i] - signal[i]).abs() < 1e-10);
406 }
407 }
408
409 #[test]
412 fn ppr_uniform_on_regular() {
413 let g = complete4();
414 let signal = vec![0.25, 0.25, 0.25, 0.25];
415 let result = ppr_diffuse(&g, &signal, 0.15, 50, 1e-10).unwrap();
416 for &v in &result {
417 assert!((v - 0.25).abs() < 1e-8);
418 }
419 }
420
421 #[test]
422 fn ppr_signal_spreads() {
423 let g = path4();
424 let signal = vec![1.0, 0.0, 0.0, 0.0];
425 let result = ppr_diffuse(&g, &signal, 0.15, 50, 1e-10).unwrap();
426 assert!(result[0] > 0.0);
428 assert!(result[1] > 0.0);
429 assert!(result[1] > result[3]);
431 assert!(result[2] > result[3]);
432 }
433
434 #[test]
435 fn ppr_alpha_one_is_identity() {
436 let g = path4();
437 let signal = vec![1.0, 2.0, 3.0, 4.0];
438 let result = ppr_diffuse(&g, &signal, 1.0, 50, 1e-10).unwrap();
439 for i in 0..4 {
440 assert!((result[i] - signal[i]).abs() < 1e-10);
441 }
442 }
443
444 #[test]
445 fn ppr_nonnegative() {
446 let g = path4();
447 let signal = vec![1.0, 0.0, 0.0, 0.0];
448 let result = ppr_diffuse(&g, &signal, 0.2, 50, 1e-10).unwrap();
449 for &v in &result {
450 assert!(v >= -1e-15);
451 }
452 }
453
454 #[test]
455 fn ppr_invalid_alpha() {
456 let g = path4();
457 assert!(ppr_diffuse(&g, &[0.0; 4], 0.0, 50, 1e-6).is_err());
458 assert!(ppr_diffuse(&g, &[0.0; 4], 1.5, 50, 1e-6).is_err());
459 assert!(ppr_diffuse(&g, &[0.0; 4], -0.1, 50, 1e-6).is_err());
460 }
461
462 #[test]
463 fn ppr_invalid_signal_length() {
464 let g = path4();
465 assert!(ppr_diffuse(&g, &[1.0], 0.15, 50, 1e-6).is_err());
466 }
467
468 #[test]
469 fn ppr_directed_error() {
470 let g = Graph::from_edges(&[(0, 1), (1, 2)], true, Some(3)).unwrap();
471 assert!(ppr_diffuse(&g, &[1.0; 3], 0.15, 50, 1e-6).is_err());
472 }
473
474 #[test]
475 fn ppr_isolated_vertex() {
476 let g = Graph::with_vertices(3);
477 let signal = vec![1.0, 2.0, 3.0];
478 let result = ppr_diffuse(&g, &signal, 0.15, 50, 1e-10).unwrap();
479 for i in 0..3 {
480 assert!((result[i] - signal[i]).abs() < 1e-10);
481 }
482 }
483
484 #[test]
487 fn symmetric_uniform_regular() {
488 let g = triangle();
489 let signal = vec![1.0, 1.0, 1.0];
490 let result = symmetric_diffuse(&g, &signal, 3).unwrap();
491 for &v in &result {
492 assert!((v - 1.0).abs() < 1e-10);
493 }
494 }
495
496 #[test]
497 fn symmetric_zero_steps() {
498 let g = path4();
499 let signal = vec![1.0, 2.0, 3.0, 4.0];
500 let result = symmetric_diffuse(&g, &signal, 0).unwrap();
501 for i in 0..4 {
502 assert!((result[i] - signal[i]).abs() < 1e-15);
503 }
504 }
505
506 #[test]
507 fn symmetric_signal_smooths() {
508 let g = path4();
509 let signal = vec![4.0, 0.0, 0.0, 0.0];
510 let result = symmetric_diffuse(&g, &signal, 1).unwrap();
511 assert!(result[0] < 4.0);
513 assert!(result[1] > 0.0);
514 }
515
516 #[test]
517 fn symmetric_invalid_signal_length() {
518 let g = path4();
519 assert!(symmetric_diffuse(&g, &[1.0], 1).is_err());
520 }
521
522 #[test]
523 fn symmetric_directed_error() {
524 let g = Graph::from_edges(&[(0, 1)], true, Some(2)).unwrap();
525 assert!(symmetric_diffuse(&g, &[1.0; 2], 1).is_err());
526 }
527
528 #[test]
529 fn symmetric_isolated_vertex() {
530 let g = Graph::with_vertices(2);
531 let signal = vec![5.0, 3.0];
532 let result = symmetric_diffuse(&g, &signal, 5).unwrap();
533 assert!((result[0]).abs() < 1e-15);
535 assert!((result[1]).abs() < 1e-15);
536 }
537}