use super::*;
use crate::matrix::agreement::spearman;
use crate::matrix::landmark_init::tests::{swiss_roll, unrolled};
use crate::matrix::test_support::fuzzy_edges;
use rand::{rngs::SmallRng, RngExt, SeedableRng};
type Edges = Vec<(usize, usize, f32)>;
fn lattice(w: usize, h: usize, offset: usize) -> (Edges, Vec<[f32; 2]>) {
let mut edges = vec![];
let mut place = vec![];
for r in 0..h {
for c in 0..w {
let i = offset + r * w + c;
place.push([c as f32, r as f32]);
if c + 1 < w {
edges.push((i, i + 1, 1.0));
}
if r + 1 < h {
edges.push((i, i + w, 1.0));
}
}
}
(edges, place)
}
fn dist(y: &Mat, i: usize, j: usize) -> f32 {
(y[(i, 0)] - y[(j, 0)]).hypot(y[(i, 1)] - y[(j, 1)])
}
fn faithful(y: &Mat, nodes: &[usize], place: &[[f32; 2]]) -> f32 {
let (mut truth, mut laid) = (vec![], vec![]);
for a in 0..nodes.len() {
for b in 0..a {
truth.push((place[a][0] - place[b][0]).hypot(place[a][1] - place[b][1]));
laid.push(dist(y, nodes[a], nodes[b]));
}
}
spearman(&truth, &laid)
}
fn flat(y: &Mat) -> Vec<f32> {
(0..y.nrows())
.flat_map(|i| [y[(i, 0)], y[(i, 1)]])
.collect()
}
#[test]
fn a_grid_is_laid_out_as_its_grid() {
let (edges, place) = lattice(15, 10, 0);
let y = stress_layout_2d_on_graph(150, &edges, 1);
let nodes: Vec<usize> = (0..150).collect();
let rho = faithful(&y, &nodes, &place);
eprintln!("grid rank correlation {rho}");
assert!(rho >= 0.98, "grid rank correlation {rho}");
}
#[test]
fn a_chain_keeps_its_order() {
let n = 40;
let edges: Vec<_> = (0..n - 1).map(|i| (i, i + 1, 1.0)).collect();
let y = stress_layout_2d_on_graph(n, &edges, 3);
let (mx, my) = (y.column(0).mean(), y.column(1).mean());
let (mut sxx, mut sxy, mut syy) = (0.0, 0.0, 0.0);
for i in 0..n {
let (a, b) = (y[(i, 0)] - mx, y[(i, 1)] - my);
sxx += a * a;
sxy += a * b;
syy += b * b;
}
let theta = 0.5 * (2.0 * sxy).atan2(sxx - syy);
let along: Vec<f32> = (0..n)
.map(|i| (y[(i, 0)] - mx) * theta.cos() + (y[(i, 1)] - my) * theta.sin())
.collect();
let index: Vec<f32> = (0..n).map(|i| i as f32).collect();
let rho = spearman(&index, &along).abs();
eprintln!("chain order {rho}");
assert!(rho >= 0.9999, "chain order {rho}");
}
#[test]
fn edge_lengths_follow_inverse_weights() {
let weights = [1.0_f32, 0.25, 0.5, 2.0];
let edges: Vec<_> = weights
.iter()
.enumerate()
.map(|(i, &w)| (i, i + 1, w))
.collect();
let y = stress_layout_2d_on_graph(5, &edges, 5);
for (i, &w) in weights.iter().enumerate() {
let got = dist(&y, i, i + 1);
eprintln!("segment {i}: {got} vs {}", 1.0 / w);
assert!(
(got * w - 1.0).abs() < 0.02,
"segment {i}: {got} vs {}",
1.0 / w
);
}
}
#[test]
fn a_swiss_roll_graph_is_unrolled() {
let (data, sheet) = swiss_roll(2000, 4);
let edges = fuzzy_edges(&data, 15);
let y = stress_layout_2d_on_graph(2000, &edges, 4);
let rho = unrolled(&sheet, &flat(&y));
eprintln!("swiss roll unrolled {rho}");
assert!(rho >= 0.9, "swiss roll unrolled {rho}");
}
#[test]
fn separate_pieces_do_not_overlap() {
let (mut edges, place_a) = lattice(8, 5, 0);
let (edges_b, place_b) = lattice(6, 6, 40);
edges.extend(edges_b);
edges.extend((76..105).map(|i| (i, i + 1, 1.0)));
let n = 40 + 36 + 30 + 2;
let y = stress_layout_2d_on_graph(n, &edges, 7);
assert!(y.iter().all(|v| v.is_finite()));
let pieces: Vec<Vec<usize>> = vec![
(0..40).collect(),
(40..76).collect(),
(76..106).collect(),
vec![106],
vec![107],
];
for (p, piece) in pieces.iter().enumerate() {
let (mut lo, mut hi) = ([f32::MAX; 2], [f32::MIN; 2]);
for &i in piece {
for c in 0..2 {
lo[c] = lo[c].min(y[(i, c)]);
hi[c] = hi[c].max(y[(i, c)]);
}
}
for (q, other) in pieces.iter().enumerate() {
if q == p {
continue;
}
for &i in other {
let inside = (0..2).all(|c| y[(i, c)] >= lo[c] && y[(i, c)] <= hi[c]);
assert!(!inside, "node {i} of piece {q} falls in piece {p}");
}
}
}
let a = faithful(&y, &pieces[0], &place_a);
let b = faithful(&y, &pieces[1], &place_b);
eprintln!("pieces: lattice a {a}, lattice b {b}");
assert!(a >= 0.98 && b >= 0.98, "lattices {a} {b}");
}
#[test]
fn tiny_inputs() {
assert_eq!(stress_layout_2d_on_graph(0, &[], 1).shape(), (0, 2));
let one = stress_layout_2d_on_graph(1, &[], 1);
assert_eq!(one.shape(), (1, 2));
assert!(one.iter().all(|v| v.is_finite()));
let joined = stress_layout_2d_on_graph(2, &[(0, 1, 0.5)], 1);
assert_eq!(joined.shape(), (2, 2));
eprintln!("two joined nodes {} apart", dist(&joined, 0, 1));
assert!((dist(&joined, 0, 1) - 2.0).abs() < 0.02);
for (n, edges) in [(2, vec![]), (3, vec![])] {
let y = stress_layout_2d_on_graph(n, &edges, 1);
assert_eq!(y.shape(), (n, 2));
assert!(y.iter().all(|v| v.is_finite()));
for i in 0..n {
for j in 0..i {
assert!(dist(&y, i, j) > 0.0, "nodes {i} and {j} coincide");
}
}
}
}
#[test]
fn same_seed_same_layout() {
let (data, _) = swiss_roll(300, 9);
let edges = fuzzy_edges(&data, 10);
let a = stress_layout_2d_on_graph(300, &edges, 42);
let b = stress_layout_2d_on_graph(300, &edges, 42);
assert_eq!(a, b);
}
#[test]
fn the_layout_scales_with_the_weights() {
let (mut edges, _) = lattice(5, 4, 0);
edges.push((20, 21, 1.0));
let n = 23;
let a = stress_layout_2d_on_graph(n, &edges, 2);
let scaled: Vec<_> = edges.iter().map(|&(i, j, w)| (i, j, 100.0 * w)).collect();
let b = stress_layout_2d_on_graph(n, &scaled, 2) * 100.0;
let extent = a.iter().fold(0.0_f32, |m, v| m.max(v.abs()));
let worst = (a - b).iter().fold(0.0_f32, |m, v| m.max(v.abs()));
eprintln!("scale mismatch {worst} of extent {extent}");
assert!(
worst <= 1e-3 * extent,
"scale mismatch {worst} of extent {extent}"
);
}
#[test]
fn a_zero_weight_is_no_edge() {
let edges = vec![(0, 1, 1.0), (1, 2, 1.0), (2, 3, 0.0)];
let with = stress_layout_2d_on_graph(4, &edges, 6);
let without = stress_layout_2d_on_graph(4, &edges[..2], 6);
assert!(with.iter().all(|v| v.is_finite()));
assert_eq!(with, without);
}
#[test]
fn a_star_spreads_its_leaves() {
let edges: Vec<_> = (1..7).map(|i| (0, i, 1.0)).collect();
let y = stress_layout_2d_on_graph(7, &edges, 8);
let mut closest = f32::MAX;
for i in 0..7 {
for j in 0..i {
closest = closest.min(dist(&y, i, j));
}
}
let spokes: Vec<f32> = (1..7).map(|i| dist(&y, 0, i)).collect();
let (lo, hi) = spokes
.iter()
.fold((f32::MAX, 0.0_f32), |(lo, hi), &s| (lo.min(s), hi.max(s)));
let spread = hi / lo - 1.0;
eprintln!("star: closest pair {closest}, spokes {lo}..{hi}, spread {spread}");
assert!(
closest >= 0.3 && spread <= 0.05,
"star: closest {closest}, spread {spread}"
);
}
fn relative_stress(n: usize, edges: &[(usize, usize, f32)], y: &Mat) -> f64 {
let d = shortest_paths(&PreparedGraph::new(n, edges));
let mut s = 0.0;
for i in 0..n {
for j in 0..i {
let dij = f64::from(d[i * n + j]);
s += ((f64::from(dist(y, i, j)) - dij) / dij).powi(2);
}
}
s / (n * (n - 1) / 2) as f64
}
#[test]
fn random_pair_order_lowers_stress() {
let mut rng = SmallRng::seed_from_u64(3);
let mut edges = vec![];
for i in 1..500 {
for _ in 0..2 {
edges.push((rng.random_range(0..i), i, rng.random_range(0.2_f32..1.0)));
}
}
let s = relative_stress(500, &edges, &stress_layout_2d_on_graph(500, &edges, 0));
eprintln!("random graph relative stress {s}");
assert!(s <= 0.14, "random graph relative stress {s}");
}
#[test]
fn twins_that_start_on_one_point_stay_finite() {
let edges = vec![
(0, 2, 1.0),
(0, 3, 1.0),
(1, 2, 1.0),
(1, 3, 1.0),
(2, 3, 1.0),
];
let y = stress_layout_2d_on_graph(4, &edges, 1);
assert!(y.iter().all(|v| v.is_finite()), "{y}");
}