1use crate::dimensionality_reduction::QuantumDistanceMetric;
4use crate::error::{MLError, Result};
5use scirs2_core::ndarray::{Array1, Array2, ArrayView1, Axis};
6
7use super::config::*;
8
9#[derive(Debug, Clone)]
11pub struct ClusteringResult {
12 pub labels: Array1<usize>,
14 pub n_clusters: usize,
16 pub cluster_centers: Option<Array2<f64>>,
18 pub inertia: Option<f64>,
20 pub probabilities: Option<Array2<f64>>,
22}
23
24#[derive(Debug)]
26pub struct QuantumClusterer {
27 config: QuantumClusteringConfig,
28 cluster_centers: Option<Array2<f64>>,
29 labels: Option<Array1<usize>>,
30 pub kmeans_config: Option<QuantumKMeansConfig>,
32 pub dbscan_config: Option<QuantumDBSCANConfig>,
33 pub spectral_config: Option<QuantumSpectralConfig>,
34 pub fuzzy_config: Option<QuantumFuzzyCMeansConfig>,
35 pub gmm_config: Option<QuantumGMMConfig>,
36}
37
38impl QuantumClusterer {
39 pub fn new(config: QuantumClusteringConfig) -> Self {
41 Self {
42 config,
43 cluster_centers: None,
44 labels: None,
45 kmeans_config: None,
46 dbscan_config: None,
47 spectral_config: None,
48 fuzzy_config: None,
49 gmm_config: None,
50 }
51 }
52
53 pub fn kmeans(config: QuantumKMeansConfig) -> Self {
55 let mut clusterer = Self::new(QuantumClusteringConfig {
56 algorithm: ClusteringAlgorithm::QuantumKMeans,
57 n_clusters: config.n_clusters,
58 max_iterations: config.max_iterations,
59 tolerance: config.tolerance,
60 num_qubits: 4,
61 random_state: config.seed,
62 });
63 clusterer.kmeans_config = Some(config);
64 clusterer
65 }
66
67 pub fn dbscan(config: QuantumDBSCANConfig) -> Self {
69 let mut clusterer = Self::new(QuantumClusteringConfig {
70 algorithm: ClusteringAlgorithm::QuantumDBSCAN,
71 n_clusters: 0, max_iterations: 100,
73 tolerance: 1e-4,
74 num_qubits: 4,
75 random_state: config.seed,
76 });
77 clusterer.dbscan_config = Some(config);
78 clusterer
79 }
80
81 pub fn spectral(config: QuantumSpectralConfig) -> Self {
83 let mut clusterer = Self::new(QuantumClusteringConfig {
84 algorithm: ClusteringAlgorithm::QuantumSpectral,
85 n_clusters: config.n_clusters,
86 max_iterations: 100,
87 tolerance: 1e-4,
88 num_qubits: 4,
89 random_state: config.seed,
90 });
91 clusterer.spectral_config = Some(config);
92 clusterer
93 }
94
95 fn squared_dist(&self, a: &ArrayView1<f64>, b: &ArrayView1<f64>) -> f64 {
97 a.iter().zip(b.iter()).map(|(x, y)| (x - y).powi(2)).sum()
98 }
99
100 fn uf_find(parent: &mut [usize], mut x: usize) -> usize {
102 while parent[x] != x {
103 parent[x] = parent[parent[x]];
105 x = parent[x];
106 }
107 x
108 }
109
110 fn run_kmeans(
114 &self,
115 data: &Array2<f64>,
116 k: usize,
117 ) -> Result<(Array2<f64>, Array1<usize>, f64)> {
118 let n_samples = data.nrows();
119 let n_features = data.ncols();
120 let max_iter = self.config.max_iterations;
121
122 let mut centers = Array2::<f64>::zeros((k, n_features));
128
129 let first_idx = self
131 .config
132 .random_state
133 .map(|s| (s as usize) % n_samples)
134 .unwrap_or(0);
135 centers.row_mut(0).assign(&data.row(first_idx));
136
137 for c in 1..k {
139 let mut min_dists_sq = vec![f64::INFINITY; n_samples];
141 for i in 0..n_samples {
142 for prev_c in 0..c {
143 let d = self.squared_dist(&data.row(i), ¢ers.row(prev_c));
144 if d < min_dists_sq[i] {
145 min_dists_sq[i] = d;
146 }
147 }
148 }
149 let next_idx = min_dists_sq
151 .iter()
152 .enumerate()
153 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
154 .map(|(i, _)| i)
155 .unwrap_or(c % n_samples);
156 centers.row_mut(c).assign(&data.row(next_idx));
157 }
158
159 let mut labels = vec![0usize; n_samples];
163
164 for _iter in 0..max_iter {
165 let mut changed = false;
167 for i in 0..n_samples {
168 let mut best_c = 0;
169 let mut best_d = f64::INFINITY;
170 for c in 0..k {
171 let d = self.squared_dist(&data.row(i), ¢ers.row(c));
172 if d < best_d {
173 best_d = d;
174 best_c = c;
175 }
176 }
177 if labels[i] != best_c {
178 changed = true;
179 labels[i] = best_c;
180 }
181 }
182
183 let mut new_centers = Array2::<f64>::zeros((k, n_features));
185 let mut counts = vec![0usize; k];
186 for i in 0..n_samples {
187 let c = labels[i];
188 new_centers.row_mut(c).scaled_add(1.0, &data.row(i));
189 counts[c] += 1;
190 }
191 for c in 0..k {
192 if counts[c] > 0 {
193 new_centers
194 .row_mut(c)
195 .mapv_inplace(|v| v / counts[c] as f64);
196 } else {
197 new_centers.row_mut(c).assign(&data.row(c % n_samples));
199 }
200 }
201 centers = new_centers;
202
203 if !changed {
204 break;
205 }
206 }
207
208 let mut inertia = 0.0f64;
212 for i in 0..n_samples {
213 inertia += self.squared_dist(&data.row(i), ¢ers.row(labels[i]));
214 }
215
216 let labels_arr = Array1::from_iter(labels);
217 Ok((centers, labels_arr, inertia))
218 }
219
220 fn fit_dbscan(&self, data: &Array2<f64>) -> Result<usize> {
225 let n = data.nrows();
226
227 let (eps, min_samples) = if let Some(cfg) = &self.dbscan_config {
228 (cfg.eps, cfg.min_samples)
229 } else {
230 let mut max_sq = 0.0f64;
232 for i in 0..n {
233 for j in (i + 1)..n {
234 let d = self.squared_dist(&data.row(i), &data.row(j));
235 if d > max_sq {
236 max_sq = d;
237 }
238 }
239 }
240 (max_sq.sqrt() * 0.1, 2usize)
241 };
242
243 let mut parent: Vec<usize> = (0..n).collect();
245
246 for i in 0..n {
247 let mut neighbor_count = 0usize;
248 for j in 0..n {
249 if i == j {
250 continue;
251 }
252 let d = self.squared_dist(&data.row(i), &data.row(j)).sqrt();
253 if d <= eps {
254 neighbor_count += 1;
255 let pi = Self::uf_find(&mut parent, i);
257 let pj = Self::uf_find(&mut parent, j);
258 if pi != pj {
259 parent[pi] = pj;
260 }
261 }
262 }
263 let _ = neighbor_count;
265 }
266
267 let n_clusters = (0..n)
269 .filter(|&i| Self::uf_find(&mut parent, i) == i)
270 .count();
271
272 Ok(n_clusters.max(1))
273 }
274
275 pub fn fit(&mut self, data: &Array2<f64>) -> Result<ClusteringResult> {
277 let n_samples = data.nrows();
278
279 if n_samples == 0 {
280 return Err(MLError::InvalidInput("Empty data".to_string()));
281 }
282
283 let n_clusters = match self.config.algorithm {
285 ClusteringAlgorithm::QuantumDBSCAN => {
286 let auto_k = self.fit_dbscan(data)?;
288 auto_k
289 }
290 _ => {
291 self.config.n_clusters.min(n_samples).max(1)
293 }
294 };
295
296 let (cluster_centers, labels, inertia) = self.run_kmeans(data, n_clusters)?;
298
299 self.cluster_centers = Some(cluster_centers.clone());
300 self.labels = Some(labels.clone());
301
302 Ok(ClusteringResult {
303 labels,
304 n_clusters,
305 cluster_centers: Some(cluster_centers),
306 inertia: Some(inertia),
307 probabilities: None,
308 })
309 }
310
311 pub fn predict(&self, data: &Array2<f64>) -> Result<Array1<usize>> {
313 let centers = self.cluster_centers.as_ref().ok_or_else(|| {
314 MLError::ModelNotTrained("Clusterer must be fitted before predict".to_string())
315 })?;
316
317 let k = centers.nrows();
318 let labels: Vec<usize> = (0..data.nrows())
319 .map(|i| {
320 let mut best_c = 0;
321 let mut best_d = f64::INFINITY;
322 for c in 0..k {
323 let d = self.squared_dist(&data.row(i), ¢ers.row(c));
324 if d < best_d {
325 best_d = d;
326 best_c = c;
327 }
328 }
329 best_c
330 })
331 .collect();
332
333 Ok(Array1::from_iter(labels))
334 }
335
336 pub fn predict_proba(&self, data: &Array2<f64>) -> Result<Array2<f64>> {
343 let centers = self.cluster_centers.as_ref().ok_or_else(|| {
344 MLError::ModelNotTrained("Clusterer must be fitted before predict_proba".to_string())
345 })?;
346
347 let n_samples = data.nrows();
348 let k = centers.nrows();
349 let mut probabilities = Array2::<f64>::zeros((n_samples, k));
350
351 for i in 0..n_samples {
352 let row = data.row(i);
353 let neg_distances: Vec<f64> = (0..k)
354 .map(|c| -self.squared_dist(&row, ¢ers.row(c)))
355 .collect();
356 let max_neg_dist = neg_distances
357 .iter()
358 .cloned()
359 .fold(f64::NEG_INFINITY, f64::max);
360 let exp_vals: Vec<f64> = neg_distances
361 .iter()
362 .map(|&d| (d - max_neg_dist).exp())
363 .collect();
364 let sum_exp: f64 = exp_vals.iter().sum();
365 for c in 0..k {
366 probabilities[[i, c]] = if sum_exp > 0.0 {
367 exp_vals[c] / sum_exp
368 } else {
369 1.0 / k as f64
370 };
371 }
372 }
373
374 Ok(probabilities)
375 }
376
377 pub fn compute_quantum_distance(
379 &self,
380 point1: &Array1<f64>,
381 point2: &Array1<f64>,
382 metric: QuantumDistanceMetric,
383 ) -> Result<f64> {
384 match metric {
386 QuantumDistanceMetric::QuantumEuclidean => {
387 let diff = point1 - point2;
388 Ok(diff.dot(&diff).sqrt())
389 }
390 QuantumDistanceMetric::QuantumManhattan => {
391 Ok((point1 - point2).mapv(|x| x.abs()).sum())
392 }
393 QuantumDistanceMetric::QuantumCosine => {
394 let dot_product = point1.dot(point2);
395 let norm1 = point1.dot(point1).sqrt();
396 let norm2 = point2.dot(point2).sqrt();
397 Ok(1.0 - (dot_product / (norm1 * norm2)))
398 }
399 _ => {
400 let diff = point1 - point2;
402 Ok(diff.dot(&diff).sqrt())
403 }
404 }
405 }
406
407 pub fn fit_predict(&mut self, data: &Array2<f64>) -> Result<Array1<usize>> {
409 let result = self.fit(data)?;
410 Ok(result.labels)
411 }
412
413 pub fn cluster_centers(&self) -> Option<&Array2<f64>> {
415 self.cluster_centers.as_ref()
416 }
417
418 pub fn evaluate(
427 &self,
428 data: &Array2<f64>,
429 true_labels: Option<&Array1<usize>>,
430 ) -> Result<ClusteringMetrics> {
431 let centers = self.cluster_centers.as_ref().ok_or_else(|| {
432 MLError::ModelNotTrained("Clusterer must be fitted before evaluation".to_string())
433 })?;
434
435 if data.nrows() == 0 {
436 return Err(MLError::InvalidInput("Empty data".to_string()));
437 }
438
439 let predicted_labels = self.predict(data)?;
440 let n_samples = data.nrows();
441 let k = centers.nrows();
442
443 let inertia: f64 = (0..n_samples)
444 .map(|i| self.squared_dist(&data.row(i), ¢ers.row(predicted_labels[i])))
445 .sum();
446
447 let mut cluster_members: Vec<Vec<usize>> = vec![Vec::new(); k];
448 for (i, &label) in predicted_labels.iter().enumerate() {
449 cluster_members[label].push(i);
450 }
451
452 let silhouette_score =
453 Self::compute_silhouette_score(data, &predicted_labels, &cluster_members);
454 let davies_bouldin_index =
455 Self::compute_davies_bouldin_index(data, centers, &cluster_members);
456 let calinski_harabasz_index =
457 Self::compute_calinski_harabasz_index(data, centers, &cluster_members, inertia);
458
459 let (adjusted_rand_index, normalized_mutual_info) = match true_labels {
460 Some(truth) if truth.len() == n_samples => (
461 Some(Self::adjusted_rand_index(&predicted_labels, truth)),
462 Some(Self::normalized_mutual_info(&predicted_labels, truth)),
463 ),
464 _ => (None, None),
465 };
466
467 Ok(ClusteringMetrics {
468 silhouette_score,
469 davies_bouldin_index,
470 calinski_harabasz_index,
471 inertia,
472 adjusted_rand_index,
473 normalized_mutual_info,
474 })
475 }
476
477 fn compute_silhouette_score(
482 data: &Array2<f64>,
483 labels: &Array1<usize>,
484 cluster_members: &[Vec<usize>],
485 ) -> f64 {
486 let n_samples = data.nrows();
487 let non_empty_clusters = cluster_members.iter().filter(|m| !m.is_empty()).count();
488 if n_samples < 2 || non_empty_clusters < 2 {
489 return 0.0;
490 }
491
492 let mut silhouette_sum = 0.0;
493 let mut counted = 0usize;
494 for i in 0..n_samples {
495 let own_cluster = labels[i];
496 let own_members = &cluster_members[own_cluster];
497 let a_i = if own_members.len() > 1 {
498 own_members
499 .iter()
500 .filter(|&&j| j != i)
501 .map(|&j| euclidean_distance(&data.row(i), &data.row(j)))
502 .sum::<f64>()
503 / (own_members.len() - 1) as f64
504 } else {
505 0.0
506 };
507
508 let mut b_i = f64::INFINITY;
509 for (c, members) in cluster_members.iter().enumerate() {
510 if c == own_cluster || members.is_empty() {
511 continue;
512 }
513 let mean_dist = members
514 .iter()
515 .map(|&j| euclidean_distance(&data.row(i), &data.row(j)))
516 .sum::<f64>()
517 / members.len() as f64;
518 if mean_dist < b_i {
519 b_i = mean_dist;
520 }
521 }
522
523 if b_i.is_finite() {
524 let denom = a_i.max(b_i);
525 let s_i = if denom > 0.0 {
526 (b_i - a_i) / denom
527 } else {
528 0.0
529 };
530 silhouette_sum += s_i;
531 counted += 1;
532 }
533 }
534
535 if counted > 0 {
536 silhouette_sum / counted as f64
537 } else {
538 0.0
539 }
540 }
541
542 fn compute_davies_bouldin_index(
546 data: &Array2<f64>,
547 centers: &Array2<f64>,
548 cluster_members: &[Vec<usize>],
549 ) -> f64 {
550 let k = centers.nrows();
551 if k < 2 {
552 return 0.0;
553 }
554
555 let scatter: Vec<f64> = (0..k)
556 .map(|c| {
557 if cluster_members[c].is_empty() {
558 0.0
559 } else {
560 cluster_members[c]
561 .iter()
562 .map(|&i| euclidean_distance(&data.row(i), ¢ers.row(c)))
563 .sum::<f64>()
564 / cluster_members[c].len() as f64
565 }
566 })
567 .collect();
568
569 let mut db_sum = 0.0;
570 for i in 0..k {
571 let mut max_ratio = 0.0_f64;
572 for j in 0..k {
573 if i == j {
574 continue;
575 }
576 let center_distance = euclidean_distance(¢ers.row(i), ¢ers.row(j));
577 if center_distance > 1e-12 {
578 let ratio = (scatter[i] + scatter[j]) / center_distance;
579 if ratio > max_ratio {
580 max_ratio = ratio;
581 }
582 }
583 }
584 db_sum += max_ratio;
585 }
586
587 db_sum / k as f64
588 }
589
590 fn compute_calinski_harabasz_index(
593 data: &Array2<f64>,
594 centers: &Array2<f64>,
595 cluster_members: &[Vec<usize>],
596 within_cluster_dispersion: f64,
597 ) -> f64 {
598 let n_samples = data.nrows();
599 let k = centers.nrows();
600 if k < 2 || n_samples <= k || within_cluster_dispersion <= 1e-12 {
601 return 0.0;
602 }
603
604 let overall_mean = data
605 .mean_axis(Axis(0))
606 .unwrap_or_else(|| Array1::zeros(data.ncols()));
607
608 let between_cluster_dispersion: f64 = (0..k)
609 .map(|c| {
610 let n_c = cluster_members[c].len() as f64;
611 if n_c == 0.0 {
612 0.0
613 } else {
614 n_c * euclidean_distance(¢ers.row(c), &overall_mean.view()).powi(2)
615 }
616 })
617 .sum();
618
619 (between_cluster_dispersion / within_cluster_dispersion)
620 * ((n_samples - k) as f64 / (k - 1) as f64)
621 }
622
623 fn adjusted_rand_index(predicted: &Array1<usize>, truth: &Array1<usize>) -> f64 {
626 let n = predicted.len();
627 if n == 0 {
628 return 0.0;
629 }
630 let pred_max = predicted.iter().cloned().max().unwrap_or(0) + 1;
631 let true_max = truth.iter().cloned().max().unwrap_or(0) + 1;
632
633 let mut contingency = vec![vec![0usize; true_max]; pred_max];
634 for i in 0..n {
635 contingency[predicted[i]][truth[i]] += 1;
636 }
637
638 let comb2 = |x: usize| -> f64 {
639 if x < 2 {
640 0.0
641 } else {
642 (x * (x - 1)) as f64 / 2.0
643 }
644 };
645
646 let sum_comb_nij: f64 = contingency.iter().flatten().map(|&v| comb2(v)).sum();
647 let row_sums: Vec<usize> = contingency.iter().map(|row| row.iter().sum()).collect();
648 let col_sums: Vec<usize> = (0..true_max)
649 .map(|j| contingency.iter().map(|row| row[j]).sum())
650 .collect();
651
652 let sum_comb_a: f64 = row_sums.iter().map(|&a| comb2(a)).sum();
653 let sum_comb_b: f64 = col_sums.iter().map(|&b| comb2(b)).sum();
654 let comb_n = comb2(n);
655 if comb_n <= 0.0 {
656 return 0.0;
657 }
658
659 let expected_index = (sum_comb_a * sum_comb_b) / comb_n;
660 let max_index = 0.5 * (sum_comb_a + sum_comb_b);
661 let denom = max_index - expected_index;
662
663 if denom.abs() < 1e-12 {
664 0.0
665 } else {
666 (sum_comb_nij - expected_index) / denom
667 }
668 }
669
670 fn normalized_mutual_info(predicted: &Array1<usize>, truth: &Array1<usize>) -> f64 {
674 let n = predicted.len();
675 if n == 0 {
676 return 0.0;
677 }
678 let pred_max = predicted.iter().cloned().max().unwrap_or(0) + 1;
679 let true_max = truth.iter().cloned().max().unwrap_or(0) + 1;
680
681 let mut contingency = vec![vec![0usize; true_max]; pred_max];
682 for i in 0..n {
683 contingency[predicted[i]][truth[i]] += 1;
684 }
685
686 let row_sums: Vec<usize> = contingency.iter().map(|row| row.iter().sum()).collect();
687 let col_sums: Vec<usize> = (0..true_max)
688 .map(|j| contingency.iter().map(|row| row[j]).sum())
689 .collect();
690
691 let n_f = n as f64;
692 let mutual_information: f64 = contingency
693 .iter()
694 .enumerate()
695 .flat_map(|(i, row)| row.iter().enumerate().map(move |(j, &n_ij)| (i, j, n_ij)))
696 .filter(|&(_, _, n_ij)| n_ij > 0)
697 .map(|(i, j, n_ij)| {
698 let p_ij = n_ij as f64 / n_f;
699 let p_i = row_sums[i] as f64 / n_f;
700 let p_j = col_sums[j] as f64 / n_f;
701 p_ij * (p_ij / (p_i * p_j)).ln()
702 })
703 .sum();
704
705 let entropy = |sums: &[usize]| -> f64 {
706 sums.iter()
707 .filter(|&&s| s > 0)
708 .map(|&s| {
709 let p = s as f64 / n_f;
710 -p * p.ln()
711 })
712 .sum::<f64>()
713 };
714
715 let h_pred = entropy(&row_sums);
716 let h_true = entropy(&col_sums);
717
718 if h_pred <= 1e-12 || h_true <= 1e-12 {
719 if mutual_information.abs() < 1e-12 {
720 1.0
721 } else {
722 0.0
723 }
724 } else {
725 (mutual_information / (h_pred * h_true).sqrt()).clamp(0.0, 1.0)
726 }
727 }
728}
729
730fn euclidean_distance(a: &ArrayView1<f64>, b: &ArrayView1<f64>) -> f64 {
732 a.iter()
733 .zip(b.iter())
734 .map(|(x, y)| (x - y).powi(2))
735 .sum::<f64>()
736 .sqrt()
737}
738
739#[derive(Debug, Clone)]
741pub struct ClusteringMetrics {
742 pub silhouette_score: f64,
744 pub davies_bouldin_index: f64,
746 pub calinski_harabasz_index: f64,
748 pub inertia: f64,
750 pub adjusted_rand_index: Option<f64>,
752 pub normalized_mutual_info: Option<f64>,
754}
755
756pub fn create_default_quantum_kmeans(n_clusters: usize) -> QuantumClusterer {
758 let config = QuantumKMeansConfig {
759 n_clusters,
760 ..Default::default()
761 };
762 QuantumClusterer::kmeans(config)
763}
764
765pub fn create_default_quantum_dbscan(eps: f64, min_samples: usize) -> QuantumClusterer {
767 let config = QuantumDBSCANConfig {
768 eps,
769 min_samples,
770 ..Default::default()
771 };
772 QuantumClusterer::dbscan(config)
773}
774
775#[cfg(test)]
776mod regression_tests {
777 use super::*;
778
779 fn two_blob_data() -> Array2<f64> {
781 Array2::from_shape_vec(
782 (8, 2),
783 vec![
784 0.0, 0.0, 0.1, 0.0, 0.0, 0.1, 0.1, 0.1, 10.0, 10.0, 10.1, 10.0, 10.0, 10.1, 10.1, 10.1, ],
787 )
788 .expect("valid shape")
789 }
790
791 #[test]
792 fn predict_proba_reflects_actual_distances_not_uniform() {
793 let mut clusterer = create_default_quantum_kmeans(2);
794 let data = two_blob_data();
795 clusterer.fit(&data).expect("fit should succeed");
796
797 let probabilities = clusterer
798 .predict_proba(&data)
799 .expect("predict_proba should succeed");
800
801 for i in 0..probabilities.nrows() {
805 let row = probabilities.row(i);
806 let max_prob = row.iter().cloned().fold(f64::MIN, f64::max);
807 assert!(
808 max_prob > 0.9,
809 "expected a confident cluster assignment, got {row:?}"
810 );
811 }
812
813 for i in 0..probabilities.nrows() {
815 let sum: f64 = probabilities.row(i).iter().sum();
816 assert!((sum - 1.0).abs() < 1e-9, "row {i} does not sum to 1: {sum}");
817 }
818 }
819
820 #[test]
821 fn evaluate_computes_real_metrics_for_well_separated_clusters() {
822 let mut clusterer = create_default_quantum_kmeans(2);
823 let data = two_blob_data();
824 clusterer.fit(&data).expect("fit should succeed");
825
826 let metrics = clusterer
827 .evaluate(&data, None)
828 .expect("evaluate should succeed");
829
830 assert!(
833 metrics.silhouette_score > 0.9,
834 "silhouette_score should be near 1.0 for well-separated blobs, got {}",
835 metrics.silhouette_score
836 );
837 assert!(
840 metrics.davies_bouldin_index < 0.1,
841 "davies_bouldin_index should be small for well-separated blobs, got {}",
842 metrics.davies_bouldin_index
843 );
844 assert!(
847 metrics.calinski_harabasz_index > 100.0,
848 "calinski_harabasz_index should be large for well-separated blobs, got {}",
849 metrics.calinski_harabasz_index
850 );
851 assert!(
854 metrics.inertia > 0.0,
855 "inertia should be positive, got {}",
856 metrics.inertia
857 );
858
859 let truth = Array1::from_vec(vec![0usize, 0, 0, 0, 1, 1, 1, 1]);
862 let metrics_with_truth = clusterer
863 .evaluate(&data, Some(&truth))
864 .expect("evaluate with truth should succeed");
865 let ari = metrics_with_truth
866 .adjusted_rand_index
867 .expect("ARI should be Some when true_labels given");
868 let nmi = metrics_with_truth
869 .normalized_mutual_info
870 .expect("NMI should be Some when true_labels given");
871 assert!(
872 ari > 0.9,
873 "ARI should be near 1.0 for matching truth, got {ari}"
874 );
875 assert!(
876 nmi > 0.9,
877 "NMI should be near 1.0 for matching truth, got {nmi}"
878 );
879 }
880
881 #[test]
882 fn evaluate_without_true_labels_leaves_external_metrics_none() {
883 let mut clusterer = create_default_quantum_kmeans(2);
884 let data = two_blob_data();
885 clusterer.fit(&data).expect("fit should succeed");
886
887 let metrics = clusterer.evaluate(&data, None).expect("evaluate ok");
888 assert!(metrics.adjusted_rand_index.is_none());
889 assert!(metrics.normalized_mutual_info.is_none());
890 }
891}