Skip to main content

scirs2_optimize/global/
clustering.rs

1//! Clustering algorithms for local minima in multi-start optimization
2//!
3//! This module provides algorithms to identify, cluster, and analyze local minima
4//! found during multi-start optimization strategies. It helps distinguish between
5//! unique local minima and provides insights into the optimization landscape.
6
7use crate::error::OptimizeError;
8use crate::global::qmc::SobolGenerator;
9use crate::unconstrained::{minimize, Method, Options};
10use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
11use std::collections::HashMap;
12
13/// Configuration for clustering local minima
14#[derive(Debug, Clone)]
15pub struct ClusteringOptions {
16    /// Distance threshold for considering minima as belonging to the same cluster
17    pub distance_threshold: f64,
18    /// Relative tolerance for function values when clustering
19    pub function_tolerance: f64,
20    /// Maximum number of clusters to form
21    pub max_clusters: Option<usize>,
22    /// Clustering algorithm to use
23    pub algorithm: ClusteringAlgorithm,
24    /// Whether to normalize coordinates before clustering
25    pub normalize_coordinates: bool,
26    /// Whether to use function values in clustering
27    pub use_function_values: bool,
28    /// Weight for function values vs coordinates in distance calculation
29    pub function_weight: f64,
30}
31
32impl Default for ClusteringOptions {
33    fn default() -> Self {
34        Self {
35            distance_threshold: 1e-3,
36            function_tolerance: 1e-6,
37            max_clusters: None,
38            algorithm: ClusteringAlgorithm::Hierarchical,
39            normalize_coordinates: true,
40            use_function_values: true,
41            function_weight: 0.1,
42        }
43    }
44}
45
46/// Clustering algorithms available
47#[derive(Debug, Clone, Copy)]
48pub enum ClusteringAlgorithm {
49    /// Hierarchical clustering with single linkage
50    Hierarchical,
51    /// K-means clustering
52    KMeans,
53    /// Density-based clustering (DBSCAN-like)
54    Density,
55    /// Custom threshold-based clustering
56    Threshold,
57}
58
59/// Represents a local minimum found during optimization
60#[derive(Debug, Clone)]
61pub struct LocalMinimum<S> {
62    /// Location of the minimum
63    pub x: Array1<f64>,
64    /// Function value at the minimum
65    pub f: f64,
66    /// Original function value (for generic type)
67    pub fun_value: S,
68    /// Number of optimization iterations to reach this minimum
69    pub nit: usize,
70    /// Number of function evaluations
71    pub func_evals: usize,
72    /// Whether optimization was successful
73    pub success: bool,
74    /// Starting point that led to this minimum
75    pub start_point: Array1<f64>,
76    /// Cluster ID (assigned after clustering)
77    pub cluster_id: Option<usize>,
78    /// Distance to cluster centroid
79    pub cluster_distance: Option<f64>,
80}
81
82/// Result of clustering analysis
83#[derive(Debug, Clone)]
84pub struct ClusteringResult<S> {
85    /// All local minima found
86    pub minima: Vec<LocalMinimum<S>>,
87    /// Cluster centroids
88    pub centroids: Vec<ClusterCentroid>,
89    /// Number of clusters formed
90    pub num_clusters: usize,
91    /// Silhouette score (quality measure)
92    pub silhouette_score: Option<f64>,
93    /// Within-cluster sum of squares
94    pub wcss: f64,
95    /// Best minimum found (global optimum candidate)
96    pub best_minimum: Option<LocalMinimum<S>>,
97}
98
99/// Cluster centroid information
100#[derive(Debug, Clone)]
101pub struct ClusterCentroid {
102    /// Centroid coordinates
103    pub x: Array1<f64>,
104    /// Average function value in cluster
105    pub f_avg: f64,
106    /// Best (minimum) function value in cluster
107    pub f_min: f64,
108    /// Number of minima in this cluster
109    pub size: usize,
110    /// Cluster radius (max distance from centroid)
111    pub radius: f64,
112}
113
114/// Multi-start optimization with clustering
115#[allow(dead_code)]
116pub fn multi_start_with_clustering<F, S>(
117    fun: F,
118    start_points: &[Array1<f64>],
119    method: Method,
120    options: Option<Options>,
121    clustering_options: Option<ClusteringOptions>,
122) -> Result<ClusteringResult<S>, OptimizeError>
123where
124    F: FnMut(&ArrayView1<f64>) -> S + Clone,
125    S: Into<f64> + Clone + From<f64>,
126{
127    let clustering_opts = clustering_options.unwrap_or_default();
128    let mut minima = Vec::new();
129
130    // Run optimization from each starting point
131    for start_point in start_points {
132        let fun_clone = fun.clone();
133
134        match minimize(
135            fun_clone,
136            start_point.as_slice().expect("Operation failed"),
137            method,
138            options.clone(),
139        ) {
140            Ok(result) => {
141                let minimum = LocalMinimum {
142                    x: result.x.clone(),
143                    f: result.fun.clone().into(),
144                    fun_value: result.fun,
145                    nit: result.nit,
146                    func_evals: result.func_evals,
147                    success: result.success,
148                    start_point: start_point.clone(),
149                    cluster_id: None,
150                    cluster_distance: None,
151                };
152                minima.push(minimum);
153            }
154            Err(_) => {
155                // Skip failed optimizations but could log them
156                continue;
157            }
158        }
159    }
160
161    if minima.is_empty() {
162        return Err(OptimizeError::ComputationError(
163            "No successful optimizations found".to_string(),
164        ));
165    }
166
167    // Perform clustering
168    cluster_minima(&mut minima, &clustering_opts)?;
169
170    // Compute cluster centroids and statistics
171    let centroids = compute_cluster_centroids(&minima)?;
172    let num_clusters = centroids.len();
173    let wcss = compute_wcss(&minima, &centroids);
174    let silhouette_score = compute_silhouette_score(&minima);
175
176    // Find best minimum
177    let best_minimum = minima
178        .iter()
179        .filter(|m| m.success)
180        .min_by(|a, b| a.f.partial_cmp(&b.f).expect("Operation failed"))
181        .cloned();
182
183    Ok(ClusteringResult {
184        minima,
185        centroids,
186        num_clusters,
187        silhouette_score,
188        wcss,
189        best_minimum,
190    })
191}
192
193/// Cluster local minima using the specified algorithm
194#[allow(dead_code)]
195fn cluster_minima<S>(
196    minima: &mut [LocalMinimum<S>],
197    options: &ClusteringOptions,
198) -> Result<(), OptimizeError>
199where
200    S: Clone,
201{
202    if minima.is_empty() {
203        return Ok(());
204    }
205
206    match options.algorithm {
207        ClusteringAlgorithm::Hierarchical => hierarchical_clustering(minima, options),
208        ClusteringAlgorithm::KMeans => kmeans_clustering(minima, options),
209        ClusteringAlgorithm::Density => density_clustering(minima, options),
210        ClusteringAlgorithm::Threshold => threshold_clustering(minima, options),
211    }
212}
213
214/// Hierarchical clustering implementation
215#[allow(dead_code)]
216fn hierarchical_clustering<S>(
217    minima: &mut [LocalMinimum<S>],
218    options: &ClusteringOptions,
219) -> Result<(), OptimizeError>
220where
221    S: Clone,
222{
223    let n = minima.len();
224    if n <= 1 {
225        if n == 1 {
226            minima[0].cluster_id = Some(0);
227        }
228        return Ok(());
229    }
230
231    // Compute distance matrix
232    let distances = compute_distance_matrix(minima, options);
233
234    // Perform hierarchical clustering using single linkage
235    let mut cluster_assignments = vec![None; n];
236    let mut next_cluster_id = n;
237
238    // Initialize: each point is its own cluster
239    for (i, assignment) in cluster_assignments.iter_mut().enumerate().take(n) {
240        *assignment = Some(i);
241    }
242
243    // Build hierarchy by merging closest clusters
244    let mut active_clusters: Vec<Vec<usize>> = (0..n).map(|i| vec![i]).collect();
245
246    while active_clusters.len() > 1 {
247        let mut min_dist = f64::INFINITY;
248        let mut merge_pair = (0, 1);
249
250        // Find closest pair of clusters
251        for i in 0..active_clusters.len() {
252            for j in (i + 1)..active_clusters.len() {
253                let cluster_dist =
254                    compute_cluster_distance(&active_clusters[i], &active_clusters[j], &distances);
255                if cluster_dist < min_dist {
256                    min_dist = cluster_dist;
257                    merge_pair = (i, j);
258                }
259            }
260        }
261
262        // Stop if distance threshold exceeded
263        if min_dist > options.distance_threshold {
264            break;
265        }
266
267        // Check max clusters constraint
268        if let Some(max_clusters) = options.max_clusters {
269            if active_clusters.len() <= max_clusters {
270                break;
271            }
272        }
273
274        // Merge clusters
275        let (i, j) = merge_pair;
276        let mut merged_cluster = active_clusters[i].clone();
277        merged_cluster.extend(&active_clusters[j]);
278
279        // Update cluster assignments
280        for &point_idx in &merged_cluster {
281            cluster_assignments[point_idx] = Some(next_cluster_id);
282        }
283
284        // Remove old clusters and add merged one
285        let mut new_clusters = Vec::new();
286        for (k, cluster) in active_clusters.iter().enumerate() {
287            if k != i && k != j {
288                new_clusters.push(cluster.clone());
289            }
290        }
291        new_clusters.push(merged_cluster);
292        active_clusters = new_clusters;
293        next_cluster_id += 1;
294    }
295
296    // Assign final cluster IDs (renumber to be sequential)
297    let mut cluster_map = HashMap::new();
298    let mut final_cluster_id = 0;
299
300    for cluster_id in cluster_assignments.iter().flatten() {
301        if !cluster_map.contains_key(cluster_id) {
302            cluster_map.insert(*cluster_id, final_cluster_id);
303            final_cluster_id += 1;
304        }
305    }
306
307    // Update minima with final cluster assignments
308    for (i, minimum) in minima.iter_mut().enumerate() {
309        if let Some(cluster_id) = cluster_assignments[i] {
310            minimum.cluster_id = cluster_map.get(&cluster_id).copied();
311        }
312    }
313
314    Ok(())
315}
316
317/// K-means clustering implementation
318#[allow(dead_code)]
319fn kmeans_clustering<S>(
320    minima: &mut [LocalMinimum<S>],
321    options: &ClusteringOptions,
322) -> Result<(), OptimizeError>
323where
324    S: Clone,
325{
326    let n = minima.len();
327    if n <= 1 {
328        if n == 1 {
329            minima[0].cluster_id = Some(0);
330        }
331        return Ok(());
332    }
333
334    // Determine number of clusters
335    let k = if let Some(max_k) = options.max_clusters {
336        std::cmp::min(max_k, n)
337    } else {
338        // Use elbow method or default to sqrt(n)
339        std::cmp::min((n as f64).sqrt() as usize + 1, n)
340    };
341
342    if k >= n {
343        // Each point is its own cluster
344        for (i, minimum) in minima.iter_mut().enumerate() {
345            minimum.cluster_id = Some(i);
346        }
347        return Ok(());
348    }
349
350    // Get feature vectors for clustering
351    let features = extract_features(minima, options);
352    let dim = features.ncols();
353
354    // Initialize centroids randomly (or use k-means++)
355    let mut centroids = initialize_centroids_plus_plus(&features, k);
356    let mut assignments = vec![0; n];
357    let max_iter = 100;
358    let tolerance = 1e-6;
359
360    for _iter in 0..max_iter {
361        let mut changed = false;
362
363        // Assign points to nearest centroids
364        for (i, assignment) in assignments.iter_mut().enumerate().take(n) {
365            let mut min_dist = f64::INFINITY;
366            let mut best_cluster = 0;
367
368            for j in 0..k {
369                let dist = euclidean_distance(&features.row(i), &centroids.row(j));
370                if dist < min_dist {
371                    min_dist = dist;
372                    best_cluster = j;
373                }
374            }
375
376            if *assignment != best_cluster {
377                *assignment = best_cluster;
378                changed = true;
379            }
380        }
381
382        if !changed {
383            break;
384        }
385
386        // Update centroids
387        let mut new_centroids = Array2::zeros((k, dim));
388        let mut cluster_sizes = vec![0; k];
389
390        for i in 0..n {
391            let cluster = assignments[i];
392            cluster_sizes[cluster] += 1;
393            for d in 0..dim {
394                new_centroids[[cluster, d]] += features[[i, d]];
395            }
396        }
397
398        for j in 0..k {
399            if cluster_sizes[j] > 0 {
400                for d in 0..dim {
401                    new_centroids[[j, d]] /= cluster_sizes[j] as f64;
402                }
403            }
404        }
405
406        // Check convergence
407        let centroid_change = (&centroids - &new_centroids).mapv(|x| x.abs()).sum();
408
409        centroids = new_centroids;
410
411        if centroid_change < tolerance {
412            break;
413        }
414    }
415
416    // Assign cluster IDs to minima
417    for (i, minimum) in minima.iter_mut().enumerate() {
418        minimum.cluster_id = Some(assignments[i]);
419    }
420
421    Ok(())
422}
423
424/// Density-based clustering (DBSCAN-like)
425#[allow(dead_code)]
426fn density_clustering<S>(
427    minima: &mut [LocalMinimum<S>],
428    options: &ClusteringOptions,
429) -> Result<(), OptimizeError>
430where
431    S: Clone,
432{
433    let n = minima.len();
434    if n <= 1 {
435        if n == 1 {
436            minima[0].cluster_id = Some(0);
437        }
438        return Ok(());
439    }
440
441    let eps = options.distance_threshold;
442    let min_pts = 2; // Minimum points to form a cluster
443
444    let distances = compute_distance_matrix(minima, options);
445    let mut cluster_assignments = vec![None; n];
446    let mut visited = vec![false; n];
447    let mut cluster_id = 0;
448
449    for i in 0..n {
450        if visited[i] {
451            continue;
452        }
453        visited[i] = true;
454
455        // Find neighbors
456        let neighbors: Vec<usize> = (0..n).filter(|&j| distances[[i, j]] <= eps).collect();
457
458        if neighbors.len() < min_pts {
459            // Mark as noise (will remain None)
460            continue;
461        }
462
463        // Start new cluster
464        let mut to_visit = neighbors.clone();
465        cluster_assignments[i] = Some(cluster_id);
466
467        let mut idx = 0;
468        while idx < to_visit.len() {
469            let point = to_visit[idx];
470
471            if !visited[point] {
472                visited[point] = true;
473
474                let point_neighbors: Vec<usize> =
475                    (0..n).filter(|&j| distances[[point, j]] <= eps).collect();
476
477                if point_neighbors.len() >= min_pts {
478                    // Add new neighbors to visit list
479                    for &neighbor in &point_neighbors {
480                        if !to_visit.contains(&neighbor) {
481                            to_visit.push(neighbor);
482                        }
483                    }
484                }
485            }
486
487            if cluster_assignments[point].is_none() {
488                cluster_assignments[point] = Some(cluster_id);
489            }
490
491            idx += 1;
492        }
493
494        cluster_id += 1;
495    }
496
497    // Assign cluster IDs to minima
498    for (i, minimum) in minima.iter_mut().enumerate() {
499        minimum.cluster_id = cluster_assignments[i];
500    }
501
502    Ok(())
503}
504
505/// Simple threshold-based clustering
506#[allow(dead_code)]
507fn threshold_clustering<S>(
508    minima: &mut [LocalMinimum<S>],
509    options: &ClusteringOptions,
510) -> Result<(), OptimizeError>
511where
512    S: Clone,
513{
514    let n = minima.len();
515    if n <= 1 {
516        if n == 1 {
517            minima[0].cluster_id = Some(0);
518        }
519        return Ok(());
520    }
521
522    let mut cluster_assignments = vec![None; n];
523    let mut cluster_id = 0;
524
525    for i in 0..n {
526        if cluster_assignments[i].is_some() {
527            continue;
528        }
529
530        // Start new cluster
531        cluster_assignments[i] = Some(cluster_id);
532
533        // Find all points within threshold
534        for j in (i + 1)..n {
535            if cluster_assignments[j].is_some() {
536                continue;
537            }
538
539            let distance = compute_distance(&minima[i], &minima[j], options);
540            if distance <= options.distance_threshold {
541                cluster_assignments[j] = Some(cluster_id);
542            }
543        }
544
545        cluster_id += 1;
546    }
547
548    // Assign cluster IDs to minima
549    for (i, minimum) in minima.iter_mut().enumerate() {
550        minimum.cluster_id = cluster_assignments[i];
551    }
552
553    Ok(())
554}
555
556/// Compute distance matrix between all minima
557#[allow(dead_code)]
558fn compute_distance_matrix<S>(
559    minima: &[LocalMinimum<S>],
560    options: &ClusteringOptions,
561) -> Array2<f64>
562where
563    S: Clone,
564{
565    let n = minima.len();
566    let mut distances = Array2::zeros((n, n));
567
568    for i in 0..n {
569        for j in (i + 1)..n {
570            let dist = compute_distance(&minima[i], &minima[j], options);
571            distances[[i, j]] = dist;
572            distances[[j, i]] = dist;
573        }
574    }
575
576    distances
577}
578
579/// Compute distance between two local minima
580#[allow(dead_code)]
581fn compute_distance<S>(
582    min1: &LocalMinimum<S>,
583    min2: &LocalMinimum<S>,
584    options: &ClusteringOptions,
585) -> f64
586where
587    S: Clone,
588{
589    // Coordinate distance
590    let coord_dist = (&min1.x - &min2.x).mapv(|x| x.powi(2)).sum().sqrt();
591
592    if !options.use_function_values {
593        return coord_dist;
594    }
595
596    // Function value distance
597    let func_dist = (min1.f - min2.f).abs();
598
599    // Combined distance
600    coord_dist + options.function_weight * func_dist
601}
602
603/// Compute distance between two clusters (single linkage)
604#[allow(dead_code)]
605fn compute_cluster_distance(
606    cluster1: &[usize],
607    cluster2: &[usize],
608    distances: &Array2<f64>,
609) -> f64 {
610    let mut min_dist = f64::INFINITY;
611
612    for &i in cluster1 {
613        for &j in cluster2 {
614            let dist = distances[[i, j]];
615            if dist < min_dist {
616                min_dist = dist;
617            }
618        }
619    }
620
621    min_dist
622}
623
624/// Extract feature vectors for clustering
625#[allow(dead_code)]
626fn extract_features<S>(minima: &[LocalMinimum<S>], options: &ClusteringOptions) -> Array2<f64>
627where
628    S: Clone,
629{
630    let n = minima.len();
631    if n == 0 {
632        return Array2::zeros((0, 0));
633    }
634
635    let coord_dim = minima[0].x.len();
636    let func_dim = if options.use_function_values { 1 } else { 0 };
637    let total_dim = coord_dim + func_dim;
638
639    let mut features = Array2::zeros((n, total_dim));
640
641    // Extract coordinates
642    for (i, minimum) in minima.iter().enumerate() {
643        for j in 0..coord_dim {
644            features[[i, j]] = minimum.x[j];
645        }
646    }
647
648    // Add function values if requested
649    if options.use_function_values {
650        for (i, minimum) in minima.iter().enumerate() {
651            features[[i, coord_dim]] = minimum.f * options.function_weight;
652        }
653    }
654
655    // Normalize if requested
656    if options.normalize_coordinates {
657        normalize_features(&mut features, coord_dim);
658    }
659
660    features
661}
662
663/// Normalize feature matrix
664#[allow(dead_code)]
665fn normalize_features(features: &mut Array2<f64>, coord_dim: usize) {
666    let n = features.nrows();
667    if n == 0 {
668        return;
669    }
670
671    // Normalize coordinates only
672    for j in 0..coord_dim {
673        let col = features.column(j);
674        let min_val = col.iter().fold(f64::INFINITY, |a, &b| f64::min(a, b));
675        let max_val = col.iter().fold(f64::NEG_INFINITY, |a, &b| f64::max(a, b));
676
677        if (max_val - min_val).abs() > 1e-10 {
678            for i in 0..n {
679                features[[i, j]] = (features[[i, j]] - min_val) / (max_val - min_val);
680            }
681        }
682    }
683}
684
685/// Initialize centroids using k-means++ algorithm
686#[allow(dead_code)]
687fn initialize_centroids_plus_plus(features: &Array2<f64>, k: usize) -> Array2<f64> {
688    let (n, dim) = features.dim();
689    let mut centroids = Array2::zeros((k, dim));
690
691    if n == 0 || k == 0 {
692        return centroids;
693    }
694
695    // Choose first centroid randomly
696    let first_idx = 0; // In practice, should be random
697    centroids.row_mut(0).assign(&features.row(first_idx));
698
699    // Choose remaining centroids
700    for c in 1..k {
701        let mut distances = vec![f64::INFINITY; n];
702
703        // Compute distance to nearest centroid for each point
704        for (i, distance) in distances.iter_mut().enumerate().take(n) {
705            let point = features.row(i);
706            for j in 0..c {
707                let centroid = centroids.row(j);
708                let dist = euclidean_distance(&point, &centroid);
709                *distance = distance.min(dist);
710            }
711        }
712
713        // Choose next centroid (should be weighted random, using max for simplicity)
714        let next_idx = distances
715            .iter()
716            .enumerate()
717            .max_by(|a, b| a.1.partial_cmp(b.1).expect("Operation failed"))
718            .map(|(i, _)| i)
719            .unwrap_or(0);
720
721        centroids.row_mut(c).assign(&features.row(next_idx));
722    }
723
724    centroids
725}
726
727/// Compute Euclidean distance between two points
728#[allow(dead_code)]
729fn euclidean_distance(a: &ArrayView1<f64>, b: &ArrayView1<f64>) -> f64 {
730    (a - b).mapv(|x| x.powi(2)).sum().sqrt()
731}
732
733/// Compute cluster centroids and statistics
734#[allow(dead_code)]
735fn compute_cluster_centroids<S>(
736    minima: &[LocalMinimum<S>],
737) -> Result<Vec<ClusterCentroid>, OptimizeError>
738where
739    S: Clone,
740{
741    if minima.is_empty() {
742        return Ok(Vec::new());
743    }
744
745    // Group minima by cluster
746    let mut clusters: HashMap<usize, Vec<&LocalMinimum<S>>> = HashMap::new();
747
748    for minimum in minima {
749        if let Some(cluster_id) = minimum.cluster_id {
750            clusters.entry(cluster_id).or_default().push(minimum);
751        }
752    }
753
754    let mut centroids = Vec::new();
755
756    for (_, cluster_minima) in clusters {
757        if cluster_minima.is_empty() {
758            continue;
759        }
760
761        let dim = cluster_minima[0].x.len();
762        let mut centroid_x = Array1::zeros(dim);
763        let mut f_sum = 0.0;
764        let mut f_min = f64::INFINITY;
765
766        // Compute centroid coordinates and function statistics
767        for minimum in &cluster_minima {
768            centroid_x = &centroid_x + &minimum.x;
769            f_sum += minimum.f;
770            f_min = f_min.min(minimum.f);
771        }
772
773        let size = cluster_minima.len();
774        centroid_x /= size as f64;
775        let f_avg = f_sum / size as f64;
776
777        // Compute cluster radius
778        let mut max_radius = 0.0;
779        for minimum in &cluster_minima {
780            let dist = (&minimum.x - &centroid_x).mapv(|x| x.powi(2)).sum().sqrt();
781            max_radius = f64::max(max_radius, dist);
782        }
783
784        centroids.push(ClusterCentroid {
785            x: centroid_x,
786            f_avg,
787            f_min,
788            size,
789            radius: max_radius,
790        });
791    }
792
793    Ok(centroids)
794}
795
796/// Compute within-cluster sum of squares
797#[allow(dead_code)]
798fn compute_wcss<S>(minima: &[LocalMinimum<S>], centroids: &[ClusterCentroid]) -> f64
799where
800    S: Clone,
801{
802    let mut wcss = 0.0;
803
804    for minimum in minima {
805        if let Some(cluster_id) = minimum.cluster_id {
806            if cluster_id < centroids.len() {
807                let centroid = &centroids[cluster_id];
808                let dist = (&minimum.x - &centroid.x).mapv(|x| x.powi(2)).sum();
809                wcss += dist;
810            }
811        }
812    }
813
814    wcss
815}
816
817/// Compute silhouette score for clustering quality
818#[allow(dead_code)]
819fn compute_silhouette_score<S>(minima: &[LocalMinimum<S>]) -> Option<f64>
820where
821    S: Clone,
822{
823    if minima.len() < 2 {
824        return None;
825    }
826
827    let mut silhouette_sum = 0.0;
828    let mut valid_points = 0;
829
830    for (i, minimum) in minima.iter().enumerate() {
831        if let Some(cluster_id) = minimum.cluster_id {
832            // Compute intra-cluster distance
833            let mut intra_sum = 0.0;
834            let mut intra_count = 0;
835
836            // Compute inter-cluster distances
837            let mut min_inter = f64::INFINITY;
838            let mut cluster_inter_sums: HashMap<usize, f64> = HashMap::new();
839            let mut cluster_inter_counts: HashMap<usize, usize> = HashMap::new();
840
841            for (j, other) in minima.iter().enumerate() {
842                if i == j {
843                    continue;
844                }
845
846                if let Some(other_cluster_id) = other.cluster_id {
847                    let dist = (&minimum.x - &other.x).mapv(|x| x.powi(2)).sum().sqrt();
848
849                    if other_cluster_id == cluster_id {
850                        intra_sum += dist;
851                        intra_count += 1;
852                    } else {
853                        *cluster_inter_sums.entry(other_cluster_id).or_insert(0.0) += dist;
854                        *cluster_inter_counts.entry(other_cluster_id).or_insert(0) += 1;
855                    }
856                }
857            }
858
859            // Find minimum inter-cluster distance
860            for (other_cluster_id, sum) in cluster_inter_sums {
861                let count = cluster_inter_counts[&other_cluster_id];
862                if count > 0 {
863                    let avg_inter = sum / count as f64;
864                    min_inter = min_inter.min(avg_inter);
865                }
866            }
867
868            if intra_count > 0 && min_inter < f64::INFINITY {
869                let a = intra_sum / intra_count as f64;
870                let b = min_inter;
871                let silhouette = (b - a) / f64::max(a, b);
872                silhouette_sum += silhouette;
873                valid_points += 1;
874            }
875        }
876    }
877
878    if valid_points > 0 {
879        Some(silhouette_sum / valid_points as f64)
880    } else {
881        None
882    }
883}
884
885/// Generate diverse starting points for multi-start optimization
886#[allow(dead_code)]
887pub fn generate_diverse_start_points(
888    bounds: &[(f64, f64)],
889    num_points: usize,
890    strategy: StartPointStrategy,
891) -> Vec<Array1<f64>> {
892    match strategy {
893        StartPointStrategy::Random => generate_random_points(bounds, num_points),
894        StartPointStrategy::LatinHypercube => generate_latin_hypercube_points(bounds, num_points),
895        StartPointStrategy::Grid => generate_grid_points(bounds, num_points),
896        StartPointStrategy::Sobol => generate_sobol_points(bounds, num_points),
897    }
898}
899
900/// Strategy for generating starting points
901#[derive(Debug, Clone, Copy)]
902pub enum StartPointStrategy {
903    Random,
904    LatinHypercube,
905    Grid,
906    Sobol,
907}
908
909/// Generate random starting points
910#[allow(dead_code)]
911fn generate_random_points(bounds: &[(f64, f64)], num_points: usize) -> Vec<Array1<f64>> {
912    let dim = bounds.len();
913    let mut points = Vec::new();
914
915    for _ in 0..num_points {
916        let mut point = Array1::zeros(dim);
917        for (i, &(low, high)) in bounds.iter().enumerate() {
918            // Simple pseudo-random (in practice, use proper RNG)
919            let t = (i * num_points + points.len()) as f64 / (num_points * dim) as f64;
920            let random_val = (t * 17.0).fract(); // Simple pseudo-random
921            point[i] = low + random_val * (high - low);
922        }
923        points.push(point);
924    }
925
926    points
927}
928
929/// Generate Latin Hypercube sampling points
930#[allow(dead_code)]
931fn generate_latin_hypercube_points(bounds: &[(f64, f64)], num_points: usize) -> Vec<Array1<f64>> {
932    let dim = bounds.len();
933    let mut points = Vec::new();
934
935    // Simple Latin Hypercube implementation
936    for i in 0..num_points {
937        let mut point = Array1::zeros(dim);
938        for (j, &(low, high)) in bounds.iter().enumerate() {
939            let segment = (i as f64 + 0.5) / num_points as f64; // Center of segment
940            point[j] = low + segment * (high - low);
941        }
942        points.push(point);
943    }
944
945    points
946}
947
948/// Generate grid points
949#[allow(dead_code)]
950fn generate_grid_points(bounds: &[(f64, f64)], num_points: usize) -> Vec<Array1<f64>> {
951    let dim = bounds.len();
952    if dim == 0 {
953        return Vec::new();
954    }
955
956    let points_per_dim = ((num_points as f64).powf(1.0 / dim as f64)).ceil() as usize;
957    let mut _points = Vec::new();
958
959    // Generate grid coordinates recursively
960    fn generate_grid_recursive(
961        bounds: &[(f64, f64)],
962        points_per_dim: usize,
963        current_point: &mut Array1<f64>,
964        dim_idx: usize,
965        points: &mut Vec<Array1<f64>>,
966    ) {
967        if dim_idx >= bounds.len() {
968            points.push(current_point.clone());
969            return;
970        }
971
972        let (low, high) = bounds[dim_idx];
973        for i in 0..points_per_dim {
974            let t = if points_per_dim == 1 {
975                0.5
976            } else {
977                i as f64 / (points_per_dim - 1) as f64
978            };
979            current_point[dim_idx] = low + t * (high - low);
980            generate_grid_recursive(bounds, points_per_dim, current_point, dim_idx + 1, points);
981        }
982    }
983
984    let mut current_point = Array1::zeros(dim);
985    generate_grid_recursive(bounds, points_per_dim, &mut current_point, 0, &mut _points);
986
987    // Truncate to requested number of _points
988    _points.truncate(num_points);
989    _points
990}
991
992/// Generate Sobol sequence points: validated direction numbers for the
993/// first [`crate::global::qmc::MAX_SOBOL_DIM`] dimensions, Halton fallback
994/// beyond that (see [`crate::global::qmc`]). A previous version of this
995/// function used a Van-der-Corput sequence with a shifted base per
996/// dimension -- a real low-discrepancy sequence, but not Sobol despite the
997/// name.
998#[allow(dead_code)]
999fn generate_sobol_points(bounds: &[(f64, f64)], num_points: usize) -> Vec<Array1<f64>> {
1000    let dim = bounds.len();
1001    let mut sobol_gen = SobolGenerator::new(dim);
1002    let mut points = Vec::with_capacity(num_points);
1003
1004    for _ in 0..num_points {
1005        let sobol_point = sobol_gen.next_point();
1006        let mut point = Array1::zeros(dim);
1007        for (j, &(low, high)) in bounds.iter().enumerate() {
1008            point[j] = low + sobol_point[j] * (high - low);
1009        }
1010        points.push(point);
1011    }
1012
1013    points
1014}
1015
1016#[cfg(test)]
1017mod tests {
1018    use super::*;
1019    use approx::assert_abs_diff_eq;
1020
1021    #[test]
1022    fn test_simple_clustering() {
1023        // Create some test minima
1024        let mut minima = vec![
1025            LocalMinimum {
1026                x: Array1::from_vec(vec![0.0, 0.0]),
1027                f: 1.0,
1028                fun_value: 1.0,
1029                nit: 10,
1030                func_evals: 20,
1031                success: true,
1032                start_point: Array1::from_vec(vec![1.0, 1.0]),
1033                cluster_id: None,
1034                cluster_distance: None,
1035            },
1036            LocalMinimum {
1037                x: Array1::from_vec(vec![0.1, 0.1]),
1038                f: 1.1,
1039                fun_value: 1.1,
1040                nit: 12,
1041                func_evals: 24,
1042                success: true,
1043                start_point: Array1::from_vec(vec![1.1, 1.1]),
1044                cluster_id: None,
1045                cluster_distance: None,
1046            },
1047            LocalMinimum {
1048                x: Array1::from_vec(vec![5.0, 5.0]),
1049                f: 2.0,
1050                fun_value: 2.0,
1051                nit: 15,
1052                func_evals: 30,
1053                success: true,
1054                start_point: Array1::from_vec(vec![5.5, 5.5]),
1055                cluster_id: None,
1056                cluster_distance: None,
1057            },
1058        ];
1059
1060        let options = ClusteringOptions {
1061            distance_threshold: 1.0,
1062            algorithm: ClusteringAlgorithm::Threshold,
1063            ..Default::default()
1064        };
1065
1066        threshold_clustering(&mut minima, &options).expect("Operation failed");
1067
1068        // First two should be in same cluster, third in different cluster
1069        assert_eq!(minima[0].cluster_id, minima[1].cluster_id);
1070        assert_ne!(minima[0].cluster_id, minima[2].cluster_id);
1071    }
1072
1073    #[test]
1074    fn test_distance_computation() {
1075        let min1 = LocalMinimum {
1076            x: Array1::from_vec(vec![0.0, 0.0]),
1077            f: 1.0,
1078            fun_value: 1.0,
1079            nit: 10,
1080            func_evals: 20,
1081            success: true,
1082            start_point: Array1::from_vec(vec![1.0, 1.0]),
1083            cluster_id: None,
1084            cluster_distance: None,
1085        };
1086
1087        let min2 = LocalMinimum {
1088            x: Array1::from_vec(vec![3.0, 4.0]),
1089            f: 2.0,
1090            fun_value: 2.0,
1091            nit: 12,
1092            func_evals: 24,
1093            success: true,
1094            start_point: Array1::from_vec(vec![3.5, 4.5]),
1095            cluster_id: None,
1096            cluster_distance: None,
1097        };
1098
1099        let options = ClusteringOptions::default();
1100        let distance = compute_distance(&min1, &min2, &options);
1101
1102        // Euclidean distance is 5.0, plus function weight * |1.0 - 2.0| = 5.0 + 0.1 * 1.0 = 5.1
1103        assert_abs_diff_eq!(distance, 5.1, epsilon = 1e-10);
1104    }
1105
1106    #[test]
1107    fn test_start_point_generation() {
1108        let bounds = vec![(0.0, 10.0), (-5.0, 5.0)];
1109        let num_points = 5;
1110
1111        let random_points =
1112            generate_diverse_start_points(&bounds, num_points, StartPointStrategy::Random);
1113        assert_eq!(random_points.len(), num_points);
1114
1115        for point in &random_points {
1116            assert!(point[0] >= 0.0 && point[0] <= 10.0);
1117            assert!(point[1] >= -5.0 && point[1] <= 5.0);
1118        }
1119
1120        let grid_points =
1121            generate_diverse_start_points(&bounds, num_points, StartPointStrategy::Grid);
1122        assert_eq!(grid_points.len(), num_points);
1123
1124        for point in &grid_points {
1125            assert!(point[0] >= 0.0 && point[0] <= 10.0);
1126            assert!(point[1] >= -5.0 && point[1] <= 5.0);
1127        }
1128    }
1129}