scirs2_cluster/hierarchy/
mod.rs

1//! Hierarchical clustering functions
2//!
3//! This module provides hierarchical clustering algorithms for agglomerative clustering,
4//! linkage methods, and dendrogram visualization.
5//!
6//! ## Examples
7//!
8//! ```
9//! use scirs2_core::ndarray::{Array2, ArrayView2};
10//! use scirs2_cluster::hierarchy::{linkage, fcluster, LinkageMethod, Metric};
11//!
12//! // Example data
13//! let data = Array2::from_shape_vec((6, 2), vec![
14//!     1.0, 2.0,
15//!     1.2, 1.8,
16//!     0.8, 1.9,
17//!     3.7, 4.2,
18//!     3.9, 3.9,
19//!     4.2, 4.1,
20//! ]).unwrap();
21//!
22//! // Calculate linkage matrix using Ward's method
23//! let linkage_matrix = linkage(data.view(), LinkageMethod::Ward, Metric::Euclidean).unwrap();
24//!
25//! // Form flat clusters by cutting the dendrogram at a height that gives 2 clusters
26//! let num_clusters = 2;
27//! let labels = fcluster(&linkage_matrix, num_clusters, None).unwrap();
28//!
29//! // Print the results
30//! println!("Cluster assignments: {:?}", labels);
31//! ```
32
33use scirs2_core::ndarray::{Array1, Array2, ArrayView2};
34use scirs2_core::numeric::{Float, FromPrimitive};
35use std::fmt::Debug;
36
37use crate::error::{ClusteringError, Result};
38
39// Module definitions
40pub mod agglomerative;
41pub mod cluster_extraction;
42pub mod condensed_matrix;
43pub mod dendrogram;
44pub mod disjoint_set;
45pub mod leaf_ordering;
46pub mod linkage;
47pub mod optimized_ward;
48pub mod parallel_linkage;
49pub mod validation;
50pub mod visualization;
51
52// Re-exports
53pub use self::agglomerative::{cut_tree_by_distance, cut_tree_by_inconsistency};
54pub use self::cluster_extraction::{
55    estimate_optimal_clusters, extract_clusters_multi_criteria, prune_clusters,
56};
57pub use self::condensed_matrix::{
58    condensed_size, condensed_to_square, get_distance, points_from_condensed_size,
59    square_to_condensed, validate_condensed_matrix,
60};
61pub use self::dendrogram::{cophenet, dendrogram, inconsistent, optimal_leaf_ordering};
62pub use self::disjoint_set::DisjointSet;
63pub use self::leaf_ordering::{
64    apply_leaf_ordering, optimal_leaf_ordering_exact, optimal_leaf_ordering_heuristic,
65};
66pub use self::optimized_ward::{
67    lance_williams_ward_update, memory_efficient_ward_linkage, optimized_ward_linkage,
68};
69pub use self::validation::{
70    validate_cluster_consistency, validate_cluster_extraction_params, validate_distance_matrix,
71    validate_linkage_matrix, validate_monotonic_distances, validate_square_distance_matrix,
72};
73pub use self::visualization::{
74    create_dendrogramplot, get_color_palette, Branch, ColorScheme, ColorThreshold,
75    DendrogramConfig, DendrogramOrientation, DendrogramPlot, Leaf, LegendEntry, TruncateMode,
76};
77
78/// Linkage methods for hierarchical clustering
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum LinkageMethod {
81    /// Single linkage (minimum distance between any two points in clusters)
82    Single,
83
84    /// Complete linkage (maximum distance between any two points in clusters)
85    Complete,
86
87    /// Average linkage (average distance between all points in clusters)
88    Average,
89
90    /// Ward's method (minimizes variance of merged clusters)
91    Ward,
92
93    /// Centroid method (distance between cluster centroids)
94    Centroid,
95
96    /// Median method (uses weighted centroids)
97    Median,
98
99    /// Weighted average (weights by cluster size)
100    Weighted,
101}
102
103/// Distance metrics for hierarchical clustering
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum Metric {
106    /// Euclidean distance (L2 norm)
107    Euclidean,
108
109    /// Manhattan distance (L1 norm)
110    Manhattan,
111
112    /// Maximum distance (L∞ norm)
113    Chebyshev,
114
115    /// Cosine distance (1 - cosine similarity)
116    Cosine,
117
118    /// Correlation distance (1 - correlation)
119    Correlation,
120}
121
122/// Criterion for forming flat clusters from a hierarchical clustering
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum ClusterCriterion {
125    /// Number of clusters desired
126    MaxClust,
127
128    /// Distance threshold for clusters
129    Distance,
130
131    /// Inconsistency threshold
132    Inconsistent,
133}
134
135/// Computes distances between observations
136#[allow(dead_code)]
137fn compute_distances<F: Float + FromPrimitive>(data: ArrayView2<F>, metric: Metric) -> Array1<F> {
138    let n_samples = data.shape()[0];
139    let n_features = data.shape()[1];
140
141    // For n samples, we need n*(n-1)/2 distances (condensed distance matrix)
142    let num_distances = n_samples * (n_samples - 1) / 2;
143    let mut distances = Array1::zeros(num_distances);
144
145    let mut idx = 0;
146    for i in 0..n_samples {
147        for j in (i + 1)..n_samples {
148            let dist = match metric {
149                Metric::Euclidean => {
150                    // Euclidean distance
151                    let mut sum = F::zero();
152                    for k in 0..n_features {
153                        let diff = data[[i, k]] - data[[j, k]];
154                        sum = sum + diff * diff;
155                    }
156                    sum.sqrt()
157                }
158                Metric::Manhattan => {
159                    // Manhattan distance
160                    let mut sum = F::zero();
161                    for k in 0..n_features {
162                        let diff = (data[[i, k]] - data[[j, k]]).abs();
163                        sum = sum + diff;
164                    }
165                    sum
166                }
167                Metric::Chebyshev => {
168                    // Chebyshev distance
169                    let mut max_diff = F::zero();
170                    for k in 0..n_features {
171                        let diff = (data[[i, k]] - data[[j, k]]).abs();
172                        if diff > max_diff {
173                            max_diff = diff;
174                        }
175                    }
176                    max_diff
177                }
178                Metric::Cosine => {
179                    // Cosine distance
180                    // Formula: 1 - cosine similarity
181                    let mut dot_product = F::zero();
182                    let mut norm_i = F::zero();
183                    let mut norm_j = F::zero();
184
185                    for k in 0..n_features {
186                        let val_i = data[[i, k]];
187                        let val_j = data[[j, k]];
188
189                        dot_product = dot_product + val_i * val_j;
190                        norm_i = norm_i + val_i * val_i;
191                        norm_j = norm_j + val_j * val_j;
192                    }
193
194                    let norm_product = (norm_i * norm_j).sqrt();
195
196                    if norm_product < F::from_f64(1e-10).unwrap() {
197                        // If either vector is zero, distance is 1
198                        F::one()
199                    } else {
200                        F::one() - (dot_product / norm_product)
201                    }
202                }
203                Metric::Correlation => {
204                    // Correlation distance
205                    // Formula: 1 - correlation coefficient
206
207                    // Compute means for both vectors
208                    let mut mean_i = F::zero();
209                    let mut mean_j = F::zero();
210
211                    for k in 0..n_features {
212                        mean_i = mean_i + data[[i, k]];
213                        mean_j = mean_j + data[[j, k]];
214                    }
215
216                    mean_i = mean_i / F::from_usize(n_features).unwrap();
217                    mean_j = mean_j / F::from_usize(n_features).unwrap();
218
219                    // Compute correlation coefficient
220                    let mut numerator = F::zero();
221                    let mut denom_i = F::zero();
222                    let mut denom_j = F::zero();
223
224                    for k in 0..n_features {
225                        let diff_i = data[[i, k]] - mean_i;
226                        let diff_j = data[[j, k]] - mean_j;
227
228                        numerator = numerator + diff_i * diff_j;
229                        denom_i = denom_i + diff_i * diff_i;
230                        denom_j = denom_j + diff_j * diff_j;
231                    }
232
233                    let denom = (denom_i * denom_j).sqrt();
234
235                    if denom < F::from_f64(1e-10).unwrap() {
236                        // If vectors are constant, distance is 0
237                        F::zero()
238                    } else {
239                        F::one() - (numerator / denom)
240                    }
241                }
242            };
243
244            distances[idx] = dist;
245            idx += 1;
246        }
247    }
248
249    distances
250}
251
252/// Converts a condensed distance matrix index to (i, j) coordinates
253#[allow(dead_code)]
254pub fn condensed_index_to_coords(n: usize, idx: usize) -> (usize, usize) {
255    // Find i and j from the condensed index
256    let mut i = 0;
257    let mut j = 0;
258    let mut k = 0;
259
260    for i_temp in 0..n {
261        for j_temp in (i_temp + 1)..n {
262            if k == idx {
263                i = i_temp;
264                j = j_temp;
265                break;
266            }
267            k += 1;
268        }
269
270        if k == idx {
271            break;
272        }
273    }
274
275    (i, j)
276}
277
278/// Converts (i, j) coordinates to a condensed distance matrix index
279#[allow(dead_code)]
280pub fn coords_to_condensed_index(n: usize, i: usize, j: usize) -> Result<usize> {
281    if i == j {
282        return Err(ClusteringError::InvalidInput(
283            "Cannot compute diagonal index in condensed matrix".into(),
284        ));
285    }
286
287    if i >= n || j >= n {
288        return Err(ClusteringError::InvalidInput(format!(
289            "Indices ({}, {}) out of bounds for matrix size {}",
290            i, j, n
291        )));
292    }
293
294    let (i_min, j_min) = if i < j { (i, j) } else { (j, i) };
295    Ok((n * i_min) - ((i_min * (i_min + 1)) / 2) + (j_min - i_min - 1))
296}
297
298/// Performs hierarchical clustering using the specified linkage method
299///
300/// # Arguments
301///
302/// * `data` - The input data as a 2D array (n_samples x n_features)
303/// * `method` - The linkage method to use
304/// * `metric` - The distance metric to use
305///
306/// # Returns
307///
308/// * `Result<Array2<F>>` - The linkage matrix, which describes the dendrogram
309#[allow(dead_code)]
310pub fn linkage<
311    F: Float
312        + FromPrimitive
313        + Debug
314        + PartialOrd
315        + Send
316        + Sync
317        + scirs2_core::ndarray::ScalarOperand
318        + 'static,
319>(
320    data: ArrayView2<F>,
321    method: LinkageMethod,
322    metric: Metric,
323) -> Result<Array2<F>> {
324    let n_samples = data.shape()[0];
325
326    if n_samples < 2 {
327        return Err(ClusteringError::InvalidInput(
328            "Need at least 2 samples for hierarchical clustering".into(),
329        ));
330    }
331
332    if n_samples > 10000 {
333        // Hierarchical clustering on large datasets can be very memory-intensive
334        // and slow. We'll add a warning here.
335        eprintln!("Warning: Performing hierarchical clustering on {n_samples} samples. This may be slow and memory-intensive.");
336    }
337
338    // Use optimized Ward's method if requested
339    if method == LinkageMethod::Ward {
340        return optimized_ward::optimized_ward_linkage(data, metric);
341    }
342
343    // Calculate distances between observations
344    let distances = compute_distances(data, metric);
345
346    // Run the clustering
347    linkage::hierarchical_clustering(&distances, n_samples, method)
348}
349
350/// Performs parallel hierarchical clustering using the specified linkage method
351///
352/// This function uses parallelization to speed up the clustering process,
353/// particularly beneficial for large datasets and computationally intensive linkage methods.
354///
355/// # Arguments
356///
357/// * `data` - The input data as a 2D array (n_samples x n_features)
358/// * `method` - The linkage method to use
359/// * `metric` - The distance metric to use
360///
361/// # Returns
362///
363/// * `Result<Array2<F>>` - The linkage matrix, which describes the dendrogram
364///
365/// # Examples
366///
367/// ```
368/// use scirs2_core::ndarray::{Array2, ArrayView2};
369/// use scirs2_cluster::hierarchy::{parallel_linkage, LinkageMethod, Metric};
370///
371/// // Example data
372/// let data = Array2::from_shape_vec((6, 2), vec![
373///     1.0, 2.0,
374///     1.2, 1.8,
375///     0.8, 1.9,
376///     3.7, 4.2,
377///     3.9, 3.9,
378///     4.2, 4.1,
379/// ]).unwrap();
380///
381/// // Calculate linkage matrix using parallel Ward's method
382/// let linkage_matrix = parallel_linkage(data.view(), LinkageMethod::Ward, Metric::Euclidean).unwrap();
383///
384/// println!("Linkage matrix shape: {:?}", linkage_matrix.shape());
385/// ```
386#[allow(dead_code)]
387pub fn parallel_linkage<
388    F: Float
389        + FromPrimitive
390        + Debug
391        + PartialOrd
392        + Send
393        + Sync
394        + std::iter::Sum
395        + scirs2_core::ndarray::ScalarOperand
396        + 'static,
397>(
398    data: ArrayView2<F>,
399    method: LinkageMethod,
400    metric: Metric,
401) -> Result<Array2<F>> {
402    let n_samples = data.shape()[0];
403
404    if n_samples < 2 {
405        return Err(ClusteringError::InvalidInput(
406            "Need at least 2 samples for hierarchical clustering".into(),
407        ));
408    }
409
410    if n_samples > 10000 {
411        // Hierarchical clustering on large datasets can be very memory-intensive
412        // and slow. We'll add a warning here.
413        eprintln!("Warning: Performing parallel hierarchical clustering on {n_samples} samples. This may still be slow for very large datasets.");
414    }
415
416    // Use optimized Ward's method if requested (already parallel-optimized)
417    if method == LinkageMethod::Ward {
418        return optimized_ward::optimized_ward_linkage(data, metric);
419    }
420
421    // Calculate distances between observations
422    let distances = compute_distances(data, metric);
423
424    // Run the parallel clustering
425    parallel_linkage::parallel_hierarchical_clustering(&distances, n_samples, method)
426}
427
428/// Forms flat clusters from a hierarchical clustering result
429///
430/// # Arguments
431///
432/// * `z` - The linkage matrix from the `linkage` function
433/// * `t` - The threshold or number of clusters (depends on criterion)
434/// * `criterion` - The criterion to use for forming clusters (default: MaxClust)
435///
436/// # Returns
437///
438/// * `Result<Array1<usize>>` - Cluster assignments (0-indexed)
439///
440/// # Note
441///
442/// For Distance and Inconsistent criteria, consider using `fcluster_generic` which accepts
443/// float thresholds directly.
444#[allow(dead_code)]
445pub fn fcluster<F: Float + FromPrimitive + PartialOrd + Debug>(
446    z: &Array2<F>,
447    t: usize,
448    criterion: Option<ClusterCriterion>,
449) -> Result<Array1<usize>> {
450    let n_samples = z.shape()[0] + 1;
451    let crit = criterion.unwrap_or(ClusterCriterion::MaxClust);
452
453    match crit {
454        ClusterCriterion::MaxClust => {
455            // t represents the number of clusters
456            if t == 0 || t > n_samples {
457                return Err(ClusteringError::InvalidInput(format!(
458                    "Number of clusters must be between 1 and {}",
459                    n_samples
460                )));
461            }
462
463            agglomerative::cut_tree(z, t)
464        }
465        ClusterCriterion::Distance => {
466            // t represents a distance threshold
467            let t_float = F::from_usize(t).unwrap();
468            agglomerative::cut_tree_by_distance(z, t_float)
469        }
470        ClusterCriterion::Inconsistent => {
471            // t represents an inconsistency threshold
472            let t_float = F::from_usize(t).unwrap();
473
474            // Calculate inconsistency values with default depth of 2
475            let inconsistency_matrix = dendrogram::inconsistent(z, None)?;
476
477            // Cut tree based on inconsistency threshold
478            agglomerative::cut_tree_by_inconsistency(z, t_float, &inconsistency_matrix)
479        }
480    }
481}
482
483/// Forms flat clusters from a hierarchical clustering result with generic threshold type
484///
485/// This function is more flexible than `fcluster` as it accepts float thresholds directly,
486/// which is useful for Distance and Inconsistent criteria.
487///
488/// # Arguments
489///
490/// * `z` - The linkage matrix from the `linkage` function
491/// * `t` - The threshold value (float for Distance/Inconsistent, can be integer for MaxClust)
492/// * `criterion` - The criterion to use for forming clusters
493///
494/// # Returns
495///
496/// * `Result<Array1<usize>>` - Cluster assignments (0-indexed)
497///
498/// # Examples
499///
500/// ```
501/// use scirs2_core::ndarray::Array2;
502/// use scirs2_cluster::hierarchy::{linkage, fcluster_generic, LinkageMethod, Metric, ClusterCriterion};
503///
504/// let data = Array2::from_shape_vec((6, 2), vec![
505///     1.0, 2.0, 1.2, 1.8, 0.8, 1.9,
506///     3.7, 4.2, 3.9, 3.9, 4.2, 4.1,
507/// ]).unwrap();
508///
509/// let linkage_matrix = linkage(data.view(), LinkageMethod::Ward, Metric::Euclidean).unwrap();
510///
511/// // Cut at distance threshold 2.5
512/// let labels = fcluster_generic(&linkage_matrix, 2.5, ClusterCriterion::Distance).unwrap();
513///
514/// // Cut at inconsistency threshold 0.8
515/// let labels2 = fcluster_generic(&linkage_matrix, 0.8, ClusterCriterion::Inconsistent).unwrap();
516/// ```
517#[allow(dead_code)]
518pub fn fcluster_generic<F: Float + FromPrimitive + PartialOrd + Debug>(
519    z: &Array2<F>,
520    t: F,
521    criterion: ClusterCriterion,
522) -> Result<Array1<usize>> {
523    let n_samples = z.shape()[0] + 1;
524
525    match criterion {
526        ClusterCriterion::MaxClust => {
527            // t represents the number of clusters
528            let n_clusters = t.to_usize().ok_or_else(|| {
529                ClusteringError::InvalidInput("Invalid number of clusters".into())
530            })?;
531
532            if n_clusters == 0 || n_clusters > n_samples {
533                return Err(ClusteringError::InvalidInput(format!(
534                    "Number of clusters must be between 1 and {}",
535                    n_samples
536                )));
537            }
538
539            agglomerative::cut_tree(z, n_clusters)
540        }
541        ClusterCriterion::Distance => {
542            // t represents a distance threshold
543            agglomerative::cut_tree_by_distance(z, t)
544        }
545        ClusterCriterion::Inconsistent => {
546            // t represents an inconsistency threshold
547            // Calculate inconsistency values with default depth of 2
548            let inconsistency_matrix = dendrogram::inconsistent(z, None)?;
549
550            // Cut tree based on inconsistency threshold
551            agglomerative::cut_tree_by_inconsistency(z, t, &inconsistency_matrix)
552        }
553    }
554}
555
556#[cfg(test)]
557mod tests {
558    use super::*;
559    use approx::assert_abs_diff_eq;
560
561    #[test]
562    fn test_linkage_simple() {
563        // Simple dataset with two clear clusters
564        let data = Array2::from_shape_vec(
565            (6, 2),
566            vec![1.0, 2.0, 1.2, 1.8, 0.8, 1.9, 3.7, 4.2, 3.9, 3.9, 4.2, 4.1],
567        )
568        .unwrap();
569
570        // Run with Ward's method
571        let linkage_matrix = linkage(data.view(), LinkageMethod::Ward, Metric::Euclidean).unwrap();
572
573        // Check dimensions
574        assert_eq!(linkage_matrix.shape(), &[5, 4]);
575
576        // Check the first row
577        // (We can't check exact values due to implementation differences, but we can check ranges)
578        assert!(linkage_matrix[[0, 2]] > 0.0); // Distance should be positive
579        assert_eq!(linkage_matrix[[0, 3]] as usize, 2); // Size should be 2
580    }
581
582    #[test]
583    fn test_fcluster() {
584        // Create a simple linkage matrix
585        let data = Array2::from_shape_vec(
586            (6, 2),
587            vec![1.0, 2.0, 1.2, 1.8, 0.8, 1.9, 3.7, 4.2, 3.9, 3.9, 4.2, 4.1],
588        )
589        .unwrap();
590
591        let linkage_matrix = linkage(data.view(), LinkageMethod::Ward, Metric::Euclidean).unwrap();
592
593        // Get 2 clusters
594        let labels = fcluster(&linkage_matrix, 2, None).unwrap();
595
596        // Should have 6 labels
597        assert_eq!(labels.len(), 6);
598
599        // Verify clusters make sense - first 3 and last 3 should be different clusters
600        assert_eq!(labels[0], labels[1]);
601        assert_eq!(labels[1], labels[2]);
602        assert_eq!(labels[3], labels[4]);
603        assert_eq!(labels[4], labels[5]);
604        assert_ne!(labels[0], labels[3]);
605    }
606
607    #[test]
608    fn test_distance_metrics() {
609        // Test data
610        let data =
611            Array2::from_shape_vec((4, 2), vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0]).unwrap();
612
613        // Calculate distances with different metrics
614        let euclidean_distances = compute_distances(data.view(), Metric::Euclidean);
615        let manhattan_distances = compute_distances(data.view(), Metric::Manhattan);
616        let chebyshev_distances = compute_distances(data.view(), Metric::Chebyshev);
617
618        // Check dimensions
619        assert_eq!(euclidean_distances.len(), 6); // (4 choose 2)
620
621        // Check specific distances
622        // Distance between (0,0) and (1,0) should be 1.0 for Euclidean
623        assert_abs_diff_eq!(euclidean_distances[0], 1.0, epsilon = 1e-10);
624
625        // Distance between (0,0) and (1,1) should be sqrt(2) for Euclidean
626        assert_abs_diff_eq!(
627            euclidean_distances[2],
628            std::f64::consts::SQRT_2,
629            epsilon = 1e-10
630        );
631
632        // Distance between (0,0) and (1,0) should be 1.0 for Manhattan
633        assert_abs_diff_eq!(manhattan_distances[0], 1.0, epsilon = 1e-10);
634
635        // Distance between (0,0) and (1,1) should be 2.0 for Manhattan
636        assert_abs_diff_eq!(manhattan_distances[2], 2.0, epsilon = 1e-10);
637
638        // Distance between (0,0) and (1,1) should be 1.0 for Chebyshev
639        assert_abs_diff_eq!(chebyshev_distances[2], 1.0, epsilon = 1e-10);
640    }
641
642    #[test]
643    fn test_hierarchy_with_different_linkage_methods() {
644        // Simple dataset
645        let data = Array2::from_shape_vec(
646            (6, 2),
647            vec![1.0, 2.0, 1.2, 1.8, 0.8, 1.9, 3.7, 4.2, 3.9, 3.9, 4.2, 4.1],
648        )
649        .unwrap();
650
651        // Test different linkage methods
652        let methods = vec![
653            LinkageMethod::Single,
654            LinkageMethod::Complete,
655            LinkageMethod::Average,
656            LinkageMethod::Ward,
657        ];
658
659        for method in methods {
660            let linkage_matrix = linkage(data.view(), method, Metric::Euclidean).unwrap();
661
662            // Check dimensions
663            assert_eq!(linkage_matrix.shape(), &[5, 4]);
664
665            // Get 2 clusters
666            let labels = fcluster(&linkage_matrix, 2, None).unwrap();
667
668            // Should have 6 labels
669            assert_eq!(labels.len(), 6);
670        }
671    }
672
673    #[test]
674    fn test_fcluster_inconsistent_criterion() {
675        // Create a simple linkage matrix
676        let data = Array2::from_shape_vec(
677            (6, 2),
678            vec![1.0, 2.0, 1.2, 1.8, 0.8, 1.9, 3.7, 4.2, 3.9, 3.9, 4.2, 4.1],
679        )
680        .unwrap();
681
682        let linkage_matrix = linkage(data.view(), LinkageMethod::Ward, Metric::Euclidean).unwrap();
683
684        // Test with Inconsistent criterion using fcluster_generic
685        let labels =
686            fcluster_generic(&linkage_matrix, 1.0, ClusterCriterion::Inconsistent).unwrap();
687
688        // Should have 6 labels
689        assert_eq!(labels.len(), 6);
690
691        // All labels should be valid cluster indices
692        assert!(labels.iter().all(|&l| l < 6));
693    }
694
695    #[test]
696    fn test_fcluster_generic_all_criteria() {
697        let data = Array2::from_shape_vec(
698            (6, 2),
699            vec![1.0, 2.0, 1.2, 1.8, 0.8, 1.9, 3.7, 4.2, 3.9, 3.9, 4.2, 4.1],
700        )
701        .unwrap();
702
703        let linkage_matrix = linkage(data.view(), LinkageMethod::Ward, Metric::Euclidean).unwrap();
704
705        // Test MaxClust
706        let labels_maxclust =
707            fcluster_generic(&linkage_matrix, 2.0, ClusterCriterion::MaxClust).unwrap();
708        assert_eq!(labels_maxclust.len(), 6);
709        let unique_maxclust: std::collections::HashSet<_> =
710            labels_maxclust.iter().cloned().collect();
711        assert_eq!(unique_maxclust.len(), 2);
712
713        // Test Distance
714        let labels_distance =
715            fcluster_generic(&linkage_matrix, 2.5, ClusterCriterion::Distance).unwrap();
716        assert_eq!(labels_distance.len(), 6);
717
718        // Test Inconsistent
719        let labels_inconsistent =
720            fcluster_generic(&linkage_matrix, 0.5, ClusterCriterion::Inconsistent).unwrap();
721        assert_eq!(labels_inconsistent.len(), 6);
722    }
723}