Skip to main content

sklears_simd/
clustering.rs

1//! SIMD-optimized clustering operations
2//!
3//! This module provides vectorized implementations of common clustering operations
4//! including k-means distance computations, centroid updates, and validity indices.
5
6#[cfg(feature = "no-std")]
7use alloc::{vec, vec::Vec};
8
9/// SIMD-optimized k-means distance computation
10/// Computes distances from each point to each centroid
11pub fn kmeans_distances(
12    points: &[&[f32]],          // Points (n_samples x n_features)
13    centroids: &[&[f32]],       // Centroids (n_clusters x n_features)
14    distances: &mut [Vec<f32>], // Output distances (n_samples x n_clusters)
15) {
16    let n_samples = points.len();
17    let _n_clusters = centroids.len();
18
19    assert!(!points.is_empty(), "Points cannot be empty");
20    assert!(!centroids.is_empty(), "Centroids cannot be empty");
21    assert_eq!(distances.len(), n_samples, "Distance array size mismatch");
22
23    let n_features = points[0].len();
24    for centroid in centroids {
25        assert_eq!(
26            centroid.len(),
27            n_features,
28            "All points must have the same number of features"
29        );
30    }
31
32    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
33    {
34        if crate::simd_feature_detected!("avx2") {
35            unsafe { kmeans_distances_avx2(points, centroids, distances) };
36            return;
37        } else if crate::simd_feature_detected!("sse2") {
38            unsafe { kmeans_distances_sse2(points, centroids, distances) };
39            return;
40        }
41    }
42
43    kmeans_distances_scalar(points, centroids, distances);
44}
45
46fn kmeans_distances_scalar(points: &[&[f32]], centroids: &[&[f32]], distances: &mut [Vec<f32>]) {
47    let n_samples = points.len();
48    let n_clusters = centroids.len();
49    let n_features = points[0].len();
50
51    for i in 0..n_samples {
52        if distances[i].len() != n_clusters {
53            distances[i] = vec![0.0; n_clusters];
54        }
55
56        for j in 0..n_clusters {
57            let mut sum = 0.0;
58            for k in 0..n_features {
59                let diff = points[i][k] - centroids[j][k];
60                sum += diff * diff;
61            }
62            distances[i][j] = sum.sqrt();
63        }
64    }
65}
66
67#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
68#[target_feature(enable = "sse2")]
69unsafe fn kmeans_distances_sse2(
70    points: &[&[f32]],
71    centroids: &[&[f32]],
72    distances: &mut [Vec<f32>],
73) {
74    use core::arch::x86_64::*;
75
76    let n_samples = points.len();
77    let n_clusters = centroids.len();
78    let n_features = points[0].len();
79
80    for i in 0..n_samples {
81        if distances[i].len() != n_clusters {
82            distances[i] = vec![0.0; n_clusters];
83        }
84
85        for j in 0..n_clusters {
86            let mut sum = _mm_setzero_ps();
87            let mut k = 0;
88
89            while k + 4 <= n_features {
90                let p_vec = _mm_loadu_ps(&points[i][k]);
91                let c_vec = _mm_loadu_ps(&centroids[j][k]);
92                let diff = _mm_sub_ps(p_vec, c_vec);
93                let squared = _mm_mul_ps(diff, diff);
94                sum = _mm_add_ps(sum, squared);
95                k += 4;
96            }
97
98            let mut result = [0.0f32; 4];
99            _mm_storeu_ps(result.as_mut_ptr(), sum);
100            let mut scalar_sum = result[0] + result[1] + result[2] + result[3];
101
102            while k < n_features {
103                let diff = points[i][k] - centroids[j][k];
104                scalar_sum += diff * diff;
105                k += 1;
106            }
107
108            distances[i][j] = scalar_sum.sqrt();
109        }
110    }
111}
112
113#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
114#[target_feature(enable = "avx2")]
115unsafe fn kmeans_distances_avx2(
116    points: &[&[f32]],
117    centroids: &[&[f32]],
118    distances: &mut [Vec<f32>],
119) {
120    use core::arch::x86_64::*;
121
122    let n_samples = points.len();
123    let n_clusters = centroids.len();
124    let n_features = points[0].len();
125
126    for i in 0..n_samples {
127        if distances[i].len() != n_clusters {
128            distances[i] = vec![0.0; n_clusters];
129        }
130
131        for j in 0..n_clusters {
132            let mut sum = _mm256_setzero_ps();
133            let mut k = 0;
134
135            while k + 8 <= n_features {
136                let p_vec = _mm256_loadu_ps(&points[i][k]);
137                let c_vec = _mm256_loadu_ps(&centroids[j][k]);
138                let diff = _mm256_sub_ps(p_vec, c_vec);
139                let squared = _mm256_mul_ps(diff, diff);
140                sum = _mm256_add_ps(sum, squared);
141                k += 8;
142            }
143
144            let mut result = [0.0f32; 8];
145            _mm256_storeu_ps(result.as_mut_ptr(), sum);
146            let mut scalar_sum = result.iter().sum::<f32>();
147
148            while k < n_features {
149                let diff = points[i][k] - centroids[j][k];
150                scalar_sum += diff * diff;
151                k += 1;
152            }
153
154            distances[i][j] = scalar_sum.sqrt();
155        }
156    }
157}
158
159/// SIMD-optimized centroid update for k-means
160/// Updates centroids based on assigned points
161pub fn update_centroids(
162    points: &[&[f32]],          // Points (n_samples x n_features)
163    assignments: &[usize],      // Cluster assignments (n_samples)
164    n_clusters: usize,          // Number of clusters
165    centroids: &mut [Vec<f32>], // Output centroids (n_clusters x n_features)
166) {
167    let n_samples = points.len();
168    let n_features = if n_samples > 0 { points[0].len() } else { 0 };
169
170    assert_eq!(
171        assignments.len(),
172        n_samples,
173        "Assignments length must match number of samples"
174    );
175    assert_eq!(
176        centroids.len(),
177        n_clusters,
178        "Centroids length must match number of clusters"
179    );
180
181    // Initialize centroids and counts
182    for centroid in centroids.iter_mut().take(n_clusters) {
183        if centroid.len() != n_features {
184            *centroid = vec![0.0; n_features];
185        } else {
186            centroid.fill(0.0);
187        }
188    }
189
190    let mut counts = vec![0usize; n_clusters];
191
192    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
193    {
194        if crate::simd_feature_detected!("avx2") {
195            unsafe {
196                update_centroids_avx2(points, assignments, n_clusters, centroids, &mut counts)
197            };
198            return;
199        } else if crate::simd_feature_detected!("sse2") {
200            unsafe {
201                update_centroids_sse2(points, assignments, n_clusters, centroids, &mut counts)
202            };
203            return;
204        }
205    }
206
207    update_centroids_scalar(points, assignments, n_clusters, centroids, &mut counts);
208}
209
210fn update_centroids_scalar(
211    points: &[&[f32]],
212    assignments: &[usize],
213    n_clusters: usize,
214    centroids: &mut [Vec<f32>],
215    counts: &mut [usize],
216) {
217    let n_samples = points.len();
218    let n_features = if n_samples > 0 { points[0].len() } else { 0 };
219
220    // Accumulate points
221    for i in 0..n_samples {
222        let cluster_id = assignments[i];
223        if cluster_id < n_clusters {
224            counts[cluster_id] += 1;
225            for j in 0..n_features {
226                centroids[cluster_id][j] += points[i][j];
227            }
228        }
229    }
230
231    // Compute averages
232    for (centroid, &count) in centroids.iter_mut().zip(counts.iter()).take(n_clusters) {
233        if count > 0 {
234            for v in centroid.iter_mut() {
235                *v /= count as f32;
236            }
237        }
238    }
239}
240
241#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
242#[target_feature(enable = "sse2")]
243unsafe fn update_centroids_sse2(
244    points: &[&[f32]],
245    assignments: &[usize],
246    n_clusters: usize,
247    centroids: &mut [Vec<f32>],
248    counts: &mut [usize],
249) {
250    use core::arch::x86_64::*;
251
252    let n_samples = points.len();
253    let n_features = if n_samples > 0 { points[0].len() } else { 0 };
254
255    // Accumulate points
256    for i in 0..n_samples {
257        let cluster_id = assignments[i];
258        if cluster_id < n_clusters {
259            counts[cluster_id] += 1;
260
261            let mut j = 0;
262            while j + 4 <= n_features {
263                let centroid_vec = _mm_loadu_ps(&centroids[cluster_id][j]);
264                let point_vec = _mm_loadu_ps(&points[i][j]);
265                let result = _mm_add_ps(centroid_vec, point_vec);
266                _mm_storeu_ps(&mut centroids[cluster_id][j], result);
267                j += 4;
268            }
269
270            while j < n_features {
271                centroids[cluster_id][j] += points[i][j];
272                j += 1;
273            }
274        }
275    }
276
277    // Compute averages
278    for i in 0..n_clusters {
279        if counts[i] > 0 {
280            let count_vec = _mm_set1_ps(counts[i] as f32);
281            let mut j = 0;
282
283            while j + 4 <= n_features {
284                let centroid_vec = _mm_loadu_ps(&centroids[i][j]);
285                let result = _mm_div_ps(centroid_vec, count_vec);
286                _mm_storeu_ps(&mut centroids[i][j], result);
287                j += 4;
288            }
289
290            while j < n_features {
291                centroids[i][j] /= counts[i] as f32;
292                j += 1;
293            }
294        }
295    }
296}
297
298#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
299#[target_feature(enable = "avx2")]
300unsafe fn update_centroids_avx2(
301    points: &[&[f32]],
302    assignments: &[usize],
303    n_clusters: usize,
304    centroids: &mut [Vec<f32>],
305    counts: &mut [usize],
306) {
307    use core::arch::x86_64::*;
308
309    let n_samples = points.len();
310    let n_features = if n_samples > 0 { points[0].len() } else { 0 };
311
312    // Accumulate points
313    for i in 0..n_samples {
314        let cluster_id = assignments[i];
315        if cluster_id < n_clusters {
316            counts[cluster_id] += 1;
317
318            let mut j = 0;
319            while j + 8 <= n_features {
320                let centroid_vec = _mm256_loadu_ps(&centroids[cluster_id][j]);
321                let point_vec = _mm256_loadu_ps(&points[i][j]);
322                let result = _mm256_add_ps(centroid_vec, point_vec);
323                _mm256_storeu_ps(&mut centroids[cluster_id][j], result);
324                j += 8;
325            }
326
327            while j < n_features {
328                centroids[cluster_id][j] += points[i][j];
329                j += 1;
330            }
331        }
332    }
333
334    // Compute averages
335    for i in 0..n_clusters {
336        if counts[i] > 0 {
337            let count_vec = _mm256_set1_ps(counts[i] as f32);
338            let mut j = 0;
339
340            while j + 8 <= n_features {
341                let centroid_vec = _mm256_loadu_ps(&centroids[i][j]);
342                let result = _mm256_div_ps(centroid_vec, count_vec);
343                _mm256_storeu_ps(&mut centroids[i][j], result);
344                j += 8;
345            }
346
347            while j < n_features {
348                centroids[i][j] /= counts[i] as f32;
349                j += 1;
350            }
351        }
352    }
353}
354
355/// SIMD-optimized within-cluster sum of squares (WCSS)
356/// Computes the sum of squared distances from points to their assigned centroids
357pub fn wcss(
358    points: &[&[f32]],     // Points (n_samples x n_features)
359    centroids: &[&[f32]],  // Centroids (n_clusters x n_features)
360    assignments: &[usize], // Cluster assignments (n_samples)
361) -> f32 {
362    let n_samples = points.len();
363    assert_eq!(
364        assignments.len(),
365        n_samples,
366        "Assignments length must match number of samples"
367    );
368
369    if n_samples == 0 {
370        return 0.0;
371    }
372
373    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
374    {
375        if crate::simd_feature_detected!("avx2") {
376            return unsafe { wcss_avx2(points, centroids, assignments) };
377        } else if crate::simd_feature_detected!("sse2") {
378            return unsafe { wcss_sse2(points, centroids, assignments) };
379        }
380    }
381
382    wcss_scalar(points, centroids, assignments)
383}
384
385fn wcss_scalar(points: &[&[f32]], centroids: &[&[f32]], assignments: &[usize]) -> f32 {
386    let n_samples = points.len();
387    let n_features = points[0].len();
388    let mut total_wcss = 0.0;
389
390    for i in 0..n_samples {
391        let cluster_id = assignments[i];
392        if cluster_id < centroids.len() {
393            let mut sum_squared = 0.0;
394            for j in 0..n_features {
395                let diff = points[i][j] - centroids[cluster_id][j];
396                sum_squared += diff * diff;
397            }
398            total_wcss += sum_squared;
399        }
400    }
401
402    total_wcss
403}
404
405#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
406#[target_feature(enable = "sse2")]
407unsafe fn wcss_sse2(points: &[&[f32]], centroids: &[&[f32]], assignments: &[usize]) -> f32 {
408    use core::arch::x86_64::*;
409
410    let n_samples = points.len();
411    let n_features = points[0].len();
412    let mut total_wcss = 0.0;
413
414    for i in 0..n_samples {
415        let cluster_id = assignments[i];
416        if cluster_id < centroids.len() {
417            let mut sum = _mm_setzero_ps();
418            let mut j = 0;
419
420            while j + 4 <= n_features {
421                let p_vec = _mm_loadu_ps(&points[i][j]);
422                let c_vec = _mm_loadu_ps(&centroids[cluster_id][j]);
423                let diff = _mm_sub_ps(p_vec, c_vec);
424                let squared = _mm_mul_ps(diff, diff);
425                sum = _mm_add_ps(sum, squared);
426                j += 4;
427            }
428
429            let mut result = [0.0f32; 4];
430            _mm_storeu_ps(result.as_mut_ptr(), sum);
431            let mut sum_squared = result[0] + result[1] + result[2] + result[3];
432
433            while j < n_features {
434                let diff = points[i][j] - centroids[cluster_id][j];
435                sum_squared += diff * diff;
436                j += 1;
437            }
438
439            total_wcss += sum_squared;
440        }
441    }
442
443    total_wcss
444}
445
446#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
447#[target_feature(enable = "avx2")]
448unsafe fn wcss_avx2(points: &[&[f32]], centroids: &[&[f32]], assignments: &[usize]) -> f32 {
449    use core::arch::x86_64::*;
450
451    let n_samples = points.len();
452    let n_features = points[0].len();
453    let mut total_wcss = 0.0;
454
455    for i in 0..n_samples {
456        let cluster_id = assignments[i];
457        if cluster_id < centroids.len() {
458            let mut sum = _mm256_setzero_ps();
459            let mut j = 0;
460
461            while j + 8 <= n_features {
462                let p_vec = _mm256_loadu_ps(&points[i][j]);
463                let c_vec = _mm256_loadu_ps(&centroids[cluster_id][j]);
464                let diff = _mm256_sub_ps(p_vec, c_vec);
465                let squared = _mm256_mul_ps(diff, diff);
466                sum = _mm256_add_ps(sum, squared);
467                j += 8;
468            }
469
470            let mut result = [0.0f32; 8];
471            _mm256_storeu_ps(result.as_mut_ptr(), sum);
472            let mut sum_squared = result.iter().sum::<f32>();
473
474            while j < n_features {
475                let diff = points[i][j] - centroids[cluster_id][j];
476                sum_squared += diff * diff;
477                j += 1;
478            }
479
480            total_wcss += sum_squared;
481        }
482    }
483
484    total_wcss
485}
486
487/// SIMD-optimized silhouette coefficient computation
488/// Computes silhouette scores for clustering quality assessment
489pub fn silhouette_score(
490    points: &[&[f32]],     // Points (n_samples x n_features)
491    assignments: &[usize], // Cluster assignments (n_samples)
492    n_clusters: usize,     // Number of clusters
493) -> f32 {
494    let n_samples = points.len();
495
496    if n_samples <= 1 || n_clusters <= 1 {
497        return 0.0;
498    }
499
500    let mut silhouette_scores = vec![0.0; n_samples];
501
502    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
503    {
504        if crate::simd_feature_detected!("avx2") {
505            unsafe {
506                silhouette_score_avx2(points, assignments, n_clusters, &mut silhouette_scores)
507            };
508        } else if crate::simd_feature_detected!("sse2") {
509            unsafe {
510                silhouette_score_sse2(points, assignments, n_clusters, &mut silhouette_scores)
511            };
512        } else {
513            silhouette_score_scalar(points, assignments, n_clusters, &mut silhouette_scores);
514        }
515    }
516    #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
517    {
518        silhouette_score_scalar(points, assignments, n_clusters, &mut silhouette_scores);
519    }
520
521    // Return average silhouette score
522    silhouette_scores.iter().sum::<f32>() / n_samples as f32
523}
524
525fn silhouette_score_scalar(
526    points: &[&[f32]],
527    assignments: &[usize],
528    n_clusters: usize,
529    silhouette_scores: &mut [f32],
530) {
531    let n_samples = points.len();
532    let _n_features = points[0].len();
533
534    for i in 0..n_samples {
535        let cluster_i = assignments[i];
536
537        // Compute a(i): average distance to points in the same cluster
538        let mut intra_distance = 0.0;
539        let mut intra_count = 0;
540
541        for j in 0..n_samples {
542            if i != j && assignments[j] == cluster_i {
543                let dist: f32 = points[i]
544                    .iter()
545                    .zip(points[j].iter())
546                    .map(|(a, b)| (a - b).powi(2))
547                    .sum::<f32>();
548                intra_distance += dist.sqrt();
549                intra_count += 1;
550            }
551        }
552
553        let a_i = if intra_count > 0 {
554            intra_distance / intra_count as f32
555        } else {
556            0.0
557        };
558
559        // Compute b(i): minimum average distance to points in other clusters
560        let mut min_inter_distance = f32::INFINITY;
561
562        for c in 0..n_clusters {
563            if c != cluster_i {
564                let mut inter_distance = 0.0;
565                let mut inter_count = 0;
566
567                for j in 0..n_samples {
568                    if assignments[j] == c {
569                        let dist: f32 = points[i]
570                            .iter()
571                            .zip(points[j].iter())
572                            .map(|(a, b)| (a - b).powi(2))
573                            .sum::<f32>();
574                        inter_distance += dist.sqrt();
575                        inter_count += 1;
576                    }
577                }
578
579                if inter_count > 0 {
580                    let avg_inter = inter_distance / inter_count as f32;
581                    min_inter_distance = min_inter_distance.min(avg_inter);
582                }
583            }
584        }
585
586        let b_i = min_inter_distance;
587
588        // Compute silhouette coefficient
589        silhouette_scores[i] = if a_i < b_i {
590            1.0 - a_i / b_i
591        } else if a_i > b_i {
592            b_i / a_i - 1.0
593        } else {
594            0.0
595        };
596    }
597}
598
599#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
600#[target_feature(enable = "sse2")]
601unsafe fn silhouette_score_sse2(
602    points: &[&[f32]],
603    assignments: &[usize],
604    n_clusters: usize,
605    silhouette_scores: &mut [f32],
606) {
607    use core::arch::x86_64::*;
608
609    let n_samples = points.len();
610    let n_features = points[0].len();
611
612    for i in 0..n_samples {
613        let cluster_i = assignments[i];
614
615        // Compute a(i): average distance to points in the same cluster
616        let mut intra_distance = 0.0;
617        let mut intra_count = 0;
618
619        for j in 0..n_samples {
620            if i != j && assignments[j] == cluster_i {
621                let mut sum = _mm_setzero_ps();
622                let mut k = 0;
623
624                while k + 4 <= n_features {
625                    let p1_vec = _mm_loadu_ps(&points[i][k]);
626                    let p2_vec = _mm_loadu_ps(&points[j][k]);
627                    let diff = _mm_sub_ps(p1_vec, p2_vec);
628                    let squared = _mm_mul_ps(diff, diff);
629                    sum = _mm_add_ps(sum, squared);
630                    k += 4;
631                }
632
633                let mut result = [0.0f32; 4];
634                _mm_storeu_ps(result.as_mut_ptr(), sum);
635                let mut dist_squared = result[0] + result[1] + result[2] + result[3];
636
637                while k < n_features {
638                    let diff = points[i][k] - points[j][k];
639                    dist_squared += diff * diff;
640                    k += 1;
641                }
642
643                intra_distance += dist_squared.sqrt();
644                intra_count += 1;
645            }
646        }
647
648        let a_i = if intra_count > 0 {
649            intra_distance / intra_count as f32
650        } else {
651            0.0
652        };
653
654        // Compute b(i): minimum average distance to points in other clusters
655        let mut min_inter_distance = f32::INFINITY;
656
657        for c in 0..n_clusters {
658            if c != cluster_i {
659                let mut inter_distance = 0.0;
660                let mut inter_count = 0;
661
662                for j in 0..n_samples {
663                    if assignments[j] == c {
664                        let mut sum = _mm_setzero_ps();
665                        let mut k = 0;
666
667                        while k + 4 <= n_features {
668                            let p1_vec = _mm_loadu_ps(&points[i][k]);
669                            let p2_vec = _mm_loadu_ps(&points[j][k]);
670                            let diff = _mm_sub_ps(p1_vec, p2_vec);
671                            let squared = _mm_mul_ps(diff, diff);
672                            sum = _mm_add_ps(sum, squared);
673                            k += 4;
674                        }
675
676                        let mut result = [0.0f32; 4];
677                        _mm_storeu_ps(result.as_mut_ptr(), sum);
678                        let mut dist_squared = result[0] + result[1] + result[2] + result[3];
679
680                        while k < n_features {
681                            let diff = points[i][k] - points[j][k];
682                            dist_squared += diff * diff;
683                            k += 1;
684                        }
685
686                        inter_distance += dist_squared.sqrt();
687                        inter_count += 1;
688                    }
689                }
690
691                if inter_count > 0 {
692                    let avg_inter = inter_distance / inter_count as f32;
693                    min_inter_distance = min_inter_distance.min(avg_inter);
694                }
695            }
696        }
697
698        let b_i = min_inter_distance;
699
700        // Compute silhouette coefficient
701        silhouette_scores[i] = if a_i < b_i {
702            1.0 - a_i / b_i
703        } else if a_i > b_i {
704            b_i / a_i - 1.0
705        } else {
706            0.0
707        };
708    }
709}
710
711#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
712#[target_feature(enable = "avx2")]
713unsafe fn silhouette_score_avx2(
714    points: &[&[f32]],
715    assignments: &[usize],
716    n_clusters: usize,
717    silhouette_scores: &mut [f32],
718) {
719    use core::arch::x86_64::*;
720
721    let n_samples = points.len();
722    let n_features = points[0].len();
723
724    for i in 0..n_samples {
725        let cluster_i = assignments[i];
726
727        // Compute a(i): average distance to points in the same cluster
728        let mut intra_distance = 0.0;
729        let mut intra_count = 0;
730
731        for j in 0..n_samples {
732            if i != j && assignments[j] == cluster_i {
733                let mut sum = _mm256_setzero_ps();
734                let mut k = 0;
735
736                while k + 8 <= n_features {
737                    let p1_vec = _mm256_loadu_ps(&points[i][k]);
738                    let p2_vec = _mm256_loadu_ps(&points[j][k]);
739                    let diff = _mm256_sub_ps(p1_vec, p2_vec);
740                    let squared = _mm256_mul_ps(diff, diff);
741                    sum = _mm256_add_ps(sum, squared);
742                    k += 8;
743                }
744
745                let mut result = [0.0f32; 8];
746                _mm256_storeu_ps(result.as_mut_ptr(), sum);
747                let mut dist_squared = result.iter().sum::<f32>();
748
749                while k < n_features {
750                    let diff = points[i][k] - points[j][k];
751                    dist_squared += diff * diff;
752                    k += 1;
753                }
754
755                intra_distance += dist_squared.sqrt();
756                intra_count += 1;
757            }
758        }
759
760        let a_i = if intra_count > 0 {
761            intra_distance / intra_count as f32
762        } else {
763            0.0
764        };
765
766        // Compute b(i): minimum average distance to points in other clusters
767        let mut min_inter_distance = f32::INFINITY;
768
769        for c in 0..n_clusters {
770            if c != cluster_i {
771                let mut inter_distance = 0.0;
772                let mut inter_count = 0;
773
774                for j in 0..n_samples {
775                    if assignments[j] == c {
776                        let mut sum = _mm256_setzero_ps();
777                        let mut k = 0;
778
779                        while k + 8 <= n_features {
780                            let p1_vec = _mm256_loadu_ps(&points[i][k]);
781                            let p2_vec = _mm256_loadu_ps(&points[j][k]);
782                            let diff = _mm256_sub_ps(p1_vec, p2_vec);
783                            let squared = _mm256_mul_ps(diff, diff);
784                            sum = _mm256_add_ps(sum, squared);
785                            k += 8;
786                        }
787
788                        let mut result = [0.0f32; 8];
789                        _mm256_storeu_ps(result.as_mut_ptr(), sum);
790                        let mut dist_squared = result.iter().sum::<f32>();
791
792                        while k < n_features {
793                            let diff = points[i][k] - points[j][k];
794                            dist_squared += diff * diff;
795                            k += 1;
796                        }
797
798                        inter_distance += dist_squared.sqrt();
799                        inter_count += 1;
800                    }
801                }
802
803                if inter_count > 0 {
804                    let avg_inter = inter_distance / inter_count as f32;
805                    min_inter_distance = min_inter_distance.min(avg_inter);
806                }
807            }
808        }
809
810        let b_i = min_inter_distance;
811
812        // Compute silhouette coefficient
813        silhouette_scores[i] = if a_i < b_i {
814            1.0 - a_i / b_i
815        } else if a_i > b_i {
816            b_i / a_i - 1.0
817        } else {
818            0.0
819        };
820    }
821}
822
823/// SIMD-optimized DBSCAN neighbor finding
824/// Finds all points within epsilon distance of each point
825pub fn dbscan_neighbors(
826    points: &[&[f32]],            // Points (n_samples x n_features)
827    eps: f32,                     // Epsilon distance threshold
828    neighbors: &mut [Vec<usize>], // Output neighbors for each point
829) {
830    let n_samples = points.len();
831    let eps_squared = eps * eps;
832
833    assert!(!points.is_empty(), "Points cannot be empty");
834    assert_eq!(
835        neighbors.len(),
836        n_samples,
837        "Neighbors array size must match points"
838    );
839
840    let n_features = points[0].len();
841    for point in points {
842        assert_eq!(
843            point.len(),
844            n_features,
845            "All points must have the same number of features"
846        );
847    }
848
849    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
850    {
851        if crate::simd_feature_detected!("avx2") {
852            unsafe { dbscan_neighbors_avx2(points, eps_squared, neighbors) };
853            return;
854        } else if crate::simd_feature_detected!("sse2") {
855            unsafe { dbscan_neighbors_sse2(points, eps_squared, neighbors) };
856            return;
857        }
858    }
859
860    dbscan_neighbors_scalar(points, eps_squared, neighbors);
861}
862
863fn dbscan_neighbors_scalar(points: &[&[f32]], eps_squared: f32, neighbors: &mut [Vec<usize>]) {
864    let n_samples = points.len();
865    let _n_features = points[0].len();
866
867    for i in 0..n_samples {
868        neighbors[i].clear();
869
870        for j in 0..n_samples {
871            if i != j {
872                let dist_squared: f32 = points[i]
873                    .iter()
874                    .zip(points[j].iter())
875                    .map(|(a, b)| (a - b).powi(2))
876                    .sum();
877
878                if dist_squared <= eps_squared {
879                    neighbors[i].push(j);
880                }
881            }
882        }
883    }
884}
885
886#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
887#[target_feature(enable = "sse2")]
888unsafe fn dbscan_neighbors_sse2(points: &[&[f32]], eps_squared: f32, neighbors: &mut [Vec<usize>]) {
889    use core::arch::x86_64::*;
890
891    let n_samples = points.len();
892    let n_features = points[0].len();
893
894    for i in 0..n_samples {
895        neighbors[i].clear();
896
897        for j in 0..n_samples {
898            if i != j {
899                let mut sum = _mm_setzero_ps();
900                let mut k = 0;
901
902                while k + 4 <= n_features {
903                    let p1_vec = _mm_loadu_ps(&points[i][k]);
904                    let p2_vec = _mm_loadu_ps(&points[j][k]);
905                    let diff = _mm_sub_ps(p1_vec, p2_vec);
906                    let squared = _mm_mul_ps(diff, diff);
907                    sum = _mm_add_ps(sum, squared);
908                    k += 4;
909                }
910
911                let mut result = [0.0f32; 4];
912                _mm_storeu_ps(result.as_mut_ptr(), sum);
913                let mut dist_squared = result[0] + result[1] + result[2] + result[3];
914
915                while k < n_features {
916                    let diff = points[i][k] - points[j][k];
917                    dist_squared += diff * diff;
918                    k += 1;
919                }
920
921                if dist_squared <= eps_squared {
922                    neighbors[i].push(j);
923                }
924            }
925        }
926    }
927}
928
929#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
930#[target_feature(enable = "avx2")]
931unsafe fn dbscan_neighbors_avx2(points: &[&[f32]], eps_squared: f32, neighbors: &mut [Vec<usize>]) {
932    use core::arch::x86_64::*;
933
934    let n_samples = points.len();
935    let n_features = points[0].len();
936
937    for i in 0..n_samples {
938        neighbors[i].clear();
939
940        for j in 0..n_samples {
941            if i != j {
942                let mut sum = _mm256_setzero_ps();
943                let mut k = 0;
944
945                while k + 8 <= n_features {
946                    let p1_vec = _mm256_loadu_ps(&points[i][k]);
947                    let p2_vec = _mm256_loadu_ps(&points[j][k]);
948                    let diff = _mm256_sub_ps(p1_vec, p2_vec);
949                    let squared = _mm256_mul_ps(diff, diff);
950                    sum = _mm256_add_ps(sum, squared);
951                    k += 8;
952                }
953
954                let mut result = [0.0f32; 8];
955                _mm256_storeu_ps(result.as_mut_ptr(), sum);
956                let mut dist_squared = result.iter().sum::<f32>();
957
958                while k < n_features {
959                    let diff = points[i][k] - points[j][k];
960                    dist_squared += diff * diff;
961                    k += 1;
962                }
963
964                if dist_squared <= eps_squared {
965                    neighbors[i].push(j);
966                }
967            }
968        }
969    }
970}
971
972/// SIMD-optimized core point identification for DBSCAN
973/// Identifies points that have at least min_samples neighbors
974pub fn dbscan_core_points(
975    neighbors: &[Vec<usize>], // Neighbors for each point
976    min_samples: usize,       // Minimum samples for core point
977    core_points: &mut [bool], // Output core point flags
978) {
979    let n_samples = neighbors.len();
980
981    assert_eq!(
982        core_points.len(),
983        n_samples,
984        "Core points array size must match points"
985    );
986
987    for i in 0..n_samples {
988        // Include the point itself in the count (neighbors + 1)
989        core_points[i] = neighbors[i].len() + 1 >= min_samples;
990    }
991}
992
993/// SIMD-optimized hierarchical clustering distance computation
994/// Computes linkage distances for hierarchical clustering
995pub fn hierarchical_linkage_distances(
996    points: &[&[f32]],    // Points (n_samples x n_features)
997    cluster1: &[usize],   // Indices of points in cluster 1
998    cluster2: &[usize],   // Indices of points in cluster 2
999    linkage: LinkageType, // Type of linkage to compute
1000) -> f32 {
1001    assert!(!cluster1.is_empty(), "Cluster 1 cannot be empty");
1002    assert!(!cluster2.is_empty(), "Cluster 2 cannot be empty");
1003    assert!(!points.is_empty(), "Points cannot be empty");
1004
1005    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
1006    {
1007        if crate::simd_feature_detected!("avx2") {
1008            return unsafe {
1009                hierarchical_linkage_distances_avx2(points, cluster1, cluster2, linkage)
1010            };
1011        } else if crate::simd_feature_detected!("sse2") {
1012            return unsafe {
1013                hierarchical_linkage_distances_sse2(points, cluster1, cluster2, linkage)
1014            };
1015        }
1016    }
1017
1018    hierarchical_linkage_distances_scalar(points, cluster1, cluster2, linkage)
1019}
1020
1021/// Types of linkage for hierarchical clustering
1022#[derive(Debug, Clone, Copy)]
1023pub enum LinkageType {
1024    Single,   // Minimum distance between any two points
1025    Complete, // Maximum distance between any two points
1026    Average,  // Average distance between all pairs
1027    Ward,     // Ward's minimum variance method
1028}
1029
1030fn hierarchical_linkage_distances_scalar(
1031    points: &[&[f32]],
1032    cluster1: &[usize],
1033    cluster2: &[usize],
1034    linkage: LinkageType,
1035) -> f32 {
1036    let n_features = points[0].len();
1037    let mut distances: Vec<f32> = Vec::new();
1038
1039    // Compute all pairwise distances between clusters
1040    for &i in cluster1 {
1041        for &j in cluster2 {
1042            let dist_squared: f32 = points[i]
1043                .iter()
1044                .zip(points[j].iter())
1045                .map(|(a, b)| (a - b).powi(2))
1046                .sum();
1047            distances.push(dist_squared.sqrt());
1048        }
1049    }
1050
1051    match linkage {
1052        LinkageType::Single => distances.iter().fold(f32::INFINITY, |acc, &x| acc.min(x)),
1053        LinkageType::Complete => distances.iter().fold(0.0, |acc, &x| acc.max(x)),
1054        LinkageType::Average => distances.iter().sum::<f32>() / distances.len() as f32,
1055        LinkageType::Ward => {
1056            // Ward linkage: compute centroids and distance between them
1057            let mut centroid1 = vec![0.0; n_features];
1058            let mut centroid2 = vec![0.0; n_features];
1059
1060            // Compute centroids
1061            for &i in cluster1 {
1062                for (c, &p) in centroid1.iter_mut().zip(points[i].iter()) {
1063                    *c += p;
1064                }
1065            }
1066            for c in centroid1.iter_mut() {
1067                *c /= cluster1.len() as f32;
1068            }
1069
1070            for &i in cluster2 {
1071                for (c, &p) in centroid2.iter_mut().zip(points[i].iter()) {
1072                    *c += p;
1073                }
1074            }
1075            for c in centroid2.iter_mut() {
1076                *c /= cluster2.len() as f32;
1077            }
1078
1079            // Compute distance between centroids
1080            let dist_squared: f32 = centroid1
1081                .iter()
1082                .zip(centroid2.iter())
1083                .map(|(a, b)| (a - b).powi(2))
1084                .sum();
1085
1086            // Ward distance includes cluster sizes
1087            let n1 = cluster1.len() as f32;
1088            let n2 = cluster2.len() as f32;
1089            let ward_factor = (n1 * n2) / (n1 + n2);
1090
1091            (ward_factor * dist_squared).sqrt()
1092        }
1093    }
1094}
1095
1096#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
1097#[target_feature(enable = "sse2")]
1098unsafe fn hierarchical_linkage_distances_sse2(
1099    points: &[&[f32]],
1100    cluster1: &[usize],
1101    cluster2: &[usize],
1102    linkage: LinkageType,
1103) -> f32 {
1104    use core::arch::x86_64::*;
1105
1106    let n_features = points[0].len();
1107    let mut distances = Vec::new();
1108
1109    // Compute all pairwise distances between clusters using SIMD
1110    for &i in cluster1 {
1111        for &j in cluster2 {
1112            let mut sum = _mm_setzero_ps();
1113            let mut k = 0;
1114
1115            while k + 4 <= n_features {
1116                let p1_vec = _mm_loadu_ps(&points[i][k]);
1117                let p2_vec = _mm_loadu_ps(&points[j][k]);
1118                let diff = _mm_sub_ps(p1_vec, p2_vec);
1119                let squared = _mm_mul_ps(diff, diff);
1120                sum = _mm_add_ps(sum, squared);
1121                k += 4;
1122            }
1123
1124            let mut result = [0.0f32; 4];
1125            _mm_storeu_ps(result.as_mut_ptr(), sum);
1126            let mut dist_squared = result[0] + result[1] + result[2] + result[3];
1127
1128            while k < n_features {
1129                let diff = points[i][k] - points[j][k];
1130                dist_squared += diff * diff;
1131                k += 1;
1132            }
1133
1134            distances.push(dist_squared.sqrt());
1135        }
1136    }
1137
1138    match linkage {
1139        LinkageType::Single => distances.iter().fold(f32::INFINITY, |acc, &x| acc.min(x)),
1140        LinkageType::Complete => distances.iter().fold(0.0, |acc, &x| acc.max(x)),
1141        LinkageType::Average => distances.iter().sum::<f32>() / distances.len() as f32,
1142        LinkageType::Ward => {
1143            hierarchical_linkage_distances_scalar(points, cluster1, cluster2, linkage)
1144        }
1145    }
1146}
1147
1148#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
1149#[target_feature(enable = "avx2")]
1150unsafe fn hierarchical_linkage_distances_avx2(
1151    points: &[&[f32]],
1152    cluster1: &[usize],
1153    cluster2: &[usize],
1154    linkage: LinkageType,
1155) -> f32 {
1156    use core::arch::x86_64::*;
1157
1158    let n_features = points[0].len();
1159    let mut distances = Vec::new();
1160
1161    // Compute all pairwise distances between clusters using SIMD
1162    for &i in cluster1 {
1163        for &j in cluster2 {
1164            let mut sum = _mm256_setzero_ps();
1165            let mut k = 0;
1166
1167            while k + 8 <= n_features {
1168                let p1_vec = _mm256_loadu_ps(&points[i][k]);
1169                let p2_vec = _mm256_loadu_ps(&points[j][k]);
1170                let diff = _mm256_sub_ps(p1_vec, p2_vec);
1171                let squared = _mm256_mul_ps(diff, diff);
1172                sum = _mm256_add_ps(sum, squared);
1173                k += 8;
1174            }
1175
1176            let mut result = [0.0f32; 8];
1177            _mm256_storeu_ps(result.as_mut_ptr(), sum);
1178            let mut dist_squared = result.iter().sum::<f32>();
1179
1180            while k < n_features {
1181                let diff = points[i][k] - points[j][k];
1182                dist_squared += diff * diff;
1183                k += 1;
1184            }
1185
1186            distances.push(dist_squared.sqrt());
1187        }
1188    }
1189
1190    match linkage {
1191        LinkageType::Single => distances.iter().fold(f32::INFINITY, |acc, &x| acc.min(x)),
1192        LinkageType::Complete => distances.iter().fold(0.0, |acc, &x| acc.max(x)),
1193        LinkageType::Average => distances.iter().sum::<f32>() / distances.len() as f32,
1194        LinkageType::Ward => {
1195            hierarchical_linkage_distances_scalar(points, cluster1, cluster2, linkage)
1196        }
1197    }
1198}
1199
1200#[allow(non_snake_case)]
1201#[cfg(all(test, not(feature = "no-std")))]
1202mod tests {
1203    use super::*;
1204    use approx::assert_relative_eq;
1205
1206    #[test]
1207    fn test_kmeans_distances() {
1208        let p1 = [1.0, 2.0];
1209        let p2 = [3.0, 4.0];
1210        let points = vec![&p1[..], &p2[..]];
1211
1212        let c1 = [0.0, 0.0];
1213        let c2 = [2.0, 3.0];
1214        let centroids = vec![&c1[..], &c2[..]];
1215
1216        let mut distances = vec![vec![]; 2];
1217
1218        kmeans_distances(&points, &centroids, &mut distances);
1219
1220        // Distance from point 1 to centroid 1: sqrt((1-0)^2 + (2-0)^2) = sqrt(5)
1221        assert_relative_eq!(distances[0][0], (5.0f32).sqrt(), epsilon = 1e-6);
1222
1223        // Distance from point 1 to centroid 2: sqrt((1-2)^2 + (2-3)^2) = sqrt(2)
1224        assert_relative_eq!(distances[0][1], (2.0f32).sqrt(), epsilon = 1e-6);
1225    }
1226
1227    #[test]
1228    fn test_update_centroids() {
1229        let p1 = [1.0, 2.0];
1230        let p2 = [3.0, 4.0];
1231        let p3 = [5.0, 6.0];
1232        let points = vec![&p1[..], &p2[..], &p3[..]];
1233
1234        let assignments = vec![0, 1, 0]; // Points 1 and 3 in cluster 0, point 2 in cluster 1
1235        let mut centroids = vec![vec![]; 2];
1236
1237        update_centroids(&points, &assignments, 2, &mut centroids);
1238
1239        // Cluster 0 centroid: average of (1,2) and (5,6) = (3,4)
1240        assert_relative_eq!(centroids[0][0], 3.0, epsilon = 1e-6);
1241        assert_relative_eq!(centroids[0][1], 4.0, epsilon = 1e-6);
1242
1243        // Cluster 1 centroid: average of (3,4) = (3,4)
1244        assert_relative_eq!(centroids[1][0], 3.0, epsilon = 1e-6);
1245        assert_relative_eq!(centroids[1][1], 4.0, epsilon = 1e-6);
1246    }
1247
1248    #[test]
1249    fn test_wcss() {
1250        let p1 = [1.0, 1.0];
1251        let p2 = [2.0, 2.0];
1252        let points = vec![&p1[..], &p2[..]];
1253
1254        let c1 = [0.0, 0.0];
1255        let centroids = vec![&c1[..]];
1256
1257        let assignments = vec![0, 0]; // Both points assigned to cluster 0
1258
1259        let wcss_value = wcss(&points, &centroids, &assignments);
1260
1261        // WCSS = (1^2 + 1^2) + (2^2 + 2^2) = 2 + 8 = 10
1262        assert_relative_eq!(wcss_value, 10.0, epsilon = 1e-6);
1263    }
1264
1265    #[test]
1266    fn test_silhouette_score() {
1267        // Create two clear clusters
1268        let p1 = [1.0, 1.0];
1269        let p2 = [1.1, 1.1];
1270        let p3 = [5.0, 5.0];
1271        let p4 = [5.1, 5.1];
1272        let points = vec![&p1[..], &p2[..], &p3[..], &p4[..]];
1273
1274        let assignments = vec![0, 0, 1, 1]; // Two clusters with 2 points each
1275
1276        let score = silhouette_score(&points, &assignments, 2);
1277
1278        // Should be positive for well-separated clusters
1279        assert!(score > 0.0);
1280        assert!(score <= 1.0);
1281    }
1282
1283    #[test]
1284    fn test_dbscan_neighbors() {
1285        // Create points in two clear clusters
1286        let p1 = [1.0, 1.0];
1287        let p2 = [1.1, 1.1];
1288        let p3 = [5.0, 5.0];
1289        let p4 = [5.1, 5.1];
1290        let points = vec![&p1[..], &p2[..], &p3[..], &p4[..]];
1291
1292        let mut neighbors = vec![vec![]; 4];
1293        let eps = 0.5; // Small epsilon to separate clusters
1294
1295        dbscan_neighbors(&points, eps, &mut neighbors);
1296
1297        // Points 0 and 1 should be neighbors (distance ~0.14)
1298        assert!(neighbors[0].contains(&1));
1299        assert!(neighbors[1].contains(&0));
1300
1301        // Points 2 and 3 should be neighbors (distance ~0.14)
1302        assert!(neighbors[2].contains(&3));
1303        assert!(neighbors[3].contains(&2));
1304
1305        // Points from different clusters should not be neighbors
1306        assert!(!neighbors[0].contains(&2));
1307        assert!(!neighbors[0].contains(&3));
1308        assert!(!neighbors[1].contains(&2));
1309        assert!(!neighbors[1].contains(&3));
1310    }
1311
1312    #[test]
1313    fn test_dbscan_core_points() {
1314        // Create neighbor data
1315        let neighbors = vec![
1316            vec![1],    // Point 0 has 1 neighbor (+ itself = 2 total)
1317            vec![0, 2], // Point 1 has 2 neighbors (+ itself = 3 total)
1318            vec![1],    // Point 2 has 1 neighbor (+ itself = 2 total)
1319        ];
1320
1321        let mut core_points = vec![false; 3];
1322        let min_samples = 3;
1323
1324        dbscan_core_points(&neighbors, min_samples, &mut core_points);
1325
1326        // Only point 1 should be a core point (has 3+ total points including itself)
1327        assert!(!core_points[0]);
1328        assert!(core_points[1]);
1329        assert!(!core_points[2]);
1330    }
1331
1332    #[test]
1333    fn test_hierarchical_linkage_single() {
1334        let p1 = [1.0, 1.0];
1335        let p2 = [2.0, 2.0];
1336        let p3 = [5.0, 5.0];
1337        let p4 = [6.0, 6.0];
1338        let points = vec![&p1[..], &p2[..], &p3[..], &p4[..]];
1339
1340        let cluster1 = vec![0, 1]; // Points 0 and 1
1341        let cluster2 = vec![2, 3]; // Points 2 and 3
1342
1343        let distance =
1344            hierarchical_linkage_distances(&points, &cluster1, &cluster2, LinkageType::Single);
1345
1346        // Single linkage should return minimum distance between any two points
1347        // Minimum distance is between (2,2) and (5,5) = sqrt(18) ≈ 4.24
1348        let expected = ((5.0f32 - 2.0f32).powi(2) + (5.0f32 - 2.0f32).powi(2)).sqrt();
1349        assert_relative_eq!(distance, expected, epsilon = 1e-6);
1350    }
1351
1352    #[test]
1353    fn test_hierarchical_linkage_complete() {
1354        let p1 = [1.0, 1.0];
1355        let p2 = [2.0, 2.0];
1356        let p3 = [5.0, 5.0];
1357        let p4 = [6.0, 6.0];
1358        let points = vec![&p1[..], &p2[..], &p3[..], &p4[..]];
1359
1360        let cluster1 = vec![0, 1]; // Points 0 and 1
1361        let cluster2 = vec![2, 3]; // Points 2 and 3
1362
1363        let distance =
1364            hierarchical_linkage_distances(&points, &cluster1, &cluster2, LinkageType::Complete);
1365
1366        // Complete linkage should return maximum distance between any two points
1367        // Maximum distance is between (1,1) and (6,6) = sqrt(50) ≈ 7.07
1368        let expected = ((6.0f32 - 1.0f32).powi(2) + (6.0f32 - 1.0f32).powi(2)).sqrt();
1369        assert_relative_eq!(distance, expected, epsilon = 1e-6);
1370    }
1371
1372    #[test]
1373    fn test_hierarchical_linkage_average() {
1374        let p1 = [0.0, 0.0];
1375        let p2 = [1.0, 0.0];
1376        let p3 = [3.0, 0.0];
1377        let points = vec![&p1[..], &p2[..], &p3[..]];
1378
1379        let cluster1 = vec![0]; // Point 0
1380        let cluster2 = vec![1, 2]; // Points 1 and 2
1381
1382        let distance =
1383            hierarchical_linkage_distances(&points, &cluster1, &cluster2, LinkageType::Average);
1384
1385        // Average linkage: average of distances from point 0 to points 1 and 2
1386        // Distance to point 1: 1.0, Distance to point 2: 3.0
1387        // Average: (1.0 + 3.0) / 2 = 2.0
1388        assert_relative_eq!(distance, 2.0, epsilon = 1e-6);
1389    }
1390
1391    #[test]
1392    fn test_hierarchical_linkage_ward() {
1393        let p1 = [0.0, 0.0];
1394        let p2 = [2.0, 0.0];
1395        let p3 = [4.0, 0.0];
1396        let points = vec![&p1[..], &p2[..], &p3[..]];
1397
1398        let cluster1 = vec![0]; // Point (0,0)
1399        let cluster2 = vec![1, 2]; // Points (2,0) and (4,0)
1400
1401        let distance =
1402            hierarchical_linkage_distances(&points, &cluster1, &cluster2, LinkageType::Ward);
1403
1404        // Ward linkage: weighted distance between centroids
1405        // Centroid of cluster1: (0,0)
1406        // Centroid of cluster2: (3,0) [average of (2,0) and (4,0)]
1407        // Distance between centroids: 3.0
1408        // Ward factor: (1 * 2) / (1 + 2) = 2/3
1409        // Ward distance: sqrt(2/3 * 9) = sqrt(6) ≈ 2.45
1410        let expected = (2.0f32 / 3.0f32 * 9.0f32).sqrt();
1411        assert_relative_eq!(distance, expected, epsilon = 1e-6);
1412    }
1413}