use crate::kmeans::run_kmeans;
use crate::math::l2_distance_sqr;
use rand::prelude::*;
#[derive(Debug, Clone)]
pub struct Cluster {
data: Vec<Vec<f32>>,
centroid: Vec<f32>,
}
impl Cluster {
pub fn from_data(data: Vec<Vec<f32>>) -> Self {
let centroid = compute_centroid(&data);
Self { data, centroid }
}
pub fn data(&self) -> &[Vec<f32>] {
&self.data
}
pub fn centroid(&self) -> &[f32] {
&self.centroid
}
pub fn size(&self) -> usize {
self.data.len()
}
}
pub struct HierarchicalClustering {
pub max_cluster_size: usize,
pub branching_factor: usize,
pub balance_weight: f32,
pub max_iterations: usize,
}
impl HierarchicalClustering {
pub fn new(max_cluster_size: usize, branching_factor: usize, balance_weight: f32) -> Self {
Self {
max_cluster_size,
branching_factor,
balance_weight,
max_iterations: 100,
}
}
pub fn cluster(&self, data: &[Vec<f32>]) -> Vec<Cluster> {
if data.is_empty() {
return Vec::new();
}
let mut rng = StdRng::seed_from_u64(42);
let mut active_clusters = vec![Cluster::from_data(data.to_vec())];
let mut final_clusters = Vec::new();
println!(
"Starting hierarchical clustering: {} vectors, max_size={}, branching={}",
data.len(),
self.max_cluster_size,
self.branching_factor
);
let mut iteration = 0;
while let Some(cluster) = active_clusters.pop() {
if cluster.size() <= self.max_cluster_size {
final_clusters.push(cluster);
} else {
iteration += 1;
println!(
" Iteration {}: Splitting cluster of size {} into {} subclusters",
iteration,
cluster.size(),
self.branching_factor
);
let subclusters = self.split_cluster(&cluster, &mut rng);
for subcluster in subclusters {
active_clusters.push(subcluster);
}
}
}
println!(
"Hierarchical clustering complete: {} final clusters",
final_clusters.len()
);
final_clusters
}
fn split_cluster(&self, cluster: &Cluster, rng: &mut StdRng) -> Vec<Cluster> {
let k = self.branching_factor;
let data = cluster.data();
let kmeans_result = run_kmeans(data, k, self.max_iterations, rng);
let mut subcluster_data: Vec<Vec<Vec<f32>>> = vec![Vec::new(); k];
for (vec_idx, &cluster_id) in kmeans_result.assignments.iter().enumerate() {
subcluster_data[cluster_id].push(data[vec_idx].clone());
}
if self.balance_weight > 0.0 {
subcluster_data = self.balance_clusters(subcluster_data, &kmeans_result.centroids);
}
subcluster_data
.into_iter()
.filter(|data| !data.is_empty())
.map(Cluster::from_data)
.collect()
}
fn balance_clusters(
&self,
mut clusters: Vec<Vec<Vec<f32>>>,
centroids: &[Vec<f32>],
) -> Vec<Vec<Vec<f32>>> {
let total_size: usize = clusters.iter().map(|c| c.len()).sum();
let k = clusters.len();
let target_size = total_size / k;
let max_allowed = (target_size as f32 * (1.0 + self.balance_weight)) as usize;
for _iteration in 0..10 {
let sizes: Vec<usize> = clusters.iter().map(|c| c.len()).collect();
let mut over_idx = None;
let mut under_idx = None;
for (i, &size) in sizes.iter().enumerate() {
if size > max_allowed {
over_idx = Some(i);
break;
}
}
for (i, &size) in sizes.iter().enumerate() {
if size < target_size {
under_idx = Some(i);
break;
}
}
if over_idx.is_none() || under_idx.is_none() {
break; }
let over_i = over_idx.unwrap();
let under_i = under_idx.unwrap();
if let Some(closest_idx) =
self.find_closest_vector_to_centroid(&clusters[over_i], ¢roids[under_i])
{
let vec = clusters[over_i].remove(closest_idx);
clusters[under_i].push(vec);
} else {
break;
}
}
clusters
}
fn find_closest_vector_to_centroid(
&self,
vectors: &[Vec<f32>],
centroid: &[f32],
) -> Option<usize> {
if vectors.is_empty() {
return None;
}
let mut best_idx = 0;
let mut best_dist = l2_distance_sqr(&vectors[0], centroid);
for (idx, vec) in vectors.iter().enumerate().skip(1) {
let dist = l2_distance_sqr(vec, centroid);
if dist < best_dist {
best_dist = dist;
best_idx = idx;
}
}
Some(best_idx)
}
}
fn compute_centroid(data: &[Vec<f32>]) -> Vec<f32> {
if data.is_empty() {
return Vec::new();
}
let dim = data[0].len();
let mut centroid = vec![0.0; dim];
for vec in data {
for (i, &val) in vec.iter().enumerate() {
centroid[i] += val;
}
}
let n = data.len() as f32;
for val in &mut centroid {
*val /= n;
}
centroid
}
#[cfg(test)]
mod tests {
use super::*;
fn generate_test_data(n: usize, dim: usize) -> Vec<Vec<f32>> {
let mut rng = StdRng::seed_from_u64(12345);
(0..n)
.map(|_| (0..dim).map(|_| rng.gen()).collect())
.collect()
}
#[test]
fn test_compute_centroid() {
let data = vec![
vec![0.0, 0.0],
vec![2.0, 0.0],
vec![0.0, 2.0],
vec![2.0, 2.0],
];
let centroid = compute_centroid(&data);
assert_eq!(centroid.len(), 2);
assert!((centroid[0] - 1.0).abs() < 1e-5);
assert!((centroid[1] - 1.0).abs() < 1e-5);
}
#[test]
fn test_cluster_creation() {
let data = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
let cluster = Cluster::from_data(data);
assert_eq!(cluster.size(), 2);
assert_eq!(cluster.data().len(), 2);
assert_eq!(cluster.centroid().len(), 2);
assert!((cluster.centroid()[0] - 2.0).abs() < 1e-5);
assert!((cluster.centroid()[1] - 3.0).abs() < 1e-5);
}
#[test]
fn test_hierarchical_clustering_basic() {
let data = generate_test_data(100, 8);
let clustering = HierarchicalClustering::new(20, 4, 1.0);
let clusters = clustering.cluster(&data);
for cluster in &clusters {
assert!(
cluster.size() <= 20,
"Cluster size {} exceeds limit 20",
cluster.size()
);
}
let total_size: usize = clusters.iter().map(|c| c.size()).sum();
assert_eq!(total_size, 100);
}
#[test]
fn test_hierarchical_clustering_balance() {
let data = generate_test_data(1000, 32);
let clustering = HierarchicalClustering::new(100, 5, 1.0);
let clusters = clustering.cluster(&data);
let sizes: Vec<usize> = clusters.iter().map(|c| c.size()).collect();
let mean = sizes.iter().sum::<usize>() as f32 / sizes.len() as f32;
let variance: f32 = sizes
.iter()
.map(|&s| {
let diff = s as f32 - mean;
diff * diff
})
.sum::<f32>()
/ sizes.len() as f32;
let coefficient_of_variation = variance.sqrt() / mean;
println!(
"Mean cluster size: {:.1}, CoV: {:.3}",
mean, coefficient_of_variation
);
assert!(coefficient_of_variation < 0.6);
for cluster in &clusters {
assert!(cluster.size() > 0, "Empty cluster found");
}
}
}