use clump::{Dbscan, Kmeans, NOISE};
use serde::Deserialize;
const FIXTURE: &str = include_str!("fixtures/rosetta/clump_clustering.json");
#[derive(Deserialize)]
struct Fixture {
eps: f64,
min_samples: usize,
k: usize,
blobs: Vec<Vec<f64>>,
dbscan_points: Vec<Vec<f64>>,
expected: Expected,
}
#[derive(Deserialize)]
struct Expected {
dbscan_labels: Vec<i64>, kmeans_inertia: f64,
}
fn to_f32_rows(rows: &[Vec<f64>]) -> Vec<Vec<f32>> {
rows.iter()
.map(|r| r.iter().map(|&x| x as f32).collect())
.collect()
}
#[test]
fn rosetta_dbscan_partition_matches_sklearn() {
let fx: Fixture = serde_json::from_str(FIXTURE).expect("parse rosetta fixture");
let pts = to_f32_rows(&fx.dbscan_points);
let labels = Dbscan::new(fx.eps as f32, fx.min_samples)
.fit_predict(&pts)
.expect("dbscan");
let sk = &fx.expected.dbscan_labels;
let n = labels.len();
assert_eq!(n, sk.len(), "label count");
for i in 0..n {
assert_eq!(
labels[i] == NOISE,
sk[i] == -1,
"noise disagreement at point {i}: clump={} sklearn={}",
labels[i],
sk[i]
);
}
for i in 0..n {
for j in (i + 1)..n {
let clump_same = labels[i] != NOISE && labels[i] == labels[j];
let sk_same = sk[i] != -1 && sk[i] == sk[j];
assert_eq!(
clump_same, sk_same,
"co-cluster disagreement for pair ({i},{j})"
);
}
}
}
#[test]
fn rosetta_kmeans_inertia_matches_sklearn() {
let fx: Fixture = serde_json::from_str(FIXTURE).expect("parse rosetta fixture");
let blobs = to_f32_rows(&fx.blobs);
let fit = Kmeans::new(fx.k).with_seed(0).fit(&blobs).expect("kmeans");
let inertia = fit.wcss(&blobs) as f64;
let want = fx.expected.kmeans_inertia;
let rel = (inertia - want).abs() / want;
assert!(
rel < 0.02,
"kmeans inertia: clump={inertia} sklearn={want} relative_diff={rel}"
);
}