1pub const NUMERICAL_EPS: f64 = 1e-10;
5
6pub const DEFAULT_CONVERGENCE_TOL: f64 = 1e-6;
8
9pub(crate) 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
14#[inline]
28pub(crate) fn seed_for_thread(seed: u64, k: usize) -> rand::rngs::StdRng {
29 use rand::SeedableRng;
30 rand::rngs::StdRng::seed_from_u64(seed.wrapping_add(k as u64))
31}
32
33pub fn extract_curves(data: &crate::matrix::FdMatrix) -> Vec<Vec<f64>> {
44 data.rows()
45}
46
47pub fn l2_distance(curve1: &[f64], curve2: &[f64], weights: &[f64]) -> f64 {
57 let mut dist_sq = 0.0;
58 for i in 0..curve1.len() {
59 let diff = curve1[i] - curve2[i];
60 dist_sq += diff * diff * weights[i];
61 }
62 dist_sq.sqrt()
63}
64
65pub fn simpsons_weights(argvals: &[f64]) -> Vec<f64> {
77 let n = argvals.len();
78 if n < 2 {
79 return vec![1.0; n];
80 }
81
82 let mut weights = vec![0.0; n];
83
84 if n == 2 {
85 let h = argvals[1] - argvals[0];
87 weights[0] = h / 2.0;
88 weights[1] = h / 2.0;
89 return weights;
90 }
91
92 let h0 = argvals[1] - argvals[0];
94 let is_uniform = argvals
95 .windows(2)
96 .all(|w| ((w[1] - w[0]) - h0).abs() < 1e-12 * h0.abs());
97
98 if is_uniform {
99 simpsons_weights_uniform(&mut weights, n, h0);
100 } else {
101 simpsons_weights_nonuniform(&mut weights, argvals, n);
102 }
103
104 weights
105}
106
107fn simpsons_weights_uniform(weights: &mut [f64], n: usize, h0: f64) {
109 let n_intervals = n - 1;
110 if n_intervals % 2 == 0 {
111 weights[0] = h0 / 3.0;
113 weights[n - 1] = h0 / 3.0;
114 for i in 1..n - 1 {
115 weights[i] = if i % 2 == 1 {
116 4.0 * h0 / 3.0
117 } else {
118 2.0 * h0 / 3.0
119 };
120 }
121 } else {
122 let n_simp = n - 1;
124 weights[0] = h0 / 3.0;
125 weights[n_simp - 1] = h0 / 3.0;
126 for i in 1..n_simp - 1 {
127 weights[i] = if i % 2 == 1 {
128 4.0 * h0 / 3.0
129 } else {
130 2.0 * h0 / 3.0
131 };
132 }
133 weights[n_simp - 1] += h0 / 2.0;
134 weights[n - 1] += h0 / 2.0;
135 }
136}
137
138fn simpsons_weights_nonuniform(weights: &mut [f64], argvals: &[f64], n: usize) {
140 let n_intervals = n - 1;
141 let n_pairs = n_intervals / 2;
142
143 for k in 0..n_pairs {
144 let i0 = 2 * k;
145 let i1 = i0 + 1;
146 let i2 = i0 + 2;
147 let h1 = argvals[i1] - argvals[i0];
148 let h2 = argvals[i2] - argvals[i1];
149 let h_sum = h1 + h2;
150
151 weights[i0] += (2.0 * h1 - h2) * h_sum / (6.0 * h1);
152 weights[i1] += h_sum * h_sum * h_sum / (6.0 * h1 * h2);
153 weights[i2] += (2.0 * h2 - h1) * h_sum / (6.0 * h2);
154 }
155
156 if n_intervals % 2 == 1 {
157 let h_last = argvals[n - 1] - argvals[n - 2];
158 weights[n - 2] += h_last / 2.0;
159 weights[n - 1] += h_last / 2.0;
160 }
161}
162
163pub fn simpsons_weights_2d(argvals_s: &[f64], argvals_t: &[f64]) -> Vec<f64> {
174 let weights_s = simpsons_weights(argvals_s);
175 let weights_t = simpsons_weights(argvals_t);
176 let m1 = argvals_s.len();
177 let m2 = argvals_t.len();
178
179 let mut weights = vec![0.0; m1 * m2];
180 for i in 0..m1 {
181 for j in 0..m2 {
182 weights[i + j * m1] = weights_s[i] * weights_t[j];
183 }
184 }
185 weights
186}
187
188pub fn linear_interp(x: &[f64], y: &[f64], t: f64) -> f64 {
192 if t <= x[0] {
193 return y[0];
194 }
195 let last = x.len() - 1;
196 if t >= x[last] {
197 return y[last];
198 }
199
200 let idx = match x.binary_search_by(|v| v.partial_cmp(&t).unwrap_or(std::cmp::Ordering::Equal)) {
201 Ok(i) => return y[i],
202 Err(i) => i,
203 };
204
205 let t0 = x[idx - 1];
206 let t1 = x[idx];
207 let y0 = y[idx - 1];
208 let y1 = y[idx];
209 y0 + (y1 - y0) * (t - t0) / (t1 - t0)
210}
211
212pub fn cumulative_trapz(y: &[f64], x: &[f64]) -> Vec<f64> {
217 let n = y.len();
218 let mut out = vec![0.0; n];
219 if n < 2 {
220 return out;
221 }
222
223 let mut k = 1;
225 while k + 1 < n {
226 let h1 = x[k] - x[k - 1];
227 let h2 = x[k + 1] - x[k];
228 let h_sum = h1 + h2;
229
230 let integral = h_sum / 6.0
232 * (y[k - 1] * (2.0 * h1 - h2) / h1
233 + y[k] * h_sum * h_sum / (h1 * h2)
234 + y[k + 1] * (2.0 * h2 - h1) / h2);
235
236 out[k] = out[k - 1] + {
237 0.5 * (y[k] + y[k - 1]) * h1
239 };
240 out[k + 1] = out[k - 1] + integral;
241 k += 2;
242 }
243
244 if k < n {
246 out[k] = out[k - 1] + 0.5 * (y[k] + y[k - 1]) * (x[k] - x[k - 1]);
247 }
248
249 out
250}
251
252pub fn trapz(y: &[f64], x: &[f64]) -> f64 {
254 let mut sum = 0.0;
255 for k in 1..y.len() {
256 sum += 0.5 * (y[k] + y[k - 1]) * (x[k] - x[k - 1]);
257 }
258 sum
259}
260
261pub fn gaussian_kernel(d: f64, h: f64) -> f64 {
267 if h < 1e-15 {
268 return 0.0;
269 }
270 (-d * d / (2.0 * h * h)).exp()
271}
272
273pub fn bandwidth_candidates_from_dists(dists: &[f64], n: usize, n_quantiles: usize) -> Vec<f64> {
279 let mut nonzero: Vec<f64> = (0..n)
280 .flat_map(|i| ((i + 1)..n).map(move |j| dists[i * n + j]))
281 .filter(|&d| d > 0.0)
282 .collect();
283 sort_nan_safe(&mut nonzero);
284
285 if nonzero.is_empty() {
286 return Vec::new();
287 }
288
289 (1..=n_quantiles)
290 .map(|q| {
291 let p = q as f64 / (n_quantiles + 1) as f64;
292 let idx = ((nonzero.len() as f64 * p) as usize).min(nonzero.len() - 1);
293 nonzero[idx]
294 })
295 .filter(|&h| h > 1e-15)
296 .collect()
297}
298
299pub fn quantile_sorted(sorted: &[f64], p: f64) -> f64 {
303 if sorted.is_empty() {
304 return f64::NAN;
305 }
306 if sorted.len() == 1 || p <= 0.0 {
307 return sorted[0];
308 }
309 if p >= 1.0 {
310 return sorted[sorted.len() - 1];
311 }
312 let pos = p * (sorted.len() - 1) as f64;
313 let lo = pos.floor() as usize;
314 let hi = (lo + 1).min(sorted.len() - 1);
315 let frac = pos - lo as f64;
316 sorted[lo] * (1.0 - frac) + sorted[hi] * frac
317}
318
319pub fn r_squared(y_true: &[f64], residuals: &[f64]) -> f64 {
321 let n = y_true.len();
322 if n == 0 {
323 return f64::NAN;
324 }
325 let mean = y_true.iter().sum::<f64>() / n as f64;
326 let ss_tot: f64 = y_true.iter().map(|&y| (y - mean).powi(2)).sum();
327 let ss_res: f64 = residuals.iter().map(|r| r * r).sum();
328 if ss_tot > 1e-15 {
329 1.0 - ss_res / ss_tot
330 } else {
331 0.0
332 }
333}
334
335pub fn r_squared_adj(y_true: &[f64], residuals: &[f64], p: usize) -> f64 {
337 let n = y_true.len();
338 let r2 = r_squared(y_true, residuals);
339 if n <= p + 1 {
340 return r2;
341 }
342 1.0 - (1.0 - r2) * (n - 1) as f64 / (n - p - 1) as f64
343}
344
345pub fn aic(n: usize, rss: f64, p: usize) -> f64 {
349 let nf = n as f64;
350 nf * (rss / nf).ln() + 2.0 * p as f64
351}
352
353pub fn bic(n: usize, rss: f64, p: usize) -> f64 {
357 let nf = n as f64;
358 nf * (rss / nf).ln() + nf.ln() * p as f64
359}
360
361#[derive(Debug, Clone, Copy, PartialEq)]
363#[non_exhaustive]
364pub enum InterpolationMethod {
365 Linear,
367 CubicHermite,
369}
370
371#[must_use]
385pub fn fdata_interpolate(
386 data: &crate::matrix::FdMatrix,
387 argvals: &[f64],
388 new_argvals: &[f64],
389 method: InterpolationMethod,
390) -> crate::matrix::FdMatrix {
391 let (n, m) = data.shape();
392 let m_new = new_argvals.len();
393 if n == 0 || m < 2 || m_new == 0 {
394 return crate::matrix::FdMatrix::zeros(n.max(1), m_new.max(1));
395 }
396
397 let mut result = crate::matrix::FdMatrix::zeros(n, m_new);
398
399 for i in 0..n {
400 let y: Vec<f64> = (0..m).map(|j| data[(i, j)]).collect();
401 for (j, &t) in new_argvals.iter().enumerate() {
402 result[(i, j)] = match method {
403 InterpolationMethod::Linear => linear_interp(argvals, &y, t),
404 InterpolationMethod::CubicHermite => cubic_hermite_interp(argvals, &y, t),
405 };
406 }
407 }
408
409 result
410}
411
412pub fn spline_interpolate(
436 data: &crate::matrix::FdMatrix,
437 argvals: &[f64],
438 query_points: &[f64],
439 order: usize,
440) -> Result<crate::matrix::FdMatrix, crate::FdarError> {
441 let (n, m) = data.shape();
442
443 if argvals.len() != m {
445 return Err(crate::FdarError::InvalidDimension {
446 parameter: "argvals",
447 expected: format!("{m}"),
448 actual: format!("{}", argvals.len()),
449 });
450 }
451 if query_points.is_empty() {
452 return Err(crate::FdarError::InvalidDimension {
453 parameter: "query_points",
454 expected: ">= 1".to_string(),
455 actual: "0".to_string(),
456 });
457 }
458 if order == 0 || order >= m {
459 return Err(crate::FdarError::InvalidParameter {
460 parameter: "order",
461 message: format!("must be in [1, {m}), got {order}"),
462 });
463 }
464 let t_min = argvals[0];
465 let t_max = argvals[m - 1];
466 for &q in query_points {
467 if q < t_min || q > t_max {
468 return Err(crate::FdarError::InvalidParameter {
469 parameter: "query_points",
470 message: format!(
471 "all query points must lie in [{t_min}, {t_max}]; found {q} which is outside the interpolation domain"
472 ),
473 });
474 }
475 }
476
477 let nknots = m.saturating_sub(order).max(2);
480 let knots = crate::basis::bspline::construct_bspline_knots(t_min, t_max, nknots, order);
481
482 let basis_vals = crate::basis::bspline::bspline_basis(argvals, nknots, order);
484 let nbasis = basis_vals.len() / m;
485
486 let b_mat = nalgebra::DMatrix::from_column_slice(m, nbasis, &basis_vals);
488
489 let tol = NUMERICAL_EPS * b_mat.nrows().max(b_mat.ncols()) as f64;
492 let svd = nalgebra::SVD::new(b_mat.clone(), true, true);
493 let pinv = svd
494 .pseudo_inverse(tol)
495 .map_err(|e| crate::FdarError::ComputationFailed {
496 operation: "spline_interpolate SVD pseudoinverse",
497 detail: e.to_string(),
498 })?;
499 let m_q = query_points.len();
504 let basis_query = crate::basis::bspline::bspline_basis_from_knots(query_points, &knots, order);
505
506 let mut out = crate::matrix::FdMatrix::zeros(n, m_q);
508 for i in 0..n {
509 let y_vec: Vec<f64> = (0..m).map(|j| data[(i, j)]).collect();
511 let y_col = nalgebra::DVector::from_vec(y_vec);
512
513 let coefs = &pinv * y_col;
515
516 for j in 0..m_q {
518 let mut val = 0.0;
519 for k in 0..nbasis {
520 val += coefs[k] * basis_query[j + k * m_q];
521 }
522 out[(i, j)] = val;
523 }
524 }
525
526 Ok(out)
527}
528
529pub fn spline_interpolate_with_policy(
559 data: &crate::matrix::FdMatrix,
560 argvals: &[f64],
561 query_points: &[f64],
562 order: usize,
563 policy: ExtrapolationPolicy,
564) -> Result<crate::matrix::FdMatrix, crate::FdarError> {
565 let (n, m) = data.shape();
566
567 if argvals.len() != m {
569 return Err(crate::FdarError::InvalidDimension {
570 parameter: "argvals",
571 expected: format!("{m}"),
572 actual: format!("{}", argvals.len()),
573 });
574 }
575 if query_points.is_empty() {
576 return Err(crate::FdarError::InvalidDimension {
577 parameter: "query_points",
578 expected: ">= 1".to_string(),
579 actual: "0".to_string(),
580 });
581 }
582 if order == 0 || order >= m {
583 return Err(crate::FdarError::InvalidParameter {
584 parameter: "order",
585 message: format!("must be in [1, {m}), got {order}"),
586 });
587 }
588
589 let t_min = argvals[0];
590 let t_max = argvals[m - 1];
591 let domain_len = t_max - t_min;
592
593 if domain_len <= 0.0 && matches!(policy, ExtrapolationPolicy::Periodic) {
595 return Err(crate::FdarError::InvalidParameter {
596 parameter: "argvals",
597 message: "Periodic extrapolation requires a positive domain length \
598 (argvals[0] < argvals[m-1])"
599 .to_string(),
600 });
601 }
602
603 let m_q = query_points.len();
604
605 let mut effective = Vec::with_capacity(m_q);
609 let mut fill_mask = vec![false; m_q];
610
611 for (j, &q) in query_points.iter().enumerate() {
612 let in_range = q >= t_min && q <= t_max;
613 if in_range {
614 effective.push(q);
615 } else {
616 match &policy {
617 ExtrapolationPolicy::Boundary => effective.push(q.clamp(t_min, t_max)),
618 ExtrapolationPolicy::Exception => {
619 return Err(crate::FdarError::InvalidParameter {
620 parameter: "query_points",
621 message: format!("query {q} is outside domain [{t_min}, {t_max}]"),
622 });
623 }
624 ExtrapolationPolicy::Fill(_) => {
625 fill_mask[j] = true;
627 effective.push(t_min); }
629 ExtrapolationPolicy::Periodic => {
630 let wrapped = t_min + ((q - t_min) % domain_len + domain_len) % domain_len;
631 effective.push(wrapped);
632 }
633 }
634 }
635 }
636
637 let nknots = m.saturating_sub(order).max(2);
640 let knots = crate::basis::bspline::construct_bspline_knots(t_min, t_max, nknots, order);
641 let basis_vals = crate::basis::bspline::bspline_basis(argvals, nknots, order);
642 let nbasis = basis_vals.len() / m;
643 let b_mat = nalgebra::DMatrix::from_column_slice(m, nbasis, &basis_vals);
644 let tol = NUMERICAL_EPS * b_mat.nrows().max(b_mat.ncols()) as f64;
645 let svd = nalgebra::SVD::new(b_mat.clone(), true, true);
646 let pinv = svd
647 .pseudo_inverse(tol)
648 .map_err(|e| crate::FdarError::ComputationFailed {
649 operation: "spline_interpolate_with_policy SVD pseudoinverse",
650 detail: e.to_string(),
651 })?;
652 let basis_query = crate::basis::bspline::bspline_basis_from_knots(&effective, &knots, order);
653
654 let mut out = crate::matrix::FdMatrix::zeros(n, m_q);
655 for i in 0..n {
656 let y_vec: Vec<f64> = (0..m).map(|j| data[(i, j)]).collect();
657 let y_col = nalgebra::DVector::from_vec(y_vec);
658 let coefs = &pinv * y_col;
659
660 for j in 0..m_q {
661 if fill_mask[j] {
662 if let ExtrapolationPolicy::Fill(v) = policy {
664 out[(i, j)] = v;
665 }
666 } else {
667 let mut val = 0.0_f64;
668 for k in 0..nbasis {
669 val += coefs[k] * basis_query[j + k * m_q];
670 }
671 out[(i, j)] = val;
672 }
673 }
674 }
675
676 Ok(out)
677}
678
679fn cubic_hermite_interp(x: &[f64], y: &[f64], t: f64) -> f64 {
683 let n = x.len();
684 if n < 2 {
685 return if n == 1 { y[0] } else { 0.0 };
686 }
687
688 if t <= x[0] {
690 return y[0];
691 }
692 if t >= x[n - 1] {
693 return y[n - 1];
694 }
695
696 let k = match x.binary_search_by(|v| v.partial_cmp(&t).unwrap_or(std::cmp::Ordering::Equal)) {
698 Ok(i) => return y[i],
699 Err(i) => {
700 if i == 0 {
701 0
702 } else {
703 i - 1
704 }
705 }
706 };
707
708 let slopes: Vec<f64> = x
710 .windows(2)
711 .zip(y.windows(2))
712 .map(|(xw, yw)| (yw[1] - yw[0]) / (xw[1] - xw[0]))
713 .collect();
714
715 let mut tangents = vec![0.0; n];
717 tangents[0] = slopes[0];
718 tangents[n - 1] = slopes[n - 2];
719 for i in 1..n - 1 {
720 if slopes[i - 1].signum() != slopes[i].signum() {
721 tangents[i] = 0.0;
722 } else {
723 tangents[i] = (slopes[i - 1] + slopes[i]) / 2.0;
724 }
725 }
726
727 let h = x[k + 1] - x[k];
729 let s = (t - x[k]) / h;
730 let s2 = s * s;
731 let s3 = s2 * s;
732
733 let h00 = 2.0 * s3 - 3.0 * s2 + 1.0;
734 let h10 = s3 - 2.0 * s2 + s;
735 let h01 = -2.0 * s3 + 3.0 * s2;
736 let h11 = s3 - s2;
737
738 h00 * y[k] + h10 * h * tangents[k] + h01 * y[k + 1] + h11 * h * tangents[k + 1]
739}
740
741pub fn gradient_uniform(y: &[f64], h: f64) -> Vec<f64> {
748 let n = y.len();
749 let mut g = vec![0.0; n];
750 if n < 2 {
751 return g;
752 }
753 if n == 2 {
754 g[0] = (y[1] - y[0]) / h;
755 g[1] = (y[1] - y[0]) / h;
756 return g;
757 }
758 if n == 3 {
759 g[0] = (-3.0 * y[0] + 4.0 * y[1] - y[2]) / (2.0 * h);
760 g[1] = (y[2] - y[0]) / (2.0 * h);
761 g[2] = (y[0] - 4.0 * y[1] + 3.0 * y[2]) / (2.0 * h);
762 return g;
763 }
764 if n == 4 {
765 g[0] = (-3.0 * y[0] + 4.0 * y[1] - y[2]) / (2.0 * h);
766 g[1] = (y[2] - y[0]) / (2.0 * h);
767 g[2] = (y[3] - y[1]) / (2.0 * h);
768 g[3] = (y[1] - 4.0 * y[2] + 3.0 * y[3]) / (2.0 * h);
769 return g;
770 }
771
772 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);
775 g[1] = (-3.0 * y[0] - 10.0 * y[1] + 18.0 * y[2] - 6.0 * y[3] + y[4]) / (12.0 * h);
776
777 for i in 2..n - 2 {
779 g[i] = (-y[i + 2] + 8.0 * y[i + 1] - 8.0 * y[i - 1] + y[i - 2]) / (12.0 * h);
780 }
781
782 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])
784 / (12.0 * h);
785 g[n - 1] = (3.0 * y[n - 5] - 16.0 * y[n - 4] + 36.0 * y[n - 3] - 48.0 * y[n - 2]
786 + 25.0 * y[n - 1])
787 / (12.0 * h);
788 g
789}
790
791pub fn gradient_nonuniform(y: &[f64], t: &[f64]) -> Vec<f64> {
799 let n = y.len();
800 assert_eq!(n, t.len(), "y and t must have the same length");
801 let mut g = vec![0.0; n];
802 if n < 2 {
803 return g;
804 }
805 if n == 2 {
806 let h = t[1] - t[0];
807 if h.abs() < 1e-15 {
808 return g;
809 }
810 g[0] = (y[1] - y[0]) / h;
811 g[1] = g[0];
812 return g;
813 }
814
815 let h0 = t[1] - t[0];
817 let h1 = t[2] - t[0];
818 if h0.abs() > 1e-15 && h1.abs() > 1e-15 && (h1 - h0).abs() > 1e-15 {
819 g[0] = y[0] * (-h1 - h0) / (h0 * h1) + y[1] * h1 / (h0 * (h1 - h0))
820 - y[2] * h0 / (h1 * (h1 - h0));
821 } else {
822 g[0] = (y[1] - y[0]) / h0.max(1e-15);
823 }
824
825 for i in 1..n - 1 {
827 let h_l = t[i] - t[i - 1];
828 let h_r = t[i + 1] - t[i];
829 let h_sum = h_l + h_r;
830 if h_l.abs() < 1e-15 || h_r.abs() < 1e-15 || h_sum.abs() < 1e-15 {
831 g[i] = 0.0;
832 continue;
833 }
834 g[i] = -y[i - 1] * h_r / (h_l * h_sum)
835 + y[i] * (h_r - h_l) / (h_l * h_r)
836 + y[i + 1] * h_l / (h_r * h_sum);
837 }
838
839 let h_last = t[n - 1] - t[n - 2];
841 let h_prev = t[n - 1] - t[n - 3];
842 let h_mid = t[n - 2] - t[n - 3];
843 if h_last.abs() > 1e-15 && h_prev.abs() > 1e-15 && h_mid.abs() > 1e-15 {
844 g[n - 1] = y[n - 3] * h_last / (h_mid * h_prev) - y[n - 2] * h_prev / (h_mid * h_last)
845 + y[n - 1] * (h_prev + h_last) / (h_prev * h_last);
846 } else {
847 g[n - 1] = (y[n - 1] - y[n - 2]) / h_last.max(1e-15);
848 }
849
850 g
851}
852
853pub fn gradient(y: &[f64], t: &[f64]) -> Vec<f64> {
859 let n = t.len();
860 if n < 2 {
861 return vec![0.0; y.len()];
862 }
863
864 let h0 = t[1] - t[0];
865 let is_uniform = t
866 .windows(2)
867 .all(|w| ((w[1] - w[0]) - h0).abs() < 1e-12 * h0.abs().max(1.0));
868
869 if is_uniform {
870 gradient_uniform(y, h0)
871 } else {
872 gradient_nonuniform(y, t)
873 }
874}
875
876#[non_exhaustive]
883#[derive(Debug, Clone, PartialEq)]
884#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
885pub enum ExtrapolationPolicy {
886 Boundary,
891 Exception,
896 Fill(f64),
898 Periodic,
904}
905
906pub fn fdata_interpolate_with_policy(
927 data: &crate::matrix::FdMatrix,
928 argvals: &[f64],
929 new_argvals: &[f64],
930 method: InterpolationMethod,
931 policy: ExtrapolationPolicy,
932) -> Result<crate::matrix::FdMatrix, crate::FdarError> {
933 let (n, m) = data.shape();
934 if argvals.len() != m {
935 return Err(crate::FdarError::InvalidDimension {
936 parameter: "argvals",
937 expected: format!("{m}"),
938 actual: format!("{}", argvals.len()),
939 });
940 }
941 let m_new = new_argvals.len();
942 if n == 0 || m < 2 || m_new == 0 {
943 return Ok(crate::matrix::FdMatrix::zeros(n.max(1), m_new.max(1)));
944 }
945 let t_min = argvals[0];
946 let t_max = argvals[m - 1];
947 let domain_len = t_max - t_min;
948
949 if domain_len <= 0.0 && matches!(policy, ExtrapolationPolicy::Periodic) {
953 return Err(crate::FdarError::InvalidParameter {
954 parameter: "argvals",
955 message: "Periodic extrapolation requires a positive domain length \
956 (argvals[0] < argvals[m-1])"
957 .to_string(),
958 });
959 }
960
961 let mut result = crate::matrix::FdMatrix::zeros(n, m_new);
962 for i in 0..n {
963 let y: Vec<f64> = (0..m).map(|j| data[(i, j)]).collect();
964 for (j, &t) in new_argvals.iter().enumerate() {
965 let in_range = t >= t_min && t <= t_max;
966 result[(i, j)] = if in_range {
967 match method {
968 InterpolationMethod::Linear => linear_interp(argvals, &y, t),
969 InterpolationMethod::CubicHermite => cubic_hermite_interp(argvals, &y, t),
970 }
971 } else {
972 match &policy {
973 ExtrapolationPolicy::Boundary => {
974 let t_clamped = t.clamp(t_min, t_max);
975 match method {
976 InterpolationMethod::Linear => linear_interp(argvals, &y, t_clamped),
977 InterpolationMethod::CubicHermite => {
978 cubic_hermite_interp(argvals, &y, t_clamped)
979 }
980 }
981 }
982 ExtrapolationPolicy::Exception => {
983 return Err(crate::FdarError::InvalidParameter {
984 parameter: "new_argvals",
985 message: format!("query {t} is outside domain [{t_min}, {t_max}]"),
986 });
987 }
988 ExtrapolationPolicy::Fill(v) => *v,
989 ExtrapolationPolicy::Periodic => {
990 let wrapped = t_min + ((t - t_min) % domain_len + domain_len) % domain_len;
991 match method {
992 InterpolationMethod::Linear => linear_interp(argvals, &y, wrapped),
993 InterpolationMethod::CubicHermite => {
994 cubic_hermite_interp(argvals, &y, wrapped)
995 }
996 }
997 }
998 }
999 };
1000 }
1001 }
1002 Ok(result)
1003}
1004
1005#[non_exhaustive]
1015#[derive(Debug, Clone, PartialEq)]
1016#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1017pub enum ImputationMethod {
1018 Linear,
1023 Mean,
1025 Constant(f64),
1027}
1028
1029pub fn impute_missing_values(
1046 data: &crate::matrix::FdMatrix,
1047 argvals: &[f64],
1048 method: ImputationMethod,
1049) -> Result<crate::matrix::FdMatrix, crate::FdarError> {
1050 let (n, m) = data.shape();
1051 if argvals.len() != m {
1052 return Err(crate::FdarError::InvalidDimension {
1053 parameter: "argvals",
1054 expected: format!("{m}"),
1055 actual: format!("{}", argvals.len()),
1056 });
1057 }
1058 if m == 0 {
1062 return Err(crate::FdarError::InvalidDimension {
1063 parameter: "data",
1064 expected: "m >= 1".to_string(),
1065 actual: "m=0".to_string(),
1066 });
1067 }
1068 let mut out_data = vec![0.0_f64; n * m]; for i in 0..n {
1070 let row: Vec<f64> = data.row(i);
1071 let valid_count = row.iter().filter(|v| !v.is_nan()).count();
1072 if valid_count == 0 {
1073 return Err(crate::FdarError::InvalidParameter {
1074 parameter: "data",
1075 message: format!("curve {i} contains only NaN values"),
1076 });
1077 }
1078 let imputed = impute_row(&row, argvals, &method);
1079 for j in 0..m {
1080 out_data[i + j * n] = imputed[j]; }
1082 }
1083 crate::matrix::FdMatrix::from_column_major(out_data, n, m)
1084}
1085
1086fn impute_row(row: &[f64], argvals: &[f64], method: &ImputationMethod) -> Vec<f64> {
1088 let mut result = row.to_vec();
1089 match method {
1090 ImputationMethod::Mean => {
1091 let sum: f64 = row.iter().filter(|v| !v.is_nan()).sum();
1092 let count = row.iter().filter(|v| !v.is_nan()).count();
1093 let mean = sum / count as f64;
1094 for v in &mut result {
1095 if v.is_nan() {
1096 *v = mean;
1097 }
1098 }
1099 }
1100 ImputationMethod::Constant(c) => {
1101 for v in &mut result {
1102 if v.is_nan() {
1103 *v = *c;
1104 }
1105 }
1106 }
1107 ImputationMethod::Linear => {
1108 let valid_idxs: Vec<usize> = (0..row.len()).filter(|&j| !row[j].is_nan()).collect();
1109 for j in 0..row.len() {
1110 if result[j].is_nan() {
1111 let left = valid_idxs.iter().rev().find(|&&k| k < j).copied();
1112 let right = valid_idxs.iter().find(|&&k| k > j).copied();
1113 result[j] = match (left, right) {
1114 (Some(l), Some(r)) => {
1115 linear_interp(&[argvals[l], argvals[r]], &[row[l], row[r]], argvals[j])
1116 }
1117 (Some(l), None) => row[l], (None, Some(r)) => row[r], (None, None) => unreachable!(), };
1121 }
1122 }
1123 }
1124 }
1125 result
1126}
1127
1128#[cfg(test)]
1129mod tests {
1130 use super::*;
1131
1132 #[test]
1133 fn test_simpsons_weights_uniform() {
1134 let argvals = vec![0.0, 0.25, 0.5, 0.75, 1.0];
1135 let weights = simpsons_weights(&argvals);
1136 let sum: f64 = weights.iter().sum();
1137 assert!((sum - 1.0).abs() < NUMERICAL_EPS);
1138 }
1139
1140 #[test]
1141 fn test_simpsons_weights_2d() {
1142 let argvals_s = vec![0.0, 0.5, 1.0];
1143 let argvals_t = vec![0.0, 0.5, 1.0];
1144 let weights = simpsons_weights_2d(&argvals_s, &argvals_t);
1145 let sum: f64 = weights.iter().sum();
1146 assert!((sum - 1.0).abs() < NUMERICAL_EPS);
1147 }
1148
1149 #[test]
1150 fn test_extract_curves() {
1151 let data = vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0];
1154 let mat = crate::matrix::FdMatrix::from_column_major(data, 2, 3).unwrap();
1155 let curves = extract_curves(&mat);
1156 assert_eq!(curves.len(), 2);
1157 assert_eq!(curves[0], vec![1.0, 2.0, 3.0]);
1158 assert_eq!(curves[1], vec![4.0, 5.0, 6.0]);
1159 }
1160
1161 #[test]
1162 fn test_l2_distance_identical() {
1163 let curve = vec![1.0, 2.0, 3.0];
1164 let weights = vec![0.25, 0.5, 0.25];
1165 let dist = l2_distance(&curve, &curve, &weights);
1166 assert!(dist.abs() < NUMERICAL_EPS);
1167 }
1168
1169 #[test]
1170 fn test_l2_distance_different() {
1171 let curve1 = vec![0.0, 0.0, 0.0];
1172 let curve2 = vec![1.0, 1.0, 1.0];
1173 let weights = vec![0.25, 0.5, 0.25]; let dist = l2_distance(&curve1, &curve2, &weights);
1175 assert!((dist - 1.0).abs() < NUMERICAL_EPS);
1177 }
1178
1179 #[test]
1180 fn test_n1_weights() {
1181 let w = simpsons_weights(&[0.5]);
1183 assert_eq!(w.len(), 1);
1184 assert!((w[0] - 1.0).abs() < 1e-12);
1185 }
1186
1187 #[test]
1188 fn test_n2_weights() {
1189 let w = simpsons_weights(&[0.0, 1.0]);
1190 assert_eq!(w.len(), 2);
1191 assert!((w[0] - 0.5).abs() < 1e-12);
1193 assert!((w[1] - 0.5).abs() < 1e-12);
1194 }
1195
1196 #[test]
1197 fn test_mismatched_l2_distance() {
1198 let a = vec![1.0, 2.0, 3.0];
1200 let b = vec![1.0, 2.0, 3.0];
1201 let w = vec![0.5, 0.5, 0.5];
1202 let d = l2_distance(&a, &b, &w);
1203 assert!(d.abs() < 1e-12, "Same vectors should have zero distance");
1204 }
1205
1206 #[test]
1209 fn test_trapz_sine() {
1210 let m = 1000;
1212 let x: Vec<f64> = (0..m)
1213 .map(|i| std::f64::consts::PI * i as f64 / (m - 1) as f64)
1214 .collect();
1215 let y: Vec<f64> = x.iter().map(|&xi| xi.sin()).collect();
1216 let result = trapz(&y, &x);
1217 assert!(
1218 (result - 2.0).abs() < 1e-4,
1219 "∫ sin(x) dx over [0,π] should be ~2, got {result}"
1220 );
1221 }
1222
1223 #[test]
1226 fn test_cumulative_trapz_matches_final() {
1227 let m = 100;
1228 let x: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
1229 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi).collect();
1230 let cum = cumulative_trapz(&y, &x);
1231 let total = trapz(&y, &x);
1232 assert!(
1233 (cum[m - 1] - total).abs() < 1e-12,
1234 "Final cumulative value should match trapz"
1235 );
1236 }
1237
1238 #[test]
1241 fn test_linear_interp_boundary_clamp() {
1242 let x = vec![0.0, 0.5, 1.0];
1243 let y = vec![10.0, 20.0, 30.0];
1244 assert!((linear_interp(&x, &y, -1.0) - 10.0).abs() < 1e-12);
1245 assert!((linear_interp(&x, &y, 2.0) - 30.0).abs() < 1e-12);
1246 assert!((linear_interp(&x, &y, 0.25) - 15.0).abs() < 1e-12);
1247 }
1248
1249 #[test]
1252 fn test_gradient_uniform_linear() {
1253 let m = 50;
1255 let h = 1.0 / (m - 1) as f64;
1256 let y: Vec<f64> = (0..m).map(|i| 3.0 * i as f64 * h).collect();
1257 let g = gradient_uniform(&y, h);
1258 for i in 0..m {
1259 assert!(
1260 (g[i] - 3.0).abs() < 1e-10,
1261 "gradient of 3x should be 3 at i={i}, got {}",
1262 g[i]
1263 );
1264 }
1265 }
1266
1267 #[test]
1270 fn test_gaussian_kernel() {
1271 assert!((gaussian_kernel(0.0, 1.0) - 1.0).abs() < 1e-12);
1272 assert!(gaussian_kernel(3.0, 1.0) < 0.02); assert!((gaussian_kernel(1.0, 0.0)).abs() < 1e-12); }
1275
1276 #[test]
1277 fn test_bandwidth_candidates() {
1278 let n = 5;
1279 let mut dists = vec![0.0; n * n];
1280 for i in 0..n {
1281 for j in 0..n {
1282 dists[i * n + j] = (i as f64 - j as f64).abs();
1283 }
1284 }
1285 let cands = bandwidth_candidates_from_dists(&dists, n, 10);
1286 assert!(!cands.is_empty());
1287 assert!(cands.iter().all(|&h| h > 0.0));
1288 for w in cands.windows(2) {
1290 assert!(w[1] >= w[0]);
1291 }
1292 }
1293
1294 #[test]
1295 fn test_quantile_sorted() {
1296 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1297 assert!((quantile_sorted(&data, 0.0) - 1.0).abs() < 1e-12);
1298 assert!((quantile_sorted(&data, 1.0) - 5.0).abs() < 1e-12);
1299 assert!((quantile_sorted(&data, 0.5) - 3.0).abs() < 1e-12);
1300 assert!((quantile_sorted(&data, 0.25) - 2.0).abs() < 1e-12);
1301 }
1302
1303 #[test]
1304 fn test_r_squared_perfect() {
1305 let y = vec![1.0, 2.0, 3.0, 4.0];
1306 let resid = vec![0.0, 0.0, 0.0, 0.0];
1307 assert!((r_squared(&y, &resid) - 1.0).abs() < 1e-12);
1308 }
1309
1310 #[test]
1311 fn test_r_squared_mean_model() {
1312 let y = vec![1.0, 2.0, 3.0, 4.0];
1313 let mean = 2.5;
1314 let resid: Vec<f64> = y.iter().map(|&yi| yi - mean).collect();
1315 assert!(r_squared(&y, &resid).abs() < 1e-12); }
1317
1318 #[test]
1319 fn test_aic_bic() {
1320 let a = aic(100, 50.0, 5);
1321 let b = bic(100, 50.0, 5);
1322 assert!(a.is_finite());
1323 assert!(b.is_finite());
1324 assert!(b > a); }
1326
1327 #[test]
1328 fn fdata_interpolate_linear_identity() {
1329 let t: Vec<f64> = (0..20).map(|i| i as f64 / 19.0).collect();
1330 let vals: Vec<f64> = t.iter().map(|&x| x.sin()).collect();
1331 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1332 let result = fdata_interpolate(&data, &t, &t, InterpolationMethod::Linear);
1333 for j in 0..20 {
1334 assert!((result[(0, j)] - data[(0, j)]).abs() < 1e-12);
1335 }
1336 }
1337
1338 #[test]
1339 fn fdata_interpolate_cubic_hermite_smooth() {
1340 let t: Vec<f64> = (0..20).map(|i| i as f64 / 19.0).collect();
1341 let vals: Vec<f64> = t.iter().map(|&x| x.sin()).collect();
1342 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1343
1344 let t_fine: Vec<f64> = (0..100).map(|i| i as f64 / 99.0).collect();
1345 let result = fdata_interpolate(&data, &t, &t_fine, InterpolationMethod::CubicHermite);
1346
1347 for (j, &tj) in t_fine.iter().enumerate() {
1349 assert!(
1350 (result[(0, j)] - tj.sin()).abs() < 0.02,
1351 "at t={tj:.2}: got {:.4}, expected {:.4}",
1352 result[(0, j)],
1353 tj.sin()
1354 );
1355 }
1356 }
1357
1358 #[test]
1359 fn fdata_interpolate_multiple_curves() {
1360 let t: Vec<f64> = (0..30).map(|i| i as f64 / 29.0).collect();
1361 let n = 5;
1362 let m = 30;
1363 let mut col_major = vec![0.0; n * m];
1365 for i in 0..n {
1366 for j in 0..m {
1367 col_major[i + j * n] = ((i + 1) as f64 * t[j]).sin();
1368 }
1369 }
1370 let data = crate::matrix::FdMatrix::from_column_major(col_major, n, m).unwrap();
1371
1372 let t_new: Vec<f64> = (0..50).map(|i| i as f64 / 49.0).collect();
1373 let result = fdata_interpolate(&data, &t, &t_new, InterpolationMethod::Linear);
1374 assert_eq!(result.shape(), (n, 50));
1375 for i in 0..n {
1377 for j in 0..50 {
1378 assert!(result[(i, j)].is_finite());
1379 }
1380 }
1381 }
1382
1383 #[test]
1386 fn spline_interpolate_reproduces_argvals() {
1387 use crate::test_helpers::uniform_grid;
1388 let t = uniform_grid(20);
1389 let vals: Vec<f64> = t.iter().map(|&x| x.powi(3)).collect();
1390 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1392 let result = spline_interpolate(&data, &t, &t, 4).unwrap();
1393 for j in 0..20 {
1394 assert!(
1395 (result[(0, j)] - data[(0, j)]).abs() < 1e-10,
1396 "at j={j}: got {}, expected {}",
1397 result[(0, j)],
1398 data[(0, j)]
1399 );
1400 }
1401 }
1402
1403 #[test]
1404 fn spline_interpolate_cubic_offgrid() {
1405 use crate::test_helpers::uniform_grid;
1409 let t = uniform_grid(20); let poly = |x: f64| 2.0 * x.powi(3) - x.powi(2) + 0.5 * x - 0.1;
1411 let vals: Vec<f64> = t.iter().map(|&x| poly(x)).collect();
1412 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1413
1414 let q: Vec<f64> = t.windows(2).map(|w| (w[0] + w[1]) / 2.0).collect();
1416 let result = spline_interpolate(&data, &t, &q, 4).unwrap();
1417
1418 for (j, &qj) in q.iter().enumerate() {
1419 let expected = poly(qj);
1420 let got = result[(0, j)];
1421 assert!(
1422 (got - expected).abs() < 1e-10,
1423 "off-grid at q={qj:.4}: got {got}, expected {expected}"
1424 );
1425 }
1426 }
1427
1428 #[test]
1429 fn spline_interpolate_rejects_out_of_range() {
1430 use crate::test_helpers::uniform_grid;
1431 let t = uniform_grid(20);
1432 let vals: Vec<f64> = t.to_vec();
1433 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1434
1435 let q_below = vec![-0.1_f64];
1437 let err = spline_interpolate(&data, &t, &q_below, 4).unwrap_err();
1438 assert!(
1439 matches!(
1440 err,
1441 crate::FdarError::InvalidParameter {
1442 parameter: "query_points",
1443 ..
1444 }
1445 ),
1446 "expected InvalidParameter for query below domain, got {err:?}"
1447 );
1448
1449 let q_above = vec![1.1_f64];
1451 let err2 = spline_interpolate(&data, &t, &q_above, 4).unwrap_err();
1452 assert!(
1453 matches!(
1454 err2,
1455 crate::FdarError::InvalidParameter {
1456 parameter: "query_points",
1457 ..
1458 }
1459 ),
1460 "expected InvalidParameter for query above domain, got {err2:?}"
1461 );
1462 }
1463
1464 #[test]
1465 fn spline_interpolate_rejects_bad_order() {
1466 use crate::test_helpers::uniform_grid;
1467 let t = uniform_grid(20);
1468 let vals: Vec<f64> = t.to_vec();
1469 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1470 let q = vec![0.5_f64];
1471
1472 let err = spline_interpolate(&data, &t, &q, 0).unwrap_err();
1474 assert!(
1475 matches!(
1476 err,
1477 crate::FdarError::InvalidParameter {
1478 parameter: "order",
1479 ..
1480 }
1481 ),
1482 "expected InvalidParameter for order=0, got {err:?}"
1483 );
1484
1485 let err2 = spline_interpolate(&data, &t, &q, 20).unwrap_err();
1487 assert!(
1488 matches!(
1489 err2,
1490 crate::FdarError::InvalidParameter {
1491 parameter: "order",
1492 ..
1493 }
1494 ),
1495 "expected InvalidParameter for order=20 (>=m=20), got {err2:?}"
1496 );
1497 }
1498
1499 #[test]
1500 fn spline_interpolate_rejects_dim_mismatch() {
1501 use crate::test_helpers::uniform_grid;
1502 let t = uniform_grid(20);
1503 let vals: Vec<f64> = t.to_vec();
1504 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1505
1506 let bad_argvals: Vec<f64> = (0..15).map(|i| i as f64 / 14.0).collect();
1508 let q = vec![0.5_f64];
1509 let err = spline_interpolate(&data, &bad_argvals, &q, 4).unwrap_err();
1510 assert!(
1511 matches!(
1512 err,
1513 crate::FdarError::InvalidDimension {
1514 parameter: "argvals",
1515 ..
1516 }
1517 ),
1518 "expected InvalidDimension for argvals mismatch, got {err:?}"
1519 );
1520
1521 let err2 = spline_interpolate(&data, &t, &[], 4).unwrap_err();
1523 assert!(
1524 matches!(
1525 err2,
1526 crate::FdarError::InvalidDimension {
1527 parameter: "query_points",
1528 ..
1529 }
1530 ),
1531 "expected InvalidDimension for empty query_points, got {err2:?}"
1532 );
1533 }
1534
1535 #[test]
1538 fn test_spline_with_policy_in_range_matches_spline() {
1539 use crate::test_helpers::uniform_grid;
1541 let t = uniform_grid(20);
1542 let poly = |x: f64| 2.0 * x.powi(3) - x.powi(2) + 0.5 * x - 0.1;
1543 let vals: Vec<f64> = t.iter().map(|&x| poly(x)).collect();
1544 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1545 let q: Vec<f64> = t.windows(2).map(|w| (w[0] + w[1]) / 2.0).collect();
1547 let expected = spline_interpolate(&data, &t, &q, 4).unwrap();
1548 let actual =
1549 spline_interpolate_with_policy(&data, &t, &q, 4, ExtrapolationPolicy::Boundary)
1550 .unwrap();
1551 for j in 0..q.len() {
1552 assert!(
1553 (actual[(0, j)] - expected[(0, j)]).abs() < 1e-10,
1554 "in-range mismatch at j={j}: policy={} vs plain={}",
1555 actual[(0, j)],
1556 expected[(0, j)]
1557 );
1558 }
1559 }
1560
1561 #[test]
1562 fn test_spline_with_policy_boundary() {
1563 use crate::test_helpers::uniform_grid;
1565 let t = uniform_grid(20); let vals: Vec<f64> = t.iter().map(|&x| x.powi(2)).collect(); let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1568 let q = vec![-0.5_f64, 0.5, 1.5];
1570 let result =
1571 spline_interpolate_with_policy(&data, &t, &q, 4, ExtrapolationPolicy::Boundary)
1572 .unwrap();
1573 assert!(
1575 result[(0, 0)].abs() < 1e-9,
1576 "below boundary should clamp, got {}",
1577 result[(0, 0)]
1578 );
1579 assert!(
1581 (result[(0, 2)] - 1.0).abs() < 1e-9,
1582 "above boundary should clamp, got {}",
1583 result[(0, 2)]
1584 );
1585 assert!(
1587 (result[(0, 1)] - 0.25).abs() < 1e-9,
1588 "in-range should be ~0.25, got {}",
1589 result[(0, 1)]
1590 );
1591 }
1592
1593 #[test]
1594 fn test_spline_with_policy_exception() {
1595 use crate::test_helpers::uniform_grid;
1597 let t = uniform_grid(20);
1598 let vals: Vec<f64> = t.to_vec();
1599 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1600 let q_oob = vec![1.5_f64];
1601 let err =
1602 spline_interpolate_with_policy(&data, &t, &q_oob, 4, ExtrapolationPolicy::Exception)
1603 .unwrap_err();
1604 assert!(
1605 matches!(
1606 err,
1607 crate::FdarError::InvalidParameter {
1608 parameter: "query_points",
1609 ..
1610 }
1611 ),
1612 "Exception policy should error on OOB, got {err:?}"
1613 );
1614 let q_ok = vec![0.0_f64, 0.5, 1.0];
1616 let ok =
1617 spline_interpolate_with_policy(&data, &t, &q_ok, 4, ExtrapolationPolicy::Exception);
1618 assert!(
1619 ok.is_ok(),
1620 "Exception policy should succeed for in-range queries"
1621 );
1622 }
1623
1624 #[test]
1625 fn test_spline_with_policy_fill() {
1626 use crate::test_helpers::uniform_grid;
1628 let t = uniform_grid(20); let vals: Vec<f64> = t.iter().map(|&x| x.powi(2)).collect();
1630 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1631 let fill_val = 42.0_f64;
1632 let q = vec![-0.5_f64, 0.5, 2.0];
1633 let result =
1634 spline_interpolate_with_policy(&data, &t, &q, 4, ExtrapolationPolicy::Fill(fill_val))
1635 .unwrap();
1636 assert!(
1637 (result[(0, 0)] - fill_val).abs() < 1e-10,
1638 "OOB below should be fill value, got {}",
1639 result[(0, 0)]
1640 );
1641 assert!(
1642 (result[(0, 2)] - fill_val).abs() < 1e-10,
1643 "OOB above should be fill value, got {}",
1644 result[(0, 2)]
1645 );
1646 assert!(
1648 (result[(0, 1)] - 0.25).abs() < 1e-9,
1649 "in-range should be ~0.25, got {}",
1650 result[(0, 1)]
1651 );
1652 }
1653
1654 #[test]
1655 fn test_spline_with_policy_periodic() {
1656 use crate::test_helpers::uniform_grid;
1659 let t = uniform_grid(20); let vals: Vec<f64> = t.to_vec(); let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1662 let q = vec![1.3_f64];
1663 let result =
1664 spline_interpolate_with_policy(&data, &t, &q, 4, ExtrapolationPolicy::Periodic)
1665 .unwrap();
1666 assert!(
1668 (result[(0, 0)] - 0.3).abs() < 1e-9,
1669 "Periodic wrap of 1.3 should give ~0.3, got {}",
1670 result[(0, 0)]
1671 );
1672 let q2 = vec![-0.2_f64];
1674 let result2 =
1675 spline_interpolate_with_policy(&data, &t, &q2, 4, ExtrapolationPolicy::Periodic)
1676 .unwrap();
1677 assert!(
1678 (result2[(0, 0)] - 0.8).abs() < 1e-9,
1679 "Periodic wrap of -0.2 should give ~0.8, got {}",
1680 result2[(0, 0)]
1681 );
1682 }
1683
1684 #[test]
1685 fn test_spline_with_policy_periodic_zero_length_domain_errors() {
1686 let argvals = vec![3.0_f64, 3.0, 3.0];
1688 let vals = vec![1.0_f64, 1.0, 1.0];
1689 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 3).unwrap();
1690 let q = vec![4.0_f64]; let err =
1692 spline_interpolate_with_policy(&data, &argvals, &q, 1, ExtrapolationPolicy::Periodic)
1693 .unwrap_err();
1694 assert!(
1695 matches!(
1696 err,
1697 crate::FdarError::InvalidParameter {
1698 parameter: "argvals",
1699 ..
1700 }
1701 ),
1702 "Periodic + zero-length domain should error, got {err:?}"
1703 );
1704 }
1705
1706 fn make_linear_curve(n_pts: usize) -> (crate::matrix::FdMatrix, Vec<f64>) {
1710 use crate::test_helpers::uniform_grid;
1711 let t = uniform_grid(n_pts);
1712 let vals: Vec<f64> = t.to_vec();
1713 let mat = crate::matrix::FdMatrix::from_column_major(vals, 1, n_pts).unwrap();
1714 (mat, t)
1715 }
1716
1717 #[test]
1718 fn test_extrapolation_boundary() {
1719 let (data, t) = make_linear_curve(11); let q = vec![-0.2_f64, 0.5, 1.3];
1722 let result = fdata_interpolate_with_policy(
1723 &data,
1724 &t,
1725 &q,
1726 InterpolationMethod::Linear,
1727 ExtrapolationPolicy::Boundary,
1728 )
1729 .unwrap();
1730 assert!(
1732 (result[(0, 0)] - 0.0).abs() < 1e-10,
1733 "below boundary should clamp to 0"
1734 );
1735 assert!(
1737 (result[(0, 1)] - 0.5).abs() < 1e-10,
1738 "in-range should interpolate correctly"
1739 );
1740 assert!(
1742 (result[(0, 2)] - 1.0).abs() < 1e-10,
1743 "above boundary should clamp to 1"
1744 );
1745 }
1746
1747 #[test]
1748 fn test_extrapolation_exception() {
1749 let (data, t) = make_linear_curve(11);
1750 let q_bad = vec![1.5_f64]; let err = fdata_interpolate_with_policy(
1752 &data,
1753 &t,
1754 &q_bad,
1755 InterpolationMethod::Linear,
1756 ExtrapolationPolicy::Exception,
1757 )
1758 .unwrap_err();
1759 assert!(
1760 matches!(
1761 err,
1762 crate::FdarError::InvalidParameter {
1763 parameter: "new_argvals",
1764 ..
1765 }
1766 ),
1767 "expected InvalidParameter for OOB query, got {err:?}"
1768 );
1769
1770 let q_ok = vec![0.0_f64, 0.5, 1.0];
1772 let result = fdata_interpolate_with_policy(
1773 &data,
1774 &t,
1775 &q_ok,
1776 InterpolationMethod::Linear,
1777 ExtrapolationPolicy::Exception,
1778 )
1779 .unwrap();
1780 assert!((result[(0, 1)] - 0.5).abs() < 1e-10);
1781 }
1782
1783 #[test]
1784 fn test_extrapolation_fill() {
1785 let (data, t) = make_linear_curve(11);
1786 let fill_val = 99.0_f64;
1787 let q = vec![-0.5_f64, 0.5, 2.0];
1788 let result = fdata_interpolate_with_policy(
1789 &data,
1790 &t,
1791 &q,
1792 InterpolationMethod::Linear,
1793 ExtrapolationPolicy::Fill(fill_val),
1794 )
1795 .unwrap();
1796 assert!(
1797 (result[(0, 0)] - fill_val).abs() < 1e-10,
1798 "below range should be fill value"
1799 );
1800 assert!(
1801 (result[(0, 1)] - 0.5).abs() < 1e-10,
1802 "in-range should interpolate"
1803 );
1804 assert!(
1805 (result[(0, 2)] - fill_val).abs() < 1e-10,
1806 "above range should be fill value"
1807 );
1808 }
1809
1810 #[test]
1811 fn test_extrapolation_periodic() {
1812 let (data, t) = make_linear_curve(11); let q = vec![-0.1_f64, 0.5, 1.1];
1815 let result = fdata_interpolate_with_policy(
1816 &data,
1817 &t,
1818 &q,
1819 InterpolationMethod::Linear,
1820 ExtrapolationPolicy::Periodic,
1821 )
1822 .unwrap();
1823 assert!(
1825 (result[(0, 0)] - 0.9).abs() < 1e-9,
1826 "t=-0.1 should wrap to 0.9, got {}",
1827 result[(0, 0)]
1828 );
1829 assert!(
1830 (result[(0, 1)] - 0.5).abs() < 1e-10,
1831 "in-range point unchanged"
1832 );
1833 assert!(
1835 (result[(0, 2)] - 0.1).abs() < 1e-9,
1836 "t=1.1 should wrap to 0.1, got {}",
1837 result[(0, 2)]
1838 );
1839 }
1840
1841 #[test]
1842 fn test_extrapolation_in_range_equivalence() {
1843 let (data, t) = make_linear_curve(21);
1845 let q: Vec<f64> = (0..=10).map(|i| i as f64 / 10.0).collect();
1846 let expected = fdata_interpolate(&data, &t, &q, InterpolationMethod::Linear);
1847 let actual = fdata_interpolate_with_policy(
1848 &data,
1849 &t,
1850 &q,
1851 InterpolationMethod::Linear,
1852 ExtrapolationPolicy::Boundary,
1853 )
1854 .unwrap();
1855 let (_, m_new) = actual.shape();
1856 for j in 0..m_new {
1857 assert!(
1858 (actual[(0, j)] - expected[(0, j)]).abs() < 1e-12,
1859 "in-range mismatch at j={j}: policy={} vs plain={}",
1860 actual[(0, j)],
1861 expected[(0, j)]
1862 );
1863 }
1864 }
1865
1866 #[test]
1867 fn test_extrapolation_policy_dim_guard() {
1868 let (data, _t) = make_linear_curve(11);
1869 let bad_argvals: Vec<f64> = (0..5).map(|i| i as f64 / 4.0).collect(); let q = vec![0.5_f64];
1871 let err = fdata_interpolate_with_policy(
1872 &data,
1873 &bad_argvals,
1874 &q,
1875 InterpolationMethod::Linear,
1876 ExtrapolationPolicy::Boundary,
1877 )
1878 .unwrap_err();
1879 assert!(
1880 matches!(
1881 err,
1882 crate::FdarError::InvalidDimension {
1883 parameter: "argvals",
1884 ..
1885 }
1886 ),
1887 "expected InvalidDimension for argvals mismatch, got {err:?}"
1888 );
1889 }
1890
1891 fn make_curve_with_vals(vals: Vec<f64>) -> (crate::matrix::FdMatrix, Vec<f64>) {
1895 let m = vals.len();
1896 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
1897 let mat = crate::matrix::FdMatrix::from_column_major(vals, 1, m).unwrap();
1898 (mat, argvals)
1899 }
1900
1901 #[test]
1902 fn test_impute_linear() {
1903 let (data, argvals) = make_curve_with_vals(vec![0.0_f64, f64::NAN, 1.0]);
1906 let result = impute_missing_values(&data, &argvals, ImputationMethod::Linear).unwrap();
1907 assert!(
1909 (result[(0, 1)] - 0.5).abs() < 1e-10,
1910 "linear imputation should give 0.5, got {}",
1911 result[(0, 1)]
1912 );
1913 assert!((result[(0, 0)] - 0.0).abs() < 1e-10);
1915 assert!((result[(0, 2)] - 1.0).abs() < 1e-10);
1916 }
1917
1918 #[test]
1919 fn test_impute_mean() {
1920 let (data, argvals) = make_curve_with_vals(vec![1.0_f64, f64::NAN, 3.0]);
1922 let result = impute_missing_values(&data, &argvals, ImputationMethod::Mean).unwrap();
1923 assert!(
1924 (result[(0, 1)] - 2.0).abs() < 1e-10,
1925 "mean imputation should give 2.0, got {}",
1926 result[(0, 1)]
1927 );
1928 }
1929
1930 #[test]
1931 fn test_impute_constant() {
1932 let (data, argvals) = make_curve_with_vals(vec![1.0_f64, f64::NAN, 3.0]);
1934 let result =
1935 impute_missing_values(&data, &argvals, ImputationMethod::Constant(99.0)).unwrap();
1936 assert!(
1937 (result[(0, 1)] - 99.0).abs() < 1e-10,
1938 "constant imputation should give 99.0, got {}",
1939 result[(0, 1)]
1940 );
1941 assert!((result[(0, 0)] - 1.0).abs() < 1e-10);
1943 assert!((result[(0, 2)] - 3.0).abs() < 1e-10);
1944 }
1945
1946 #[test]
1947 fn test_impute_all_nan() {
1948 let (data, argvals) = make_curve_with_vals(vec![f64::NAN, f64::NAN, f64::NAN]);
1950 let err = impute_missing_values(&data, &argvals, ImputationMethod::Linear).unwrap_err();
1951 assert!(
1952 matches!(
1953 err,
1954 crate::FdarError::InvalidParameter {
1955 parameter: "data",
1956 ..
1957 }
1958 ),
1959 "expected InvalidParameter for all-NaN curve, got {err:?}"
1960 );
1961 }
1962
1963 #[test]
1964 fn test_impute_boundary_nan() {
1965 let (data, argvals) = make_curve_with_vals(vec![f64::NAN, 0.5_f64, 1.0]);
1967 let result = impute_missing_values(&data, &argvals, ImputationMethod::Linear).unwrap();
1968 assert!(
1969 (result[(0, 0)] - 0.5).abs() < 1e-10,
1970 "leading NaN should be filled with nearest valid (0.5), got {}",
1971 result[(0, 0)]
1972 );
1973
1974 let (data2, argvals2) = make_curve_with_vals(vec![0.0_f64, 0.5, f64::NAN]);
1976 let result2 = impute_missing_values(&data2, &argvals2, ImputationMethod::Linear).unwrap();
1977 assert!(
1978 (result2[(0, 2)] - 0.5).abs() < 1e-10,
1979 "trailing NaN should be filled with nearest valid (0.5), got {}",
1980 result2[(0, 2)]
1981 );
1982 }
1983
1984 #[test]
1987 fn test_extrapolation_periodic_zero_length_domain_errors() {
1988 let degenerate_argvals = vec![5.0_f64, 5.0, 5.0];
1990 let vals = vec![1.0_f64, 1.0, 1.0];
1991 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 3).unwrap();
1992 let q = vec![6.0_f64]; let err = fdata_interpolate_with_policy(
1995 &data,
1996 °enerate_argvals,
1997 &q,
1998 InterpolationMethod::Linear,
1999 ExtrapolationPolicy::Periodic,
2000 )
2001 .unwrap_err();
2002 assert!(
2003 matches!(
2004 err,
2005 crate::FdarError::InvalidParameter {
2006 parameter: "argvals",
2007 ..
2008 }
2009 ),
2010 "expected InvalidParameter for zero-length domain + Periodic, got {err:?}"
2011 );
2012 }
2013
2014 #[test]
2017 fn test_impute_zero_columns_errors() {
2018 let data = crate::matrix::FdMatrix::zeros(2, 0);
2020 let argvals: Vec<f64> = vec![];
2021 let err = impute_missing_values(&data, &argvals, ImputationMethod::Linear).unwrap_err();
2022 assert!(
2023 matches!(
2024 err,
2025 crate::FdarError::InvalidDimension {
2026 parameter: "data",
2027 ..
2028 }
2029 ),
2030 "expected InvalidDimension for m=0 matrix, got {err:?}"
2031 );
2032 }
2033
2034 #[test]
2035 fn test_impute_dim_mismatch() {
2036 let (data, _argvals) = make_curve_with_vals(vec![1.0, 2.0, 3.0]);
2038 let bad_argvals = vec![0.0_f64, 1.0]; let err = impute_missing_values(&data, &bad_argvals, ImputationMethod::Linear).unwrap_err();
2040 assert!(
2041 matches!(
2042 err,
2043 crate::FdarError::InvalidDimension {
2044 parameter: "argvals",
2045 ..
2046 }
2047 ),
2048 "expected InvalidDimension for argvals mismatch, got {err:?}"
2049 );
2050 }
2051}