Skip to main content

lance_index/vector/
kmeans.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! KMeans implementation for Apache Arrow Arrays.
5//!
6//! Support ``l2``, ``cosine`` and ``dot`` distances, see [DistanceType].
7//!
8//! ``Cosine`` distance are calculated by normalizing the vectors to unit length,
9//! and run ``l2`` distance on the unit vectors.
10//!
11
12use core::f32;
13use std::cmp::Ordering;
14use std::collections::BinaryHeap;
15use std::ops::{AddAssign, DivAssign};
16use std::sync::Arc;
17use std::vec;
18use std::{collections::HashMap, ops::MulAssign};
19
20use arrow_array::{
21    Array, ArrayRef, FixedSizeListArray, Float32Array, PrimitiveArray, UInt32Array,
22    cast::AsArray,
23    types::{ArrowPrimitiveType, Float16Type, Float32Type, Float64Type, UInt8Type},
24};
25use arrow_array::{ArrowNumericType, UInt8Array};
26use arrow_ord::sort::sort_to_indices;
27use arrow_schema::{ArrowError, DataType};
28use bitvec::prelude::*;
29use lance_arrow::FixedSizeListArrayExt;
30use lance_core::utils::tokio::get_num_compute_intensive_cpus;
31use lance_linalg::distance::hamming::{hamming, hamming_distance_batch};
32use lance_linalg::distance::{DistanceType, Normalize, dot_distance_batch};
33use lance_linalg::kernels::{argmin_value_float, argmin_value_float_with_bias};
34use log::{info, warn};
35use num_traits::One;
36use num_traits::{AsPrimitive, Float, FromPrimitive, Num, Zero};
37use rand::prelude::*;
38use rayon::prelude::*;
39use {
40    lance_linalg::distance::{
41        Dot,
42        l2::{L2, l2_distance_batch},
43    },
44    lance_linalg::kernels::argmin_value,
45};
46
47use crate::vector::utils::SimpleIndex;
48use crate::{Error, Result};
49
50/// KMean initialization method.
51#[derive(Debug, PartialEq)]
52pub enum KMeanInit {
53    Random,
54    Incremental(Arc<FixedSizeListArray>),
55}
56
57/// KMean Training Parameters
58pub struct KMeansParams {
59    /// Max number of iterations.
60    pub max_iters: u32,
61
62    /// When the difference of mean distance to the centroids is less than this `tolerance`
63    /// threshold, stop the training.
64    pub tolerance: f64,
65
66    /// Run kmeans multiple times and pick the best (balanced) one.
67    pub redos: usize,
68
69    /// Init methods.
70    pub init: KMeanInit,
71
72    /// The metric to calculate distance.
73    pub distance_type: DistanceType,
74
75    /// Balance factor for the kmeans clustering.
76    /// Higher value means more balanced clustering.
77    ///
78    /// Setting this value to 0 means no balance factor,
79    /// which is the same as normal kmeans clustering.
80    pub balance_factor: f32,
81
82    /// The number of clusters to train in each hierarchical level.
83    ///
84    /// Default is 16, which performs the best performance in our experiments.
85    /// Higher would split the clusters more aggressively, which would be more accurate but slower.
86    /// hierarchical kmeans is enabled only if hierarchical_k > 1 and k > 256.
87    pub hierarchical_k: usize,
88
89    /// Optional sync callback for iteration progress: (current_iteration, max_iterations).
90    pub on_progress: Option<Arc<dyn Fn(u32, u32) + Send + Sync>>,
91}
92
93impl std::fmt::Debug for KMeansParams {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        f.debug_struct("KMeansParams")
96            .field("max_iters", &self.max_iters)
97            .field("tolerance", &self.tolerance)
98            .field("redos", &self.redos)
99            .field("init", &self.init)
100            .field("distance_type", &self.distance_type)
101            .field("balance_factor", &self.balance_factor)
102            .field("hierarchical_k", &self.hierarchical_k)
103            .field("on_progress", &self.on_progress.as_ref().map(|_| "..."))
104            .finish()
105    }
106}
107
108impl Default for KMeansParams {
109    fn default() -> Self {
110        Self {
111            max_iters: 50,
112            tolerance: 1e-4,
113            redos: 1,
114            init: KMeanInit::Random,
115            distance_type: DistanceType::L2,
116            balance_factor: 0.0,
117            hierarchical_k: 16,
118            on_progress: None,
119        }
120    }
121}
122
123impl KMeansParams {
124    pub fn new(
125        centroids: Option<Arc<FixedSizeListArray>>,
126        max_iters: u32,
127        redos: usize,
128        distance_type: DistanceType,
129    ) -> Self {
130        let init = match centroids {
131            Some(centroids) => KMeanInit::Incremental(centroids),
132            None => KMeanInit::Random,
133        };
134        Self {
135            max_iters,
136            redos,
137            distance_type,
138            init,
139            ..Default::default()
140        }
141    }
142
143    /// Set the balance factor for the kmeans clustering.
144    ///
145    /// Higher value means more balanced clustering.
146    /// Setting this value to 0 means no balance factor,
147    /// which is the same as normal kmeans clustering.
148    pub fn with_balance_factor(mut self, balance_factor: f32) -> Self {
149        self.balance_factor = balance_factor;
150        self
151    }
152
153    pub fn with_on_progress(mut self, cb: Arc<dyn Fn(u32, u32) + Send + Sync>) -> Self {
154        self.on_progress = Some(cb);
155        self
156    }
157
158    /// Set the number of clusters to train in each hierarchical level.
159    ///
160    /// Higher would split the clusters more aggressively, which would be more accurate but slower.
161    /// hierarchical kmeans is enabled only if hierarchical_k > 1 and k > 256.
162    pub fn with_hierarchical_k(mut self, hierarchical_k: usize) -> Self {
163        self.hierarchical_k = hierarchical_k;
164        self
165    }
166}
167
168/// Randomly initialize kmeans centroids.
169///
170///
171fn kmeans_random_init<T: ArrowPrimitiveType>(
172    data: &[T::Native],
173    dimension: usize,
174    k: usize,
175    mut rng: impl Rng,
176    distance_type: DistanceType,
177) -> KMeans {
178    assert!(data.len() >= k * dimension);
179    let chosen = (0..data.len() / dimension).choose_multiple(&mut rng, k);
180    let centroids = PrimitiveArray::<T>::from_iter_values(
181        chosen
182            .iter()
183            .flat_map(|&i| data[i * dimension..(i + 1) * dimension].iter())
184            .copied(),
185    );
186    KMeans {
187        centroids: Arc::new(centroids),
188        dimension,
189        distance_type,
190        loss: f64::MAX,
191    }
192}
193
194/// Split one big cluster into two smaller clusters. After split, each
195/// cluster has approximately half of the vectors.
196fn split_clusters<T: Float + MulAssign>(
197    n: usize,
198    cnts: &mut [usize],
199    centroids: &mut [T],
200    dim: usize,
201) {
202    let eps = T::from(1.0 / 1024.0).unwrap();
203    let mut rng = SmallRng::from_os_rng();
204    for i in 0..cnts.len() {
205        if cnts[i] == 0 {
206            let mut j = 0;
207            loop {
208                let p = (cnts[j] as f32 - 1.0) / (n - cnts.len()) as f32;
209                if rng.random::<f32>() < p {
210                    break;
211                }
212                j += 1;
213                j %= cnts.len();
214            }
215
216            cnts[i] = cnts[j] / 2;
217            cnts[j] -= cnts[i];
218            for k in 0..dim {
219                if k % 2 == 0 {
220                    centroids[i * dim + k] = centroids[j * dim + k] * (T::one() + eps);
221                    centroids[j * dim + k] *= T::one() - eps;
222                } else {
223                    centroids[i * dim + k] = centroids[j * dim + k] * (T::one() - eps);
224                    centroids[j * dim + k] *= T::one() + eps;
225                }
226            }
227        }
228    }
229}
230
231// compute the cluster sizes and return adjusted balance factor
232fn compute_cluster_sizes(
233    membership: &[Option<u32>],
234    radius: &[f32],
235    losses: &[f64],
236    cluster_sizes: &mut [usize],
237) -> f32 {
238    cluster_sizes.fill(0);
239    let mut max_cluster_id = 0;
240    let mut max_cluster_size = 0;
241    membership.iter().for_each(|cluster_id| {
242        if let Some(cluster_id) = cluster_id {
243            let cluster_id = *cluster_id as usize;
244            cluster_sizes[cluster_id] += 1;
245            if cluster_sizes[cluster_id] > max_cluster_size {
246                max_cluster_size = cluster_sizes[cluster_id];
247                max_cluster_id = cluster_id;
248            }
249        }
250    });
251
252    (radius[max_cluster_id] - losses[max_cluster_id] as f32 / cluster_sizes[max_cluster_id] as f32)
253        / membership.len() as f32
254}
255
256fn compute_balance_loss(cluster_sizes: &[usize], n: usize, balance_factor: f32) -> f32 {
257    let size_loss = cluster_sizes.iter().map(|size| size.pow(2)).sum::<usize>() as f32;
258    balance_factor * (size_loss - n.pow(2) as f32 / cluster_sizes.len() as f32)
259}
260
261pub trait KMeansAlgo<T: Num> {
262    /// Recompute the membership of each vector.
263    ///
264    /// Parameters:
265    ///
266    /// - *data*: a `N * dimension` floating array. Not necessarily normalized.
267    ///
268    /// Returns:
269    /// - *membership*: the membership of each vector.
270    /// - *cluster_radius*: the radius of each cluster.
271    /// - *losses*: the losses of each cluster.
272    fn compute_membership_and_loss(
273        centroids: &[T],
274        data: &[T],
275        dimension: usize,
276        distance_type: DistanceType,
277        balance_factor: f32,
278        cluster_sizes: Option<&[usize]>,
279        index: Option<&SimpleIndex>,
280    ) -> (Vec<Option<u32>>, Vec<f32>, Vec<f64>) {
281        let (membership, dists) = Self::compute_membership_and_dist(
282            centroids,
283            data,
284            dimension,
285            distance_type,
286            balance_factor,
287            cluster_sizes,
288            index,
289        );
290
291        let k = centroids.len() / dimension;
292        let mut cluster_radius = vec![0.0; k];
293        let mut losses = vec![0.0; k];
294        for (cluster_id, dist) in membership.iter().zip(dists.iter()) {
295            if let (Some(cluster_id), Some(dist)) = (cluster_id, dist) {
296                let cluster_id = *cluster_id as usize;
297                cluster_radius[cluster_id] = cluster_radius[cluster_id].max(*dist);
298                losses[cluster_id] += *dist as f64;
299            }
300        }
301
302        (membership, cluster_radius, losses)
303    }
304
305    fn compute_membership_and_dist(
306        centroids: &[T],
307        data: &[T],
308        dimension: usize,
309        distance_type: DistanceType,
310        balance_factor: f32,
311        cluster_sizes: Option<&[usize]>,
312        index: Option<&SimpleIndex>,
313    ) -> (Vec<Option<u32>>, Vec<Option<f32>>);
314
315    /// Construct a new KMeans model.
316    fn to_kmeans(
317        data: &[T],
318        dimension: usize,
319        k: usize,
320        membership: &[Option<u32>],
321        cluster_sizes: &mut [usize],
322        distance_type: DistanceType,
323        loss: f64,
324    ) -> KMeans;
325}
326
327pub struct KMeansAlgoFloat<T: ArrowNumericType>
328where
329    T::Native: Float + Num,
330{
331    phantom_data: std::marker::PhantomData<T>,
332}
333
334impl<T: ArrowNumericType> KMeansAlgo<T::Native> for KMeansAlgoFloat<T>
335where
336    T::Native: Float + Dot + L2 + MulAssign + DivAssign + AddAssign + FromPrimitive + Sync,
337    PrimitiveArray<T>: From<Vec<T::Native>>,
338{
339    fn compute_membership_and_dist(
340        centroids: &[T::Native],
341        data: &[T::Native],
342        dimension: usize,
343        distance_type: DistanceType,
344        balance_factor: f32,
345        cluster_sizes: Option<&[usize]>,
346        index: Option<&SimpleIndex>,
347    ) -> (Vec<Option<u32>>, Vec<Option<f32>>) {
348        let cluster_and_dists = match index {
349            Some(index) => data
350                .par_chunks(dimension)
351                .map(|vec| {
352                    let query = PrimitiveArray::<T>::from_iter_values(vec.iter().copied());
353                    // unable to use balance_factor here because index.search returns the closest centroid
354                    index
355                        .search(Arc::new(query))
356                        .map(|(id, dist)| Some((id, dist)))
357                        .unwrap()
358                })
359                .collect::<Vec<_>>(),
360            None => match distance_type {
361                DistanceType::L2 => data
362                    .par_chunks(dimension)
363                    .map(|vec| {
364                        argmin_value_float_with_bias(
365                            l2_distance_batch(vec, centroids, dimension),
366                            cluster_sizes
367                                .map(|size| size.iter().map(|size| balance_factor * *size as f32)),
368                        )
369                    })
370                    .collect::<Vec<_>>(),
371                DistanceType::Dot => data
372                    .par_chunks(dimension)
373                    .map(|vec| {
374                        argmin_value_float_with_bias(
375                            dot_distance_batch(vec, centroids, dimension),
376                            cluster_sizes
377                                .map(|size| size.iter().map(|size| balance_factor * *size as f32)),
378                        )
379                    })
380                    .collect::<Vec<_>>(),
381                _ => {
382                    panic!(
383                        "KMeans::find_partitions: {} is not supported",
384                        distance_type
385                    );
386                }
387            },
388        };
389
390        cluster_and_dists.into_iter().map(Option::unzip).unzip()
391    }
392
393    fn to_kmeans(
394        data: &[T::Native],
395        dimension: usize,
396        k: usize,
397        membership: &[Option<u32>],
398        cluster_sizes: &mut [usize],
399        distance_type: DistanceType,
400        loss: f64,
401    ) -> KMeans {
402        let mut centroids = vec![T::Native::zero(); k * dimension];
403
404        let mut num_cpus = get_num_compute_intensive_cpus();
405        if k < num_cpus || k < 16 {
406            num_cpus = 1;
407        }
408        let chunk_size = k / num_cpus;
409
410        centroids
411            .par_chunks_mut(dimension * chunk_size)
412            .enumerate()
413            .with_max_len(1)
414            .for_each(|(i, centroids)| {
415                let start = i * chunk_size;
416                let end = ((i + 1) * chunk_size).min(k);
417                data.chunks(dimension)
418                    .zip(membership.iter())
419                    .filter_map(|(vector, cluster_id)| {
420                        cluster_id.map(|cluster_id| (vector, cluster_id as usize))
421                    })
422                    .for_each(|(vector, cluster_id)| {
423                        if start <= cluster_id && cluster_id < end {
424                            let local_id = cluster_id - start;
425                            let centroid =
426                                &mut centroids[local_id * dimension..(local_id + 1) * dimension];
427                            centroid.iter_mut().zip(vector).for_each(|(c, v)| *c += *v);
428                        }
429                    });
430            });
431
432        centroids
433            .par_chunks_mut(dimension)
434            .zip(cluster_sizes.par_iter())
435            .for_each(|(centroid, &cnt)| {
436                if cnt > 0 {
437                    let norm = T::Native::one() / T::Native::from_usize(cnt).unwrap();
438                    centroid.iter_mut().for_each(|v| *v *= norm);
439                }
440            });
441
442        let empty_clusters = cluster_sizes.iter().filter(|&cnt| *cnt == 0).count();
443        if empty_clusters as f32 / k as f32 > 0.1 {
444            if data.len() / dimension < k * 256 {
445                warn!(
446                    "KMeans: more than 10% of clusters are empty: {} of {}.\nHelp: this could mean your dataset \
447                is too small to have a meaningful index ({} < {}) or has many duplicate vectors.",
448                    empty_clusters,
449                    k,
450                    data.len() / dimension,
451                    k * 256
452                );
453            } else {
454                warn!(
455                    "KMeans: more than 10% of clusters are empty: {} of {}.\nHelp: this could mean your dataset \
456                has many duplicate vectors.",
457                    empty_clusters, k
458                );
459            }
460        }
461
462        split_clusters(
463            data.len() / dimension,
464            cluster_sizes,
465            &mut centroids,
466            dimension,
467        );
468
469        KMeans {
470            centroids: Arc::new(PrimitiveArray::<T>::from(centroids)),
471            dimension,
472            distance_type,
473            loss,
474        }
475    }
476}
477
478struct KModeAlgo {}
479
480impl KMeansAlgo<u8> for KModeAlgo {
481    fn compute_membership_and_dist(
482        centroids: &[u8],
483        data: &[u8],
484        dimension: usize,
485        distance_type: DistanceType,
486        balance_factor: f32,
487        cluster_sizes: Option<&[usize]>,
488        _: Option<&SimpleIndex>,
489    ) -> (Vec<Option<u32>>, Vec<Option<f32>>) {
490        assert_eq!(distance_type, DistanceType::Hamming);
491        let cluster_and_dists = data
492            .par_chunks(dimension)
493            .map(|vec| {
494                argmin_value(
495                    centroids
496                        .chunks_exact(dimension)
497                        .enumerate()
498                        .map(|(id, c)| {
499                            hamming(vec, c)
500                                + balance_factor
501                                    * cluster_sizes.map(|sizes| sizes[id] as f32).unwrap_or(0.0)
502                        }),
503                )
504            })
505            .collect::<Vec<_>>();
506        cluster_and_dists.into_iter().map(Option::unzip).unzip()
507    }
508
509    fn to_kmeans(
510        data: &[u8],
511        dimension: usize,
512        k: usize,
513        membership: &[Option<u32>],
514        _cluster_sizes: &mut [usize],
515        distance_type: DistanceType,
516        loss: f64,
517    ) -> KMeans {
518        assert_eq!(distance_type, DistanceType::Hamming);
519
520        let mut clusters = HashMap::<u32, Vec<usize>>::new();
521        membership.iter().enumerate().for_each(|(i, part_id)| {
522            if let Some(part_id) = part_id {
523                clusters.entry(*part_id).or_default().push(i);
524            }
525        });
526        let centroids = (0..k as u32)
527            .into_par_iter()
528            .flat_map(|part_id| {
529                if let Some(vecs) = clusters.get(&part_id) {
530                    let mut ones = vec![0_u32; dimension * 8];
531                    let cnt = vecs.len() as u32;
532                    vecs.iter().for_each(|&i| {
533                        let vec = &data[i * dimension..(i + 1) * dimension];
534                        ones.iter_mut()
535                            .zip(vec.view_bits::<Lsb0>())
536                            .for_each(|(c, v)| {
537                                if *v.as_ref() {
538                                    *c += 1;
539                                }
540                            });
541                    });
542
543                    let bits = ones.iter().map(|&c| c * 2 > cnt).collect::<BitVec<u8>>();
544                    bits.as_raw_slice()
545                        .iter()
546                        .copied()
547                        .map(Some)
548                        .collect::<Vec<_>>()
549                } else {
550                    vec![None; dimension]
551                }
552            })
553            .collect::<Vec<_>>();
554
555        KMeans {
556            centroids: Arc::new(UInt8Array::from(centroids)),
557            dimension,
558            distance_type,
559            loss,
560        }
561    }
562}
563
564/// KMeans implementation for Apache Arrow Arrays.
565#[derive(Debug, Clone)]
566pub struct KMeans {
567    /// Flattened array of centroids.
568    ///
569    /// dimension * k of floating number.
570    pub centroids: ArrayRef,
571
572    /// The dimension of each vector.
573    pub dimension: usize,
574
575    /// How to calculate distance between two vectors.
576    pub distance_type: DistanceType,
577
578    /// The loss of the last training.
579    pub loss: f64,
580}
581
582impl KMeans {
583    fn empty(dimension: usize, distance_type: DistanceType) -> Self {
584        Self {
585            centroids: arrow_array::array::new_empty_array(&DataType::Float32),
586            dimension,
587            distance_type,
588            loss: f64::MAX,
589        }
590    }
591
592    /// Create a [`KMeans`] with existing centroids.
593    /// It is useful for continuing training.
594    pub fn with_centroids(
595        centroids: ArrayRef,
596        dimension: usize,
597        distance_type: DistanceType,
598        loss: f64,
599    ) -> Self {
600        assert!(matches!(
601            centroids.data_type(),
602            DataType::Float16 | DataType::Float32 | DataType::Float64 | DataType::UInt8
603        ));
604        Self {
605            centroids,
606            dimension,
607            distance_type,
608            loss,
609        }
610    }
611
612    /// Initialize a [`KMeans`] with random centroids.
613    ///
614    /// Parameters
615    /// - *data*: training data. provided to do samplings.
616    /// - *k*: the number of clusters.
617    /// - *distance_type*: the distance type to calculate distance.
618    /// - *rng*: random generator.
619    fn init_random<T: ArrowPrimitiveType>(
620        data: &[T::Native],
621        dimension: usize,
622        k: usize,
623        rng: impl Rng,
624        distance_type: DistanceType,
625    ) -> Self {
626        kmeans_random_init::<T>(data, dimension, k, rng, distance_type)
627    }
628
629    /// Train a KMeans model on data with `k` clusters.
630    pub fn new(data: &FixedSizeListArray, k: usize, max_iters: u32) -> arrow::error::Result<Self> {
631        let params = KMeansParams {
632            max_iters,
633            distance_type: DistanceType::L2,
634            ..Default::default()
635        };
636        Self::new_with_params(data, k, &params)
637    }
638
639    fn train_kmeans<T: ArrowNumericType, Algo: KMeansAlgo<T::Native>>(
640        data: &FixedSizeListArray,
641        k: usize,
642        params: &KMeansParams,
643    ) -> arrow::error::Result<Self>
644    where
645        T::Native: Num,
646    {
647        // the data is `num_partitions * sample_rate` vectors,
648        // but here `k` may be not `num_partitions` in the case of hierarchical kmeans,
649        // so we need to sample the sampled data again here.
650        // we have to limit the number of data to avoid division underflow,
651        // the threshold 512 is chosen because the minimal normal f16 value will be 0 if divided by 1024.
652        let data = if data.len() >= k * 512 {
653            data.slice(0, k * 512)
654        } else {
655            data.clone()
656        };
657
658        let n = data.len();
659        let dimension = data.value_length() as usize;
660
661        let data =
662            data.values()
663                .as_primitive_opt::<T>()
664                .ok_or(ArrowError::InvalidArgumentError(format!(
665                    "KMeans: data must be {}, got: {}",
666                    T::DATA_TYPE,
667                    data.value_type()
668                )))?;
669
670        let mut best_kmeans = Self::empty(dimension, params.distance_type);
671        let mut cluster_sizes = vec![0; k];
672        let mut adjusted_balance_factor = f32::MAX;
673
674        // TODO: use seed for Rng.
675        let rng = SmallRng::from_os_rng();
676        for redo in 1..=params.redos {
677            let mut kmeans: Self = match &params.init {
678                KMeanInit::Random => Self::init_random::<T>(
679                    data.values(),
680                    dimension,
681                    k,
682                    rng.clone(),
683                    params.distance_type,
684                ),
685                KMeanInit::Incremental(centroids) => Self::with_centroids(
686                    centroids.values().clone(),
687                    dimension,
688                    params.distance_type,
689                    f64::MAX,
690                ),
691            };
692
693            let mut loss = f64::MAX;
694            for i in 1..=params.max_iters {
695                if let Some(cb) = &params.on_progress {
696                    cb(i, params.max_iters);
697                }
698                if i % 10 == 0 {
699                    info!(
700                        "KMeans training: iteration {} / {}, redo={}",
701                        i, params.max_iters, redo
702                    );
703                };
704
705                let index = SimpleIndex::may_train_index(
706                    kmeans.centroids.clone(),
707                    kmeans.dimension,
708                    kmeans.distance_type,
709                )?;
710
711                let balance_factor = adjusted_balance_factor.min(params.balance_factor);
712                let (membership, radius, losses) = Algo::compute_membership_and_loss(
713                    kmeans.centroids.as_primitive::<T>().values(),
714                    data.values(),
715                    dimension,
716                    params.distance_type,
717                    balance_factor,
718                    Some(&cluster_sizes),
719                    index.as_ref(),
720                );
721
722                adjusted_balance_factor =
723                    compute_cluster_sizes(&membership, &radius, &losses, &mut cluster_sizes);
724                let balance_loss = compute_balance_loss(&cluster_sizes, n, balance_factor);
725                let last_loss = losses.iter().sum::<f64>() + balance_loss as f64;
726
727                kmeans = Algo::to_kmeans(
728                    data.values(),
729                    dimension,
730                    k,
731                    &membership,
732                    &mut cluster_sizes,
733                    params.distance_type,
734                    last_loss,
735                );
736                if (loss - last_loss).abs() < params.tolerance * last_loss {
737                    info!(
738                        "KMeans training: converged at iteration {} / {}, redo={}, loss={}, last_loss={}, loss_diff={}",
739                        i,
740                        params.max_iters,
741                        redo,
742                        loss,
743                        last_loss,
744                        (loss - last_loss).abs() / last_loss
745                    );
746                    break;
747                }
748                loss = last_loss;
749            }
750            if kmeans.loss < best_kmeans.loss {
751                best_kmeans = kmeans;
752            }
753        }
754
755        Ok(best_kmeans)
756    }
757
758    /// Helper function to create a FixedSizeListArray from indices
759    fn create_array_from_indices<T: ArrowNumericType>(
760        indices: &[usize],
761        data_values: &[T::Native],
762        dimension: usize,
763    ) -> arrow::error::Result<FixedSizeListArray>
764    where
765        T::Native: Clone,
766        PrimitiveArray<T>: From<Vec<T::Native>>,
767    {
768        let mut subset_data = Vec::with_capacity(indices.len() * dimension);
769        for &idx in indices {
770            let start = idx * dimension;
771            let end = start + dimension;
772            subset_data.extend_from_slice(&data_values[start..end]);
773        }
774        let array = PrimitiveArray::<T>::from(subset_data);
775        FixedSizeListArray::try_new_from_values(array, dimension as i32)
776    }
777
778    /// Train a hierarchical KMeans model when k > 256
779    ///
780    /// This function implements a hierarchical clustering approach:
781    /// 1. Start with k'=256 initial clusters
782    /// 2. Iteratively split the largest cluster until we have k clusters
783    fn train_hierarchical_kmeans<T: ArrowNumericType, Algo: KMeansAlgo<T::Native>>(
784        data: &FixedSizeListArray,
785        target_k: usize,
786        params: &KMeansParams,
787    ) -> arrow::error::Result<Self>
788    where
789        T::Native: Num,
790        PrimitiveArray<T>: From<Vec<T::Native>>,
791    {
792        // Cluster structure for the heap
793        #[derive(Clone, Debug)]
794        struct Cluster<N> {
795            id: usize,
796            indices: Vec<usize>,
797            centroid: Vec<N>,
798            finalized: bool,
799        }
800
801        impl<N> Eq for Cluster<N> {}
802
803        impl<N> PartialEq for Cluster<N> {
804            fn eq(&self, other: &Self) -> bool {
805                self.indices.len() == other.indices.len()
806            }
807        }
808
809        impl<N> Ord for Cluster<N> {
810            fn cmp(&self, other: &Self) -> Ordering {
811                // Non-finalized clusters should always have higher priority than finalized ones
812                match (self.finalized, other.finalized) {
813                    (false, true) => Ordering::Greater,
814                    (true, false) => Ordering::Less,
815                    _ => {
816                        // Max heap: larger clusters first
817                        self.indices.len().cmp(&other.indices.len())
818                    }
819                }
820            }
821        }
822
823        impl<N> PartialOrd for Cluster<N> {
824            fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
825                Some(self.cmp(other))
826            }
827        }
828
829        let n = data.len();
830        let dimension = data.value_length() as usize;
831
832        let data_values = data
833            .values()
834            .as_primitive_opt::<T>()
835            .ok_or(ArrowError::InvalidArgumentError(format!(
836                "KMeans: data must be {}, got: {}",
837                T::DATA_TYPE,
838                data.value_type()
839            )))?
840            .values();
841
842        // Initial clustering with k'=16
843        let initial_k = params.hierarchical_k.min(target_k).min(n);
844        info!(
845            "Hierarchical clustering: initial k={}, target k={}",
846            initial_k, target_k
847        );
848
849        let initial_kmeans = Self::train_kmeans::<T, Algo>(data, initial_k, params)?;
850
851        // Get membership for all data points
852        let (membership, _, _) = Algo::compute_membership_and_loss(
853            initial_kmeans.centroids.as_primitive::<T>().values(),
854            data_values,
855            dimension,
856            params.distance_type,
857            0.0, // No balance factor for membership computation
858            None,
859            None,
860        );
861
862        // Build initial clusters and add to heap
863        let mut heap: BinaryHeap<Cluster<T::Native>> = BinaryHeap::new();
864        let mut next_cluster_id = 0;
865        let initial_centroids = initial_kmeans.centroids.as_primitive::<T>().values();
866
867        for i in 0..initial_k {
868            let mut cluster_indices = Vec::new();
869            for (idx, &cluster_id) in membership.iter().enumerate() {
870                if let Some(cid) = cluster_id
871                    && cid as usize == i
872                {
873                    cluster_indices.push(idx);
874                }
875            }
876
877            if !cluster_indices.is_empty() {
878                let centroid_start = i * dimension;
879                let centroid_end = centroid_start + dimension;
880                let centroid = initial_centroids[centroid_start..centroid_end].to_vec();
881
882                heap.push(Cluster {
883                    id: next_cluster_id,
884                    indices: cluster_indices,
885                    centroid,
886                    finalized: false,
887                });
888                next_cluster_id += 1;
889            }
890        }
891
892        // Iteratively split largest clusters until we have target_k clusters
893        while heap.len() < target_k {
894            // Get the largest cluster
895            let mut largest_cluster = heap.pop().ok_or(ArrowError::InvalidArgumentError(
896                "No cluster can be further split".to_string(),
897            ))?;
898
899            // If this cluster is already finalized, no further split is possible; stop splitting
900            if largest_cluster.finalized {
901                log::warn!(
902                    "Cluster {} is already finalized, no further split is possible, finish with {} clusters",
903                    largest_cluster.id,
904                    heap.len() + 1
905                );
906                heap.push(largest_cluster);
907                break;
908            }
909
910            // Because the clusters are sorted by size, if the cluster has only 1 point, no further split is possible; stop splitting
911            if largest_cluster.indices.len() <= 1 {
912                log::warn!(
913                    "Cluster {} has only 1 point, no further split is possible, finish with {} clusters",
914                    largest_cluster.id,
915                    heap.len() + 1
916                );
917                heap.push(largest_cluster);
918                break;
919            }
920
921            let cluster_size = largest_cluster.indices.len();
922            log::debug!(
923                "Splitting cluster {} with {} points (current total clusters: {})",
924                largest_cluster.id,
925                cluster_size,
926                heap.len() + 1 // +1 for the cluster we just popped
927            );
928
929            // Determine k' for this cluster based on its size
930            let remaining_k = target_k - heap.len(); // Spaces left to fill
931            let cluster_k = if cluster_size <= params.hierarchical_k {
932                2.min(remaining_k).min(cluster_size)
933            } else {
934                // For larger clusters, split more aggressively
935                let suggested_k = cluster_size / params.hierarchical_k;
936                suggested_k
937                    .min(remaining_k)
938                    .min(params.hierarchical_k)
939                    .max(2)
940            };
941
942            // Create sub-dataset for this cluster using indices
943            let sub_data = Self::create_array_from_indices::<T>(
944                &largest_cluster.indices,
945                data_values,
946                dimension,
947            )?;
948
949            // Run kmeans on this cluster
950            let sub_kmeans = Self::train_kmeans::<T, Algo>(&sub_data, cluster_k, params)?;
951
952            // Get membership for points in the sub-cluster
953            let sub_data = sub_data.values().as_primitive::<T>().values();
954            let (sub_membership, _, _) = Algo::compute_membership_and_loss(
955                sub_kmeans.centroids.as_primitive::<T>().values(),
956                sub_data,
957                dimension,
958                params.distance_type,
959                0.0,
960                None,
961                None,
962            );
963
964            // Build per-cluster membership while checking whether the split is effective
965            let approx_cluster_capacity = if cluster_k > 0 {
966                largest_cluster.indices.len().div_ceil(cluster_k)
967            } else {
968                0
969            };
970            let mut cluster_assignments: Vec<Vec<usize>> = (0..cluster_k)
971                .map(|_| Vec::with_capacity(approx_cluster_capacity))
972                .collect();
973
974            let mut first_sid: Option<u32> = None;
975            let mut all_same = true;
976            for (local_idx, &membership) in sub_membership.iter().enumerate() {
977                let Some(sub_cluster_id) = membership else {
978                    continue;
979                };
980
981                if let Some(first) = first_sid {
982                    if sub_cluster_id != first {
983                        all_same = false;
984                    }
985                } else {
986                    first_sid = Some(sub_cluster_id);
987                }
988
989                let sub_cluster_id = sub_cluster_id as usize;
990                if let Some(indices) = cluster_assignments.get_mut(sub_cluster_id) {
991                    indices.push(largest_cluster.indices[local_idx]);
992                } else {
993                    // Unexpected assignment outside [0, cluster_k); treat as ineffective split.
994                    all_same = false;
995                }
996            }
997
998            // If all memberships are identical, the split is ineffective; finalize the original cluster
999            if all_same {
1000                largest_cluster.finalized = true;
1001                heap.push(largest_cluster);
1002                continue;
1003            }
1004
1005            // Create new sub-clusters and add to heap
1006            let sub_centroids = sub_kmeans.centroids.as_primitive::<T>().values();
1007            for (i, new_cluster_indices) in cluster_assignments.into_iter().enumerate() {
1008                if new_cluster_indices.is_empty() {
1009                    continue;
1010                }
1011
1012                let centroid_start = i * dimension;
1013                let centroid_end = centroid_start + dimension;
1014                let centroid = sub_centroids[centroid_start..centroid_end].to_vec();
1015
1016                heap.push(Cluster {
1017                    id: next_cluster_id,
1018                    indices: new_cluster_indices,
1019                    centroid,
1020                    finalized: false,
1021                });
1022                next_cluster_id += 1;
1023            }
1024
1025            log::debug!(
1026                "Split complete: now have {} clusters (target: {})",
1027                heap.len(),
1028                target_k
1029            );
1030        }
1031        debug_assert_eq!(heap.len(), target_k);
1032
1033        // Construct final KMeans model with all centroids
1034        let mut all_clusters: Vec<Cluster<T::Native>> = heap.into_vec();
1035        // Sort by ID to ensure consistent ordering
1036        all_clusters.sort_by_key(|c| c.id);
1037
1038        let flat_centroids: Vec<T::Native> =
1039            all_clusters.into_iter().flat_map(|c| c.centroid).collect();
1040        let centroids_array = PrimitiveArray::<T>::from(flat_centroids);
1041
1042        Ok(Self {
1043            centroids: Arc::new(centroids_array),
1044            dimension,
1045            distance_type: params.distance_type,
1046            loss: 0.0, // Loss is not meaningful for hierarchical clustering
1047        })
1048    }
1049
1050    /// Train a [`KMeans`] model with full parameters.
1051    ///
1052    /// If the DistanceType is `Cosine`, the input vectors will be normalized with each iteration.
1053    pub fn new_with_params(
1054        data: &FixedSizeListArray,
1055        k: usize,
1056        params: &KMeansParams,
1057    ) -> arrow::error::Result<Self> {
1058        let n = data.len();
1059        if n < k {
1060            return Err(ArrowError::InvalidArgumentError(format!(
1061                "KMeans: training does not have sufficient data points: n({}) is smaller than k({})",
1062                n, k
1063            )));
1064        }
1065
1066        // use hierarchical clustering if k > 256 and hierarchical_k > 1
1067        // we set 256 as the threshold because:
1068        // 1. PQ would run kmeans with k=256, in that case we don't want to use hierarchical clustering for accuracy
1069        // 2. kmeans with k=256 is small enough that we don't need to use hierarchical clustering for efficiency
1070        if k > 256 && params.hierarchical_k > 1 {
1071            log::debug!("Using hierarchical clustering for k={}", k);
1072            return match (data.value_type(), params.distance_type) {
1073                (DataType::Float16, _) => Self::train_hierarchical_kmeans::<
1074                    Float16Type,
1075                    KMeansAlgoFloat<Float16Type>,
1076                >(data, k, params),
1077                (DataType::Float32, _) => Self::train_hierarchical_kmeans::<
1078                    Float32Type,
1079                    KMeansAlgoFloat<Float32Type>,
1080                >(data, k, params),
1081                (DataType::Float64, _) => Self::train_hierarchical_kmeans::<
1082                    Float64Type,
1083                    KMeansAlgoFloat<Float64Type>,
1084                >(data, k, params),
1085                (DataType::UInt8, DistanceType::Hamming) => {
1086                    Self::train_hierarchical_kmeans::<UInt8Type, KModeAlgo>(data, k, params)
1087                }
1088                _ => Err(ArrowError::InvalidArgumentError(format!(
1089                    "KMeans: can not train data type {} with distance type: {}",
1090                    data.value_type(),
1091                    params.distance_type
1092                ))),
1093            };
1094        }
1095
1096        match (data.value_type(), params.distance_type) {
1097            (DataType::Float16, _) => {
1098                Self::train_kmeans::<Float16Type, KMeansAlgoFloat<Float16Type>>(data, k, params)
1099            }
1100
1101            (DataType::Float32, _) => {
1102                Self::train_kmeans::<Float32Type, KMeansAlgoFloat<Float32Type>>(data, k, params)
1103            }
1104            (DataType::Float64, _) => {
1105                Self::train_kmeans::<Float64Type, KMeansAlgoFloat<Float64Type>>(data, k, params)
1106            }
1107            (DataType::UInt8, DistanceType::Hamming) => {
1108                Self::train_kmeans::<UInt8Type, KModeAlgo>(data, k, params)
1109            }
1110            _ => Err(ArrowError::InvalidArgumentError(format!(
1111                "KMeans: can not train data type {} with distance type: {}",
1112                data.value_type(),
1113                params.distance_type
1114            ))),
1115        }
1116    }
1117}
1118
1119pub fn kmeans_find_partitions_arrow_array(
1120    centroids: &FixedSizeListArray,
1121    query: &dyn Array,
1122    nprobes: usize,
1123    distance_type: DistanceType,
1124) -> arrow::error::Result<(UInt32Array, Float32Array)> {
1125    if centroids.value_length() as usize != query.len() {
1126        return Err(ArrowError::InvalidArgumentError(format!(
1127            "Centroids and vectors have different dimensions: {} != {}",
1128            centroids.value_length(),
1129            query.len()
1130        )));
1131    }
1132
1133    match (centroids.value_type(), query.data_type()) {
1134        (DataType::Float16, DataType::Float16) => Ok(kmeans_find_partitions(
1135            centroids.values().as_primitive::<Float16Type>().values(),
1136            query.as_primitive::<Float16Type>().values(),
1137            nprobes,
1138            distance_type,
1139        )?),
1140        (DataType::Float32, DataType::Float32) => Ok(kmeans_find_partitions(
1141            centroids.values().as_primitive::<Float32Type>().values(),
1142            query.as_primitive::<Float32Type>().values(),
1143            nprobes,
1144            distance_type,
1145        )?),
1146        (DataType::Float64, DataType::Float64) => Ok(kmeans_find_partitions(
1147            centroids.values().as_primitive::<Float64Type>().values(),
1148            query.as_primitive::<Float64Type>().values(),
1149            nprobes,
1150            distance_type,
1151        )?),
1152        (DataType::UInt8, DataType::UInt8) => Ok(kmeans_find_partitions_binary(
1153            centroids.values().as_primitive::<UInt8Type>().values(),
1154            query.as_primitive::<UInt8Type>().values(),
1155            nprobes,
1156            distance_type,
1157        )?),
1158        _ => Err(ArrowError::InvalidArgumentError(format!(
1159            "Centroids and vectors have different types: {} != {}",
1160            centroids.value_type(),
1161            query.data_type()
1162        ))),
1163    }
1164}
1165
1166/// KMeans finds N nearest partitions.
1167///
1168/// Parameters:
1169/// - *centroids*: a `k * dimension` floating array.
1170/// - *query*: a `dimension` floating array.
1171/// - *nprobes*: the number of partitions to find.
1172/// - *distance_type*: the distance type to calculate distance.
1173///
1174/// This function allows to conduct kmeans search without constructing
1175/// `Arrow Array` or `Vec<Float>` types.
1176///
1177pub fn kmeans_find_partitions<T: Float + L2 + Dot>(
1178    centroids: &[T],
1179    query: &[T],
1180    nprobes: usize,
1181    distance_type: DistanceType,
1182) -> arrow::error::Result<(UInt32Array, Float32Array)> {
1183    let dists: Vec<f32> = match distance_type {
1184        DistanceType::L2 => l2_distance_batch(query, centroids, query.len()).collect(),
1185        DistanceType::Dot => dot_distance_batch(query, centroids, query.len()).collect(),
1186        _ => {
1187            panic!(
1188                "KMeans::find_partitions: {} is not supported",
1189                distance_type
1190            );
1191        }
1192    };
1193
1194    // TODO: use heap to just keep nprobes smallest values.
1195    let dists_arr = Float32Array::from(dists);
1196    let indices = sort_to_indices(&dists_arr, None, Some(nprobes))?;
1197    let dists = arrow::compute::take(&dists_arr, &indices, None)?
1198        .as_primitive::<Float32Type>()
1199        .clone();
1200    Ok((indices, dists))
1201}
1202
1203pub fn kmeans_find_partitions_binary(
1204    centroids: &[u8],
1205    query: &[u8],
1206    nprobes: usize,
1207    distance_type: DistanceType,
1208) -> arrow::error::Result<(UInt32Array, Float32Array)> {
1209    let dists: Vec<f32> = match distance_type {
1210        DistanceType::Hamming => hamming_distance_batch(query, centroids, query.len()).collect(),
1211        _ => {
1212            panic!(
1213                "KMeans::find_partitions: {} is not supported",
1214                distance_type
1215            );
1216        }
1217    };
1218
1219    // TODO: use heap to just keep nprobes smallest values.
1220    let dists_arr = Float32Array::from(dists);
1221    let indices = sort_to_indices(&dists_arr, None, Some(nprobes))?;
1222    let dists = arrow::compute::take(&dists_arr, &indices, None)?
1223        .as_primitive::<Float32Type>()
1224        .clone();
1225    Ok((indices, dists))
1226}
1227
1228/// Compute partitions from Arrow FixedSizeListArray.
1229#[allow(clippy::type_complexity)]
1230pub fn compute_partitions_arrow_array(
1231    centroids: &FixedSizeListArray,
1232    vectors: &FixedSizeListArray,
1233    distance_type: DistanceType,
1234) -> arrow::error::Result<(Vec<Option<u32>>, Vec<Option<f32>>)> {
1235    if centroids.value_length() != vectors.value_length() {
1236        return Err(ArrowError::InvalidArgumentError(
1237            "Centroids and vectors have different dimensions".to_string(),
1238        ));
1239    }
1240    match (centroids.value_type(), vectors.value_type()) {
1241        (DataType::Float16, DataType::Float16) => Ok(compute_partitions_with_dists::<
1242            Float16Type,
1243            KMeansAlgoFloat<Float16Type>,
1244        >(
1245            centroids.values().as_primitive(),
1246            vectors.values().as_primitive(),
1247            centroids.value_length(),
1248            distance_type,
1249        )),
1250        (DataType::Float32, DataType::Float32) => Ok(compute_partitions_with_dists::<
1251            Float32Type,
1252            KMeansAlgoFloat<Float32Type>,
1253        >(
1254            centroids.values().as_primitive(),
1255            vectors.values().as_primitive(),
1256            centroids.value_length(),
1257            distance_type,
1258        )),
1259        (DataType::Float32, DataType::Int8) => Ok(compute_partitions_with_dists::<
1260            Float32Type,
1261            KMeansAlgoFloat<Float32Type>,
1262        >(
1263            centroids.values().as_primitive(),
1264            vectors.convert_to_floating_point()?.values().as_primitive(),
1265            centroids.value_length(),
1266            distance_type,
1267        )),
1268        (DataType::Float64, DataType::Float64) => Ok(compute_partitions_with_dists::<
1269            Float64Type,
1270            KMeansAlgoFloat<Float64Type>,
1271        >(
1272            centroids.values().as_primitive(),
1273            vectors.values().as_primitive(),
1274            centroids.value_length(),
1275            distance_type,
1276        )),
1277        (DataType::UInt8, DataType::UInt8) => {
1278            Ok(compute_partitions_with_dists::<UInt8Type, KModeAlgo>(
1279                centroids.values().as_primitive(),
1280                vectors.values().as_primitive(),
1281                centroids.value_length(),
1282                distance_type,
1283            ))
1284        }
1285        _ => Err(ArrowError::InvalidArgumentError(
1286            "Centroids and vectors have incompatible types".to_string(),
1287        )),
1288    }
1289}
1290
1291/// Compute partition ID of each vector in the KMeans.
1292///
1293/// If returns `None`, means the vector is not valid, i.e., all `NaN`.
1294pub fn compute_partitions<T: ArrowNumericType, K: KMeansAlgo<T::Native>>(
1295    centroids: &PrimitiveArray<T>,
1296    vectors: &PrimitiveArray<T>,
1297    dimension: impl AsPrimitive<usize>,
1298    distance_type: DistanceType,
1299) -> (Vec<Option<u32>>, f64)
1300where
1301    T::Native: Num,
1302{
1303    let dimension = dimension.as_();
1304    let (membership, _, losses) = K::compute_membership_and_loss(
1305        centroids.values(),
1306        vectors.values(),
1307        dimension,
1308        distance_type,
1309        0.0,
1310        None,
1311        None,
1312    );
1313    (membership, losses.iter().sum::<f64>())
1314}
1315
1316/// compute the partition id and the distance to the centroid for each vector,
1317/// NOTE the distance is squared distance for L2
1318pub fn compute_partitions_with_dists<T: ArrowNumericType, K: KMeansAlgo<T::Native>>(
1319    centroids: &PrimitiveArray<T>,
1320    vectors: &PrimitiveArray<T>,
1321    dimension: impl AsPrimitive<usize>,
1322    distance_type: DistanceType,
1323) -> (Vec<Option<u32>>, Vec<Option<f32>>)
1324where
1325    T::Native: Num,
1326{
1327    let dimension = dimension.as_();
1328    K::compute_membership_and_dist(
1329        centroids.values(),
1330        vectors.values(),
1331        dimension,
1332        distance_type,
1333        0.0,
1334        None,
1335        None,
1336    )
1337}
1338
1339/// Train KMeans model and returns the centroids of each cluster.
1340///
1341/// Parameters
1342/// ----------
1343/// - *centroids*: initial centroids, use the random initialization if None
1344/// - *array*: a flatten floating number array of vectors
1345/// - *dimension*: dimension of the vector
1346/// - *k*: number of clusters
1347/// - *max_iterations*: maximum number of iterations
1348/// - *redos*: number of times to redo the k-means clustering
1349/// - *distance_type*: distance type to compute pair-wise vector distance
1350/// - *sample_rate*: sample rate to select the data for training
1351#[allow(clippy::too_many_arguments)]
1352pub fn train_kmeans<T: ArrowPrimitiveType>(
1353    array: &PrimitiveArray<T>,
1354    mut params: KMeansParams,
1355    dimension: usize,
1356    k: usize,
1357    sample_rate: usize,
1358) -> Result<KMeans>
1359where
1360    T::Native: Dot + L2 + Normalize,
1361    PrimitiveArray<T>: From<Vec<T::Native>>,
1362{
1363    let num_rows = array.len() / dimension;
1364    if num_rows < k {
1365        return Err(Error::unprocessable(format!(
1366            "KMeans cannot train {k} centroids with {num_rows} vectors; choose a smaller K (< {num_rows})"
1367        )));
1368    }
1369
1370    // Only sample sample_rate * num_clusters. See Faiss
1371    let data = if num_rows > sample_rate * k {
1372        log::info!(
1373            "Sample {} out of {} to train kmeans of {} dim, {} clusters",
1374            sample_rate * k,
1375            array.len() / dimension,
1376            dimension,
1377            k,
1378        );
1379        let sample_size = sample_rate * k;
1380        array.slice(0, sample_size * dimension)
1381    } else {
1382        array.clone()
1383    };
1384
1385    let data = FixedSizeListArray::try_new_from_values(data, dimension as i32)?;
1386
1387    params.balance_factor /= data.len() as f32;
1388    let model = KMeans::new_with_params(&data, k, &params)?;
1389    Ok(model)
1390}
1391
1392#[inline]
1393pub fn compute_partition<T: Float + L2 + Dot>(
1394    centroids: &[T],
1395    vector: &[T],
1396    distance_type: DistanceType,
1397) -> Option<u32> {
1398    match distance_type {
1399        DistanceType::L2 => {
1400            argmin_value_float(l2_distance_batch(vector, centroids, vector.len())).map(|(c, _)| c)
1401        }
1402        DistanceType::Dot => {
1403            argmin_value_float(dot_distance_batch(vector, centroids, vector.len())).map(|(c, _)| c)
1404        }
1405        _ => {
1406            panic!(
1407                "KMeans::compute_partition: distance type {} is not supported",
1408                distance_type
1409            );
1410        }
1411    }
1412}
1413
1414#[cfg(test)]
1415mod tests {
1416    use std::iter::repeat_n;
1417
1418    use arrow_array::Float16Array;
1419    use arrow_array::types::Float16Type;
1420    use half::f16;
1421    use lance_arrow::*;
1422    use lance_testing::datagen::generate_random_array;
1423
1424    use super::*;
1425    use lance_linalg::distance::l2;
1426    use lance_linalg::kernels::argmin;
1427
1428    #[test]
1429    fn test_train_with_small_dataset() {
1430        let data = Float32Array::from(vec![1.0, 2.0, 3.0, 4.0]);
1431        let data = FixedSizeListArray::try_new_from_values(data, 2).unwrap();
1432        match KMeans::new(&data, 128, 5) {
1433            Ok(_) => panic!("Should fail to train KMeans"),
1434            Err(e) => {
1435                assert!(e.to_string().contains("smaller than"));
1436            }
1437        }
1438    }
1439
1440    #[test]
1441    fn test_compute_partitions() {
1442        const DIM: usize = 256;
1443        let centroids = generate_random_array(DIM * 18);
1444        let data = generate_random_array(DIM * 20);
1445
1446        let expected = data
1447            .values()
1448            .chunks(DIM)
1449            .map(|row| {
1450                argmin(
1451                    centroids
1452                        .values()
1453                        .chunks(DIM)
1454                        .map(|centroid| l2(row, centroid)),
1455                )
1456            })
1457            .collect::<Vec<_>>();
1458        let (actual, _) = compute_partitions::<Float32Type, KMeansAlgoFloat<Float32Type>>(
1459            &centroids,
1460            &data,
1461            DIM,
1462            DistanceType::L2,
1463        );
1464        assert_eq!(expected, actual);
1465    }
1466
1467    #[tokio::test]
1468    async fn test_compute_membership_and_loss() {
1469        const DIM: usize = 256;
1470        let centroids = generate_random_array(DIM * 18);
1471        let data = generate_random_array(DIM * 20);
1472
1473        let (membership, _, losses) = KMeansAlgoFloat::<Float32Type>::compute_membership_and_loss(
1474            centroids.as_slice(),
1475            data.values(),
1476            DIM,
1477            DistanceType::L2,
1478            0.0,
1479            None,
1480            None,
1481        );
1482        let loss = losses.iter().sum::<f64>();
1483        assert!(loss > 0.0, "loss is not zero: {}", loss);
1484        membership.iter().for_each(|cd| {
1485            assert!(cd.is_some());
1486        });
1487    }
1488
1489    #[tokio::test]
1490    async fn test_l2_with_nans() {
1491        const DIM: usize = 8;
1492        const K: usize = 32;
1493        const NUM_CENTROIDS: usize = 16 * 2048;
1494        let centroids = generate_random_array(DIM * NUM_CENTROIDS);
1495        let values = Float32Array::from_iter_values(repeat_n(f32::NAN, DIM * K));
1496
1497        compute_partitions::<Float32Type, KMeansAlgoFloat<Float32Type>>(
1498            &centroids,
1499            &values,
1500            DIM,
1501            DistanceType::L2,
1502        )
1503        .0
1504        .iter()
1505        .for_each(|cd| {
1506            assert!(cd.is_none());
1507        });
1508    }
1509
1510    #[tokio::test]
1511    async fn test_train_l2_kmeans_with_nans() {
1512        const DIM: usize = 8;
1513        const K: usize = 32;
1514        const NUM_CENTROIDS: usize = 16 * 2048;
1515        let centroids = generate_random_array(DIM * NUM_CENTROIDS);
1516        let values = repeat_n(f32::NAN, DIM * K).collect::<Vec<_>>();
1517
1518        let (membership, _, _) = KMeansAlgoFloat::<Float32Type>::compute_membership_and_loss(
1519            centroids.as_slice(),
1520            &values,
1521            DIM,
1522            DistanceType::L2,
1523            0.0,
1524            None,
1525            None,
1526        );
1527
1528        membership.iter().for_each(|cd| assert!(cd.is_none()));
1529    }
1530
1531    #[tokio::test]
1532    async fn test_train_kmode() {
1533        const DIM: usize = 16;
1534        const K: usize = 32;
1535        const NUM_VALUES: usize = 256 * K;
1536
1537        let mut rng = SmallRng::from_os_rng();
1538        let values =
1539            UInt8Array::from_iter_values((0..NUM_VALUES * DIM).map(|_| rng.random_range(0..255)));
1540
1541        let fsl = FixedSizeListArray::try_new_from_values(values, DIM as i32).unwrap();
1542
1543        let params = KMeansParams {
1544            distance_type: DistanceType::Hamming,
1545            ..Default::default()
1546        };
1547        let kmeans = KMeans::new_with_params(&fsl, K, &params).unwrap();
1548        assert_eq!(kmeans.centroids.len(), K * DIM);
1549        assert_eq!(kmeans.dimension, DIM);
1550        assert_eq!(kmeans.centroids.data_type(), &DataType::UInt8);
1551    }
1552
1553    #[tokio::test]
1554    async fn test_hierarchical_kmeans() {
1555        const DIM: usize = 64;
1556        const K: usize = 257; // Greater than 256 to trigger hierarchical clustering
1557        const NUM_VALUES: usize = 1024 * K;
1558
1559        let values = generate_random_array(NUM_VALUES * DIM);
1560        let fsl = FixedSizeListArray::try_new_from_values(values, DIM as i32).unwrap();
1561
1562        let params = KMeansParams {
1563            max_iters: 10,
1564            hierarchical_k: 16,
1565            ..Default::default()
1566        };
1567
1568        let kmeans = KMeans::new_with_params(&fsl, K, &params).unwrap();
1569
1570        // Verify that we have the correct number of clusters
1571        assert_eq!(kmeans.centroids.len(), K * DIM);
1572        assert_eq!(kmeans.dimension, DIM);
1573        assert_eq!(kmeans.centroids.data_type(), &DataType::Float32);
1574
1575        // Verify that all centroids are valid (not NaN)
1576        let centroids = kmeans.centroids.as_primitive::<Float32Type>().values();
1577        for val in centroids {
1578            assert!(!val.is_nan(), "Centroid should not contain NaN values");
1579        }
1580    }
1581
1582    #[tokio::test]
1583    async fn test_float16_underflow_fix() {
1584        // This test verifies the fix for float16 division underflow
1585        // When training k-means on many float16 vectors with small k,
1586        // without limiting the data size, dividing centroids by count
1587        // can underflow to 0,
1588        // The fix limits data to k * 512 to prevent this
1589        const DIM: usize = 2;
1590        const K: usize = 2;
1591        const NUM_VALUES: usize = K * 65536; // Many vectors to trigger the issue
1592
1593        let f32_values = generate_random_array(NUM_VALUES * DIM);
1594        let f16_values = Float16Array::from_iter_values(
1595            f32_values.values().iter().map(|&v| half::f16::from_f32(v)),
1596        );
1597        let fsl = FixedSizeListArray::try_new_from_values(f16_values, DIM as i32).unwrap();
1598
1599        let params = KMeansParams {
1600            max_iters: 10,
1601            ..Default::default()
1602        };
1603
1604        let kmeans = KMeans::new_with_params(&fsl, K, &params).unwrap();
1605
1606        // Verify that we have the correct number of clusters
1607        assert_eq!(kmeans.centroids.len(), K * DIM);
1608        assert_eq!(kmeans.dimension, DIM);
1609        assert_eq!(kmeans.centroids.data_type(), &DataType::Float16);
1610
1611        // Verify that all centroids are valid (not zero or NaN)
1612        // Without the fix, they would all be zero due to underflow
1613        let centroids = kmeans.centroids.as_primitive::<Float16Type>().values();
1614        for &val in centroids {
1615            assert!(!val.is_nan(), "Centroid should not contain NaN values");
1616            assert!(val != f16::ZERO);
1617        }
1618    }
1619}