1pub const NUMERICAL_EPS: f64 = 1e-10;
5
6pub const DEFAULT_CONVERGENCE_TOL: f64 = 1e-6;
8
9pub fn sort_nan_safe(slice: &mut [f64]) {
11 slice.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
12}
13
14pub fn extract_curves(data: &crate::matrix::FdMatrix) -> Vec<Vec<f64>> {
25 data.rows()
26}
27
28pub fn l2_distance(curve1: &[f64], curve2: &[f64], weights: &[f64]) -> f64 {
38 let mut dist_sq = 0.0;
39 for i in 0..curve1.len() {
40 let diff = curve1[i] - curve2[i];
41 dist_sq += diff * diff * weights[i];
42 }
43 dist_sq.sqrt()
44}
45
46pub fn simpsons_weights(argvals: &[f64]) -> Vec<f64> {
58 let n = argvals.len();
59 if n < 2 {
60 return vec![1.0; n];
61 }
62
63 let mut weights = vec![0.0; n];
64
65 if n == 2 {
66 let h = argvals[1] - argvals[0];
68 weights[0] = h / 2.0;
69 weights[1] = h / 2.0;
70 return weights;
71 }
72
73 let h0 = argvals[1] - argvals[0];
75 let is_uniform = argvals
76 .windows(2)
77 .all(|w| ((w[1] - w[0]) - h0).abs() < 1e-12 * h0.abs());
78
79 if is_uniform {
80 simpsons_weights_uniform(&mut weights, n, h0);
81 } else {
82 simpsons_weights_nonuniform(&mut weights, argvals, n);
83 }
84
85 weights
86}
87
88fn simpsons_weights_uniform(weights: &mut [f64], n: usize, h0: f64) {
90 let n_intervals = n - 1;
91 if n_intervals % 2 == 0 {
92 weights[0] = h0 / 3.0;
94 weights[n - 1] = h0 / 3.0;
95 for i in 1..n - 1 {
96 weights[i] = if i % 2 == 1 {
97 4.0 * h0 / 3.0
98 } else {
99 2.0 * h0 / 3.0
100 };
101 }
102 } else {
103 let n_simp = n - 1;
105 weights[0] = h0 / 3.0;
106 weights[n_simp - 1] = h0 / 3.0;
107 for i in 1..n_simp - 1 {
108 weights[i] = if i % 2 == 1 {
109 4.0 * h0 / 3.0
110 } else {
111 2.0 * h0 / 3.0
112 };
113 }
114 weights[n_simp - 1] += h0 / 2.0;
115 weights[n - 1] += h0 / 2.0;
116 }
117}
118
119fn simpsons_weights_nonuniform(weights: &mut [f64], argvals: &[f64], n: usize) {
121 let n_intervals = n - 1;
122 let n_pairs = n_intervals / 2;
123
124 for k in 0..n_pairs {
125 let i0 = 2 * k;
126 let i1 = i0 + 1;
127 let i2 = i0 + 2;
128 let h1 = argvals[i1] - argvals[i0];
129 let h2 = argvals[i2] - argvals[i1];
130 let h_sum = h1 + h2;
131
132 weights[i0] += (2.0 * h1 - h2) * h_sum / (6.0 * h1);
133 weights[i1] += h_sum * h_sum * h_sum / (6.0 * h1 * h2);
134 weights[i2] += (2.0 * h2 - h1) * h_sum / (6.0 * h2);
135 }
136
137 if n_intervals % 2 == 1 {
138 let h_last = argvals[n - 1] - argvals[n - 2];
139 weights[n - 2] += h_last / 2.0;
140 weights[n - 1] += h_last / 2.0;
141 }
142}
143
144pub fn simpsons_weights_2d(argvals_s: &[f64], argvals_t: &[f64]) -> Vec<f64> {
155 let weights_s = simpsons_weights(argvals_s);
156 let weights_t = simpsons_weights(argvals_t);
157 let m1 = argvals_s.len();
158 let m2 = argvals_t.len();
159
160 let mut weights = vec![0.0; m1 * m2];
161 for i in 0..m1 {
162 for j in 0..m2 {
163 weights[i + j * m1] = weights_s[i] * weights_t[j];
164 }
165 }
166 weights
167}
168
169pub fn linear_interp(x: &[f64], y: &[f64], t: f64) -> f64 {
173 if t <= x[0] {
174 return y[0];
175 }
176 let last = x.len() - 1;
177 if t >= x[last] {
178 return y[last];
179 }
180
181 let idx = match x.binary_search_by(|v| v.partial_cmp(&t).unwrap_or(std::cmp::Ordering::Equal)) {
182 Ok(i) => return y[i],
183 Err(i) => i,
184 };
185
186 let t0 = x[idx - 1];
187 let t1 = x[idx];
188 let y0 = y[idx - 1];
189 let y1 = y[idx];
190 y0 + (y1 - y0) * (t - t0) / (t1 - t0)
191}
192
193pub fn cumulative_trapz(y: &[f64], x: &[f64]) -> Vec<f64> {
198 let n = y.len();
199 let mut out = vec![0.0; n];
200 if n < 2 {
201 return out;
202 }
203
204 let mut k = 1;
206 while k + 1 < n {
207 let h1 = x[k] - x[k - 1];
208 let h2 = x[k + 1] - x[k];
209 let h_sum = h1 + h2;
210
211 let integral = h_sum / 6.0
213 * (y[k - 1] * (2.0 * h1 - h2) / h1
214 + y[k] * h_sum * h_sum / (h1 * h2)
215 + y[k + 1] * (2.0 * h2 - h1) / h2);
216
217 out[k] = out[k - 1] + {
218 0.5 * (y[k] + y[k - 1]) * h1
220 };
221 out[k + 1] = out[k - 1] + integral;
222 k += 2;
223 }
224
225 if k < n {
227 out[k] = out[k - 1] + 0.5 * (y[k] + y[k - 1]) * (x[k] - x[k - 1]);
228 }
229
230 out
231}
232
233pub fn trapz(y: &[f64], x: &[f64]) -> f64 {
235 let mut sum = 0.0;
236 for k in 1..y.len() {
237 sum += 0.5 * (y[k] + y[k - 1]) * (x[k] - x[k - 1]);
238 }
239 sum
240}
241
242pub fn gaussian_kernel(d: f64, h: f64) -> f64 {
248 if h < 1e-15 {
249 return 0.0;
250 }
251 (-d * d / (2.0 * h * h)).exp()
252}
253
254pub fn bandwidth_candidates_from_dists(dists: &[f64], n: usize, n_quantiles: usize) -> Vec<f64> {
260 let mut nonzero: Vec<f64> = (0..n)
261 .flat_map(|i| ((i + 1)..n).map(move |j| dists[i * n + j]))
262 .filter(|&d| d > 0.0)
263 .collect();
264 sort_nan_safe(&mut nonzero);
265
266 if nonzero.is_empty() {
267 return Vec::new();
268 }
269
270 (1..=n_quantiles)
271 .map(|q| {
272 let p = q as f64 / (n_quantiles + 1) as f64;
273 let idx = ((nonzero.len() as f64 * p) as usize).min(nonzero.len() - 1);
274 nonzero[idx]
275 })
276 .filter(|&h| h > 1e-15)
277 .collect()
278}
279
280pub fn quantile_sorted(sorted: &[f64], p: f64) -> f64 {
284 if sorted.is_empty() {
285 return f64::NAN;
286 }
287 if sorted.len() == 1 || p <= 0.0 {
288 return sorted[0];
289 }
290 if p >= 1.0 {
291 return sorted[sorted.len() - 1];
292 }
293 let pos = p * (sorted.len() - 1) as f64;
294 let lo = pos.floor() as usize;
295 let hi = (lo + 1).min(sorted.len() - 1);
296 let frac = pos - lo as f64;
297 sorted[lo] * (1.0 - frac) + sorted[hi] * frac
298}
299
300pub fn r_squared(y_true: &[f64], residuals: &[f64]) -> f64 {
302 let n = y_true.len();
303 if n == 0 {
304 return f64::NAN;
305 }
306 let mean = y_true.iter().sum::<f64>() / n as f64;
307 let ss_tot: f64 = y_true.iter().map(|&y| (y - mean).powi(2)).sum();
308 let ss_res: f64 = residuals.iter().map(|r| r * r).sum();
309 if ss_tot > 1e-15 {
310 1.0 - ss_res / ss_tot
311 } else {
312 0.0
313 }
314}
315
316pub fn r_squared_adj(y_true: &[f64], residuals: &[f64], p: usize) -> f64 {
318 let n = y_true.len();
319 let r2 = r_squared(y_true, residuals);
320 if n <= p + 1 {
321 return r2;
322 }
323 1.0 - (1.0 - r2) * (n - 1) as f64 / (n - p - 1) as f64
324}
325
326pub fn aic(n: usize, rss: f64, p: usize) -> f64 {
330 let nf = n as f64;
331 nf * (rss / nf).ln() + 2.0 * p as f64
332}
333
334pub fn bic(n: usize, rss: f64, p: usize) -> f64 {
338 let nf = n as f64;
339 nf * (rss / nf).ln() + nf.ln() * p as f64
340}
341
342#[derive(Debug, Clone, Copy, PartialEq)]
344#[non_exhaustive]
345pub enum InterpolationMethod {
346 Linear,
348 CubicHermite,
350}
351
352#[must_use]
366pub fn fdata_interpolate(
367 data: &crate::matrix::FdMatrix,
368 argvals: &[f64],
369 new_argvals: &[f64],
370 method: InterpolationMethod,
371) -> crate::matrix::FdMatrix {
372 let (n, m) = data.shape();
373 let m_new = new_argvals.len();
374 if n == 0 || m < 2 || m_new == 0 {
375 return crate::matrix::FdMatrix::zeros(n.max(1), m_new.max(1));
376 }
377
378 let mut result = crate::matrix::FdMatrix::zeros(n, m_new);
379
380 for i in 0..n {
381 let y: Vec<f64> = (0..m).map(|j| data[(i, j)]).collect();
382 for (j, &t) in new_argvals.iter().enumerate() {
383 result[(i, j)] = match method {
384 InterpolationMethod::Linear => linear_interp(argvals, &y, t),
385 InterpolationMethod::CubicHermite => cubic_hermite_interp(argvals, &y, t),
386 };
387 }
388 }
389
390 result
391}
392
393pub fn spline_interpolate(
417 data: &crate::matrix::FdMatrix,
418 argvals: &[f64],
419 query_points: &[f64],
420 order: usize,
421) -> Result<crate::matrix::FdMatrix, crate::FdarError> {
422 let (n, m) = data.shape();
423
424 if argvals.len() != m {
426 return Err(crate::FdarError::InvalidDimension {
427 parameter: "argvals",
428 expected: format!("{m}"),
429 actual: format!("{}", argvals.len()),
430 });
431 }
432 if query_points.is_empty() {
433 return Err(crate::FdarError::InvalidDimension {
434 parameter: "query_points",
435 expected: ">= 1".to_string(),
436 actual: "0".to_string(),
437 });
438 }
439 if order == 0 || order >= m {
440 return Err(crate::FdarError::InvalidParameter {
441 parameter: "order",
442 message: format!("must be in [1, {m}), got {order}"),
443 });
444 }
445 let t_min = argvals[0];
446 let t_max = argvals[m - 1];
447 for &q in query_points {
448 if q < t_min || q > t_max {
449 return Err(crate::FdarError::InvalidParameter {
450 parameter: "query_points",
451 message: format!(
452 "all query points must lie in [{t_min}, {t_max}]; found {q} which is outside the interpolation domain"
453 ),
454 });
455 }
456 }
457
458 let nknots = m.saturating_sub(order).max(2);
461 let knots = crate::basis::bspline::construct_bspline_knots(t_min, t_max, nknots, order);
462
463 let basis_vals = crate::basis::bspline::bspline_basis(argvals, nknots, order);
465 let nbasis = basis_vals.len() / m;
466
467 let b_mat = nalgebra::DMatrix::from_column_slice(m, nbasis, &basis_vals);
469
470 let tol = NUMERICAL_EPS * b_mat.nrows().max(b_mat.ncols()) as f64;
473 let svd = nalgebra::SVD::new(b_mat.clone(), true, true);
474 let pinv = svd
475 .pseudo_inverse(tol)
476 .map_err(|e| crate::FdarError::ComputationFailed {
477 operation: "spline_interpolate SVD pseudoinverse",
478 detail: e.to_string(),
479 })?;
480 let m_q = query_points.len();
485 let basis_query = crate::basis::bspline::bspline_basis_from_knots(query_points, &knots, order);
486
487 let mut out = crate::matrix::FdMatrix::zeros(n, m_q);
489 for i in 0..n {
490 let y_vec: Vec<f64> = (0..m).map(|j| data[(i, j)]).collect();
492 let y_col = nalgebra::DVector::from_vec(y_vec);
493
494 let coefs = &pinv * y_col;
496
497 for j in 0..m_q {
499 let mut val = 0.0;
500 for k in 0..nbasis {
501 val += coefs[k] * basis_query[j + k * m_q];
502 }
503 out[(i, j)] = val;
504 }
505 }
506
507 Ok(out)
508}
509
510fn cubic_hermite_interp(x: &[f64], y: &[f64], t: f64) -> f64 {
514 let n = x.len();
515 if n < 2 {
516 return if n == 1 { y[0] } else { 0.0 };
517 }
518
519 if t <= x[0] {
521 return y[0];
522 }
523 if t >= x[n - 1] {
524 return y[n - 1];
525 }
526
527 let k = match x.binary_search_by(|v| v.partial_cmp(&t).unwrap_or(std::cmp::Ordering::Equal)) {
529 Ok(i) => return y[i],
530 Err(i) => {
531 if i == 0 {
532 0
533 } else {
534 i - 1
535 }
536 }
537 };
538
539 let slopes: Vec<f64> = x
541 .windows(2)
542 .zip(y.windows(2))
543 .map(|(xw, yw)| (yw[1] - yw[0]) / (xw[1] - xw[0]))
544 .collect();
545
546 let mut tangents = vec![0.0; n];
548 tangents[0] = slopes[0];
549 tangents[n - 1] = slopes[n - 2];
550 for i in 1..n - 1 {
551 if slopes[i - 1].signum() != slopes[i].signum() {
552 tangents[i] = 0.0;
553 } else {
554 tangents[i] = (slopes[i - 1] + slopes[i]) / 2.0;
555 }
556 }
557
558 let h = x[k + 1] - x[k];
560 let s = (t - x[k]) / h;
561 let s2 = s * s;
562 let s3 = s2 * s;
563
564 let h00 = 2.0 * s3 - 3.0 * s2 + 1.0;
565 let h10 = s3 - 2.0 * s2 + s;
566 let h01 = -2.0 * s3 + 3.0 * s2;
567 let h11 = s3 - s2;
568
569 h00 * y[k] + h10 * h * tangents[k] + h01 * y[k + 1] + h11 * h * tangents[k + 1]
570}
571
572pub fn gradient_uniform(y: &[f64], h: f64) -> Vec<f64> {
579 let n = y.len();
580 let mut g = vec![0.0; n];
581 if n < 2 {
582 return g;
583 }
584 if n == 2 {
585 g[0] = (y[1] - y[0]) / h;
586 g[1] = (y[1] - y[0]) / h;
587 return g;
588 }
589 if n == 3 {
590 g[0] = (-3.0 * y[0] + 4.0 * y[1] - y[2]) / (2.0 * h);
591 g[1] = (y[2] - y[0]) / (2.0 * h);
592 g[2] = (y[0] - 4.0 * y[1] + 3.0 * y[2]) / (2.0 * h);
593 return g;
594 }
595 if n == 4 {
596 g[0] = (-3.0 * y[0] + 4.0 * y[1] - y[2]) / (2.0 * h);
597 g[1] = (y[2] - y[0]) / (2.0 * h);
598 g[2] = (y[3] - y[1]) / (2.0 * h);
599 g[3] = (y[1] - 4.0 * y[2] + 3.0 * y[3]) / (2.0 * h);
600 return g;
601 }
602
603 g[0] = (-25.0 * y[0] + 48.0 * y[1] - 36.0 * y[2] + 16.0 * y[3] - 3.0 * y[4]) / (12.0 * h);
606 g[1] = (-3.0 * y[0] - 10.0 * y[1] + 18.0 * y[2] - 6.0 * y[3] + y[4]) / (12.0 * h);
607
608 for i in 2..n - 2 {
610 g[i] = (-y[i + 2] + 8.0 * y[i + 1] - 8.0 * y[i - 1] + y[i - 2]) / (12.0 * h);
611 }
612
613 g[n - 2] = (-y[n - 5] + 6.0 * y[n - 4] - 18.0 * y[n - 3] + 10.0 * y[n - 2] + 3.0 * y[n - 1])
615 / (12.0 * h);
616 g[n - 1] = (3.0 * y[n - 5] - 16.0 * y[n - 4] + 36.0 * y[n - 3] - 48.0 * y[n - 2]
617 + 25.0 * y[n - 1])
618 / (12.0 * h);
619 g
620}
621
622pub fn gradient_nonuniform(y: &[f64], t: &[f64]) -> Vec<f64> {
630 let n = y.len();
631 assert_eq!(n, t.len(), "y and t must have the same length");
632 let mut g = vec![0.0; n];
633 if n < 2 {
634 return g;
635 }
636 if n == 2 {
637 let h = t[1] - t[0];
638 if h.abs() < 1e-15 {
639 return g;
640 }
641 g[0] = (y[1] - y[0]) / h;
642 g[1] = g[0];
643 return g;
644 }
645
646 let h0 = t[1] - t[0];
648 let h1 = t[2] - t[0];
649 if h0.abs() > 1e-15 && h1.abs() > 1e-15 && (h1 - h0).abs() > 1e-15 {
650 g[0] = y[0] * (-h1 - h0) / (h0 * h1) + y[1] * h1 / (h0 * (h1 - h0))
651 - y[2] * h0 / (h1 * (h1 - h0));
652 } else {
653 g[0] = (y[1] - y[0]) / h0.max(1e-15);
654 }
655
656 for i in 1..n - 1 {
658 let h_l = t[i] - t[i - 1];
659 let h_r = t[i + 1] - t[i];
660 let h_sum = h_l + h_r;
661 if h_l.abs() < 1e-15 || h_r.abs() < 1e-15 || h_sum.abs() < 1e-15 {
662 g[i] = 0.0;
663 continue;
664 }
665 g[i] = -y[i - 1] * h_r / (h_l * h_sum)
666 + y[i] * (h_r - h_l) / (h_l * h_r)
667 + y[i + 1] * h_l / (h_r * h_sum);
668 }
669
670 let h_last = t[n - 1] - t[n - 2];
672 let h_prev = t[n - 1] - t[n - 3];
673 let h_mid = t[n - 2] - t[n - 3];
674 if h_last.abs() > 1e-15 && h_prev.abs() > 1e-15 && h_mid.abs() > 1e-15 {
675 g[n - 1] = y[n - 3] * h_last / (h_mid * h_prev) - y[n - 2] * h_prev / (h_mid * h_last)
676 + y[n - 1] * (h_prev + h_last) / (h_prev * h_last);
677 } else {
678 g[n - 1] = (y[n - 1] - y[n - 2]) / h_last.max(1e-15);
679 }
680
681 g
682}
683
684pub fn gradient(y: &[f64], t: &[f64]) -> Vec<f64> {
690 let n = t.len();
691 if n < 2 {
692 return vec![0.0; y.len()];
693 }
694
695 let h0 = t[1] - t[0];
696 let is_uniform = t
697 .windows(2)
698 .all(|w| ((w[1] - w[0]) - h0).abs() < 1e-12 * h0.abs().max(1.0));
699
700 if is_uniform {
701 gradient_uniform(y, h0)
702 } else {
703 gradient_nonuniform(y, t)
704 }
705}
706
707#[cfg(test)]
708mod tests {
709 use super::*;
710
711 #[test]
712 fn test_simpsons_weights_uniform() {
713 let argvals = vec![0.0, 0.25, 0.5, 0.75, 1.0];
714 let weights = simpsons_weights(&argvals);
715 let sum: f64 = weights.iter().sum();
716 assert!((sum - 1.0).abs() < NUMERICAL_EPS);
717 }
718
719 #[test]
720 fn test_simpsons_weights_2d() {
721 let argvals_s = vec![0.0, 0.5, 1.0];
722 let argvals_t = vec![0.0, 0.5, 1.0];
723 let weights = simpsons_weights_2d(&argvals_s, &argvals_t);
724 let sum: f64 = weights.iter().sum();
725 assert!((sum - 1.0).abs() < NUMERICAL_EPS);
726 }
727
728 #[test]
729 fn test_extract_curves() {
730 let data = vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0];
733 let mat = crate::matrix::FdMatrix::from_column_major(data, 2, 3).unwrap();
734 let curves = extract_curves(&mat);
735 assert_eq!(curves.len(), 2);
736 assert_eq!(curves[0], vec![1.0, 2.0, 3.0]);
737 assert_eq!(curves[1], vec![4.0, 5.0, 6.0]);
738 }
739
740 #[test]
741 fn test_l2_distance_identical() {
742 let curve = vec![1.0, 2.0, 3.0];
743 let weights = vec![0.25, 0.5, 0.25];
744 let dist = l2_distance(&curve, &curve, &weights);
745 assert!(dist.abs() < NUMERICAL_EPS);
746 }
747
748 #[test]
749 fn test_l2_distance_different() {
750 let curve1 = vec![0.0, 0.0, 0.0];
751 let curve2 = vec![1.0, 1.0, 1.0];
752 let weights = vec![0.25, 0.5, 0.25]; let dist = l2_distance(&curve1, &curve2, &weights);
754 assert!((dist - 1.0).abs() < NUMERICAL_EPS);
756 }
757
758 #[test]
759 fn test_n1_weights() {
760 let w = simpsons_weights(&[0.5]);
762 assert_eq!(w.len(), 1);
763 assert!((w[0] - 1.0).abs() < 1e-12);
764 }
765
766 #[test]
767 fn test_n2_weights() {
768 let w = simpsons_weights(&[0.0, 1.0]);
769 assert_eq!(w.len(), 2);
770 assert!((w[0] - 0.5).abs() < 1e-12);
772 assert!((w[1] - 0.5).abs() < 1e-12);
773 }
774
775 #[test]
776 fn test_mismatched_l2_distance() {
777 let a = vec![1.0, 2.0, 3.0];
779 let b = vec![1.0, 2.0, 3.0];
780 let w = vec![0.5, 0.5, 0.5];
781 let d = l2_distance(&a, &b, &w);
782 assert!(d.abs() < 1e-12, "Same vectors should have zero distance");
783 }
784
785 #[test]
788 fn test_trapz_sine() {
789 let m = 1000;
791 let x: Vec<f64> = (0..m)
792 .map(|i| std::f64::consts::PI * i as f64 / (m - 1) as f64)
793 .collect();
794 let y: Vec<f64> = x.iter().map(|&xi| xi.sin()).collect();
795 let result = trapz(&y, &x);
796 assert!(
797 (result - 2.0).abs() < 1e-4,
798 "∫ sin(x) dx over [0,π] should be ~2, got {result}"
799 );
800 }
801
802 #[test]
805 fn test_cumulative_trapz_matches_final() {
806 let m = 100;
807 let x: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
808 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi).collect();
809 let cum = cumulative_trapz(&y, &x);
810 let total = trapz(&y, &x);
811 assert!(
812 (cum[m - 1] - total).abs() < 1e-12,
813 "Final cumulative value should match trapz"
814 );
815 }
816
817 #[test]
820 fn test_linear_interp_boundary_clamp() {
821 let x = vec![0.0, 0.5, 1.0];
822 let y = vec![10.0, 20.0, 30.0];
823 assert!((linear_interp(&x, &y, -1.0) - 10.0).abs() < 1e-12);
824 assert!((linear_interp(&x, &y, 2.0) - 30.0).abs() < 1e-12);
825 assert!((linear_interp(&x, &y, 0.25) - 15.0).abs() < 1e-12);
826 }
827
828 #[test]
831 fn test_gradient_uniform_linear() {
832 let m = 50;
834 let h = 1.0 / (m - 1) as f64;
835 let y: Vec<f64> = (0..m).map(|i| 3.0 * i as f64 * h).collect();
836 let g = gradient_uniform(&y, h);
837 for i in 0..m {
838 assert!(
839 (g[i] - 3.0).abs() < 1e-10,
840 "gradient of 3x should be 3 at i={i}, got {}",
841 g[i]
842 );
843 }
844 }
845
846 #[test]
849 fn test_gaussian_kernel() {
850 assert!((gaussian_kernel(0.0, 1.0) - 1.0).abs() < 1e-12);
851 assert!(gaussian_kernel(3.0, 1.0) < 0.02); assert!((gaussian_kernel(1.0, 0.0)).abs() < 1e-12); }
854
855 #[test]
856 fn test_bandwidth_candidates() {
857 let n = 5;
858 let mut dists = vec![0.0; n * n];
859 for i in 0..n {
860 for j in 0..n {
861 dists[i * n + j] = (i as f64 - j as f64).abs();
862 }
863 }
864 let cands = bandwidth_candidates_from_dists(&dists, n, 10);
865 assert!(!cands.is_empty());
866 assert!(cands.iter().all(|&h| h > 0.0));
867 for w in cands.windows(2) {
869 assert!(w[1] >= w[0]);
870 }
871 }
872
873 #[test]
874 fn test_quantile_sorted() {
875 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
876 assert!((quantile_sorted(&data, 0.0) - 1.0).abs() < 1e-12);
877 assert!((quantile_sorted(&data, 1.0) - 5.0).abs() < 1e-12);
878 assert!((quantile_sorted(&data, 0.5) - 3.0).abs() < 1e-12);
879 assert!((quantile_sorted(&data, 0.25) - 2.0).abs() < 1e-12);
880 }
881
882 #[test]
883 fn test_r_squared_perfect() {
884 let y = vec![1.0, 2.0, 3.0, 4.0];
885 let resid = vec![0.0, 0.0, 0.0, 0.0];
886 assert!((r_squared(&y, &resid) - 1.0).abs() < 1e-12);
887 }
888
889 #[test]
890 fn test_r_squared_mean_model() {
891 let y = vec![1.0, 2.0, 3.0, 4.0];
892 let mean = 2.5;
893 let resid: Vec<f64> = y.iter().map(|&yi| yi - mean).collect();
894 assert!(r_squared(&y, &resid).abs() < 1e-12); }
896
897 #[test]
898 fn test_aic_bic() {
899 let a = aic(100, 50.0, 5);
900 let b = bic(100, 50.0, 5);
901 assert!(a.is_finite());
902 assert!(b.is_finite());
903 assert!(b > a); }
905
906 #[test]
907 fn fdata_interpolate_linear_identity() {
908 let t: Vec<f64> = (0..20).map(|i| i as f64 / 19.0).collect();
909 let vals: Vec<f64> = t.iter().map(|&x| x.sin()).collect();
910 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
911 let result = fdata_interpolate(&data, &t, &t, InterpolationMethod::Linear);
912 for j in 0..20 {
913 assert!((result[(0, j)] - data[(0, j)]).abs() < 1e-12);
914 }
915 }
916
917 #[test]
918 fn fdata_interpolate_cubic_hermite_smooth() {
919 let t: Vec<f64> = (0..20).map(|i| i as f64 / 19.0).collect();
920 let vals: Vec<f64> = t.iter().map(|&x| x.sin()).collect();
921 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
922
923 let t_fine: Vec<f64> = (0..100).map(|i| i as f64 / 99.0).collect();
924 let result = fdata_interpolate(&data, &t, &t_fine, InterpolationMethod::CubicHermite);
925
926 for (j, &tj) in t_fine.iter().enumerate() {
928 assert!(
929 (result[(0, j)] - tj.sin()).abs() < 0.02,
930 "at t={tj:.2}: got {:.4}, expected {:.4}",
931 result[(0, j)],
932 tj.sin()
933 );
934 }
935 }
936
937 #[test]
938 fn fdata_interpolate_multiple_curves() {
939 let t: Vec<f64> = (0..30).map(|i| i as f64 / 29.0).collect();
940 let n = 5;
941 let m = 30;
942 let mut col_major = vec![0.0; n * m];
944 for i in 0..n {
945 for j in 0..m {
946 col_major[i + j * n] = ((i + 1) as f64 * t[j]).sin();
947 }
948 }
949 let data = crate::matrix::FdMatrix::from_column_major(col_major, n, m).unwrap();
950
951 let t_new: Vec<f64> = (0..50).map(|i| i as f64 / 49.0).collect();
952 let result = fdata_interpolate(&data, &t, &t_new, InterpolationMethod::Linear);
953 assert_eq!(result.shape(), (n, 50));
954 for i in 0..n {
956 for j in 0..50 {
957 assert!(result[(i, j)].is_finite());
958 }
959 }
960 }
961
962 #[test]
965 fn spline_interpolate_reproduces_argvals() {
966 use crate::test_helpers::uniform_grid;
967 let t = uniform_grid(20);
968 let vals: Vec<f64> = t.iter().map(|&x| x.powi(3)).collect();
969 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
971 let result = spline_interpolate(&data, &t, &t, 4).unwrap();
972 for j in 0..20 {
973 assert!(
974 (result[(0, j)] - data[(0, j)]).abs() < 1e-10,
975 "at j={j}: got {}, expected {}",
976 result[(0, j)],
977 data[(0, j)]
978 );
979 }
980 }
981
982 #[test]
983 fn spline_interpolate_cubic_offgrid() {
984 use crate::test_helpers::uniform_grid;
988 let t = uniform_grid(20); let poly = |x: f64| 2.0 * x.powi(3) - x.powi(2) + 0.5 * x - 0.1;
990 let vals: Vec<f64> = t.iter().map(|&x| poly(x)).collect();
991 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
992
993 let q: Vec<f64> = t.windows(2).map(|w| (w[0] + w[1]) / 2.0).collect();
995 let result = spline_interpolate(&data, &t, &q, 4).unwrap();
996
997 for (j, &qj) in q.iter().enumerate() {
998 let expected = poly(qj);
999 let got = result[(0, j)];
1000 assert!(
1001 (got - expected).abs() < 1e-10,
1002 "off-grid at q={qj:.4}: got {got}, expected {expected}"
1003 );
1004 }
1005 }
1006
1007 #[test]
1008 fn spline_interpolate_rejects_out_of_range() {
1009 use crate::test_helpers::uniform_grid;
1010 let t = uniform_grid(20);
1011 let vals: Vec<f64> = t.to_vec();
1012 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1013
1014 let q_below = vec![-0.1_f64];
1016 let err = spline_interpolate(&data, &t, &q_below, 4).unwrap_err();
1017 assert!(
1018 matches!(
1019 err,
1020 crate::FdarError::InvalidParameter {
1021 parameter: "query_points",
1022 ..
1023 }
1024 ),
1025 "expected InvalidParameter for query below domain, got {err:?}"
1026 );
1027
1028 let q_above = vec![1.1_f64];
1030 let err2 = spline_interpolate(&data, &t, &q_above, 4).unwrap_err();
1031 assert!(
1032 matches!(
1033 err2,
1034 crate::FdarError::InvalidParameter {
1035 parameter: "query_points",
1036 ..
1037 }
1038 ),
1039 "expected InvalidParameter for query above domain, got {err2:?}"
1040 );
1041 }
1042
1043 #[test]
1044 fn spline_interpolate_rejects_bad_order() {
1045 use crate::test_helpers::uniform_grid;
1046 let t = uniform_grid(20);
1047 let vals: Vec<f64> = t.to_vec();
1048 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1049 let q = vec![0.5_f64];
1050
1051 let err = spline_interpolate(&data, &t, &q, 0).unwrap_err();
1053 assert!(
1054 matches!(
1055 err,
1056 crate::FdarError::InvalidParameter {
1057 parameter: "order",
1058 ..
1059 }
1060 ),
1061 "expected InvalidParameter for order=0, got {err:?}"
1062 );
1063
1064 let err2 = spline_interpolate(&data, &t, &q, 20).unwrap_err();
1066 assert!(
1067 matches!(
1068 err2,
1069 crate::FdarError::InvalidParameter {
1070 parameter: "order",
1071 ..
1072 }
1073 ),
1074 "expected InvalidParameter for order=20 (>=m=20), got {err2:?}"
1075 );
1076 }
1077
1078 #[test]
1079 fn spline_interpolate_rejects_dim_mismatch() {
1080 use crate::test_helpers::uniform_grid;
1081 let t = uniform_grid(20);
1082 let vals: Vec<f64> = t.to_vec();
1083 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1084
1085 let bad_argvals: Vec<f64> = (0..15).map(|i| i as f64 / 14.0).collect();
1087 let q = vec![0.5_f64];
1088 let err = spline_interpolate(&data, &bad_argvals, &q, 4).unwrap_err();
1089 assert!(
1090 matches!(
1091 err,
1092 crate::FdarError::InvalidDimension {
1093 parameter: "argvals",
1094 ..
1095 }
1096 ),
1097 "expected InvalidDimension for argvals mismatch, got {err:?}"
1098 );
1099
1100 let err2 = spline_interpolate(&data, &t, &[], 4).unwrap_err();
1102 assert!(
1103 matches!(
1104 err2,
1105 crate::FdarError::InvalidDimension {
1106 parameter: "query_points",
1107 ..
1108 }
1109 ),
1110 "expected InvalidDimension for empty query_points, got {err2:?}"
1111 );
1112 }
1113}