1use crate::slice_maybe_parallel;
7#[cfg(feature = "parallel")]
8use rayon::iter::ParallelIterator;
9
10fn gaussian_kernel(u: f64) -> f64 {
12 (-0.5 * u * u).exp() / (2.0 * std::f64::consts::PI).sqrt()
13}
14
15fn epanechnikov_kernel(u: f64) -> f64 {
17 if u.abs() <= 1.0 {
18 0.75 * (1.0 - u * u)
19 } else {
20 0.0
21 }
22}
23
24fn tricube_kernel(u: f64) -> f64 {
26 let abs_u = u.abs();
27 if abs_u < 1.0 {
28 (1.0 - abs_u.powi(3)).powi(3)
29 } else {
30 0.0
31 }
32}
33
34fn get_kernel(kernel_type: &str) -> fn(f64) -> f64 {
36 match kernel_type.to_lowercase().as_str() {
37 "epanechnikov" | "epan" => epanechnikov_kernel,
38 "tricube" | "tri-cube" => tricube_kernel,
39 _ => gaussian_kernel,
40 }
41}
42
43pub fn nadaraya_watson(
55 x: &[f64],
56 y: &[f64],
57 x_new: &[f64],
58 bandwidth: f64,
59 kernel: &str,
60) -> Vec<f64> {
61 let n = x.len();
62 if n == 0 || y.len() != n || x_new.is_empty() || bandwidth <= 0.0 {
63 return vec![0.0; x_new.len()];
64 }
65
66 let kernel_fn = get_kernel(kernel);
67
68 slice_maybe_parallel!(x_new)
69 .map(|&x0| {
70 let mut num = 0.0;
71 let mut denom = 0.0;
72
73 for i in 0..n {
74 let u = (x[i] - x0) / bandwidth;
75 let w = kernel_fn(u);
76 num += w * y[i];
77 denom += w;
78 }
79
80 if denom > 1e-10 {
81 num / denom
82 } else {
83 0.0
84 }
85 })
86 .collect()
87}
88
89pub fn local_linear(x: &[f64], y: &[f64], x_new: &[f64], bandwidth: f64, kernel: &str) -> Vec<f64> {
101 let n = x.len();
102 if n == 0 || y.len() != n || x_new.is_empty() || bandwidth <= 0.0 {
103 return vec![0.0; x_new.len()];
104 }
105
106 let kernel_fn = get_kernel(kernel);
107
108 slice_maybe_parallel!(x_new)
109 .map(|&x0| {
110 let mut s0 = 0.0;
112 let mut s1 = 0.0;
113 let mut s2 = 0.0;
114 let mut t0 = 0.0;
115 let mut t1 = 0.0;
116
117 for i in 0..n {
118 let u = (x[i] - x0) / bandwidth;
119 let w = kernel_fn(u);
120 let d = x[i] - x0;
121
122 s0 += w;
123 s1 += w * d;
124 s2 += w * d * d;
125 t0 += w * y[i];
126 t1 += w * y[i] * d;
127 }
128
129 let det = s0 * s2 - s1 * s1;
131 if det.abs() > 1e-10 {
132 (s2 * t0 - s1 * t1) / det
133 } else if s0 > 1e-10 {
134 t0 / s0
135 } else {
136 0.0
137 }
138 })
139 .collect()
140}
141
142fn accumulate_weighted_normal_equations(
144 x: &[f64],
145 y: &[f64],
146 x0: f64,
147 bandwidth: f64,
148 p: usize,
149 kernel_fn: impl Fn(f64) -> f64,
150) -> (Vec<f64>, Vec<f64>) {
151 let n = x.len();
152 let mut xtx = vec![0.0; p * p];
153 let mut xty = vec![0.0; p];
154
155 for i in 0..n {
156 let u = (x[i] - x0) / bandwidth;
157 let w = kernel_fn(u);
158 let d = x[i] - x0;
159
160 for j in 0..p {
161 let w_dj = w * d.powi(j as i32);
162 for k in 0..p {
163 xtx[j * p + k] += w_dj * d.powi(k as i32);
164 }
165 xty[j] += w_dj * y[i];
166 }
167 }
168
169 (xtx, xty)
170}
171
172fn find_pivot(a: &[f64], p: usize, col: usize) -> usize {
177 let mut max_idx = col;
178 for j in (col + 1)..p {
179 if a[j * p + col].abs() > a[max_idx * p + col].abs() {
180 max_idx = j;
181 }
182 }
183 max_idx
184}
185
186fn swap_rows(a: &mut [f64], b: &mut [f64], p: usize, row_a: usize, row_b: usize) {
188 for k in 0..p {
189 a.swap(row_a * p + k, row_b * p + k);
190 }
191 b.swap(row_a, row_b);
192}
193
194fn eliminate_below(a: &mut [f64], b: &mut [f64], p: usize, pivot_row: usize) {
196 let pivot = a[pivot_row * p + pivot_row];
197 for j in (pivot_row + 1)..p {
198 let factor = a[j * p + pivot_row] / pivot;
199 for k in pivot_row..p {
200 a[j * p + k] -= factor * a[pivot_row * p + k];
201 }
202 b[j] -= factor * b[pivot_row];
203 }
204}
205
206fn forward_eliminate(a: &mut [f64], b: &mut [f64], p: usize) {
207 for i in 0..p {
208 let max_idx = find_pivot(a, p, i);
209 if max_idx != i {
210 swap_rows(a, b, p, i, max_idx);
211 }
212
213 if a[i * p + i].abs() < 1e-10 {
214 continue;
215 }
216
217 eliminate_below(a, b, p, i);
218 }
219}
220
221fn back_substitute(a: &[f64], b: &[f64], p: usize) -> Vec<f64> {
223 let mut coefs = vec![0.0; p];
224 for i in (0..p).rev() {
225 let mut sum = b[i];
226 for j in (i + 1)..p {
227 sum -= a[i * p + j] * coefs[j];
228 }
229 if a[i * p + i].abs() > 1e-10 {
230 coefs[i] = sum / a[i * p + i];
231 }
232 }
233 coefs
234}
235
236fn solve_gaussian(a: &mut [f64], b: &mut [f64], p: usize) -> Vec<f64> {
237 forward_eliminate(a, b, p);
238 back_substitute(a, b, p)
239}
240
241pub fn solve_gaussian_pub(a: &mut [f64], b: &mut [f64], p: usize) -> Vec<f64> {
247 solve_gaussian(a, b, p)
248}
249
250pub fn local_polynomial(
263 x: &[f64],
264 y: &[f64],
265 x_new: &[f64],
266 bandwidth: f64,
267 degree: usize,
268 kernel: &str,
269) -> Vec<f64> {
270 let n = x.len();
271 if n == 0 || y.len() != n || x_new.is_empty() || bandwidth <= 0.0 || degree == 0 {
272 return vec![0.0; x_new.len()];
273 }
274
275 if degree == 1 {
276 return local_linear(x, y, x_new, bandwidth, kernel);
277 }
278
279 let kernel_fn = get_kernel(kernel);
280 let p = degree + 1; slice_maybe_parallel!(x_new)
283 .map(|&x0| {
284 let (mut xtx, mut xty) =
285 accumulate_weighted_normal_equations(x, y, x0, bandwidth, p, kernel_fn);
286 let coefs = solve_gaussian(&mut xtx, &mut xty, p);
287 coefs[0]
288 })
289 .collect()
290}
291
292pub fn knn_smoother(x: &[f64], y: &[f64], x_new: &[f64], k: usize) -> Vec<f64> {
303 let n = x.len();
304 if n == 0 || y.len() != n || x_new.is_empty() || k == 0 {
305 return vec![0.0; x_new.len()];
306 }
307
308 let k = k.min(n);
309
310 slice_maybe_parallel!(x_new)
311 .map(|&x0| {
312 let mut distances: Vec<(usize, f64)> = x
314 .iter()
315 .enumerate()
316 .map(|(i, &xi)| (i, (xi - x0).abs()))
317 .collect();
318
319 distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
321
322 let sum: f64 = distances.iter().take(k).map(|(i, _)| y[*i]).sum();
324 sum / k as f64
325 })
326 .collect()
327}
328
329pub fn smoothing_matrix_nw(x: &[f64], bandwidth: f64, kernel: &str) -> Vec<f64> {
333 let n = x.len();
334 if n == 0 || bandwidth <= 0.0 {
335 return Vec::new();
336 }
337
338 let kernel_fn = get_kernel(kernel);
339 let mut s = vec![0.0; n * n];
340
341 for i in 0..n {
342 let mut row_sum = 0.0;
343 for j in 0..n {
344 let u = (x[j] - x[i]) / bandwidth;
345 s[i + j * n] = kernel_fn(u);
346 row_sum += s[i + j * n];
347 }
348 if row_sum > 1e-10 {
349 for j in 0..n {
350 s[i + j * n] /= row_sum;
351 }
352 }
353 }
354
355 s
356}
357
358#[derive(Debug, Clone, Copy, PartialEq)]
362pub enum CvCriterion {
363 Cv,
365 Gcv,
367}
368
369#[derive(Debug, Clone)]
371pub struct OptimBandwidthResult {
372 pub h_opt: f64,
374 pub criterion: CvCriterion,
376 pub value: f64,
378}
379
380pub fn cv_smoother(x: &[f64], y: &[f64], bandwidth: f64, kernel: &str) -> f64 {
394 let n = x.len();
395 if n < 2 || y.len() != n || bandwidth <= 0.0 {
396 return f64::INFINITY;
397 }
398
399 let mut s = smoothing_matrix_nw(x, bandwidth, kernel);
401 if s.is_empty() {
402 return f64::INFINITY;
403 }
404
405 for i in 0..n {
407 s[i + i * n] = 0.0;
408 }
409
410 for i in 0..n {
412 let row_sum: f64 = (0..n).map(|j| s[i + j * n]).sum();
413 if row_sum > 1e-10 {
414 for j in 0..n {
415 s[i + j * n] /= row_sum;
416 }
417 }
418 }
419
420 let mut mse = 0.0;
422 for i in 0..n {
423 let y_hat: f64 = (0..n).map(|j| s[i + j * n] * y[j]).sum();
424 let resid = y[i] - y_hat;
425 mse += resid * resid;
426 }
427 mse / n as f64
428}
429
430pub fn gcv_smoother(x: &[f64], y: &[f64], bandwidth: f64, kernel: &str) -> f64 {
443 let n = x.len();
444 if n < 2 || y.len() != n || bandwidth <= 0.0 {
445 return f64::INFINITY;
446 }
447
448 let s = smoothing_matrix_nw(x, bandwidth, kernel);
449 if s.is_empty() {
450 return f64::INFINITY;
451 }
452
453 let mut rss = 0.0;
455 for i in 0..n {
456 let y_hat: f64 = (0..n).map(|j| s[i + j * n] * y[j]).sum();
457 let resid = y[i] - y_hat;
458 rss += resid * resid;
459 }
460
461 let trace_s: f64 = (0..n).map(|i| s[i + i * n]).sum();
463
464 let denom = 1.0 - trace_s / n as f64;
465 if denom.abs() < 1e-10 {
466 f64::INFINITY
467 } else {
468 (rss / n as f64) / (denom * denom)
469 }
470}
471
472pub fn optim_bandwidth(
486 x: &[f64],
487 y: &[f64],
488 h_range: Option<(f64, f64)>,
489 criterion: CvCriterion,
490 kernel: &str,
491 n_grid: usize,
492) -> OptimBandwidthResult {
493 let n = x.len();
494 let n_grid = n_grid.max(2);
495
496 let (h_min, h_max) = match h_range {
498 Some((lo, hi)) if lo > 0.0 && hi > lo => (lo, hi),
499 _ => {
500 let x_min = x.iter().cloned().fold(f64::INFINITY, f64::min);
501 let x_max = x.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
502 let h_default = (x_max - x_min) / (n as f64).powf(0.2);
503 let h_default = h_default.max(1e-10);
504 (h_default / 5.0, h_default * 5.0)
505 }
506 };
507
508 let score_fn = match criterion {
509 CvCriterion::Cv => cv_smoother,
510 CvCriterion::Gcv => gcv_smoother,
511 };
512
513 let mut best_h = h_min;
514 let mut best_score = f64::INFINITY;
515
516 for i in 0..n_grid {
517 let h = h_min + (h_max - h_min) * i as f64 / (n_grid - 1) as f64;
518 let score = score_fn(x, y, h, kernel);
519 if score < best_score {
520 best_score = score;
521 best_h = h;
522 }
523 }
524
525 OptimBandwidthResult {
526 h_opt: best_h,
527 criterion,
528 value: best_score,
529 }
530}
531
532#[derive(Debug, Clone)]
536pub struct KnnCvResult {
537 pub optimal_k: usize,
539 pub cv_errors: Vec<f64>,
541}
542
543pub fn knn_gcv(x: &[f64], y: &[f64], max_k: usize) -> KnnCvResult {
553 let n = x.len();
554 let max_k = max_k.min(n.saturating_sub(1)).max(1);
555
556 let mut sorted_neighbors: Vec<Vec<(usize, f64)>> = Vec::with_capacity(n);
558 for i in 0..n {
559 let mut dists: Vec<(usize, f64)> = (0..n)
560 .filter(|&j| j != i)
561 .map(|j| (j, (x[j] - x[i]).abs()))
562 .collect();
563 dists.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
564 sorted_neighbors.push(dists);
565 }
566
567 let mut cv_errors = Vec::with_capacity(max_k);
568
569 for k in 1..=max_k {
570 let mut mse = 0.0;
571 for i in 0..n {
572 let neighbors = &sorted_neighbors[i];
573 let d_k = if k <= neighbors.len() {
575 neighbors[k - 1].1
576 } else {
577 neighbors.last().map(|x| x.1).unwrap_or(1.0)
578 };
579 let d_k1 = if k < neighbors.len() {
580 neighbors[k].1
581 } else {
582 d_k * 2.0
583 };
584 let h = (d_k + d_k1) / 2.0;
585 let h = h.max(1e-10);
586
587 let mut num = 0.0;
589 let mut den = 0.0;
590 for &(j, dist) in neighbors.iter().take(k) {
591 let u = dist / h;
592 let w = epanechnikov_kernel(u);
593 num += w * y[j];
594 den += w;
595 }
596 let y_hat = if den > 1e-10 { num / den } else { y[i] };
597 mse += (y[i] - y_hat).powi(2);
598 }
599 cv_errors.push(mse / n as f64);
600 }
601
602 let optimal_k = cv_errors
603 .iter()
604 .enumerate()
605 .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
606 .map(|(i, _)| i + 1)
607 .unwrap_or(1);
608
609 KnnCvResult {
610 optimal_k,
611 cv_errors,
612 }
613}
614
615pub fn knn_lcv(x: &[f64], y: &[f64], max_k: usize) -> Vec<usize> {
628 let n = x.len();
629 let max_k = max_k.min(n.saturating_sub(1)).max(1);
630
631 let mut per_obs_k = vec![1usize; n];
632
633 for i in 0..n {
634 let mut neighbors: Vec<(usize, f64)> = (0..n)
636 .filter(|&j| j != i)
637 .map(|j| (j, (x[j] - x[i]).abs()))
638 .collect();
639 neighbors.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
640
641 let mut best_k = 1;
642 let mut best_err = f64::INFINITY;
643
644 for k in 1..=max_k {
645 let sum: f64 = neighbors.iter().take(k).map(|&(j, _)| y[j]).sum();
647 let y_hat = sum / k as f64;
648 let err = (y[i] - y_hat).abs();
649 if err < best_err {
650 best_err = err;
651 best_k = k;
652 }
653 }
654 per_obs_k[i] = best_k;
655 }
656
657 per_obs_k
658}
659
660#[cfg(test)]
661mod tests {
662 use super::*;
663
664 fn uniform_grid(n: usize) -> Vec<f64> {
665 (0..n).map(|i| i as f64 / (n - 1) as f64).collect()
666 }
667
668 #[test]
671 fn test_nw_constant_data() {
672 let x = uniform_grid(20);
673 let y: Vec<f64> = vec![5.0; 20];
674
675 let y_smooth = nadaraya_watson(&x, &y, &x, 0.1, "gaussian");
676
677 for &yi in &y_smooth {
679 assert!(
680 (yi - 5.0).abs() < 0.1,
681 "Constant data should remain constant"
682 );
683 }
684 }
685
686 #[test]
687 fn test_nw_linear_data() {
688 let x = uniform_grid(50);
689 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi + 1.0).collect();
690
691 let y_smooth = nadaraya_watson(&x, &y, &x, 0.2, "gaussian");
692
693 for i in 10..40 {
695 let expected = 2.0 * x[i] + 1.0;
696 assert!(
697 (y_smooth[i] - expected).abs() < 0.2,
698 "Linear trend should be approximately preserved"
699 );
700 }
701 }
702
703 #[test]
704 fn test_nw_gaussian_vs_epanechnikov() {
705 let x = uniform_grid(30);
706 let y: Vec<f64> = x
707 .iter()
708 .map(|&xi| (2.0 * std::f64::consts::PI * xi).sin())
709 .collect();
710
711 let y_gauss = nadaraya_watson(&x, &y, &x, 0.1, "gaussian");
712 let y_epan = nadaraya_watson(&x, &y, &x, 0.1, "epanechnikov");
713
714 assert_eq!(y_gauss.len(), 30);
716 assert_eq!(y_epan.len(), 30);
717
718 let diff: f64 = y_gauss
720 .iter()
721 .zip(&y_epan)
722 .map(|(a, b)| (a - b).abs())
723 .sum();
724 assert!(
725 diff > 0.0,
726 "Different kernels should give different results"
727 );
728 }
729
730 #[test]
731 fn test_nw_invalid_input() {
732 let result = nadaraya_watson(&[], &[], &[0.5], 0.1, "gaussian");
734 assert_eq!(result, vec![0.0]);
735
736 let result = nadaraya_watson(&[0.0, 1.0], &[1.0], &[0.5], 0.1, "gaussian");
738 assert_eq!(result, vec![0.0]);
739
740 let result = nadaraya_watson(&[0.0, 1.0], &[1.0, 2.0], &[0.5], 0.0, "gaussian");
742 assert_eq!(result, vec![0.0]);
743 }
744
745 #[test]
748 fn test_ll_constant_data() {
749 let x = uniform_grid(20);
750 let y: Vec<f64> = vec![3.0; 20];
751
752 let y_smooth = local_linear(&x, &y, &x, 0.15, "gaussian");
753
754 for &yi in &y_smooth {
755 assert!((yi - 3.0).abs() < 0.1, "Constant should remain constant");
756 }
757 }
758
759 #[test]
760 fn test_ll_linear_data_exact() {
761 let x = uniform_grid(30);
762 let y: Vec<f64> = x.iter().map(|&xi| 3.0 * xi + 2.0).collect();
763
764 let y_smooth = local_linear(&x, &y, &x, 0.2, "gaussian");
765
766 for i in 5..25 {
768 let expected = 3.0 * x[i] + 2.0;
769 assert!(
770 (y_smooth[i] - expected).abs() < 0.1,
771 "Local linear should fit linear data well"
772 );
773 }
774 }
775
776 #[test]
777 fn test_ll_invalid_input() {
778 let result = local_linear(&[], &[], &[0.5], 0.1, "gaussian");
779 assert_eq!(result, vec![0.0]);
780
781 let result = local_linear(&[0.0, 1.0], &[1.0, 2.0], &[0.5], -0.1, "gaussian");
782 assert_eq!(result, vec![0.0]);
783 }
784
785 #[test]
788 fn test_lp_degree1_equals_local_linear() {
789 let x = uniform_grid(25);
790 let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
791
792 let y_ll = local_linear(&x, &y, &x, 0.15, "gaussian");
793 let y_lp = local_polynomial(&x, &y, &x, 0.15, 1, "gaussian");
794
795 for i in 0..25 {
796 assert!(
797 (y_ll[i] - y_lp[i]).abs() < 1e-10,
798 "Degree 1 should equal local linear"
799 );
800 }
801 }
802
803 #[test]
804 fn test_lp_quadratic_data() {
805 let x = uniform_grid(40);
806 let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
807
808 let y_smooth = local_polynomial(&x, &y, &x, 0.15, 2, "gaussian");
809
810 for i in 8..32 {
812 let expected = x[i] * x[i];
813 assert!(
814 (y_smooth[i] - expected).abs() < 0.1,
815 "Local quadratic should fit quadratic data"
816 );
817 }
818 }
819
820 #[test]
821 fn test_lp_invalid_input() {
822 let result = local_polynomial(&[0.0, 1.0], &[1.0, 2.0], &[0.5], 0.1, 0, "gaussian");
824 assert_eq!(result, vec![0.0]);
825
826 let result = local_polynomial(&[], &[], &[0.5], 0.1, 2, "gaussian");
828 assert_eq!(result, vec![0.0]);
829 }
830
831 #[test]
834 fn test_knn_k1_nearest() {
835 let x = vec![0.0, 0.5, 1.0];
836 let y = vec![1.0, 2.0, 3.0];
837
838 let result = knn_smoother(&x, &y, &[0.1, 0.6, 0.9], 1);
839
840 assert!((result[0] - 1.0).abs() < 1e-10, "0.1 nearest to 0.0 -> 1.0");
842 assert!((result[1] - 2.0).abs() < 1e-10, "0.6 nearest to 0.5 -> 2.0");
843 assert!((result[2] - 3.0).abs() < 1e-10, "0.9 nearest to 1.0 -> 3.0");
844 }
845
846 #[test]
847 fn test_knn_k_equals_n_is_mean() {
848 let x = vec![0.0, 0.25, 0.5, 0.75, 1.0];
849 let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
850 let expected_mean = 3.0;
851
852 let result = knn_smoother(&x, &y, &[0.5], 5);
853
854 assert!(
855 (result[0] - expected_mean).abs() < 1e-10,
856 "k=n should return mean"
857 );
858 }
859
860 #[test]
861 fn test_knn_invalid_input() {
862 let result = knn_smoother(&[], &[], &[0.5], 3);
863 assert_eq!(result, vec![0.0]);
864
865 let result = knn_smoother(&[0.0, 1.0], &[1.0, 2.0], &[0.5], 0);
866 assert_eq!(result, vec![0.0]);
867 }
868
869 #[test]
872 fn test_smoothing_matrix_row_stochastic() {
873 let x = uniform_grid(10);
874 let s = smoothing_matrix_nw(&x, 0.2, "gaussian");
875
876 assert_eq!(s.len(), 100);
877
878 for i in 0..10 {
880 let row_sum: f64 = (0..10).map(|j| s[i + j * 10]).sum();
881 assert!(
882 (row_sum - 1.0).abs() < 1e-10,
883 "Row {} should sum to 1, got {}",
884 i,
885 row_sum
886 );
887 }
888 }
889
890 #[test]
891 fn test_smoothing_matrix_invalid_input() {
892 let result = smoothing_matrix_nw(&[], 0.1, "gaussian");
893 assert!(result.is_empty());
894
895 let result = smoothing_matrix_nw(&[0.0, 1.0], 0.0, "gaussian");
896 assert!(result.is_empty());
897 }
898
899 #[test]
900 fn test_nan_nw_no_panic() {
901 let x = vec![0.0, 0.25, 0.5, 0.75, 1.0];
902 let mut y = vec![0.0, 1.0, 2.0, 1.0, 0.0];
903 y[2] = f64::NAN;
904 let result = nadaraya_watson(&x, &y, &x, 0.3, "gaussian");
905 assert_eq!(result.len(), x.len());
906 }
908
909 #[test]
910 fn test_n1_smoother() {
911 let x = vec![0.5];
913 let y = vec![3.0];
914 let x_new = vec![0.5];
915 let result = nadaraya_watson(&x, &y, &x_new, 0.3, "gaussian");
916 assert_eq!(result.len(), 1);
917 assert!(
918 (result[0] - 3.0).abs() < 1e-6,
919 "Single point smoother should return the value"
920 );
921 }
922
923 #[test]
924 fn test_duplicate_x_smoother() {
925 let x = vec![0.0, 0.0, 0.5, 1.0, 1.0];
927 let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
928 let x_new = vec![0.0, 0.5, 1.0];
929 let result = nadaraya_watson(&x, &y, &x_new, 0.3, "gaussian");
930 assert_eq!(result.len(), 3);
931 for v in &result {
932 assert!(v.is_finite());
933 }
934 }
935
936 #[test]
939 fn test_cv_smoother_linear_data() {
940 let x = uniform_grid(30);
941 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi + 1.0).collect();
942 let cv = cv_smoother(&x, &y, 0.2, "gaussian");
943 assert!(cv.is_finite());
944 assert!(cv >= 0.0);
945 assert!(cv < 1.0, "CV error for smooth linear data should be small");
946 }
947
948 #[test]
949 fn test_cv_smoother_invalid() {
950 assert_eq!(cv_smoother(&[], &[], 0.1, "gaussian"), f64::INFINITY);
951 assert_eq!(
952 cv_smoother(&[0.0, 1.0], &[1.0, 2.0], -0.1, "gaussian"),
953 f64::INFINITY
954 );
955 }
956
957 #[test]
958 fn test_gcv_smoother_linear_data() {
959 let x = uniform_grid(30);
960 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi + 1.0).collect();
961 let gcv = gcv_smoother(&x, &y, 0.2, "gaussian");
962 assert!(gcv.is_finite());
963 assert!(gcv >= 0.0);
964 }
965
966 #[test]
967 fn test_gcv_smoother_invalid() {
968 assert_eq!(gcv_smoother(&[], &[], 0.1, "gaussian"), f64::INFINITY);
969 }
970
971 #[test]
972 fn test_optim_bandwidth_returns_valid() {
973 let x = uniform_grid(30);
974 let y: Vec<f64> = x
975 .iter()
976 .map(|&xi| (2.0 * std::f64::consts::PI * xi).sin())
977 .collect();
978
979 let result = optim_bandwidth(&x, &y, None, CvCriterion::Gcv, "gaussian", 20);
980 assert!(result.h_opt > 0.0);
981 assert!(result.value.is_finite());
982 assert_eq!(result.criterion, CvCriterion::Gcv);
983 }
984
985 #[test]
986 fn test_optim_bandwidth_cv_vs_gcv() {
987 let x = uniform_grid(25);
988 let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
989
990 let cv_result = optim_bandwidth(&x, &y, None, CvCriterion::Cv, "gaussian", 20);
991 let gcv_result = optim_bandwidth(&x, &y, None, CvCriterion::Gcv, "gaussian", 20);
992
993 assert!(cv_result.h_opt > 0.0);
994 assert!(gcv_result.h_opt > 0.0);
995 }
996
997 #[test]
998 fn test_optim_bandwidth_custom_range() {
999 let x = uniform_grid(20);
1000 let y: Vec<f64> = x.to_vec();
1001 let result = optim_bandwidth(&x, &y, Some((0.05, 0.5)), CvCriterion::Cv, "gaussian", 10);
1002 assert!(result.h_opt >= 0.05);
1003 assert!(result.h_opt <= 0.5);
1004 }
1005
1006 #[test]
1009 fn test_knn_gcv_returns_valid() {
1010 let x = uniform_grid(20);
1011 let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
1012
1013 let result = knn_gcv(&x, &y, 10);
1014 assert!(result.optimal_k >= 1);
1015 assert!(result.optimal_k <= 10);
1016 assert_eq!(result.cv_errors.len(), 10);
1017 for &e in &result.cv_errors {
1018 assert!(e.is_finite());
1019 assert!(e >= 0.0);
1020 }
1021 }
1022
1023 #[test]
1024 fn test_knn_gcv_constant_data() {
1025 let x = uniform_grid(15);
1026 let y = vec![5.0; 15];
1027 let result = knn_gcv(&x, &y, 5);
1028 for &e in &result.cv_errors {
1030 assert!(e < 0.01, "Constant data: CV error should be near zero");
1031 }
1032 }
1033
1034 #[test]
1035 fn test_knn_lcv_returns_valid() {
1036 let x = uniform_grid(15);
1037 let y: Vec<f64> = x.to_vec();
1038
1039 let result = knn_lcv(&x, &y, 5);
1040 assert_eq!(result.len(), 15);
1041 for &k in &result {
1042 assert!(k >= 1);
1043 assert!(k <= 5);
1044 }
1045 }
1046
1047 #[test]
1048 fn test_knn_lcv_constant_data() {
1049 let x = uniform_grid(10);
1050 let y = vec![3.0; 10];
1051 let result = knn_lcv(&x, &y, 5);
1052 assert_eq!(result.len(), 10);
1053 for &k in &result {
1056 assert!(k >= 1);
1057 }
1058 }
1059}