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
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#[derive(Debug, Clone, PartialEq)]
883#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
884pub enum ExtrapolationPolicy {
885 Boundary,
890 Exception,
895 Fill(f64),
897 Periodic,
903}
904
905pub fn fdata_interpolate_with_policy(
926 data: &crate::matrix::FdMatrix,
927 argvals: &[f64],
928 new_argvals: &[f64],
929 method: InterpolationMethod,
930 policy: ExtrapolationPolicy,
931) -> Result<crate::matrix::FdMatrix, crate::FdarError> {
932 let (n, m) = data.shape();
933 if argvals.len() != m {
934 return Err(crate::FdarError::InvalidDimension {
935 parameter: "argvals",
936 expected: format!("{m}"),
937 actual: format!("{}", argvals.len()),
938 });
939 }
940 let m_new = new_argvals.len();
941 if n == 0 || m < 2 || m_new == 0 {
942 return Ok(crate::matrix::FdMatrix::zeros(n.max(1), m_new.max(1)));
943 }
944 let t_min = argvals[0];
945 let t_max = argvals[m - 1];
946 let domain_len = t_max - t_min;
947
948 if domain_len <= 0.0 && matches!(policy, ExtrapolationPolicy::Periodic) {
952 return Err(crate::FdarError::InvalidParameter {
953 parameter: "argvals",
954 message: "Periodic extrapolation requires a positive domain length \
955 (argvals[0] < argvals[m-1])"
956 .to_string(),
957 });
958 }
959
960 let mut result = crate::matrix::FdMatrix::zeros(n, m_new);
961 for i in 0..n {
962 let y: Vec<f64> = (0..m).map(|j| data[(i, j)]).collect();
963 for (j, &t) in new_argvals.iter().enumerate() {
964 let in_range = t >= t_min && t <= t_max;
965 result[(i, j)] = if in_range {
966 match method {
967 InterpolationMethod::Linear => linear_interp(argvals, &y, t),
968 InterpolationMethod::CubicHermite => cubic_hermite_interp(argvals, &y, t),
969 }
970 } else {
971 match &policy {
972 ExtrapolationPolicy::Boundary => {
973 let t_clamped = t.clamp(t_min, t_max);
974 match method {
975 InterpolationMethod::Linear => linear_interp(argvals, &y, t_clamped),
976 InterpolationMethod::CubicHermite => {
977 cubic_hermite_interp(argvals, &y, t_clamped)
978 }
979 }
980 }
981 ExtrapolationPolicy::Exception => {
982 return Err(crate::FdarError::InvalidParameter {
983 parameter: "new_argvals",
984 message: format!("query {t} is outside domain [{t_min}, {t_max}]"),
985 });
986 }
987 ExtrapolationPolicy::Fill(v) => *v,
988 ExtrapolationPolicy::Periodic => {
989 let wrapped = t_min + ((t - t_min) % domain_len + domain_len) % domain_len;
990 match method {
991 InterpolationMethod::Linear => linear_interp(argvals, &y, wrapped),
992 InterpolationMethod::CubicHermite => {
993 cubic_hermite_interp(argvals, &y, wrapped)
994 }
995 }
996 }
997 }
998 };
999 }
1000 }
1001 Ok(result)
1002}
1003
1004#[derive(Debug, Clone, PartialEq)]
1014#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1015pub enum ImputationMethod {
1016 Linear,
1021 Mean,
1023 Constant(f64),
1025}
1026
1027pub fn impute_missing_values(
1044 data: &crate::matrix::FdMatrix,
1045 argvals: &[f64],
1046 method: ImputationMethod,
1047) -> Result<crate::matrix::FdMatrix, crate::FdarError> {
1048 let (n, m) = data.shape();
1049 if argvals.len() != m {
1050 return Err(crate::FdarError::InvalidDimension {
1051 parameter: "argvals",
1052 expected: format!("{m}"),
1053 actual: format!("{}", argvals.len()),
1054 });
1055 }
1056 if m == 0 {
1060 return Err(crate::FdarError::InvalidDimension {
1061 parameter: "data",
1062 expected: "m >= 1".to_string(),
1063 actual: "m=0".to_string(),
1064 });
1065 }
1066 let mut out_data = vec![0.0_f64; n * m]; for i in 0..n {
1068 let row: Vec<f64> = data.row(i);
1069 let valid_count = row.iter().filter(|v| !v.is_nan()).count();
1070 if valid_count == 0 {
1071 return Err(crate::FdarError::InvalidParameter {
1072 parameter: "data",
1073 message: format!("curve {i} contains only NaN values"),
1074 });
1075 }
1076 let imputed = impute_row(&row, argvals, &method);
1077 for j in 0..m {
1078 out_data[i + j * n] = imputed[j]; }
1080 }
1081 crate::matrix::FdMatrix::from_column_major(out_data, n, m)
1082}
1083
1084fn impute_row(row: &[f64], argvals: &[f64], method: &ImputationMethod) -> Vec<f64> {
1086 let mut result = row.to_vec();
1087 match method {
1088 ImputationMethod::Mean => {
1089 let sum: f64 = row.iter().filter(|v| !v.is_nan()).sum();
1090 let count = row.iter().filter(|v| !v.is_nan()).count();
1091 let mean = sum / count as f64;
1092 for v in &mut result {
1093 if v.is_nan() {
1094 *v = mean;
1095 }
1096 }
1097 }
1098 ImputationMethod::Constant(c) => {
1099 for v in &mut result {
1100 if v.is_nan() {
1101 *v = *c;
1102 }
1103 }
1104 }
1105 ImputationMethod::Linear => {
1106 let valid_idxs: Vec<usize> = (0..row.len()).filter(|&j| !row[j].is_nan()).collect();
1107 for j in 0..row.len() {
1108 if result[j].is_nan() {
1109 let left = valid_idxs.iter().rev().find(|&&k| k < j).copied();
1110 let right = valid_idxs.iter().find(|&&k| k > j).copied();
1111 result[j] = match (left, right) {
1112 (Some(l), Some(r)) => {
1113 linear_interp(&[argvals[l], argvals[r]], &[row[l], row[r]], argvals[j])
1114 }
1115 (Some(l), None) => row[l], (None, Some(r)) => row[r], (None, None) => unreachable!(), };
1119 }
1120 }
1121 }
1122 }
1123 result
1124}
1125
1126#[cfg(test)]
1127mod tests {
1128 use super::*;
1129
1130 #[test]
1131 fn test_simpsons_weights_uniform() {
1132 let argvals = vec![0.0, 0.25, 0.5, 0.75, 1.0];
1133 let weights = simpsons_weights(&argvals);
1134 let sum: f64 = weights.iter().sum();
1135 assert!((sum - 1.0).abs() < NUMERICAL_EPS);
1136 }
1137
1138 #[test]
1139 fn test_simpsons_weights_2d() {
1140 let argvals_s = vec![0.0, 0.5, 1.0];
1141 let argvals_t = vec![0.0, 0.5, 1.0];
1142 let weights = simpsons_weights_2d(&argvals_s, &argvals_t);
1143 let sum: f64 = weights.iter().sum();
1144 assert!((sum - 1.0).abs() < NUMERICAL_EPS);
1145 }
1146
1147 #[test]
1148 fn test_extract_curves() {
1149 let data = vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0];
1152 let mat = crate::matrix::FdMatrix::from_column_major(data, 2, 3).unwrap();
1153 let curves = extract_curves(&mat);
1154 assert_eq!(curves.len(), 2);
1155 assert_eq!(curves[0], vec![1.0, 2.0, 3.0]);
1156 assert_eq!(curves[1], vec![4.0, 5.0, 6.0]);
1157 }
1158
1159 #[test]
1160 fn test_l2_distance_identical() {
1161 let curve = vec![1.0, 2.0, 3.0];
1162 let weights = vec![0.25, 0.5, 0.25];
1163 let dist = l2_distance(&curve, &curve, &weights);
1164 assert!(dist.abs() < NUMERICAL_EPS);
1165 }
1166
1167 #[test]
1168 fn test_l2_distance_different() {
1169 let curve1 = vec![0.0, 0.0, 0.0];
1170 let curve2 = vec![1.0, 1.0, 1.0];
1171 let weights = vec![0.25, 0.5, 0.25]; let dist = l2_distance(&curve1, &curve2, &weights);
1173 assert!((dist - 1.0).abs() < NUMERICAL_EPS);
1175 }
1176
1177 #[test]
1178 fn test_n1_weights() {
1179 let w = simpsons_weights(&[0.5]);
1181 assert_eq!(w.len(), 1);
1182 assert!((w[0] - 1.0).abs() < 1e-12);
1183 }
1184
1185 #[test]
1186 fn test_n2_weights() {
1187 let w = simpsons_weights(&[0.0, 1.0]);
1188 assert_eq!(w.len(), 2);
1189 assert!((w[0] - 0.5).abs() < 1e-12);
1191 assert!((w[1] - 0.5).abs() < 1e-12);
1192 }
1193
1194 #[test]
1195 fn test_mismatched_l2_distance() {
1196 let a = vec![1.0, 2.0, 3.0];
1198 let b = vec![1.0, 2.0, 3.0];
1199 let w = vec![0.5, 0.5, 0.5];
1200 let d = l2_distance(&a, &b, &w);
1201 assert!(d.abs() < 1e-12, "Same vectors should have zero distance");
1202 }
1203
1204 #[test]
1207 fn test_trapz_sine() {
1208 let m = 1000;
1210 let x: Vec<f64> = (0..m)
1211 .map(|i| std::f64::consts::PI * i as f64 / (m - 1) as f64)
1212 .collect();
1213 let y: Vec<f64> = x.iter().map(|&xi| xi.sin()).collect();
1214 let result = trapz(&y, &x);
1215 assert!(
1216 (result - 2.0).abs() < 1e-4,
1217 "∫ sin(x) dx over [0,π] should be ~2, got {result}"
1218 );
1219 }
1220
1221 #[test]
1224 fn test_cumulative_trapz_matches_final() {
1225 let m = 100;
1226 let x: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
1227 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi).collect();
1228 let cum = cumulative_trapz(&y, &x);
1229 let total = trapz(&y, &x);
1230 assert!(
1231 (cum[m - 1] - total).abs() < 1e-12,
1232 "Final cumulative value should match trapz"
1233 );
1234 }
1235
1236 #[test]
1239 fn test_linear_interp_boundary_clamp() {
1240 let x = vec![0.0, 0.5, 1.0];
1241 let y = vec![10.0, 20.0, 30.0];
1242 assert!((linear_interp(&x, &y, -1.0) - 10.0).abs() < 1e-12);
1243 assert!((linear_interp(&x, &y, 2.0) - 30.0).abs() < 1e-12);
1244 assert!((linear_interp(&x, &y, 0.25) - 15.0).abs() < 1e-12);
1245 }
1246
1247 #[test]
1250 fn test_gradient_uniform_linear() {
1251 let m = 50;
1253 let h = 1.0 / (m - 1) as f64;
1254 let y: Vec<f64> = (0..m).map(|i| 3.0 * i as f64 * h).collect();
1255 let g = gradient_uniform(&y, h);
1256 for i in 0..m {
1257 assert!(
1258 (g[i] - 3.0).abs() < 1e-10,
1259 "gradient of 3x should be 3 at i={i}, got {}",
1260 g[i]
1261 );
1262 }
1263 }
1264
1265 #[test]
1268 fn test_gaussian_kernel() {
1269 assert!((gaussian_kernel(0.0, 1.0) - 1.0).abs() < 1e-12);
1270 assert!(gaussian_kernel(3.0, 1.0) < 0.02); assert!((gaussian_kernel(1.0, 0.0)).abs() < 1e-12); }
1273
1274 #[test]
1275 fn test_bandwidth_candidates() {
1276 let n = 5;
1277 let mut dists = vec![0.0; n * n];
1278 for i in 0..n {
1279 for j in 0..n {
1280 dists[i * n + j] = (i as f64 - j as f64).abs();
1281 }
1282 }
1283 let cands = bandwidth_candidates_from_dists(&dists, n, 10);
1284 assert!(!cands.is_empty());
1285 assert!(cands.iter().all(|&h| h > 0.0));
1286 for w in cands.windows(2) {
1288 assert!(w[1] >= w[0]);
1289 }
1290 }
1291
1292 #[test]
1293 fn test_quantile_sorted() {
1294 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1295 assert!((quantile_sorted(&data, 0.0) - 1.0).abs() < 1e-12);
1296 assert!((quantile_sorted(&data, 1.0) - 5.0).abs() < 1e-12);
1297 assert!((quantile_sorted(&data, 0.5) - 3.0).abs() < 1e-12);
1298 assert!((quantile_sorted(&data, 0.25) - 2.0).abs() < 1e-12);
1299 }
1300
1301 #[test]
1302 fn test_r_squared_perfect() {
1303 let y = vec![1.0, 2.0, 3.0, 4.0];
1304 let resid = vec![0.0, 0.0, 0.0, 0.0];
1305 assert!((r_squared(&y, &resid) - 1.0).abs() < 1e-12);
1306 }
1307
1308 #[test]
1309 fn test_r_squared_mean_model() {
1310 let y = vec![1.0, 2.0, 3.0, 4.0];
1311 let mean = 2.5;
1312 let resid: Vec<f64> = y.iter().map(|&yi| yi - mean).collect();
1313 assert!(r_squared(&y, &resid).abs() < 1e-12); }
1315
1316 #[test]
1317 fn test_aic_bic() {
1318 let a = aic(100, 50.0, 5);
1319 let b = bic(100, 50.0, 5);
1320 assert!(a.is_finite());
1321 assert!(b.is_finite());
1322 assert!(b > a); }
1324
1325 #[test]
1326 fn fdata_interpolate_linear_identity() {
1327 let t: Vec<f64> = (0..20).map(|i| i as f64 / 19.0).collect();
1328 let vals: Vec<f64> = t.iter().map(|&x| x.sin()).collect();
1329 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1330 let result = fdata_interpolate(&data, &t, &t, InterpolationMethod::Linear);
1331 for j in 0..20 {
1332 assert!((result[(0, j)] - data[(0, j)]).abs() < 1e-12);
1333 }
1334 }
1335
1336 #[test]
1337 fn fdata_interpolate_cubic_hermite_smooth() {
1338 let t: Vec<f64> = (0..20).map(|i| i as f64 / 19.0).collect();
1339 let vals: Vec<f64> = t.iter().map(|&x| x.sin()).collect();
1340 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1341
1342 let t_fine: Vec<f64> = (0..100).map(|i| i as f64 / 99.0).collect();
1343 let result = fdata_interpolate(&data, &t, &t_fine, InterpolationMethod::CubicHermite);
1344
1345 for (j, &tj) in t_fine.iter().enumerate() {
1347 assert!(
1348 (result[(0, j)] - tj.sin()).abs() < 0.02,
1349 "at t={tj:.2}: got {:.4}, expected {:.4}",
1350 result[(0, j)],
1351 tj.sin()
1352 );
1353 }
1354 }
1355
1356 #[test]
1357 fn fdata_interpolate_multiple_curves() {
1358 let t: Vec<f64> = (0..30).map(|i| i as f64 / 29.0).collect();
1359 let n = 5;
1360 let m = 30;
1361 let mut col_major = vec![0.0; n * m];
1363 for i in 0..n {
1364 for j in 0..m {
1365 col_major[i + j * n] = ((i + 1) as f64 * t[j]).sin();
1366 }
1367 }
1368 let data = crate::matrix::FdMatrix::from_column_major(col_major, n, m).unwrap();
1369
1370 let t_new: Vec<f64> = (0..50).map(|i| i as f64 / 49.0).collect();
1371 let result = fdata_interpolate(&data, &t, &t_new, InterpolationMethod::Linear);
1372 assert_eq!(result.shape(), (n, 50));
1373 for i in 0..n {
1375 for j in 0..50 {
1376 assert!(result[(i, j)].is_finite());
1377 }
1378 }
1379 }
1380
1381 #[test]
1384 fn spline_interpolate_reproduces_argvals() {
1385 use crate::test_helpers::uniform_grid;
1386 let t = uniform_grid(20);
1387 let vals: Vec<f64> = t.iter().map(|&x| x.powi(3)).collect();
1388 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1390 let result = spline_interpolate(&data, &t, &t, 4).unwrap();
1391 for j in 0..20 {
1392 assert!(
1393 (result[(0, j)] - data[(0, j)]).abs() < 1e-10,
1394 "at j={j}: got {}, expected {}",
1395 result[(0, j)],
1396 data[(0, j)]
1397 );
1398 }
1399 }
1400
1401 #[test]
1402 fn spline_interpolate_cubic_offgrid() {
1403 use crate::test_helpers::uniform_grid;
1407 let t = uniform_grid(20); let poly = |x: f64| 2.0 * x.powi(3) - x.powi(2) + 0.5 * x - 0.1;
1409 let vals: Vec<f64> = t.iter().map(|&x| poly(x)).collect();
1410 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1411
1412 let q: Vec<f64> = t.windows(2).map(|w| (w[0] + w[1]) / 2.0).collect();
1414 let result = spline_interpolate(&data, &t, &q, 4).unwrap();
1415
1416 for (j, &qj) in q.iter().enumerate() {
1417 let expected = poly(qj);
1418 let got = result[(0, j)];
1419 assert!(
1420 (got - expected).abs() < 1e-10,
1421 "off-grid at q={qj:.4}: got {got}, expected {expected}"
1422 );
1423 }
1424 }
1425
1426 #[test]
1427 fn spline_interpolate_rejects_out_of_range() {
1428 use crate::test_helpers::uniform_grid;
1429 let t = uniform_grid(20);
1430 let vals: Vec<f64> = t.to_vec();
1431 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1432
1433 let q_below = vec![-0.1_f64];
1435 let err = spline_interpolate(&data, &t, &q_below, 4).unwrap_err();
1436 assert!(
1437 matches!(
1438 err,
1439 crate::FdarError::InvalidParameter {
1440 parameter: "query_points",
1441 ..
1442 }
1443 ),
1444 "expected InvalidParameter for query below domain, got {err:?}"
1445 );
1446
1447 let q_above = vec![1.1_f64];
1449 let err2 = spline_interpolate(&data, &t, &q_above, 4).unwrap_err();
1450 assert!(
1451 matches!(
1452 err2,
1453 crate::FdarError::InvalidParameter {
1454 parameter: "query_points",
1455 ..
1456 }
1457 ),
1458 "expected InvalidParameter for query above domain, got {err2:?}"
1459 );
1460 }
1461
1462 #[test]
1463 fn spline_interpolate_rejects_bad_order() {
1464 use crate::test_helpers::uniform_grid;
1465 let t = uniform_grid(20);
1466 let vals: Vec<f64> = t.to_vec();
1467 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1468 let q = vec![0.5_f64];
1469
1470 let err = spline_interpolate(&data, &t, &q, 0).unwrap_err();
1472 assert!(
1473 matches!(
1474 err,
1475 crate::FdarError::InvalidParameter {
1476 parameter: "order",
1477 ..
1478 }
1479 ),
1480 "expected InvalidParameter for order=0, got {err:?}"
1481 );
1482
1483 let err2 = spline_interpolate(&data, &t, &q, 20).unwrap_err();
1485 assert!(
1486 matches!(
1487 err2,
1488 crate::FdarError::InvalidParameter {
1489 parameter: "order",
1490 ..
1491 }
1492 ),
1493 "expected InvalidParameter for order=20 (>=m=20), got {err2:?}"
1494 );
1495 }
1496
1497 #[test]
1498 fn spline_interpolate_rejects_dim_mismatch() {
1499 use crate::test_helpers::uniform_grid;
1500 let t = uniform_grid(20);
1501 let vals: Vec<f64> = t.to_vec();
1502 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1503
1504 let bad_argvals: Vec<f64> = (0..15).map(|i| i as f64 / 14.0).collect();
1506 let q = vec![0.5_f64];
1507 let err = spline_interpolate(&data, &bad_argvals, &q, 4).unwrap_err();
1508 assert!(
1509 matches!(
1510 err,
1511 crate::FdarError::InvalidDimension {
1512 parameter: "argvals",
1513 ..
1514 }
1515 ),
1516 "expected InvalidDimension for argvals mismatch, got {err:?}"
1517 );
1518
1519 let err2 = spline_interpolate(&data, &t, &[], 4).unwrap_err();
1521 assert!(
1522 matches!(
1523 err2,
1524 crate::FdarError::InvalidDimension {
1525 parameter: "query_points",
1526 ..
1527 }
1528 ),
1529 "expected InvalidDimension for empty query_points, got {err2:?}"
1530 );
1531 }
1532
1533 #[test]
1536 fn test_spline_with_policy_in_range_matches_spline() {
1537 use crate::test_helpers::uniform_grid;
1539 let t = uniform_grid(20);
1540 let poly = |x: f64| 2.0 * x.powi(3) - x.powi(2) + 0.5 * x - 0.1;
1541 let vals: Vec<f64> = t.iter().map(|&x| poly(x)).collect();
1542 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1543 let q: Vec<f64> = t.windows(2).map(|w| (w[0] + w[1]) / 2.0).collect();
1545 let expected = spline_interpolate(&data, &t, &q, 4).unwrap();
1546 let actual =
1547 spline_interpolate_with_policy(&data, &t, &q, 4, ExtrapolationPolicy::Boundary)
1548 .unwrap();
1549 for j in 0..q.len() {
1550 assert!(
1551 (actual[(0, j)] - expected[(0, j)]).abs() < 1e-10,
1552 "in-range mismatch at j={j}: policy={} vs plain={}",
1553 actual[(0, j)],
1554 expected[(0, j)]
1555 );
1556 }
1557 }
1558
1559 #[test]
1560 fn test_spline_with_policy_boundary() {
1561 use crate::test_helpers::uniform_grid;
1563 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();
1566 let q = vec![-0.5_f64, 0.5, 1.5];
1568 let result =
1569 spline_interpolate_with_policy(&data, &t, &q, 4, ExtrapolationPolicy::Boundary)
1570 .unwrap();
1571 assert!(
1573 result[(0, 0)].abs() < 1e-9,
1574 "below boundary should clamp, got {}",
1575 result[(0, 0)]
1576 );
1577 assert!(
1579 (result[(0, 2)] - 1.0).abs() < 1e-9,
1580 "above boundary should clamp, got {}",
1581 result[(0, 2)]
1582 );
1583 assert!(
1585 (result[(0, 1)] - 0.25).abs() < 1e-9,
1586 "in-range should be ~0.25, got {}",
1587 result[(0, 1)]
1588 );
1589 }
1590
1591 #[test]
1592 fn test_spline_with_policy_exception() {
1593 use crate::test_helpers::uniform_grid;
1595 let t = uniform_grid(20);
1596 let vals: Vec<f64> = t.to_vec();
1597 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1598 let q_oob = vec![1.5_f64];
1599 let err =
1600 spline_interpolate_with_policy(&data, &t, &q_oob, 4, ExtrapolationPolicy::Exception)
1601 .unwrap_err();
1602 assert!(
1603 matches!(
1604 err,
1605 crate::FdarError::InvalidParameter {
1606 parameter: "query_points",
1607 ..
1608 }
1609 ),
1610 "Exception policy should error on OOB, got {err:?}"
1611 );
1612 let q_ok = vec![0.0_f64, 0.5, 1.0];
1614 let ok =
1615 spline_interpolate_with_policy(&data, &t, &q_ok, 4, ExtrapolationPolicy::Exception);
1616 assert!(
1617 ok.is_ok(),
1618 "Exception policy should succeed for in-range queries"
1619 );
1620 }
1621
1622 #[test]
1623 fn test_spline_with_policy_fill() {
1624 use crate::test_helpers::uniform_grid;
1626 let t = uniform_grid(20); let vals: Vec<f64> = t.iter().map(|&x| x.powi(2)).collect();
1628 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1629 let fill_val = 42.0_f64;
1630 let q = vec![-0.5_f64, 0.5, 2.0];
1631 let result =
1632 spline_interpolate_with_policy(&data, &t, &q, 4, ExtrapolationPolicy::Fill(fill_val))
1633 .unwrap();
1634 assert!(
1635 (result[(0, 0)] - fill_val).abs() < 1e-10,
1636 "OOB below should be fill value, got {}",
1637 result[(0, 0)]
1638 );
1639 assert!(
1640 (result[(0, 2)] - fill_val).abs() < 1e-10,
1641 "OOB above should be fill value, got {}",
1642 result[(0, 2)]
1643 );
1644 assert!(
1646 (result[(0, 1)] - 0.25).abs() < 1e-9,
1647 "in-range should be ~0.25, got {}",
1648 result[(0, 1)]
1649 );
1650 }
1651
1652 #[test]
1653 fn test_spline_with_policy_periodic() {
1654 use crate::test_helpers::uniform_grid;
1657 let t = uniform_grid(20); let vals: Vec<f64> = t.to_vec(); let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1660 let q = vec![1.3_f64];
1661 let result =
1662 spline_interpolate_with_policy(&data, &t, &q, 4, ExtrapolationPolicy::Periodic)
1663 .unwrap();
1664 assert!(
1666 (result[(0, 0)] - 0.3).abs() < 1e-9,
1667 "Periodic wrap of 1.3 should give ~0.3, got {}",
1668 result[(0, 0)]
1669 );
1670 let q2 = vec![-0.2_f64];
1672 let result2 =
1673 spline_interpolate_with_policy(&data, &t, &q2, 4, ExtrapolationPolicy::Periodic)
1674 .unwrap();
1675 assert!(
1676 (result2[(0, 0)] - 0.8).abs() < 1e-9,
1677 "Periodic wrap of -0.2 should give ~0.8, got {}",
1678 result2[(0, 0)]
1679 );
1680 }
1681
1682 #[test]
1683 fn test_spline_with_policy_periodic_zero_length_domain_errors() {
1684 let argvals = vec![3.0_f64, 3.0, 3.0];
1686 let vals = vec![1.0_f64, 1.0, 1.0];
1687 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 3).unwrap();
1688 let q = vec![4.0_f64]; let err =
1690 spline_interpolate_with_policy(&data, &argvals, &q, 1, ExtrapolationPolicy::Periodic)
1691 .unwrap_err();
1692 assert!(
1693 matches!(
1694 err,
1695 crate::FdarError::InvalidParameter {
1696 parameter: "argvals",
1697 ..
1698 }
1699 ),
1700 "Periodic + zero-length domain should error, got {err:?}"
1701 );
1702 }
1703
1704 fn make_linear_curve(n_pts: usize) -> (crate::matrix::FdMatrix, Vec<f64>) {
1708 use crate::test_helpers::uniform_grid;
1709 let t = uniform_grid(n_pts);
1710 let vals: Vec<f64> = t.to_vec();
1711 let mat = crate::matrix::FdMatrix::from_column_major(vals, 1, n_pts).unwrap();
1712 (mat, t)
1713 }
1714
1715 #[test]
1716 fn test_extrapolation_boundary() {
1717 let (data, t) = make_linear_curve(11); let q = vec![-0.2_f64, 0.5, 1.3];
1720 let result = fdata_interpolate_with_policy(
1721 &data,
1722 &t,
1723 &q,
1724 InterpolationMethod::Linear,
1725 ExtrapolationPolicy::Boundary,
1726 )
1727 .unwrap();
1728 assert!(
1730 (result[(0, 0)] - 0.0).abs() < 1e-10,
1731 "below boundary should clamp to 0"
1732 );
1733 assert!(
1735 (result[(0, 1)] - 0.5).abs() < 1e-10,
1736 "in-range should interpolate correctly"
1737 );
1738 assert!(
1740 (result[(0, 2)] - 1.0).abs() < 1e-10,
1741 "above boundary should clamp to 1"
1742 );
1743 }
1744
1745 #[test]
1746 fn test_extrapolation_exception() {
1747 let (data, t) = make_linear_curve(11);
1748 let q_bad = vec![1.5_f64]; let err = fdata_interpolate_with_policy(
1750 &data,
1751 &t,
1752 &q_bad,
1753 InterpolationMethod::Linear,
1754 ExtrapolationPolicy::Exception,
1755 )
1756 .unwrap_err();
1757 assert!(
1758 matches!(
1759 err,
1760 crate::FdarError::InvalidParameter {
1761 parameter: "new_argvals",
1762 ..
1763 }
1764 ),
1765 "expected InvalidParameter for OOB query, got {err:?}"
1766 );
1767
1768 let q_ok = vec![0.0_f64, 0.5, 1.0];
1770 let result = fdata_interpolate_with_policy(
1771 &data,
1772 &t,
1773 &q_ok,
1774 InterpolationMethod::Linear,
1775 ExtrapolationPolicy::Exception,
1776 )
1777 .unwrap();
1778 assert!((result[(0, 1)] - 0.5).abs() < 1e-10);
1779 }
1780
1781 #[test]
1782 fn test_extrapolation_fill() {
1783 let (data, t) = make_linear_curve(11);
1784 let fill_val = 99.0_f64;
1785 let q = vec![-0.5_f64, 0.5, 2.0];
1786 let result = fdata_interpolate_with_policy(
1787 &data,
1788 &t,
1789 &q,
1790 InterpolationMethod::Linear,
1791 ExtrapolationPolicy::Fill(fill_val),
1792 )
1793 .unwrap();
1794 assert!(
1795 (result[(0, 0)] - fill_val).abs() < 1e-10,
1796 "below range should be fill value"
1797 );
1798 assert!(
1799 (result[(0, 1)] - 0.5).abs() < 1e-10,
1800 "in-range should interpolate"
1801 );
1802 assert!(
1803 (result[(0, 2)] - fill_val).abs() < 1e-10,
1804 "above range should be fill value"
1805 );
1806 }
1807
1808 #[test]
1809 fn test_extrapolation_periodic() {
1810 let (data, t) = make_linear_curve(11); let q = vec![-0.1_f64, 0.5, 1.1];
1813 let result = fdata_interpolate_with_policy(
1814 &data,
1815 &t,
1816 &q,
1817 InterpolationMethod::Linear,
1818 ExtrapolationPolicy::Periodic,
1819 )
1820 .unwrap();
1821 assert!(
1823 (result[(0, 0)] - 0.9).abs() < 1e-9,
1824 "t=-0.1 should wrap to 0.9, got {}",
1825 result[(0, 0)]
1826 );
1827 assert!(
1828 (result[(0, 1)] - 0.5).abs() < 1e-10,
1829 "in-range point unchanged"
1830 );
1831 assert!(
1833 (result[(0, 2)] - 0.1).abs() < 1e-9,
1834 "t=1.1 should wrap to 0.1, got {}",
1835 result[(0, 2)]
1836 );
1837 }
1838
1839 #[test]
1840 fn test_extrapolation_in_range_equivalence() {
1841 let (data, t) = make_linear_curve(21);
1843 let q: Vec<f64> = (0..=10).map(|i| i as f64 / 10.0).collect();
1844 let expected = fdata_interpolate(&data, &t, &q, InterpolationMethod::Linear);
1845 let actual = fdata_interpolate_with_policy(
1846 &data,
1847 &t,
1848 &q,
1849 InterpolationMethod::Linear,
1850 ExtrapolationPolicy::Boundary,
1851 )
1852 .unwrap();
1853 let (_, m_new) = actual.shape();
1854 for j in 0..m_new {
1855 assert!(
1856 (actual[(0, j)] - expected[(0, j)]).abs() < 1e-12,
1857 "in-range mismatch at j={j}: policy={} vs plain={}",
1858 actual[(0, j)],
1859 expected[(0, j)]
1860 );
1861 }
1862 }
1863
1864 #[test]
1865 fn test_extrapolation_policy_dim_guard() {
1866 let (data, _t) = make_linear_curve(11);
1867 let bad_argvals: Vec<f64> = (0..5).map(|i| i as f64 / 4.0).collect(); let q = vec![0.5_f64];
1869 let err = fdata_interpolate_with_policy(
1870 &data,
1871 &bad_argvals,
1872 &q,
1873 InterpolationMethod::Linear,
1874 ExtrapolationPolicy::Boundary,
1875 )
1876 .unwrap_err();
1877 assert!(
1878 matches!(
1879 err,
1880 crate::FdarError::InvalidDimension {
1881 parameter: "argvals",
1882 ..
1883 }
1884 ),
1885 "expected InvalidDimension for argvals mismatch, got {err:?}"
1886 );
1887 }
1888
1889 fn make_curve_with_vals(vals: Vec<f64>) -> (crate::matrix::FdMatrix, Vec<f64>) {
1893 let m = vals.len();
1894 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
1895 let mat = crate::matrix::FdMatrix::from_column_major(vals, 1, m).unwrap();
1896 (mat, argvals)
1897 }
1898
1899 #[test]
1900 fn test_impute_linear() {
1901 let (data, argvals) = make_curve_with_vals(vec![0.0_f64, f64::NAN, 1.0]);
1904 let result = impute_missing_values(&data, &argvals, ImputationMethod::Linear).unwrap();
1905 assert!(
1907 (result[(0, 1)] - 0.5).abs() < 1e-10,
1908 "linear imputation should give 0.5, got {}",
1909 result[(0, 1)]
1910 );
1911 assert!((result[(0, 0)] - 0.0).abs() < 1e-10);
1913 assert!((result[(0, 2)] - 1.0).abs() < 1e-10);
1914 }
1915
1916 #[test]
1917 fn test_impute_mean() {
1918 let (data, argvals) = make_curve_with_vals(vec![1.0_f64, f64::NAN, 3.0]);
1920 let result = impute_missing_values(&data, &argvals, ImputationMethod::Mean).unwrap();
1921 assert!(
1922 (result[(0, 1)] - 2.0).abs() < 1e-10,
1923 "mean imputation should give 2.0, got {}",
1924 result[(0, 1)]
1925 );
1926 }
1927
1928 #[test]
1929 fn test_impute_constant() {
1930 let (data, argvals) = make_curve_with_vals(vec![1.0_f64, f64::NAN, 3.0]);
1932 let result =
1933 impute_missing_values(&data, &argvals, ImputationMethod::Constant(99.0)).unwrap();
1934 assert!(
1935 (result[(0, 1)] - 99.0).abs() < 1e-10,
1936 "constant imputation should give 99.0, got {}",
1937 result[(0, 1)]
1938 );
1939 assert!((result[(0, 0)] - 1.0).abs() < 1e-10);
1941 assert!((result[(0, 2)] - 3.0).abs() < 1e-10);
1942 }
1943
1944 #[test]
1945 fn test_impute_all_nan() {
1946 let (data, argvals) = make_curve_with_vals(vec![f64::NAN, f64::NAN, f64::NAN]);
1948 let err = impute_missing_values(&data, &argvals, ImputationMethod::Linear).unwrap_err();
1949 assert!(
1950 matches!(
1951 err,
1952 crate::FdarError::InvalidParameter {
1953 parameter: "data",
1954 ..
1955 }
1956 ),
1957 "expected InvalidParameter for all-NaN curve, got {err:?}"
1958 );
1959 }
1960
1961 #[test]
1962 fn test_impute_boundary_nan() {
1963 let (data, argvals) = make_curve_with_vals(vec![f64::NAN, 0.5_f64, 1.0]);
1965 let result = impute_missing_values(&data, &argvals, ImputationMethod::Linear).unwrap();
1966 assert!(
1967 (result[(0, 0)] - 0.5).abs() < 1e-10,
1968 "leading NaN should be filled with nearest valid (0.5), got {}",
1969 result[(0, 0)]
1970 );
1971
1972 let (data2, argvals2) = make_curve_with_vals(vec![0.0_f64, 0.5, f64::NAN]);
1974 let result2 = impute_missing_values(&data2, &argvals2, ImputationMethod::Linear).unwrap();
1975 assert!(
1976 (result2[(0, 2)] - 0.5).abs() < 1e-10,
1977 "trailing NaN should be filled with nearest valid (0.5), got {}",
1978 result2[(0, 2)]
1979 );
1980 }
1981
1982 #[test]
1985 fn test_extrapolation_periodic_zero_length_domain_errors() {
1986 let degenerate_argvals = vec![5.0_f64, 5.0, 5.0];
1988 let vals = vec![1.0_f64, 1.0, 1.0];
1989 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 3).unwrap();
1990 let q = vec![6.0_f64]; let err = fdata_interpolate_with_policy(
1993 &data,
1994 °enerate_argvals,
1995 &q,
1996 InterpolationMethod::Linear,
1997 ExtrapolationPolicy::Periodic,
1998 )
1999 .unwrap_err();
2000 assert!(
2001 matches!(
2002 err,
2003 crate::FdarError::InvalidParameter {
2004 parameter: "argvals",
2005 ..
2006 }
2007 ),
2008 "expected InvalidParameter for zero-length domain + Periodic, got {err:?}"
2009 );
2010 }
2011
2012 #[test]
2015 fn test_impute_zero_columns_errors() {
2016 let data = crate::matrix::FdMatrix::zeros(2, 0);
2018 let argvals: Vec<f64> = vec![];
2019 let err = impute_missing_values(&data, &argvals, ImputationMethod::Linear).unwrap_err();
2020 assert!(
2021 matches!(
2022 err,
2023 crate::FdarError::InvalidDimension {
2024 parameter: "data",
2025 ..
2026 }
2027 ),
2028 "expected InvalidDimension for m=0 matrix, got {err:?}"
2029 );
2030 }
2031
2032 #[test]
2033 fn test_impute_dim_mismatch() {
2034 let (data, _argvals) = make_curve_with_vals(vec![1.0, 2.0, 3.0]);
2036 let bad_argvals = vec![0.0_f64, 1.0]; let err = impute_missing_values(&data, &bad_argvals, ImputationMethod::Linear).unwrap_err();
2038 assert!(
2039 matches!(
2040 err,
2041 crate::FdarError::InvalidDimension {
2042 parameter: "argvals",
2043 ..
2044 }
2045 ),
2046 "expected InvalidDimension for argvals mismatch, got {err:?}"
2047 );
2048 }
2049}