use crate::{
builder::ArrowSpaceBuilder,
tests::test_data::{make_gaussian_blob, make_moons_hd},
};
use log::debug;
#[test]
fn test_builder_basic_clustering_with_synthetic_data() {
let items: Vec<Vec<f64>> = make_moons_hd(
100, 0.15, 0.4, 10, 42, );
debug!(
"Generated {} items with {} features",
items.len(),
items[0].len()
);
let (_aspace, gl) = ArrowSpaceBuilder::default()
.with_lambda_graph(0.3, 5, 2, 2.0, None)
.with_normalisation(true)
.with_spectral(true)
.build(items.clone());
debug!("Graph has {} nodes", gl.nnodes);
}
#[test]
fn test_builder_laplacian_diagonal_properties() {
let items: Vec<Vec<f64>> = make_moons_hd(
80, 0.12, 0.5, 8, 123, );
let (aspace, gl) = ArrowSpaceBuilder::default()
.with_lambda_graph(0.2, 4, 2, 2.0, None)
.with_normalisation(true)
.build(items);
let csr = &gl.matrix;
assert!(csr.is_csr(), "Expected CSR layout");
let indptr = csr.indptr();
let indices = csr.indices();
let data = csr.data();
for i in 0..aspace.n_clusters {
let start = indptr.into_raw_storage()[i];
let end = indptr.into_raw_storage()[i + 1];
let mut found = false;
let mut diag = 0.0_f64;
for pos in start..end {
let j = indices[pos];
if j == i {
diag = data[pos];
found = true;
break;
}
}
assert!(
found,
"Diagonal entry at ({},{}) should exist in Laplacian",
i, i
);
assert!(
diag.is_finite(),
"Diagonal at ({},{}) must be finite, got {}",
i,
i,
diag
);
assert!(
diag >= 0.0,
"Diagonal at ({},{}) must be non-negative, got {}",
i,
i,
diag
);
}
debug!(
"✓ All {} diagonal entries are non-negative and finite",
aspace.n_clusters
);
}
#[test]
fn test_builder_minimum_items() {
let items: Vec<Vec<f64>> = make_moons_hd(
20, 0.1, 0.6, 5, 42,
);
let (aspace, gl) = ArrowSpaceBuilder::default()
.with_lambda_graph(0.5, 3, 2, 2.0, None)
.build(items.clone());
assert!(
aspace.n_clusters >= 1,
"Should produce at least one cluster"
);
assert_eq!(gl.nnodes, items.len());
debug!(
"Minimum items test: {} clusters from {} items",
aspace.n_clusters, 20
);
}
#[test]
fn test_builder_scale_invariance_with_normalization() {
let items: Vec<Vec<f64>> = make_moons_hd(60, 0.15, 0.4, 8, 0);
let (aspace1, gl1) = ArrowSpaceBuilder::default()
.with_lambda_graph(0.3, 4, 2, 2.0, None)
.with_normalisation(true) .build(items.clone());
let scale_factor = 5.7;
let items_scaled: Vec<Vec<f64>> = items
.iter()
.map(|item| item.iter().map(|&x| x * scale_factor).collect())
.collect();
let (aspace2, gl2) = ArrowSpaceBuilder::default()
.with_lambda_graph(0.3, 4, 2, 2.0, None)
.with_normalisation(true) .build(items_scaled);
assert!(
(aspace1.n_clusters as i32 - aspace2.n_clusters as i32).abs() <= 3,
"Normalized clustering should be scale-invariant: {} vs {}",
aspace1.n_clusters,
aspace2.n_clusters
);
assert_eq!(
gl1.nnodes, gl2.nnodes,
"Graph node counts should match under scaling"
);
debug!(
"✓ Scale invariance verified: original={} clusters, scaled={} clusters",
aspace1.n_clusters, aspace2.n_clusters
);
}
#[test]
fn test_builder_laplacian_symmetry() {
let items: Vec<Vec<f64>> = make_moons_hd(70, 0.18, 0.35, 9, 456);
let (aspace, gl) = ArrowSpaceBuilder::default()
.with_lambda_graph(0.25, 5, 2, 2.0, None)
.with_normalisation(true)
.build(items);
let csr = &gl.matrix;
assert!(csr.is_csr(), "Expected CSR layout");
let n = aspace.n_clusters;
let indptr = csr.indptr();
let indices = csr.indices();
let data = csr.data();
let eps = 1e-10;
let mut symmetric_pairs = 0;
let mut total_edges = 0;
for i in 0..n {
let start = indptr.into_raw_storage()[i];
let end = indptr.into_raw_storage()[i + 1];
for p in start..end {
let j = indices[p];
if i == j {
continue; }
total_edges += 1;
let vij = data[p];
let js = indptr.into_raw_storage()[j];
let je = indptr.into_raw_storage()[j + 1];
let mut vji_opt: Option<f64> = None;
for q in js..je {
if indices[q] == i {
vji_opt = Some(data[q]);
break;
}
}
if let Some(vji) = vji_opt {
assert!(
(vij - vji).abs() <= eps * (1.0 + vij.abs().max(vji.abs())),
"Symmetric entries must match: L[{},{}]={:.6} vs L[{},{}]={:.6}",
i,
j,
vij,
j,
i,
vji
);
symmetric_pairs += 1;
} else {
panic!(
"Graph should be symmetric: found edge ({},{}) = {:.6} but missing ({},{})",
i, j, vij, j, i
);
}
}
}
debug!(
"✓ Verified symmetry for {} edge pairs (total {} edges)",
symmetric_pairs, total_edges
);
}
#[test]
fn test_builder_parameter_preservation() {
let items: Vec<Vec<f64>> = make_moons_hd(50, 0.2, 0.4, 7, 321);
let (_, gl) = ArrowSpaceBuilder::default()
.with_lambda_graph(
0.123, 7, 3, 3.5, Some(0.456), )
.with_normalisation(false)
.build(items);
assert_eq!(gl.graph_params.eps, 0.123, "eps must match");
assert_eq!(gl.graph_params.k, 7, "k must match");
assert_eq!(gl.graph_params.topk, 3 + 1, "topk must match");
assert_eq!(gl.graph_params.p, 3.5, "p must match");
assert_eq!(gl.graph_params.sigma, Some(0.456), "sigma must match");
assert_eq!(
gl.graph_params.normalise, false,
"normalise flag must match"
);
debug!("✓ All graph parameters correctly preserved");
}
#[test]
fn test_builder_with_different_dimensions() {
let test_cases = vec![
(50, 3, "low-dimensional"),
(60, 10, "medium-dimensional"),
(70, 25, "high-dimensional"),
];
for (n_samples, dims, desc) in test_cases {
let items: Vec<Vec<f64>> = make_moons_hd(
n_samples,
0.15,
0.4,
dims,
42 + dims as u64, );
let (aspace, gl) = ArrowSpaceBuilder::default()
.with_lambda_graph(0.3, 5, 2, 2.0, None)
.with_normalisation(true)
.with_spectral(true)
.with_sparsity_check(false)
.build(items);
assert!(aspace.n_clusters > 0, "{}: Should produce clusters", desc);
assert!(
aspace.nfeatures == dims,
"{}: Features should be {}",
desc,
dims
);
debug!(
"{}: {} clusters, {} features, {} nodes",
desc, aspace.n_clusters, aspace.nfeatures, gl.nnodes
);
}
}
#[test]
fn test_builder_spectral_laplacian_shape() {
let items: Vec<Vec<f64>> = make_moons_hd(90, 0.16, 0.38, 12, 555);
let (aspace_no_spectral, _) = ArrowSpaceBuilder::default()
.with_lambda_graph(0.25, 4, 2, 2.0, None)
.with_spectral(false)
.build(items.clone());
let (aspace_spectral, _) = ArrowSpaceBuilder::default()
.with_lambda_graph(0.25, 4, 2, 2.0, None)
.with_spectral(true)
.build(items.clone());
assert_eq!(
aspace_no_spectral.signals.shape(),
(0, 0),
"Signals should be empty when spectral is disabled"
);
let expected_dim = aspace_spectral.nfeatures;
assert_eq!(
aspace_spectral.signals.shape(),
(expected_dim, expected_dim),
"Signals should be {}x{} (feature-by-feature Laplacian)",
expected_dim,
expected_dim
);
debug!(
"✓ Spectral Laplacian shape: {:?}",
aspace_spectral.signals.shape()
);
}
#[test]
fn test_builder_lambda_values_are_nonnegative() {
let items: Vec<Vec<f64>> = make_moons_hd(100, 0.2, 0.35, 11, 999);
let (aspace, _) = ArrowSpaceBuilder::default()
.with_lambda_graph(0.3, 5, 2, 2.0, None)
.with_normalisation(true)
.with_spectral(true)
.build(items);
let lambdas = aspace.lambdas();
for (i, &lam) in lambdas.iter().enumerate() {
assert!(
lam >= 0.0,
"Lambda at index {} should be non-negative, got {:.6}",
i,
lam
);
}
let min_lambda = lambdas.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let max_lambda = lambdas.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
debug!(
"✓ All {} lambdas are non-negative: min={:.6}, max={:.6}",
lambdas.len(),
min_lambda,
max_lambda
);
}
#[test]
fn test_builder_with_high_noise() {
let items = make_gaussian_blob(300, 0.9);
let (aspace, _gl) = ArrowSpaceBuilder::default()
.with_lambda_graph(0.4, 6, 3, 2.0, None)
.with_normalisation(true)
.build(items);
assert!(
aspace.n_clusters >= 2,
"Should produce valid clusters even with high noise, got {}",
aspace.n_clusters
);
debug!(
"✓ Found {} clusters (conservative estimate for noisy data)",
aspace.n_clusters
);
}
#[test]
fn test_builder_normalization_effects() {
let items: Vec<Vec<f64>> = make_moons_hd(75, 0.14, 0.45, 8, 654);
let (aspace_norm, gl_norm) = ArrowSpaceBuilder::default()
.with_lambda_graph(0.3, 5, 2, 2.0, None)
.with_normalisation(true)
.build(items.clone());
let (aspace_raw, gl_raw) = ArrowSpaceBuilder::default()
.with_lambda_graph(0.3, 5, 2, 2.0, None)
.with_normalisation(false)
.build(items);
debug!("Normalized: {} clusters", aspace_norm.n_clusters);
debug!("Raw (τ-mode): {} clusters", aspace_raw.n_clusters);
assert_eq!(gl_norm.graph_params.normalise, true);
assert_eq!(gl_raw.graph_params.normalise, false);
assert!(aspace_norm.n_clusters > 0);
assert!(aspace_raw.n_clusters > 0);
debug!("✓ Both normalization modes produce valid clusterings");
}