use super::*;
use rand::{rngs::SmallRng, RngExt, SeedableRng};
fn chain(n: usize) -> Vec<(usize, usize, f32)> {
(0..n - 1).map(|i| (i, i + 1, 1.0)).collect()
}
fn ring(n: usize) -> Vec<(usize, usize, f32)> {
(0..n)
.map(|i| (i.min((i + 1) % n), i.max((i + 1) % n), 1.0))
.collect()
}
fn bounding_box(y: &Mat, nodes: &[usize]) -> [f32; 4] {
let mut b = [
f32::INFINITY,
f32::INFINITY,
f32::NEG_INFINITY,
f32::NEG_INFINITY,
];
for &i in nodes {
b[0] = b[0].min(y[(i, 0)]);
b[1] = b[1].min(y[(i, 1)]);
b[2] = b[2].max(y[(i, 0)]);
b[3] = b[3].max(y[(i, 1)]);
}
b
}
#[test]
fn chain_first_component_is_monotone() {
let n = 30;
let edges: Vec<_> = (0..n - 1)
.map(|i| (i, i + 1, 0.5 + (i % 4) as f32 * 0.4))
.collect();
for t in [0, 3] {
let y = diffusion_map_2d_on_graph(n, &edges, t);
let steps: Vec<f32> = (0..n - 1).map(|i| y[(i + 1, 0)] - y[(i, 0)]).collect();
let up = steps.iter().all(|&s| s > 0.0);
let down = steps.iter().all(|&s| s < 0.0);
assert!(up || down, "t = {t}: steps {steps:?}");
}
}
#[test]
fn ring_is_a_circle() {
let n = 40;
let y = diffusion_map_2d_on_graph(n, &ring(n), 2);
let (cx, cy) = (y.column(0).mean(), y.column(1).mean());
let radii: Vec<f32> = (0..n)
.map(|i| (y[(i, 0)] - cx).hypot(y[(i, 1)] - cy))
.collect();
let mean = radii.iter().sum::<f32>() / n as f32;
let spread = radii.iter().map(|r| (r - mean).abs()).fold(0.0, f32::max) / mean;
eprintln!("ring: max relative radius deviation {spread:.2e}");
assert!(spread < 1e-3, "radius deviation {spread}");
let angle = |i: usize| (y[(i, 1)] - cy).atan2(y[(i, 0)] - cx);
let turns: Vec<f32> = (0..n)
.map(|i| {
let d = angle((i + 1) % n) - angle(i);
(d + std::f32::consts::PI).rem_euclid(2.0 * std::f32::consts::PI) - std::f32::consts::PI
})
.collect();
let same_way = turns.iter().all(|&d| d > 0.0) || turns.iter().all(|&d| d < 0.0);
let total: f32 = turns.iter().sum::<f32>().abs();
eprintln!(
"ring: total turn {total:.4} (2 pi = {:.4})",
2.0 * std::f32::consts::PI
);
assert!(same_way, "turns {turns:?}");
assert!((total - 2.0 * std::f32::consts::PI).abs() < 1e-3);
}
#[test]
fn weakly_linked_groups_stay_apart() {
let (groups, per) = (3, 20);
let n = groups * per;
let mut rng = SmallRng::seed_from_u64(7);
let mut edges = vec![];
for g in 0..groups {
for a in 0..per {
for b in (a + 1)..per {
if rng.random_range(0.0_f32..1.0) < 0.5 {
edges.push((g * per + a, g * per + b, rng.random_range(0.5_f32..1.0)));
}
}
}
}
for g in 0..groups {
let h = (g + 1) % groups;
for k in 0..3 {
let (a, b) = (g * per + k, h * per + k + 5);
edges.push((a.min(b), a.max(b), 0.01));
}
}
let y = diffusion_map_2d_on_graph(n, &edges, 1);
let centre = |g: usize| {
let r = (g * per)..((g + 1) * per);
let cx = r.clone().map(|i| y[(i, 0)]).sum::<f32>() / per as f32;
let cy = r.map(|i| y[(i, 1)]).sum::<f32>() / per as f32;
(cx, cy)
};
let mut widest = 0.0_f32;
for g in 0..groups {
let (cx, cy) = centre(g);
let ms = ((g * per)..((g + 1) * per))
.map(|i| (y[(i, 0)] - cx).powi(2) + (y[(i, 1)] - cy).powi(2))
.sum::<f32>()
/ per as f32;
widest = widest.max(ms.sqrt());
}
let mut closest = f32::INFINITY;
for g in 0..groups {
for h in (g + 1)..groups {
let ((a, b), (c, d)) = (centre(g), centre(h));
closest = closest.min((a - c).hypot(b - d));
}
}
eprintln!(
"groups: closest centres / widest radius = {:.1}",
closest / widest
);
assert!(closest > 5.0 * widest, "closest {closest}, widest {widest}");
}
#[test]
fn separate_pieces_do_not_overlap() {
let pieces: Vec<Vec<usize>> = vec![
(0..12).collect(),
(12..32).collect(),
vec![32, 33],
vec![34],
];
let mut edges = chain(12);
edges.extend(ring(20).into_iter().map(|(i, j, w)| (i + 12, j + 12, w)));
edges.push((32, 33, 2.0));
let n = 35;
let y = diffusion_map_2d_on_graph(n, &edges, 1);
assert!(y.iter().all(|v| v.is_finite()));
let boxes: Vec<[f32; 4]> = pieces.iter().map(|p| bounding_box(&y, p)).collect();
for a in 0..boxes.len() {
for b in (a + 1)..boxes.len() {
let (p, q) = (boxes[a], boxes[b]);
let margin = 0.01 * (p[2] - p[0]).max(q[2] - q[0]);
let apart = p[2] + margin < q[0]
|| q[2] + margin < p[0]
|| p[3] + margin < q[1]
|| q[3] + margin < p[1];
assert!(apart, "pieces {a} and {b} overlap: {p:?} {q:?}");
}
}
let alone = [
diffusion_map_2d_on_graph(12, &chain(12), 1),
diffusion_map_2d_on_graph(20, &ring(20), 1),
];
for (piece, solo) in pieces.iter().zip(&alone) {
let extent = solo.iter().fold(0.0_f32, |m, v| m.max(v.abs()));
for dim in 0..2 {
let shift = y[(piece[0], dim)] - solo[(0, dim)];
for (k, &i) in piece.iter().enumerate() {
let dev = (y[(i, dim)] - shift - solo[(k, dim)]).abs();
assert!(dev <= 1e-5 * extent, "piece node {i}: deviation {dev}");
}
}
}
}
#[test]
fn tiny_inputs() {
assert_eq!(diffusion_map_2d_on_graph(0, &[], 0).shape(), (0, 2));
for (n, edges) in [
(1, vec![]),
(1, vec![(0, 0, 1.0)]),
(2, vec![]),
(2, vec![(0, 1, 1.0)]),
(3, vec![]),
(3, vec![(0, 1, 1.0), (1, 2, 1.0)]),
(3, vec![(0, 1, 0.0)]),
] {
let y = diffusion_map_2d_on_graph(n, &edges, 1);
assert_eq!(y.shape(), (n, 2));
assert!(y.iter().all(|v| v.is_finite()), "n = {n}, {edges:?}: {y}");
}
let apart = |y: &Mat, i: usize, j: usize| (y[(i, 0)] - y[(j, 0)]).hypot(y[(i, 1)] - y[(j, 1)]);
let y = diffusion_map_2d_on_graph(2, &[(0, 1, 1.0)], 0);
assert!(apart(&y, 0, 1) > 0.0);
let y = diffusion_map_2d_on_graph(3, &[], 0);
assert!(apart(&y, 0, 1) > 0.0 && apart(&y, 0, 2) > 0.0 && apart(&y, 1, 2) > 0.0);
}
#[test]
fn diffusion_time_scales_by_eigenvalues() {
let n = 25;
let spread = |y: &Mat, d: usize| y.column(d).iter().map(|v| v * v).sum::<f32>().sqrt();
let y0 = diffusion_map_2d_on_graph(n, &chain(n), 0);
let y4 = diffusion_map_2d_on_graph(n, &chain(n), 4);
let l = |k: usize| (std::f32::consts::PI * k as f32 / (n - 1) as f32).cos();
for (d, k) in [(0, 1), (1, 2)] {
let ratio = spread(&y4, d) / spread(&y0, d);
let want = l(k).powi(4);
eprintln!("t scaling, axis {d}: {ratio:.6} vs {want:.6}");
assert!((ratio - want).abs() < 1e-3 * want);
}
}