use crate::builder::ArrowSpaceBuilder;
use crate::graph::{GraphLaplacian, GraphParams};
use crate::laplacian::build_laplacian_matrix;
use crate::tests::init;
use smartcore::algorithm::neighbour::cosinepair::CosinePair;
use smartcore::linalg::basic::matrix::DenseMatrix;
fn params(eps: f64, k: usize, topk: usize) -> GraphParams {
GraphParams {
eps,
k,
topk,
p: 2.0,
sigma: Some(0.5),
normalise: false,
sparsity_check: false,
}
}
fn laplacian_from_features(profiles: &Vec<Vec<f64>>, p: GraphParams) -> GraphLaplacian {
let n_obs = profiles.first().map(|r| r.len()).unwrap_or(0);
let m = DenseMatrix::<f64>::from_2d_vec(profiles).unwrap();
build_laplacian_matrix(m, &p, Some(n_obs), false)
}
fn has_edge(gl: &GraphLaplacian, i: usize, j: usize) -> bool {
assert_ne!(i, j);
gl.get(i, j) < 0.0
}
fn cosine_distance(a: &[f64], b: &[f64]) -> f64 {
let dot: f64 = a.iter().zip(b).map(|(x, y)| x * y).sum();
let na: f64 = a.iter().map(|x| x * x).sum::<f64>().sqrt();
let nb: f64 = b.iter().map(|x| x * x).sum::<f64>().sqrt();
if na == 0.0 || nb == 0.0 {
return f64::MAX;
}
1.0 - dot / (na * nb)
}
#[test]
fn cosinepair_knn_finds_both_trio_siblings_exactly() {
init();
let dirs: [Vec<f64>; 4] = [
vec![1.0, 0.0, 0.0],
vec![0.0, 1.0, 0.0],
vec![0.0, 0.0, 1.0],
vec![1.0, 1.0, 1.0],
];
let mut rows: Vec<Vec<f64>> = Vec::with_capacity(12);
for d in &dirs {
for m in 0..3usize {
let mut v = d.clone();
v[m % 3] += 0.01 * m as f64; rows.push(v);
}
}
let dm = DenseMatrix::<f64>::from_2d_vec(&rows).unwrap();
let fastpair = CosinePair::with_top_k(&dm, 4).unwrap();
let k = 4; for i in 0..12usize {
let trio = i / 3 * 3;
let off = i - trio;
let sib_a = trio + (off + 1) % 3;
let sib_b = trio + (off + 2) % 3;
let hits = fastpair.query_row_top_k(i, k).unwrap();
let neighbours: Vec<(usize, f64)> = hits
.iter()
.filter_map(|(d, j)| if *j != i { Some((*j, *d)) } else { None })
.collect();
let found_sibs = neighbours
.iter()
.filter(|(j, _)| *j == sib_a || *j == sib_b)
.count();
assert_eq!(
found_sibs, 2,
"row {}: exact kNN must contain both siblings {:?}, got {:?}",
i,
(sib_a, sib_b),
neighbours
);
for (j, d) in &neighbours {
if *j == sib_a || *j == sib_b {
let brute = cosine_distance(&rows[i], &rows[*j]);
assert!(
(d - brute).abs() < 1e-9,
"distance mismatch row {} -> {}: {} vs {}",
i,
j,
d,
brute
);
}
}
}
}
#[test]
fn eps_above_one_does_not_collapse_antiparallel_pair_into_range() {
init();
let f0 = vec![1.0, 1.0, 1.0, 1.0]; let f1 = vec![-1.0, -1.0, -1.0, -1.0]; let f2 = vec![1.0, 1.0, 1.0, 0.0];
assert!((cosine_distance(&f0, &f1) - 2.0).abs() < 1e-9);
assert!(cosine_distance(&f0, &f2) < 0.2);
let gl = laplacian_from_features(&vec![f0, f1, f2], params(1.2, 3, 3));
assert!(
!has_edge(&gl, 0, 1),
"anti-parallel pair connected at eps=1.2: distance was capped at 1.0"
);
assert!(has_edge(&gl, 0, 2), "positive control edge f0-f2 missing");
}
#[test]
fn antiparallel_pair_connects_when_eps_exceeds_two() {
init();
let f0 = vec![1.0, 1.0, 1.0];
let f1 = vec![-1.0, -1.0, -1.0];
let gl = laplacian_from_features(&vec![f0, f1], params(2.5, 2, 2));
assert!(
has_edge(&gl, 0, 1),
"anti-parallel pair must connect at eps=2.5 (dist 2.0); dead zone above 1.0 would forbid it"
);
}
#[test]
fn explicit_topk_survives_build_with_small_k() {
init();
let rows: Vec<Vec<f64>> = (0..20)
.map(|i| vec![(i % 5) as f64, (i / 5) as f64, 1.0])
.collect();
let (_, gl) = ArrowSpaceBuilder::new()
.with_lambda_graph(0.9, 7, 3, 2.0, None)
.build(rows);
assert_eq!(
gl.topk(),
3,
"explicit topk=3 was overridden by define_result_k"
);
}