1use crate::error::FdarError;
7use crate::slice_maybe_parallel;
8#[cfg(feature = "parallel")]
9use rayon::iter::ParallelIterator;
10
11fn gaussian_kernel(u: f64) -> f64 {
13 (-0.5 * u * u).exp() / (2.0 * std::f64::consts::PI).sqrt()
14}
15
16fn epanechnikov_kernel(u: f64) -> f64 {
18 if u.abs() <= 1.0 {
19 0.75 * (1.0 - u * u)
20 } else {
21 0.0
22 }
23}
24
25fn tricube_kernel(u: f64) -> f64 {
27 let abs_u = u.abs();
28 if abs_u < 1.0 {
29 (1.0 - abs_u.powi(3)).powi(3)
30 } else {
31 0.0
32 }
33}
34
35fn get_kernel(kernel_type: &str) -> fn(f64) -> f64 {
37 match kernel_type.to_lowercase().as_str() {
38 "epanechnikov" | "epan" => epanechnikov_kernel,
39 "tricube" | "tri-cube" => tricube_kernel,
40 _ => gaussian_kernel,
41 }
42}
43
44pub fn nadaraya_watson(
73 x: &[f64],
74 y: &[f64],
75 x_new: &[f64],
76 bandwidth: f64,
77 kernel: &str,
78) -> Result<Vec<f64>, FdarError> {
79 let n = x.len();
80 if n == 0 {
81 return Err(FdarError::InvalidDimension {
82 parameter: "x",
83 expected: "non-empty slice".to_string(),
84 actual: "empty".to_string(),
85 });
86 }
87 if y.len() != n {
88 return Err(FdarError::InvalidDimension {
89 parameter: "y",
90 expected: format!("length {n} (matching x)"),
91 actual: format!("length {}", y.len()),
92 });
93 }
94 if x_new.is_empty() {
95 return Err(FdarError::InvalidDimension {
96 parameter: "x_new",
97 expected: "non-empty slice".to_string(),
98 actual: "empty".to_string(),
99 });
100 }
101 if bandwidth <= 0.0 {
102 return Err(FdarError::InvalidParameter {
103 parameter: "bandwidth",
104 message: format!("must be positive, got {bandwidth}"),
105 });
106 }
107
108 let kernel_fn = get_kernel(kernel);
109
110 Ok(slice_maybe_parallel!(x_new)
111 .map(|&x0| {
112 let mut num = 0.0;
113 let mut denom = 0.0;
114
115 for i in 0..n {
116 let u = (x[i] - x0) / bandwidth;
117 let w = kernel_fn(u);
118 num += w * y[i];
119 denom += w;
120 }
121
122 if denom > 1e-10 {
123 num / denom
124 } else {
125 0.0
126 }
127 })
128 .collect())
129}
130
131pub fn local_linear(
161 x: &[f64],
162 y: &[f64],
163 x_new: &[f64],
164 bandwidth: f64,
165 kernel: &str,
166) -> Result<Vec<f64>, FdarError> {
167 let n = x.len();
168 if n == 0 {
169 return Err(FdarError::InvalidDimension {
170 parameter: "x",
171 expected: "non-empty slice".to_string(),
172 actual: "empty".to_string(),
173 });
174 }
175 if y.len() != n {
176 return Err(FdarError::InvalidDimension {
177 parameter: "y",
178 expected: format!("length {n} (matching x)"),
179 actual: format!("length {}", y.len()),
180 });
181 }
182 if x_new.is_empty() {
183 return Err(FdarError::InvalidDimension {
184 parameter: "x_new",
185 expected: "non-empty slice".to_string(),
186 actual: "empty".to_string(),
187 });
188 }
189 if bandwidth <= 0.0 {
190 return Err(FdarError::InvalidParameter {
191 parameter: "bandwidth",
192 message: format!("must be positive, got {bandwidth}"),
193 });
194 }
195
196 let kernel_fn = get_kernel(kernel);
197
198 Ok(slice_maybe_parallel!(x_new)
199 .map(|&x0| {
200 let mut s0 = 0.0;
202 let mut s1 = 0.0;
203 let mut s2 = 0.0;
204 let mut t0 = 0.0;
205 let mut t1 = 0.0;
206
207 for i in 0..n {
208 let u = (x[i] - x0) / bandwidth;
209 let w = kernel_fn(u);
210 let d = x[i] - x0;
211
212 s0 += w;
213 s1 += w * d;
214 s2 += w * d * d;
215 t0 += w * y[i];
216 t1 += w * y[i] * d;
217 }
218
219 let det = s0 * s2 - s1 * s1;
221 if det.abs() > 1e-10 {
222 (s2 * t0 - s1 * t1) / det
223 } else if s0 > 1e-10 {
224 t0 / s0
225 } else {
226 0.0
227 }
228 })
229 .collect())
230}
231
232fn accumulate_weighted_normal_equations(
234 x: &[f64],
235 y: &[f64],
236 x0: f64,
237 bandwidth: f64,
238 p: usize,
239 kernel_fn: impl Fn(f64) -> f64,
240) -> (Vec<f64>, Vec<f64>) {
241 let n = x.len();
242 let mut xtx = vec![0.0; p * p];
243 let mut xty = vec![0.0; p];
244
245 for i in 0..n {
246 let u = (x[i] - x0) / bandwidth;
247 let w = kernel_fn(u);
248 let d = x[i] - x0;
249
250 for j in 0..p {
251 let w_dj = w * d.powi(j as i32);
252 for k in 0..p {
253 xtx[j * p + k] += w_dj * d.powi(k as i32);
254 }
255 xty[j] += w_dj * y[i];
256 }
257 }
258
259 (xtx, xty)
260}
261
262fn find_pivot(a: &[f64], p: usize, col: usize) -> usize {
267 let mut max_idx = col;
268 for j in (col + 1)..p {
269 if a[j * p + col].abs() > a[max_idx * p + col].abs() {
270 max_idx = j;
271 }
272 }
273 max_idx
274}
275
276fn swap_rows(a: &mut [f64], b: &mut [f64], p: usize, row_a: usize, row_b: usize) {
278 for k in 0..p {
279 a.swap(row_a * p + k, row_b * p + k);
280 }
281 b.swap(row_a, row_b);
282}
283
284fn eliminate_below(a: &mut [f64], b: &mut [f64], p: usize, pivot_row: usize) {
286 let pivot = a[pivot_row * p + pivot_row];
287 for j in (pivot_row + 1)..p {
288 let factor = a[j * p + pivot_row] / pivot;
289 for k in pivot_row..p {
290 a[j * p + k] -= factor * a[pivot_row * p + k];
291 }
292 b[j] -= factor * b[pivot_row];
293 }
294}
295
296fn forward_eliminate(a: &mut [f64], b: &mut [f64], p: usize) {
297 for i in 0..p {
298 let max_idx = find_pivot(a, p, i);
299 if max_idx != i {
300 swap_rows(a, b, p, i, max_idx);
301 }
302
303 if a[i * p + i].abs() < 1e-10 {
304 continue;
305 }
306
307 eliminate_below(a, b, p, i);
308 }
309}
310
311fn back_substitute(a: &[f64], b: &[f64], p: usize) -> Vec<f64> {
313 let mut coefs = vec![0.0; p];
314 for i in (0..p).rev() {
315 let mut sum = b[i];
316 for j in (i + 1)..p {
317 sum -= a[i * p + j] * coefs[j];
318 }
319 if a[i * p + i].abs() > 1e-10 {
320 coefs[i] = sum / a[i * p + i];
321 }
322 }
323 coefs
324}
325
326fn solve_gaussian(a: &mut [f64], b: &mut [f64], p: usize) -> Vec<f64> {
327 forward_eliminate(a, b, p);
328 back_substitute(a, b, p)
329}
330
331pub(crate) fn solve_gaussian_pub(a: &mut [f64], b: &mut [f64], p: usize) -> Vec<f64> {
337 solve_gaussian(a, b, p)
338}
339
340pub fn local_polynomial(
358 x: &[f64],
359 y: &[f64],
360 x_new: &[f64],
361 bandwidth: f64,
362 degree: usize,
363 kernel: &str,
364) -> Result<Vec<f64>, FdarError> {
365 let n = x.len();
366 if n == 0 {
367 return Err(FdarError::InvalidDimension {
368 parameter: "x",
369 expected: "non-empty slice".to_string(),
370 actual: "empty".to_string(),
371 });
372 }
373 if y.len() != n {
374 return Err(FdarError::InvalidDimension {
375 parameter: "y",
376 expected: format!("length {n} (matching x)"),
377 actual: format!("length {}", y.len()),
378 });
379 }
380 if x_new.is_empty() {
381 return Err(FdarError::InvalidDimension {
382 parameter: "x_new",
383 expected: "non-empty slice".to_string(),
384 actual: "empty".to_string(),
385 });
386 }
387 if bandwidth <= 0.0 {
388 return Err(FdarError::InvalidParameter {
389 parameter: "bandwidth",
390 message: format!("must be positive, got {bandwidth}"),
391 });
392 }
393 if degree == 0 {
394 return nadaraya_watson(x, y, x_new, bandwidth, kernel);
395 }
396
397 if degree == 1 {
398 return local_linear(x, y, x_new, bandwidth, kernel);
399 }
400
401 let kernel_fn = get_kernel(kernel);
402 let p = degree + 1; Ok(slice_maybe_parallel!(x_new)
405 .map(|&x0| {
406 let (mut xtx, mut xty) =
407 accumulate_weighted_normal_equations(x, y, x0, bandwidth, p, kernel_fn);
408 let coefs = solve_gaussian(&mut xtx, &mut xty, p);
409 coefs[0]
410 })
411 .collect())
412}
413
414pub fn knn_smoother(x: &[f64], y: &[f64], x_new: &[f64], k: usize) -> Result<Vec<f64>, FdarError> {
430 let n = x.len();
431 if n == 0 {
432 return Err(FdarError::InvalidDimension {
433 parameter: "x",
434 expected: "non-empty slice".to_string(),
435 actual: "empty".to_string(),
436 });
437 }
438 if y.len() != n {
439 return Err(FdarError::InvalidDimension {
440 parameter: "y",
441 expected: format!("length {n} (matching x)"),
442 actual: format!("length {}", y.len()),
443 });
444 }
445 if x_new.is_empty() {
446 return Err(FdarError::InvalidDimension {
447 parameter: "x_new",
448 expected: "non-empty slice".to_string(),
449 actual: "empty".to_string(),
450 });
451 }
452 if k == 0 {
453 return Err(FdarError::InvalidParameter {
454 parameter: "k",
455 message: "must be at least 1".to_string(),
456 });
457 }
458
459 let k = k.min(n);
460
461 Ok(slice_maybe_parallel!(x_new)
462 .map(|&x0| {
463 let mut distances: Vec<(usize, f64)> = x
465 .iter()
466 .enumerate()
467 .map(|(i, &xi)| (i, (xi - x0).abs()))
468 .collect();
469
470 distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
472
473 let sum: f64 = distances.iter().take(k).map(|(i, _)| y[*i]).sum();
475 sum / k as f64
476 })
477 .collect())
478}
479
480pub fn smoothing_matrix_nw(x: &[f64], bandwidth: f64, kernel: &str) -> Result<Vec<f64>, FdarError> {
488 let n = x.len();
489 if n == 0 {
490 return Err(FdarError::InvalidDimension {
491 parameter: "x",
492 expected: "non-empty slice".to_string(),
493 actual: "empty".to_string(),
494 });
495 }
496 if bandwidth <= 0.0 {
497 return Err(FdarError::InvalidParameter {
498 parameter: "bandwidth",
499 message: format!("must be positive, got {bandwidth}"),
500 });
501 }
502
503 let kernel_fn = get_kernel(kernel);
504 let mut s = vec![0.0; n * n];
505
506 for i in 0..n {
507 let mut row_sum = 0.0;
508 for j in 0..n {
509 let u = (x[j] - x[i]) / bandwidth;
510 s[i + j * n] = kernel_fn(u);
511 row_sum += s[i + j * n];
512 }
513 if row_sum > 1e-10 {
514 for j in 0..n {
515 s[i + j * n] /= row_sum;
516 }
517 }
518 }
519
520 Ok(s)
521}
522
523#[derive(Debug, Clone, Copy, PartialEq)]
530#[non_exhaustive]
531pub enum CvCriterion {
532 Cv,
534 Gcv,
536 Aic,
540}
541
542#[non_exhaustive]
544#[derive(Debug, Clone, PartialEq)]
545pub struct OptimBandwidthResult {
546 pub h_opt: f64,
548 pub criterion: CvCriterion,
550 pub value: f64,
552}
553
554pub fn cv_smoother(x: &[f64], y: &[f64], bandwidth: f64, kernel: &str) -> f64 {
568 let n = x.len();
569 if n < 2 || y.len() != n || bandwidth <= 0.0 {
570 return f64::INFINITY;
571 }
572
573 let mut s = match smoothing_matrix_nw(x, bandwidth, kernel) {
575 Ok(s) => s,
576 Err(_) => return f64::INFINITY,
577 };
578
579 for i in 0..n {
581 s[i + i * n] = 0.0;
582 }
583
584 for i in 0..n {
586 let row_sum: f64 = (0..n).map(|j| s[i + j * n]).sum();
587 if row_sum > 1e-10 {
588 for j in 0..n {
589 s[i + j * n] /= row_sum;
590 }
591 }
592 }
593
594 let mut mse = 0.0;
596 for i in 0..n {
597 let y_hat: f64 = (0..n).map(|j| s[i + j * n] * y[j]).sum();
598 let resid = y[i] - y_hat;
599 mse += resid * resid;
600 }
601 mse / n as f64
602}
603
604pub fn gcv_smoother(x: &[f64], y: &[f64], bandwidth: f64, kernel: &str) -> f64 {
617 let n = x.len();
618 if n < 2 || y.len() != n || bandwidth <= 0.0 {
619 return f64::INFINITY;
620 }
621
622 let s = match smoothing_matrix_nw(x, bandwidth, kernel) {
623 Ok(s) => s,
624 Err(_) => return f64::INFINITY,
625 };
626
627 let mut rss = 0.0;
629 for i in 0..n {
630 let y_hat: f64 = (0..n).map(|j| s[i + j * n] * y[j]).sum();
631 let resid = y[i] - y_hat;
632 rss += resid * resid;
633 }
634
635 let trace_s: f64 = (0..n).map(|i| s[i + i * n]).sum();
637
638 let denom = 1.0 - trace_s / n as f64;
639 if denom.abs() < 1e-10 {
640 f64::INFINITY
641 } else {
642 (rss / n as f64) / (denom * denom)
643 }
644}
645
646pub fn aic_smoother(x: &[f64], y: &[f64], bandwidth: f64, kernel: &str) -> f64 {
665 let n = x.len();
666 if n < 2 || y.len() != n || bandwidth <= 0.0 {
667 return f64::INFINITY;
668 }
669
670 let s = match smoothing_matrix_nw(x, bandwidth, kernel) {
671 Ok(s) => s,
672 Err(_) => return f64::INFINITY,
673 };
674
675 let mut rss = 0.0;
677 for i in 0..n {
678 let y_hat: f64 = (0..n).map(|j| s[i + j * n] * y[j]).sum();
679 let resid = y[i] - y_hat;
680 rss += resid * resid;
681 }
682
683 let trace_s: f64 = (0..n).map(|i| s[i + i * n]).sum();
685
686 let n_f = n as f64;
687 n_f * (rss / n_f).max(1e-300).ln() + 2.0 * trace_s
689}
690
691pub fn optim_bandwidth(
717 x: &[f64],
718 y: &[f64],
719 h_range: Option<(f64, f64)>,
720 criterion: CvCriterion,
721 kernel: &str,
722 n_grid: usize,
723) -> OptimBandwidthResult {
724 let n = x.len();
725 let n_grid = n_grid.max(2);
726
727 let (h_min, h_max) = match h_range {
729 Some((lo, hi)) if lo > 0.0 && hi > lo => (lo, hi),
730 _ => {
731 let x_min = x.iter().copied().fold(f64::INFINITY, f64::min);
732 let x_max = x.iter().copied().fold(f64::NEG_INFINITY, f64::max);
733 let h_default = (x_max - x_min) / (n as f64).powf(0.2);
734 let h_default = h_default.max(1e-10);
735 (h_default / 5.0, h_default * 5.0)
736 }
737 };
738
739 let score_fn = match criterion {
740 CvCriterion::Cv => cv_smoother,
741 CvCriterion::Gcv => gcv_smoother,
742 CvCriterion::Aic => aic_smoother,
743 };
744
745 let mut best_h = h_min;
746 let mut best_score = f64::INFINITY;
747
748 for i in 0..n_grid {
749 let h = h_min + (h_max - h_min) * i as f64 / (n_grid - 1) as f64;
750 let score = score_fn(x, y, h, kernel);
751 if score < best_score {
752 best_score = score;
753 best_h = h;
754 }
755 }
756
757 OptimBandwidthResult {
758 h_opt: best_h,
759 criterion,
760 value: best_score,
761 }
762}
763
764#[non_exhaustive]
768#[derive(Debug, Clone, PartialEq)]
769pub struct KnnCvResult {
770 pub optimal_k: usize,
772 pub cv_errors: Vec<f64>,
774}
775
776pub fn knn_gcv(x: &[f64], y: &[f64], max_k: usize) -> KnnCvResult {
786 let n = x.len();
787 let max_k = max_k.min(n.saturating_sub(1)).max(1);
788
789 let mut sorted_neighbors: Vec<Vec<(usize, f64)>> = Vec::with_capacity(n);
791 for i in 0..n {
792 let mut dists: Vec<(usize, f64)> = (0..n)
793 .filter(|&j| j != i)
794 .map(|j| (j, (x[j] - x[i]).abs()))
795 .collect();
796 dists.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
797 sorted_neighbors.push(dists);
798 }
799
800 let mut cv_errors = Vec::with_capacity(max_k);
801
802 for k in 1..=max_k {
803 let mut mse = 0.0;
804 for i in 0..n {
805 let neighbors = &sorted_neighbors[i];
806 let d_k = if k <= neighbors.len() {
808 neighbors[k - 1].1
809 } else {
810 neighbors.last().map_or(1.0, |x| x.1)
811 };
812 let d_k1 = if k < neighbors.len() {
813 neighbors[k].1
814 } else {
815 d_k * 2.0
816 };
817 let h = (d_k + d_k1) / 2.0;
818 let h = h.max(1e-10);
819
820 let mut num = 0.0;
822 let mut den = 0.0;
823 for &(j, dist) in neighbors.iter().take(k) {
824 let u = dist / h;
825 let w = epanechnikov_kernel(u);
826 num += w * y[j];
827 den += w;
828 }
829 let y_hat = if den > 1e-10 { num / den } else { y[i] };
830 mse += (y[i] - y_hat).powi(2);
831 }
832 cv_errors.push(mse / n as f64);
833 }
834
835 let optimal_k = cv_errors
836 .iter()
837 .enumerate()
838 .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
839 .map_or(1, |(i, _)| i + 1);
840
841 KnnCvResult {
842 optimal_k,
843 cv_errors,
844 }
845}
846
847pub fn knn_lcv(x: &[f64], y: &[f64], max_k: usize) -> Vec<usize> {
860 let n = x.len();
861 let max_k = max_k.min(n.saturating_sub(1)).max(1);
862
863 let mut per_obs_k = vec![1usize; n];
864
865 for i in 0..n {
866 let mut neighbors: Vec<(usize, f64)> = (0..n)
868 .filter(|&j| j != i)
869 .map(|j| (j, (x[j] - x[i]).abs()))
870 .collect();
871 neighbors.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
872
873 let mut best_k = 1;
874 let mut best_err = f64::INFINITY;
875
876 for k in 1..=max_k {
877 let sum: f64 = neighbors.iter().take(k).map(|&(j, _)| y[j]).sum();
879 let y_hat = sum / k as f64;
880 let err = (y[i] - y_hat).abs();
881 if err < best_err {
882 best_err = err;
883 best_k = k;
884 }
885 }
886 per_obs_k[i] = best_k;
887 }
888
889 per_obs_k
890}
891
892#[cfg(test)]
893mod tests {
894 use super::*;
895 use crate::test_helpers::uniform_grid;
896
897 #[test]
900 fn test_nw_constant_data() {
901 let x = uniform_grid(20);
902 let y: Vec<f64> = vec![5.0; 20];
903
904 let y_smooth = nadaraya_watson(&x, &y, &x, 0.1, "gaussian").unwrap();
905
906 for &yi in &y_smooth {
908 assert!(
909 (yi - 5.0).abs() < 0.1,
910 "Constant data should remain constant"
911 );
912 }
913 }
914
915 #[test]
916 fn test_nw_linear_data() {
917 let x = uniform_grid(50);
918 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi + 1.0).collect();
919
920 let y_smooth = nadaraya_watson(&x, &y, &x, 0.2, "gaussian").unwrap();
921
922 for i in 10..40 {
924 let expected = 2.0 * x[i] + 1.0;
925 assert!(
926 (y_smooth[i] - expected).abs() < 0.2,
927 "Linear trend should be approximately preserved"
928 );
929 }
930 }
931
932 #[test]
933 fn test_nw_gaussian_vs_epanechnikov() {
934 let x = uniform_grid(30);
935 let y: Vec<f64> = x
936 .iter()
937 .map(|&xi| (2.0 * std::f64::consts::PI * xi).sin())
938 .collect();
939
940 let y_gauss = nadaraya_watson(&x, &y, &x, 0.1, "gaussian").unwrap();
941 let y_epan = nadaraya_watson(&x, &y, &x, 0.1, "epanechnikov").unwrap();
942
943 assert_eq!(y_gauss.len(), 30);
945 assert_eq!(y_epan.len(), 30);
946
947 let diff: f64 = y_gauss
949 .iter()
950 .zip(&y_epan)
951 .map(|(a, b)| (a - b).abs())
952 .sum();
953 assert!(
954 diff > 0.0,
955 "Different kernels should give different results"
956 );
957 }
958
959 #[test]
960 fn test_nw_invalid_input() {
961 assert!(nadaraya_watson(&[], &[], &[0.5], 0.1, "gaussian").is_err());
963
964 assert!(nadaraya_watson(&[0.0, 1.0], &[1.0], &[0.5], 0.1, "gaussian").is_err());
966
967 assert!(nadaraya_watson(&[0.0, 1.0], &[1.0, 2.0], &[0.5], 0.0, "gaussian").is_err());
969
970 assert!(nadaraya_watson(&[0.0, 1.0], &[1.0, 2.0], &[], 0.1, "gaussian").is_err());
972 }
973
974 #[test]
977 fn test_ll_constant_data() {
978 let x = uniform_grid(20);
979 let y: Vec<f64> = vec![3.0; 20];
980
981 let y_smooth = local_linear(&x, &y, &x, 0.15, "gaussian").unwrap();
982
983 for &yi in &y_smooth {
984 assert!((yi - 3.0).abs() < 0.1, "Constant should remain constant");
985 }
986 }
987
988 #[test]
989 fn test_ll_linear_data_exact() {
990 let x = uniform_grid(30);
991 let y: Vec<f64> = x.iter().map(|&xi| 3.0 * xi + 2.0).collect();
992
993 let y_smooth = local_linear(&x, &y, &x, 0.2, "gaussian").unwrap();
994
995 for i in 5..25 {
997 let expected = 3.0 * x[i] + 2.0;
998 assert!(
999 (y_smooth[i] - expected).abs() < 0.1,
1000 "Local linear should fit linear data well"
1001 );
1002 }
1003 }
1004
1005 #[test]
1006 fn test_ll_invalid_input() {
1007 assert!(local_linear(&[], &[], &[0.5], 0.1, "gaussian").is_err());
1008
1009 assert!(local_linear(&[0.0, 1.0], &[1.0, 2.0], &[0.5], -0.1, "gaussian").is_err());
1010 }
1011
1012 #[test]
1015 fn test_lp_degree1_equals_local_linear() {
1016 let x = uniform_grid(25);
1017 let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
1018
1019 let y_ll = local_linear(&x, &y, &x, 0.15, "gaussian").unwrap();
1020 let y_lp = local_polynomial(&x, &y, &x, 0.15, 1, "gaussian").unwrap();
1021
1022 for i in 0..25 {
1023 assert!(
1024 (y_ll[i] - y_lp[i]).abs() < 1e-10,
1025 "Degree 1 should equal local linear"
1026 );
1027 }
1028 }
1029
1030 #[test]
1031 fn test_lp_quadratic_data() {
1032 let x = uniform_grid(40);
1033 let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
1034
1035 let y_smooth = local_polynomial(&x, &y, &x, 0.15, 2, "gaussian").unwrap();
1036
1037 for i in 8..32 {
1039 let expected = x[i] * x[i];
1040 assert!(
1041 (y_smooth[i] - expected).abs() < 0.1,
1042 "Local quadratic should fit quadratic data"
1043 );
1044 }
1045 }
1046
1047 #[test]
1048 fn test_lp_invalid_input() {
1049 let result =
1051 local_polynomial(&[0.0, 1.0], &[1.0, 2.0], &[0.5], 0.1, 0, "gaussian").unwrap();
1052 let nw = nadaraya_watson(&[0.0, 1.0], &[1.0, 2.0], &[0.5], 0.1, "gaussian").unwrap();
1053 assert_eq!(result, nw);
1054
1055 assert!(local_polynomial(&[], &[], &[0.5], 0.1, 2, "gaussian").is_err());
1057 }
1058
1059 #[test]
1062 fn test_knn_k1_nearest() {
1063 let x = vec![0.0, 0.5, 1.0];
1064 let y = vec![1.0, 2.0, 3.0];
1065
1066 let result = knn_smoother(&x, &y, &[0.1, 0.6, 0.9], 1).unwrap();
1067
1068 assert!((result[0] - 1.0).abs() < 1e-10, "0.1 nearest to 0.0 -> 1.0");
1070 assert!((result[1] - 2.0).abs() < 1e-10, "0.6 nearest to 0.5 -> 2.0");
1071 assert!((result[2] - 3.0).abs() < 1e-10, "0.9 nearest to 1.0 -> 3.0");
1072 }
1073
1074 #[test]
1075 fn test_knn_k_equals_n_is_mean() {
1076 let x = vec![0.0, 0.25, 0.5, 0.75, 1.0];
1077 let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1078 let expected_mean = 3.0;
1079
1080 let result = knn_smoother(&x, &y, &[0.5], 5).unwrap();
1081
1082 assert!(
1083 (result[0] - expected_mean).abs() < 1e-10,
1084 "k=n should return mean"
1085 );
1086 }
1087
1088 #[test]
1089 fn test_knn_invalid_input() {
1090 assert!(knn_smoother(&[], &[], &[0.5], 3).is_err());
1091
1092 assert!(knn_smoother(&[0.0, 1.0], &[1.0, 2.0], &[0.5], 0).is_err());
1093 }
1094
1095 #[test]
1098 fn test_smoothing_matrix_row_stochastic() {
1099 let x = uniform_grid(10);
1100 let s = smoothing_matrix_nw(&x, 0.2, "gaussian").unwrap();
1101
1102 assert_eq!(s.len(), 100);
1103
1104 for i in 0..10 {
1106 let row_sum: f64 = (0..10).map(|j| s[i + j * 10]).sum();
1107 assert!(
1108 (row_sum - 1.0).abs() < 1e-10,
1109 "Row {} should sum to 1, got {}",
1110 i,
1111 row_sum
1112 );
1113 }
1114 }
1115
1116 #[test]
1117 fn test_smoothing_matrix_invalid_input() {
1118 assert!(smoothing_matrix_nw(&[], 0.1, "gaussian").is_err());
1119
1120 assert!(smoothing_matrix_nw(&[0.0, 1.0], 0.0, "gaussian").is_err());
1121 }
1122
1123 #[test]
1124 fn test_nan_nw_no_panic() {
1125 let x = vec![0.0, 0.25, 0.5, 0.75, 1.0];
1126 let mut y = vec![0.0, 1.0, 2.0, 1.0, 0.0];
1127 y[2] = f64::NAN;
1128 let result = nadaraya_watson(&x, &y, &x, 0.3, "gaussian").unwrap();
1129 assert_eq!(result.len(), x.len());
1130 }
1132
1133 #[test]
1134 fn test_n1_smoother() {
1135 let x = vec![0.5];
1137 let y = vec![3.0];
1138 let x_new = vec![0.5];
1139 let result = nadaraya_watson(&x, &y, &x_new, 0.3, "gaussian").unwrap();
1140 assert_eq!(result.len(), 1);
1141 assert!(
1142 (result[0] - 3.0).abs() < 1e-6,
1143 "Single point smoother should return the value"
1144 );
1145 }
1146
1147 #[test]
1148 fn test_duplicate_x_smoother() {
1149 let x = vec![0.0, 0.0, 0.5, 1.0, 1.0];
1151 let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1152 let x_new = vec![0.0, 0.5, 1.0];
1153 let result = nadaraya_watson(&x, &y, &x_new, 0.3, "gaussian").unwrap();
1154 assert_eq!(result.len(), 3);
1155 for v in &result {
1156 assert!(v.is_finite());
1157 }
1158 }
1159
1160 #[test]
1163 fn test_cv_smoother_linear_data() {
1164 let x = uniform_grid(30);
1165 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi + 1.0).collect();
1166 let cv = cv_smoother(&x, &y, 0.2, "gaussian");
1167 assert!(cv.is_finite());
1168 assert!(cv >= 0.0);
1169 assert!(cv < 1.0, "CV error for smooth linear data should be small");
1170 }
1171
1172 #[test]
1173 fn test_cv_smoother_invalid() {
1174 assert_eq!(cv_smoother(&[], &[], 0.1, "gaussian"), f64::INFINITY);
1175 assert_eq!(
1176 cv_smoother(&[0.0, 1.0], &[1.0, 2.0], -0.1, "gaussian"),
1177 f64::INFINITY
1178 );
1179 }
1180
1181 #[test]
1182 fn test_gcv_smoother_linear_data() {
1183 let x = uniform_grid(30);
1184 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi + 1.0).collect();
1185 let gcv = gcv_smoother(&x, &y, 0.2, "gaussian");
1186 assert!(gcv.is_finite());
1187 assert!(gcv >= 0.0);
1188 }
1189
1190 #[test]
1191 fn test_gcv_smoother_invalid() {
1192 assert_eq!(gcv_smoother(&[], &[], 0.1, "gaussian"), f64::INFINITY);
1193 }
1194
1195 #[test]
1196 fn test_optim_bandwidth_returns_valid() {
1197 let x = uniform_grid(30);
1198 let y: Vec<f64> = x
1199 .iter()
1200 .map(|&xi| (2.0 * std::f64::consts::PI * xi).sin())
1201 .collect();
1202
1203 let result = optim_bandwidth(&x, &y, None, CvCriterion::Gcv, "gaussian", 20);
1204 assert!(result.h_opt > 0.0);
1205 assert!(result.value.is_finite());
1206 assert_eq!(result.criterion, CvCriterion::Gcv);
1207 }
1208
1209 #[test]
1210 fn test_optim_bandwidth_cv_vs_gcv() {
1211 let x = uniform_grid(25);
1212 let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
1213
1214 let cv_result = optim_bandwidth(&x, &y, None, CvCriterion::Cv, "gaussian", 20);
1215 let gcv_result = optim_bandwidth(&x, &y, None, CvCriterion::Gcv, "gaussian", 20);
1216
1217 assert!(cv_result.h_opt > 0.0);
1218 assert!(gcv_result.h_opt > 0.0);
1219 }
1220
1221 #[test]
1222 fn test_optim_bandwidth_custom_range() {
1223 let x = uniform_grid(20);
1224 let y: Vec<f64> = x.to_vec();
1225 let result = optim_bandwidth(&x, &y, Some((0.05, 0.5)), CvCriterion::Cv, "gaussian", 10);
1226 assert!(result.h_opt >= 0.05);
1227 assert!(result.h_opt <= 0.5);
1228 }
1229
1230 #[test]
1233 fn test_aic_smoother_matches_hand_computed() {
1234 let x = vec![0.0, 0.25, 0.5, 0.75, 1.0];
1237 let y = vec![0.1, 0.4, 0.35, 0.8, 1.2];
1238 let bandwidth = 0.3;
1239 let kernel = "gaussian";
1240
1241 let s = smoothing_matrix_nw(&x, bandwidth, kernel).unwrap();
1242 let n = x.len();
1243 let mut rss = 0.0;
1244 for i in 0..n {
1245 let y_hat: f64 = (0..n).map(|j| s[i + j * n] * y[j]).sum();
1246 let resid = y[i] - y_hat;
1247 rss += resid * resid;
1248 }
1249 let trace_s: f64 = (0..n).map(|i| s[i + i * n]).sum();
1250 let n_f = n as f64;
1251 let expected = n_f * (rss / n_f).max(1e-300).ln() + 2.0 * trace_s;
1252
1253 let got = aic_smoother(&x, &y, bandwidth, kernel);
1254 assert!(
1255 (got - expected).abs() < 1e-12,
1256 "got={got}, expected={expected}"
1257 );
1258 }
1259
1260 #[test]
1261 fn test_aic_smoother_invalid_inputs() {
1262 assert_eq!(aic_smoother(&[0.0], &[1.0], 0.3, "gaussian"), f64::INFINITY);
1264 assert_eq!(
1265 aic_smoother(&[0.0, 1.0], &[1.0], 0.3, "gaussian"),
1266 f64::INFINITY
1267 );
1268 assert_eq!(
1269 aic_smoother(&[0.0, 1.0], &[1.0, 2.0], 0.0, "gaussian"),
1270 f64::INFINITY
1271 );
1272 }
1273
1274 #[test]
1275 fn test_optim_bandwidth_aic_matches_brute_force_grid() {
1276 let x = uniform_grid(25);
1279 let y: Vec<f64> = x
1280 .iter()
1281 .map(|&xi| (2.0 * std::f64::consts::PI * xi).sin())
1282 .collect();
1283 let kernel = "gaussian";
1284 let n_grid = 20;
1285
1286 let n = x.len();
1288 let x_min = x.iter().copied().fold(f64::INFINITY, f64::min);
1289 let x_max = x.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1290 let h_default = ((x_max - x_min) / (n as f64).powf(0.2)).max(1e-10);
1291 let (h_min, h_max) = (h_default / 5.0, h_default * 5.0);
1292
1293 let mut best_h = h_min;
1294 let mut best_score = f64::INFINITY;
1295 for i in 0..n_grid {
1296 let h = h_min + (h_max - h_min) * i as f64 / (n_grid - 1) as f64;
1297 let score = aic_smoother(&x, &y, h, kernel);
1298 if score < best_score {
1299 best_score = score;
1300 best_h = h;
1301 }
1302 }
1303
1304 let result = optim_bandwidth(&x, &y, None, CvCriterion::Aic, kernel, n_grid);
1305 assert_eq!(result.criterion, CvCriterion::Aic);
1306 assert_eq!(result.h_opt, best_h);
1307 assert_eq!(result.value, best_score);
1308 }
1309
1310 #[test]
1311 fn test_optim_bandwidth_aic_diverges_from_gcv() {
1312 let n = 50usize;
1319 let x: Vec<f64> = (0..n).map(|i| i as f64 / (n as f64 - 1.0)).collect();
1320 let mut seed = 999u64;
1322 let mut lcg = || {
1323 seed = seed
1324 .wrapping_mul(6364136223846793005)
1325 .wrapping_add(1442695040888963407);
1326 ((seed >> 11) as f64) / ((1u64 << 53) as f64) - 0.5
1327 };
1328 let y: Vec<f64> = x
1329 .iter()
1330 .map(|&xi| (2.0 * std::f64::consts::PI * xi).sin() + 0.5 * lcg())
1331 .collect();
1332 let kernel = "gaussian";
1333 let range = Some((0.01, 0.5));
1334
1335 let aic = optim_bandwidth(&x, &y, range, CvCriterion::Aic, kernel, 60);
1336 let gcv = optim_bandwidth(&x, &y, range, CvCriterion::Gcv, kernel, 60);
1337 assert!(aic.h_opt.is_finite() && gcv.h_opt.is_finite());
1338 assert_ne!(
1339 aic.h_opt, gcv.h_opt,
1340 "AIC and GCV selected the same bandwidth; expected divergence"
1341 );
1342 assert!(
1344 aic.h_opt < gcv.h_opt,
1345 "expected AIC to pick a smaller bandwidth than GCV: aic={}, gcv={}",
1346 aic.h_opt,
1347 gcv.h_opt
1348 );
1349 }
1350
1351 #[test]
1352 fn test_gcv_cv_unchanged_by_aic_addition() {
1353 let x = uniform_grid(25);
1355 let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
1356 let kernel = "gaussian";
1357 let h = 0.2;
1358 let n = x.len();
1360 let s = smoothing_matrix_nw(&x, h, kernel).unwrap();
1361 let mut rss = 0.0;
1362 for i in 0..n {
1363 let y_hat: f64 = (0..n).map(|j| s[i + j * n] * y[j]).sum();
1364 rss += (y[i] - y_hat).powi(2);
1365 }
1366 let trace_s: f64 = (0..n).map(|i| s[i + i * n]).sum();
1367 let denom = 1.0 - trace_s / n as f64;
1368 let expected_gcv = (rss / n as f64) / (denom * denom);
1369 assert!((gcv_smoother(&x, &y, h, kernel) - expected_gcv).abs() < 1e-12);
1370 }
1371
1372 #[test]
1375 fn test_knn_gcv_returns_valid() {
1376 let x = uniform_grid(20);
1377 let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
1378
1379 let result = knn_gcv(&x, &y, 10);
1380 assert!(result.optimal_k >= 1);
1381 assert!(result.optimal_k <= 10);
1382 assert_eq!(result.cv_errors.len(), 10);
1383 for &e in &result.cv_errors {
1384 assert!(e.is_finite());
1385 assert!(e >= 0.0);
1386 }
1387 }
1388
1389 #[test]
1390 fn test_knn_gcv_constant_data() {
1391 let x = uniform_grid(15);
1392 let y = vec![5.0; 15];
1393 let result = knn_gcv(&x, &y, 5);
1394 for &e in &result.cv_errors {
1396 assert!(e < 0.01, "Constant data: CV error should be near zero");
1397 }
1398 }
1399
1400 #[test]
1401 fn test_knn_lcv_returns_valid() {
1402 let x = uniform_grid(15);
1403 let y: Vec<f64> = x.to_vec();
1404
1405 let result = knn_lcv(&x, &y, 5);
1406 assert_eq!(result.len(), 15);
1407 for &k in &result {
1408 assert!(k >= 1);
1409 assert!(k <= 5);
1410 }
1411 }
1412
1413 #[test]
1414 fn test_knn_lcv_constant_data() {
1415 let x = uniform_grid(10);
1416 let y = vec![3.0; 10];
1417 let result = knn_lcv(&x, &y, 5);
1418 assert_eq!(result.len(), 10);
1419 for &k in &result {
1422 assert!(k >= 1);
1423 }
1424 }
1425
1426 #[test]
1429 fn test_tricube_kernel_values() {
1430 let k0 = tricube_kernel(0.0);
1432 assert!((k0 - 1.0).abs() < 1e-10, "tricube(0) should be 1.0");
1433
1434 assert_eq!(tricube_kernel(1.0), 0.0, "tricube(1) should be 0");
1436 assert_eq!(tricube_kernel(-1.0), 0.0, "tricube(-1) should be 0");
1437 assert_eq!(tricube_kernel(2.0), 0.0, "tricube(2) should be 0");
1438
1439 let k05 = tricube_kernel(0.5);
1441 assert!(k05 > 0.0 && k05 < 1.0, "tricube(0.5) should be in (0, 1)");
1442 }
1443
1444 #[test]
1445 fn test_nw_tricube_constant_data() {
1446 let x = uniform_grid(20);
1447 let y = vec![5.0; 20];
1448
1449 let y_smooth = nadaraya_watson(&x, &y, &x, 0.2, "tricube").unwrap();
1450
1451 for &yi in &y_smooth {
1452 assert!(
1453 (yi - 5.0).abs() < 0.1,
1454 "Tricube NW: constant data should remain constant"
1455 );
1456 }
1457 }
1458
1459 #[test]
1460 fn test_nw_tricube_linear_data() {
1461 let x = uniform_grid(50);
1462 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi + 1.0).collect();
1463
1464 let y_smooth = nadaraya_watson(&x, &y, &x, 0.2, "tricube").unwrap();
1465
1466 for i in 10..40 {
1468 let expected = 2.0 * x[i] + 1.0;
1469 assert!(
1470 (y_smooth[i] - expected).abs() < 0.3,
1471 "Tricube NW: linear trend should be approximately preserved at i={i}"
1472 );
1473 }
1474 }
1475
1476 #[test]
1477 fn test_nw_tricube_vs_gaussian() {
1478 let x = uniform_grid(30);
1479 let y: Vec<f64> = x
1480 .iter()
1481 .map(|&xi| (2.0 * std::f64::consts::PI * xi).sin())
1482 .collect();
1483
1484 let y_gauss = nadaraya_watson(&x, &y, &x, 0.15, "gaussian").unwrap();
1485 let y_tri = nadaraya_watson(&x, &y, &x, 0.15, "tricube").unwrap();
1486
1487 assert_eq!(y_gauss.len(), y_tri.len());
1488
1489 assert!(y_tri.iter().all(|v| v.is_finite()));
1491
1492 let diff: f64 = y_gauss.iter().zip(&y_tri).map(|(a, b)| (a - b).abs()).sum();
1494 assert!(
1495 diff > 0.0,
1496 "Gaussian and tricube kernels should give different results"
1497 );
1498 }
1499
1500 #[test]
1501 fn test_nw_tricube_vs_epanechnikov() {
1502 let x = uniform_grid(30);
1503 let y: Vec<f64> = x
1504 .iter()
1505 .map(|&xi| (2.0 * std::f64::consts::PI * xi).sin())
1506 .collect();
1507
1508 let y_epan = nadaraya_watson(&x, &y, &x, 0.15, "epanechnikov").unwrap();
1509 let y_tri = nadaraya_watson(&x, &y, &x, 0.15, "tricube").unwrap();
1510
1511 assert!(y_epan.iter().all(|v| v.is_finite()));
1513 assert!(y_tri.iter().all(|v| v.is_finite()));
1514
1515 let diff: f64 = y_epan.iter().zip(&y_tri).map(|(a, b)| (a - b).abs()).sum();
1517 assert!(
1518 diff > 0.0,
1519 "Epanechnikov and tricube should give different results"
1520 );
1521 }
1522
1523 #[test]
1524 fn test_ll_tricube_constant_data() {
1525 let x = uniform_grid(20);
1526 let y = vec![3.0; 20];
1527
1528 let y_smooth = local_linear(&x, &y, &x, 0.2, "tricube").unwrap();
1529
1530 for &yi in &y_smooth {
1531 assert!(
1532 (yi - 3.0).abs() < 0.1,
1533 "Tricube LL: constant should remain constant"
1534 );
1535 }
1536 }
1537
1538 #[test]
1539 fn test_ll_tricube_linear_data() {
1540 let x = uniform_grid(30);
1541 let y: Vec<f64> = x.iter().map(|&xi| 3.0 * xi + 2.0).collect();
1542
1543 let y_smooth = local_linear(&x, &y, &x, 0.2, "tricube").unwrap();
1544
1545 for i in 5..25 {
1547 let expected = 3.0 * x[i] + 2.0;
1548 assert!(
1549 (y_smooth[i] - expected).abs() < 0.2,
1550 "Tricube LL: should fit linear data well at i={i}"
1551 );
1552 }
1553 }
1554
1555 #[test]
1556 fn test_ll_tricube_vs_gaussian() {
1557 let x = uniform_grid(30);
1558 let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
1559
1560 let y_gauss = local_linear(&x, &y, &x, 0.15, "gaussian").unwrap();
1561 let y_tri = local_linear(&x, &y, &x, 0.15, "tricube").unwrap();
1562
1563 assert_eq!(y_gauss.len(), y_tri.len());
1564 assert!(y_tri.iter().all(|v| v.is_finite()));
1565
1566 let diff: f64 = y_gauss.iter().zip(&y_tri).map(|(a, b)| (a - b).abs()).sum();
1567 assert!(
1568 diff > 0.0,
1569 "Gaussian and tricube local linear should differ"
1570 );
1571 }
1572
1573 #[test]
1574 fn test_lp_tricube_quadratic() {
1575 let x = uniform_grid(40);
1576 let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
1577
1578 let y_smooth = local_polynomial(&x, &y, &x, 0.15, 2, "tricube").unwrap();
1579
1580 for i in 8..32 {
1582 let expected = x[i] * x[i];
1583 assert!(
1584 (y_smooth[i] - expected).abs() < 0.15,
1585 "Tricube LP: should fit quadratic data at i={i}"
1586 );
1587 }
1588 }
1589
1590 #[test]
1591 fn test_get_kernel_tricube_aliases() {
1592 let k1 = get_kernel("tricube");
1594 let k2 = get_kernel("tri-cube");
1595
1596 let test_val = 0.5;
1597 assert!(
1598 (k1(test_val) - k2(test_val)).abs() < 1e-15,
1599 "Both tricube aliases should give the same result"
1600 );
1601 }
1602
1603 #[test]
1604 fn test_smoothing_matrix_tricube() {
1605 let x = uniform_grid(10);
1606 let s = smoothing_matrix_nw(&x, 0.2, "tricube").unwrap();
1607
1608 assert_eq!(s.len(), 100);
1609
1610 for i in 0..10 {
1612 let row_sum: f64 = (0..10).map(|j| s[i + j * 10]).sum();
1613 assert!(
1614 (row_sum - 1.0).abs() < 1e-10,
1615 "Tricube: row {} should sum to 1, got {}",
1616 i,
1617 row_sum
1618 );
1619 }
1620 }
1621
1622 #[test]
1623 fn test_cv_smoother_tricube() {
1624 let x = uniform_grid(30);
1625 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi + 1.0).collect();
1626 let cv = cv_smoother(&x, &y, 0.2, "tricube");
1627 assert!(cv.is_finite());
1628 assert!(cv >= 0.0);
1629 }
1630
1631 #[test]
1632 fn test_optim_bandwidth_tricube() {
1633 let x = uniform_grid(25);
1634 let y: Vec<f64> = x
1635 .iter()
1636 .map(|&xi| (2.0 * std::f64::consts::PI * xi).sin())
1637 .collect();
1638
1639 let result = optim_bandwidth(&x, &y, None, CvCriterion::Gcv, "tricube", 20);
1640 assert!(result.h_opt > 0.0);
1641 assert!(result.value.is_finite());
1642 }
1643}