1use 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#[derive(Debug, Clone)]
15pub struct ClusteringOptions {
16 pub distance_threshold: f64,
18 pub function_tolerance: f64,
20 pub max_clusters: Option<usize>,
22 pub algorithm: ClusteringAlgorithm,
24 pub normalize_coordinates: bool,
26 pub use_function_values: bool,
28 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#[derive(Debug, Clone, Copy)]
48pub enum ClusteringAlgorithm {
49 Hierarchical,
51 KMeans,
53 Density,
55 Threshold,
57}
58
59#[derive(Debug, Clone)]
61pub struct LocalMinimum<S> {
62 pub x: Array1<f64>,
64 pub f: f64,
66 pub fun_value: S,
68 pub nit: usize,
70 pub func_evals: usize,
72 pub success: bool,
74 pub start_point: Array1<f64>,
76 pub cluster_id: Option<usize>,
78 pub cluster_distance: Option<f64>,
80}
81
82#[derive(Debug, Clone)]
84pub struct ClusteringResult<S> {
85 pub minima: Vec<LocalMinimum<S>>,
87 pub centroids: Vec<ClusterCentroid>,
89 pub num_clusters: usize,
91 pub silhouette_score: Option<f64>,
93 pub wcss: f64,
95 pub best_minimum: Option<LocalMinimum<S>>,
97}
98
99#[derive(Debug, Clone)]
101pub struct ClusterCentroid {
102 pub x: Array1<f64>,
104 pub f_avg: f64,
106 pub f_min: f64,
108 pub size: usize,
110 pub radius: f64,
112}
113
114#[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 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 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 cluster_minima(&mut minima, &clustering_opts)?;
169
170 let centroids = compute_cluster_centroids(&minima)?;
172 let num_clusters = centroids.len();
173 let wcss = compute_wcss(&minima, ¢roids);
174 let silhouette_score = compute_silhouette_score(&minima);
175
176 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#[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#[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 let distances = compute_distance_matrix(minima, options);
233
234 let mut cluster_assignments = vec![None; n];
236 let mut next_cluster_id = n;
237
238 for (i, assignment) in cluster_assignments.iter_mut().enumerate().take(n) {
240 *assignment = Some(i);
241 }
242
243 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 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 if min_dist > options.distance_threshold {
264 break;
265 }
266
267 if let Some(max_clusters) = options.max_clusters {
269 if active_clusters.len() <= max_clusters {
270 break;
271 }
272 }
273
274 let (i, j) = merge_pair;
276 let mut merged_cluster = active_clusters[i].clone();
277 merged_cluster.extend(&active_clusters[j]);
278
279 for &point_idx in &merged_cluster {
281 cluster_assignments[point_idx] = Some(next_cluster_id);
282 }
283
284 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 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 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#[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 let k = if let Some(max_k) = options.max_clusters {
336 std::cmp::min(max_k, n)
337 } else {
338 std::cmp::min((n as f64).sqrt() as usize + 1, n)
340 };
341
342 if k >= n {
343 for (i, minimum) in minima.iter_mut().enumerate() {
345 minimum.cluster_id = Some(i);
346 }
347 return Ok(());
348 }
349
350 let features = extract_features(minima, options);
352 let dim = features.ncols();
353
354 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 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), ¢roids.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 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 let centroid_change = (¢roids - &new_centroids).mapv(|x| x.abs()).sum();
408
409 centroids = new_centroids;
410
411 if centroid_change < tolerance {
412 break;
413 }
414 }
415
416 for (i, minimum) in minima.iter_mut().enumerate() {
418 minimum.cluster_id = Some(assignments[i]);
419 }
420
421 Ok(())
422}
423
424#[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; 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 let neighbors: Vec<usize> = (0..n).filter(|&j| distances[[i, j]] <= eps).collect();
457
458 if neighbors.len() < min_pts {
459 continue;
461 }
462
463 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 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 for (i, minimum) in minima.iter_mut().enumerate() {
499 minimum.cluster_id = cluster_assignments[i];
500 }
501
502 Ok(())
503}
504
505#[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 cluster_assignments[i] = Some(cluster_id);
532
533 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 for (i, minimum) in minima.iter_mut().enumerate() {
550 minimum.cluster_id = cluster_assignments[i];
551 }
552
553 Ok(())
554}
555
556#[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#[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 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 let func_dist = (min1.f - min2.f).abs();
598
599 coord_dist + options.function_weight * func_dist
601}
602
603#[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#[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 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 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 if options.normalize_coordinates {
657 normalize_features(&mut features, coord_dim);
658 }
659
660 features
661}
662
663#[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 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#[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 let first_idx = 0; centroids.row_mut(0).assign(&features.row(first_idx));
698
699 for c in 1..k {
701 let mut distances = vec![f64::INFINITY; n];
702
703 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, ¢roid);
709 *distance = distance.min(dist);
710 }
711 }
712
713 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#[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#[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 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 for minimum in &cluster_minima {
768 centroid_x = ¢roid_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 let mut max_radius = 0.0;
779 for minimum in &cluster_minima {
780 let dist = (&minimum.x - ¢roid_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#[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 = ¢roids[cluster_id];
808 let dist = (&minimum.x - ¢roid.x).mapv(|x| x.powi(2)).sum();
809 wcss += dist;
810 }
811 }
812 }
813
814 wcss
815}
816
817#[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 let mut intra_sum = 0.0;
834 let mut intra_count = 0;
835
836 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 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#[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#[derive(Debug, Clone, Copy)]
902pub enum StartPointStrategy {
903 Random,
904 LatinHypercube,
905 Grid,
906 Sobol,
907}
908
909#[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 let t = (i * num_points + points.len()) as f64 / (num_points * dim) as f64;
920 let random_val = (t * 17.0).fract(); point[i] = low + random_val * (high - low);
922 }
923 points.push(point);
924 }
925
926 points
927}
928
929#[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 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; point[j] = low + segment * (high - low);
941 }
942 points.push(point);
943 }
944
945 points
946}
947
948#[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 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 _points.truncate(num_points);
989 _points
990}
991
992#[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 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 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 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}