use super::*;
use crate::matrix::principal_graph::mst_from_sqdist;
use nalgebra::DMatrix;
const COORDS: [[f32; 2]; 8] = [
[0.0, 0.0],
[1.0, 0.0],
[2.0, 0.0],
[3.0, 0.0],
[4.0, 1.0],
[5.0, 2.0],
[4.0, -1.0],
[5.0, -2.0],
];
fn centroid_matrix() -> DMatrix<f32> {
let k = COORDS.len();
let mut c = DMatrix::<f32>::zeros(k, 2);
for i in 0..k {
c[(i, 0)] = COORDS[i][0];
c[(i, 1)] = COORDS[i][1];
}
c
}
fn full_sqdist(c: &DMatrix<f32>) -> DMatrix<f32> {
let k = c.nrows();
let d = c.ncols();
let mut m = DMatrix::<f32>::zeros(k, k);
for a in 0..k {
for b in 0..k {
let mut s = 0f32;
for j in 0..d {
let v = c[(a, j)] - c[(b, j)];
s += v * v;
}
m[(a, b)] = s;
}
}
m
}
fn cell_cloud(per: usize) -> DMatrix<f32> {
let k = COORDS.len();
let n = k * per;
let mut z = DMatrix::<f32>::zeros(n, 2);
for (i, coord) in COORDS.iter().enumerate() {
for p in 0..per {
let idx = i * per + p;
let jitter = (p as f32 / per as f32 - 0.5) * 0.2;
z[(idx, 0)] = coord[0] + jitter;
z[(idx, 1)] = coord[1] + jitter * 0.5;
}
}
z
}
#[test]
fn y_branch_yields_two_lineages() {
let centroids = centroid_matrix();
let (edges, _w) = mst_from_sqdist(&full_sqdist(¢roids));
assert_eq!(edges.len(), COORDS.len() - 1, "MST on 8 nodes → 7 edges");
let per = 30;
let z = cell_cloud(per);
let root = 0usize;
let res = fit_principal_curves(&z, ¢roids, &edges, root, &PrincipalCurveArgs::default())
.expect("fit");
assert_eq!(res.n_lineages(), 2, "Y tree must have exactly 2 lineages");
for c in &res.curves {
assert_eq!(
c.node_path.first().copied(),
Some(root),
"lineage starts at root"
);
assert_eq!(c.points.nrows(), PrincipalCurveArgs::default().resolution);
}
assert!(
res.pseudotime.iter().all(|v| v.is_finite() && *v >= -1e-3),
"pseudotime finite & non-negative"
);
}
#[test]
fn pseudotime_increases_toward_leaf() {
let centroids = centroid_matrix();
let (edges, _w) = mst_from_sqdist(&full_sqdist(¢roids));
let per = 30;
let z = cell_cloud(per);
let res =
fit_principal_curves(&z, ¢roids, &edges, 0, &PrincipalCurveArgs::default()).unwrap();
let pt_trunk = res.pseudotime[per / 2];
let pt_leaf = res.pseudotime[5 * per + per / 2];
assert!(
pt_leaf > pt_trunk + 1.0,
"leaf pseudotime {pt_leaf} should clearly exceed trunk {pt_trunk}"
);
}
#[test]
fn both_branches_are_populated() {
let centroids = centroid_matrix();
let (edges, _w) = mst_from_sqdist(&full_sqdist(¢roids));
let per = 30;
let z = cell_cloud(per);
let res =
fit_principal_curves(&z, ¢roids, &edges, 0, &PrincipalCurveArgs::default()).unwrap();
let n0 = res.branch.iter().filter(|&&b| b == 0).count();
let n1 = res.branch.iter().filter(|&&b| b == 1).count();
assert!(n0 > 0 && n1 > 0, "both branches populated: {n0} / {n1}");
}
#[test]
fn smoothing_does_not_increase_curvature() {
let centroids = centroid_matrix();
let (edges, _w) = mst_from_sqdist(&full_sqdist(¢roids));
let per = 40;
let z = cell_cloud(per);
let res =
fit_principal_curves(&z, ¢roids, &edges, 0, &PrincipalCurveArgs::default()).unwrap();
for curve in &res.curves {
let curv = total_curvature(&curve.points);
assert!(curv.is_finite(), "curvature finite");
}
}
fn total_curvature(pts: &DMatrix<f32>) -> f32 {
let m = pts.nrows();
let d = pts.ncols();
let mut acc = 0f32;
for i in 1..(m - 1) {
for j in 0..d {
let second = pts[(i + 1, j)] - 2.0 * pts[(i, j)] + pts[(i - 1, j)];
acc += second * second;
}
}
acc
}