1pub const NUMERICAL_EPS: f64 = 1e-10;
5
6pub const DEFAULT_CONVERGENCE_TOL: f64 = 1e-6;
8
9pub fn sort_nan_safe(slice: &mut [f64]) {
11 slice.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
12}
13
14pub fn extract_curves(data: &crate::matrix::FdMatrix) -> Vec<Vec<f64>> {
25 data.rows()
26}
27
28pub fn l2_distance(curve1: &[f64], curve2: &[f64], weights: &[f64]) -> f64 {
38 let mut dist_sq = 0.0;
39 for i in 0..curve1.len() {
40 let diff = curve1[i] - curve2[i];
41 dist_sq += diff * diff * weights[i];
42 }
43 dist_sq.sqrt()
44}
45
46pub fn simpsons_weights(argvals: &[f64]) -> Vec<f64> {
58 let n = argvals.len();
59 if n < 2 {
60 return vec![1.0; n];
61 }
62
63 let mut weights = vec![0.0; n];
64
65 if n == 2 {
66 let h = argvals[1] - argvals[0];
68 weights[0] = h / 2.0;
69 weights[1] = h / 2.0;
70 return weights;
71 }
72
73 let h0 = argvals[1] - argvals[0];
75 let is_uniform = argvals
76 .windows(2)
77 .all(|w| ((w[1] - w[0]) - h0).abs() < 1e-12 * h0.abs());
78
79 if is_uniform {
80 simpsons_weights_uniform(&mut weights, n, h0);
81 } else {
82 simpsons_weights_nonuniform(&mut weights, argvals, n);
83 }
84
85 weights
86}
87
88fn simpsons_weights_uniform(weights: &mut [f64], n: usize, h0: f64) {
90 let n_intervals = n - 1;
91 if n_intervals % 2 == 0 {
92 weights[0] = h0 / 3.0;
94 weights[n - 1] = h0 / 3.0;
95 for i in 1..n - 1 {
96 weights[i] = if i % 2 == 1 {
97 4.0 * h0 / 3.0
98 } else {
99 2.0 * h0 / 3.0
100 };
101 }
102 } else {
103 let n_simp = n - 1;
105 weights[0] = h0 / 3.0;
106 weights[n_simp - 1] = h0 / 3.0;
107 for i in 1..n_simp - 1 {
108 weights[i] = if i % 2 == 1 {
109 4.0 * h0 / 3.0
110 } else {
111 2.0 * h0 / 3.0
112 };
113 }
114 weights[n_simp - 1] += h0 / 2.0;
115 weights[n - 1] += h0 / 2.0;
116 }
117}
118
119fn simpsons_weights_nonuniform(weights: &mut [f64], argvals: &[f64], n: usize) {
121 let n_intervals = n - 1;
122 let n_pairs = n_intervals / 2;
123
124 for k in 0..n_pairs {
125 let i0 = 2 * k;
126 let i1 = i0 + 1;
127 let i2 = i0 + 2;
128 let h1 = argvals[i1] - argvals[i0];
129 let h2 = argvals[i2] - argvals[i1];
130 let h_sum = h1 + h2;
131
132 weights[i0] += (2.0 * h1 - h2) * h_sum / (6.0 * h1);
133 weights[i1] += h_sum * h_sum * h_sum / (6.0 * h1 * h2);
134 weights[i2] += (2.0 * h2 - h1) * h_sum / (6.0 * h2);
135 }
136
137 if n_intervals % 2 == 1 {
138 let h_last = argvals[n - 1] - argvals[n - 2];
139 weights[n - 2] += h_last / 2.0;
140 weights[n - 1] += h_last / 2.0;
141 }
142}
143
144pub fn simpsons_weights_2d(argvals_s: &[f64], argvals_t: &[f64]) -> Vec<f64> {
155 let weights_s = simpsons_weights(argvals_s);
156 let weights_t = simpsons_weights(argvals_t);
157 let m1 = argvals_s.len();
158 let m2 = argvals_t.len();
159
160 let mut weights = vec![0.0; m1 * m2];
161 for i in 0..m1 {
162 for j in 0..m2 {
163 weights[i + j * m1] = weights_s[i] * weights_t[j];
164 }
165 }
166 weights
167}
168
169pub fn linear_interp(x: &[f64], y: &[f64], t: f64) -> f64 {
173 if t <= x[0] {
174 return y[0];
175 }
176 let last = x.len() - 1;
177 if t >= x[last] {
178 return y[last];
179 }
180
181 let idx = match x.binary_search_by(|v| v.partial_cmp(&t).unwrap_or(std::cmp::Ordering::Equal)) {
182 Ok(i) => return y[i],
183 Err(i) => i,
184 };
185
186 let t0 = x[idx - 1];
187 let t1 = x[idx];
188 let y0 = y[idx - 1];
189 let y1 = y[idx];
190 y0 + (y1 - y0) * (t - t0) / (t1 - t0)
191}
192
193pub fn cumulative_trapz(y: &[f64], x: &[f64]) -> Vec<f64> {
198 let n = y.len();
199 let mut out = vec![0.0; n];
200 if n < 2 {
201 return out;
202 }
203
204 let mut k = 1;
206 while k + 1 < n {
207 let h1 = x[k] - x[k - 1];
208 let h2 = x[k + 1] - x[k];
209 let h_sum = h1 + h2;
210
211 let integral = h_sum / 6.0
213 * (y[k - 1] * (2.0 * h1 - h2) / h1
214 + y[k] * h_sum * h_sum / (h1 * h2)
215 + y[k + 1] * (2.0 * h2 - h1) / h2);
216
217 out[k] = out[k - 1] + {
218 0.5 * (y[k] + y[k - 1]) * h1
220 };
221 out[k + 1] = out[k - 1] + integral;
222 k += 2;
223 }
224
225 if k < n {
227 out[k] = out[k - 1] + 0.5 * (y[k] + y[k - 1]) * (x[k] - x[k - 1]);
228 }
229
230 out
231}
232
233pub fn trapz(y: &[f64], x: &[f64]) -> f64 {
235 let mut sum = 0.0;
236 for k in 1..y.len() {
237 sum += 0.5 * (y[k] + y[k - 1]) * (x[k] - x[k - 1]);
238 }
239 sum
240}
241
242pub fn gaussian_kernel(d: f64, h: f64) -> f64 {
248 if h < 1e-15 {
249 return 0.0;
250 }
251 (-d * d / (2.0 * h * h)).exp()
252}
253
254pub fn bandwidth_candidates_from_dists(dists: &[f64], n: usize, n_quantiles: usize) -> Vec<f64> {
260 let mut nonzero: Vec<f64> = (0..n)
261 .flat_map(|i| ((i + 1)..n).map(move |j| dists[i * n + j]))
262 .filter(|&d| d > 0.0)
263 .collect();
264 sort_nan_safe(&mut nonzero);
265
266 if nonzero.is_empty() {
267 return Vec::new();
268 }
269
270 (1..=n_quantiles)
271 .map(|q| {
272 let p = q as f64 / (n_quantiles + 1) as f64;
273 let idx = ((nonzero.len() as f64 * p) as usize).min(nonzero.len() - 1);
274 nonzero[idx]
275 })
276 .filter(|&h| h > 1e-15)
277 .collect()
278}
279
280pub fn quantile_sorted(sorted: &[f64], p: f64) -> f64 {
284 if sorted.is_empty() {
285 return f64::NAN;
286 }
287 if sorted.len() == 1 || p <= 0.0 {
288 return sorted[0];
289 }
290 if p >= 1.0 {
291 return sorted[sorted.len() - 1];
292 }
293 let pos = p * (sorted.len() - 1) as f64;
294 let lo = pos.floor() as usize;
295 let hi = (lo + 1).min(sorted.len() - 1);
296 let frac = pos - lo as f64;
297 sorted[lo] * (1.0 - frac) + sorted[hi] * frac
298}
299
300pub fn r_squared(y_true: &[f64], residuals: &[f64]) -> f64 {
302 let n = y_true.len();
303 if n == 0 {
304 return f64::NAN;
305 }
306 let mean = y_true.iter().sum::<f64>() / n as f64;
307 let ss_tot: f64 = y_true.iter().map(|&y| (y - mean).powi(2)).sum();
308 let ss_res: f64 = residuals.iter().map(|r| r * r).sum();
309 if ss_tot > 1e-15 {
310 1.0 - ss_res / ss_tot
311 } else {
312 0.0
313 }
314}
315
316pub fn r_squared_adj(y_true: &[f64], residuals: &[f64], p: usize) -> f64 {
318 let n = y_true.len();
319 let r2 = r_squared(y_true, residuals);
320 if n <= p + 1 {
321 return r2;
322 }
323 1.0 - (1.0 - r2) * (n - 1) as f64 / (n - p - 1) as f64
324}
325
326pub fn aic(n: usize, rss: f64, p: usize) -> f64 {
330 let nf = n as f64;
331 nf * (rss / nf).ln() + 2.0 * p as f64
332}
333
334pub fn bic(n: usize, rss: f64, p: usize) -> f64 {
338 let nf = n as f64;
339 nf * (rss / nf).ln() + nf.ln() * p as f64
340}
341
342#[derive(Debug, Clone, Copy, PartialEq)]
344#[non_exhaustive]
345pub enum InterpolationMethod {
346 Linear,
348 CubicHermite,
350}
351
352#[must_use]
366pub fn fdata_interpolate(
367 data: &crate::matrix::FdMatrix,
368 argvals: &[f64],
369 new_argvals: &[f64],
370 method: InterpolationMethod,
371) -> crate::matrix::FdMatrix {
372 let (n, m) = data.shape();
373 let m_new = new_argvals.len();
374 if n == 0 || m < 2 || m_new == 0 {
375 return crate::matrix::FdMatrix::zeros(n.max(1), m_new.max(1));
376 }
377
378 let mut result = crate::matrix::FdMatrix::zeros(n, m_new);
379
380 for i in 0..n {
381 let y: Vec<f64> = (0..m).map(|j| data[(i, j)]).collect();
382 for (j, &t) in new_argvals.iter().enumerate() {
383 result[(i, j)] = match method {
384 InterpolationMethod::Linear => linear_interp(argvals, &y, t),
385 InterpolationMethod::CubicHermite => cubic_hermite_interp(argvals, &y, t),
386 };
387 }
388 }
389
390 result
391}
392
393pub fn spline_interpolate(
417 data: &crate::matrix::FdMatrix,
418 argvals: &[f64],
419 query_points: &[f64],
420 order: usize,
421) -> Result<crate::matrix::FdMatrix, crate::FdarError> {
422 let (n, m) = data.shape();
423
424 if argvals.len() != m {
426 return Err(crate::FdarError::InvalidDimension {
427 parameter: "argvals",
428 expected: format!("{m}"),
429 actual: format!("{}", argvals.len()),
430 });
431 }
432 if query_points.is_empty() {
433 return Err(crate::FdarError::InvalidDimension {
434 parameter: "query_points",
435 expected: ">= 1".to_string(),
436 actual: "0".to_string(),
437 });
438 }
439 if order == 0 || order >= m {
440 return Err(crate::FdarError::InvalidParameter {
441 parameter: "order",
442 message: format!("must be in [1, {m}), got {order}"),
443 });
444 }
445 let t_min = argvals[0];
446 let t_max = argvals[m - 1];
447 for &q in query_points {
448 if q < t_min || q > t_max {
449 return Err(crate::FdarError::InvalidParameter {
450 parameter: "query_points",
451 message: format!(
452 "all query points must lie in [{t_min}, {t_max}]; found {q} which is outside the interpolation domain"
453 ),
454 });
455 }
456 }
457
458 let nknots = m.saturating_sub(order).max(2);
461 let knots = crate::basis::bspline::construct_bspline_knots(t_min, t_max, nknots, order);
462
463 let basis_vals = crate::basis::bspline::bspline_basis(argvals, nknots, order);
465 let nbasis = basis_vals.len() / m;
466
467 let b_mat = nalgebra::DMatrix::from_column_slice(m, nbasis, &basis_vals);
469
470 let tol = NUMERICAL_EPS * b_mat.nrows().max(b_mat.ncols()) as f64;
473 let svd = nalgebra::SVD::new(b_mat.clone(), true, true);
474 let pinv = svd
475 .pseudo_inverse(tol)
476 .map_err(|e| crate::FdarError::ComputationFailed {
477 operation: "spline_interpolate SVD pseudoinverse",
478 detail: e.to_string(),
479 })?;
480 let m_q = query_points.len();
485 let basis_query = crate::basis::bspline::bspline_basis_from_knots(query_points, &knots, order);
486
487 let mut out = crate::matrix::FdMatrix::zeros(n, m_q);
489 for i in 0..n {
490 let y_vec: Vec<f64> = (0..m).map(|j| data[(i, j)]).collect();
492 let y_col = nalgebra::DVector::from_vec(y_vec);
493
494 let coefs = &pinv * y_col;
496
497 for j in 0..m_q {
499 let mut val = 0.0;
500 for k in 0..nbasis {
501 val += coefs[k] * basis_query[j + k * m_q];
502 }
503 out[(i, j)] = val;
504 }
505 }
506
507 Ok(out)
508}
509
510pub fn spline_interpolate_with_policy(
540 data: &crate::matrix::FdMatrix,
541 argvals: &[f64],
542 query_points: &[f64],
543 order: usize,
544 policy: ExtrapolationPolicy,
545) -> Result<crate::matrix::FdMatrix, crate::FdarError> {
546 let (n, m) = data.shape();
547
548 if argvals.len() != m {
550 return Err(crate::FdarError::InvalidDimension {
551 parameter: "argvals",
552 expected: format!("{m}"),
553 actual: format!("{}", argvals.len()),
554 });
555 }
556 if query_points.is_empty() {
557 return Err(crate::FdarError::InvalidDimension {
558 parameter: "query_points",
559 expected: ">= 1".to_string(),
560 actual: "0".to_string(),
561 });
562 }
563 if order == 0 || order >= m {
564 return Err(crate::FdarError::InvalidParameter {
565 parameter: "order",
566 message: format!("must be in [1, {m}), got {order}"),
567 });
568 }
569
570 let t_min = argvals[0];
571 let t_max = argvals[m - 1];
572 let domain_len = t_max - t_min;
573
574 if domain_len <= 0.0 && matches!(policy, ExtrapolationPolicy::Periodic) {
576 return Err(crate::FdarError::InvalidParameter {
577 parameter: "argvals",
578 message: "Periodic extrapolation requires a positive domain length \
579 (argvals[0] < argvals[m-1])"
580 .to_string(),
581 });
582 }
583
584 let m_q = query_points.len();
585
586 let mut effective = Vec::with_capacity(m_q);
590 let mut fill_mask = vec![false; m_q];
591
592 for (j, &q) in query_points.iter().enumerate() {
593 let in_range = q >= t_min && q <= t_max;
594 if in_range {
595 effective.push(q);
596 } else {
597 match &policy {
598 ExtrapolationPolicy::Boundary => effective.push(q.clamp(t_min, t_max)),
599 ExtrapolationPolicy::Exception => {
600 return Err(crate::FdarError::InvalidParameter {
601 parameter: "query_points",
602 message: format!("query {q} is outside domain [{t_min}, {t_max}]"),
603 });
604 }
605 ExtrapolationPolicy::Fill(_) => {
606 fill_mask[j] = true;
608 effective.push(t_min); }
610 ExtrapolationPolicy::Periodic => {
611 let wrapped = t_min + ((q - t_min) % domain_len + domain_len) % domain_len;
612 effective.push(wrapped);
613 }
614 }
615 }
616 }
617
618 let nknots = m.saturating_sub(order).max(2);
621 let knots = crate::basis::bspline::construct_bspline_knots(t_min, t_max, nknots, order);
622 let basis_vals = crate::basis::bspline::bspline_basis(argvals, nknots, order);
623 let nbasis = basis_vals.len() / m;
624 let b_mat = nalgebra::DMatrix::from_column_slice(m, nbasis, &basis_vals);
625 let tol = NUMERICAL_EPS * b_mat.nrows().max(b_mat.ncols()) as f64;
626 let svd = nalgebra::SVD::new(b_mat.clone(), true, true);
627 let pinv = svd
628 .pseudo_inverse(tol)
629 .map_err(|e| crate::FdarError::ComputationFailed {
630 operation: "spline_interpolate_with_policy SVD pseudoinverse",
631 detail: e.to_string(),
632 })?;
633 let basis_query = crate::basis::bspline::bspline_basis_from_knots(&effective, &knots, order);
634
635 let mut out = crate::matrix::FdMatrix::zeros(n, m_q);
636 for i in 0..n {
637 let y_vec: Vec<f64> = (0..m).map(|j| data[(i, j)]).collect();
638 let y_col = nalgebra::DVector::from_vec(y_vec);
639 let coefs = &pinv * y_col;
640
641 for j in 0..m_q {
642 if fill_mask[j] {
643 if let ExtrapolationPolicy::Fill(v) = policy {
645 out[(i, j)] = v;
646 }
647 } else {
648 let mut val = 0.0_f64;
649 for k in 0..nbasis {
650 val += coefs[k] * basis_query[j + k * m_q];
651 }
652 out[(i, j)] = val;
653 }
654 }
655 }
656
657 Ok(out)
658}
659
660fn cubic_hermite_interp(x: &[f64], y: &[f64], t: f64) -> f64 {
664 let n = x.len();
665 if n < 2 {
666 return if n == 1 { y[0] } else { 0.0 };
667 }
668
669 if t <= x[0] {
671 return y[0];
672 }
673 if t >= x[n - 1] {
674 return y[n - 1];
675 }
676
677 let k = match x.binary_search_by(|v| v.partial_cmp(&t).unwrap_or(std::cmp::Ordering::Equal)) {
679 Ok(i) => return y[i],
680 Err(i) => {
681 if i == 0 {
682 0
683 } else {
684 i - 1
685 }
686 }
687 };
688
689 let slopes: Vec<f64> = x
691 .windows(2)
692 .zip(y.windows(2))
693 .map(|(xw, yw)| (yw[1] - yw[0]) / (xw[1] - xw[0]))
694 .collect();
695
696 let mut tangents = vec![0.0; n];
698 tangents[0] = slopes[0];
699 tangents[n - 1] = slopes[n - 2];
700 for i in 1..n - 1 {
701 if slopes[i - 1].signum() != slopes[i].signum() {
702 tangents[i] = 0.0;
703 } else {
704 tangents[i] = (slopes[i - 1] + slopes[i]) / 2.0;
705 }
706 }
707
708 let h = x[k + 1] - x[k];
710 let s = (t - x[k]) / h;
711 let s2 = s * s;
712 let s3 = s2 * s;
713
714 let h00 = 2.0 * s3 - 3.0 * s2 + 1.0;
715 let h10 = s3 - 2.0 * s2 + s;
716 let h01 = -2.0 * s3 + 3.0 * s2;
717 let h11 = s3 - s2;
718
719 h00 * y[k] + h10 * h * tangents[k] + h01 * y[k + 1] + h11 * h * tangents[k + 1]
720}
721
722pub fn gradient_uniform(y: &[f64], h: f64) -> Vec<f64> {
729 let n = y.len();
730 let mut g = vec![0.0; n];
731 if n < 2 {
732 return g;
733 }
734 if n == 2 {
735 g[0] = (y[1] - y[0]) / h;
736 g[1] = (y[1] - y[0]) / h;
737 return g;
738 }
739 if n == 3 {
740 g[0] = (-3.0 * y[0] + 4.0 * y[1] - y[2]) / (2.0 * h);
741 g[1] = (y[2] - y[0]) / (2.0 * h);
742 g[2] = (y[0] - 4.0 * y[1] + 3.0 * y[2]) / (2.0 * h);
743 return g;
744 }
745 if n == 4 {
746 g[0] = (-3.0 * y[0] + 4.0 * y[1] - y[2]) / (2.0 * h);
747 g[1] = (y[2] - y[0]) / (2.0 * h);
748 g[2] = (y[3] - y[1]) / (2.0 * h);
749 g[3] = (y[1] - 4.0 * y[2] + 3.0 * y[3]) / (2.0 * h);
750 return g;
751 }
752
753 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);
756 g[1] = (-3.0 * y[0] - 10.0 * y[1] + 18.0 * y[2] - 6.0 * y[3] + y[4]) / (12.0 * h);
757
758 for i in 2..n - 2 {
760 g[i] = (-y[i + 2] + 8.0 * y[i + 1] - 8.0 * y[i - 1] + y[i - 2]) / (12.0 * h);
761 }
762
763 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])
765 / (12.0 * h);
766 g[n - 1] = (3.0 * y[n - 5] - 16.0 * y[n - 4] + 36.0 * y[n - 3] - 48.0 * y[n - 2]
767 + 25.0 * y[n - 1])
768 / (12.0 * h);
769 g
770}
771
772pub fn gradient_nonuniform(y: &[f64], t: &[f64]) -> Vec<f64> {
780 let n = y.len();
781 assert_eq!(n, t.len(), "y and t must have the same length");
782 let mut g = vec![0.0; n];
783 if n < 2 {
784 return g;
785 }
786 if n == 2 {
787 let h = t[1] - t[0];
788 if h.abs() < 1e-15 {
789 return g;
790 }
791 g[0] = (y[1] - y[0]) / h;
792 g[1] = g[0];
793 return g;
794 }
795
796 let h0 = t[1] - t[0];
798 let h1 = t[2] - t[0];
799 if h0.abs() > 1e-15 && h1.abs() > 1e-15 && (h1 - h0).abs() > 1e-15 {
800 g[0] = y[0] * (-h1 - h0) / (h0 * h1) + y[1] * h1 / (h0 * (h1 - h0))
801 - y[2] * h0 / (h1 * (h1 - h0));
802 } else {
803 g[0] = (y[1] - y[0]) / h0.max(1e-15);
804 }
805
806 for i in 1..n - 1 {
808 let h_l = t[i] - t[i - 1];
809 let h_r = t[i + 1] - t[i];
810 let h_sum = h_l + h_r;
811 if h_l.abs() < 1e-15 || h_r.abs() < 1e-15 || h_sum.abs() < 1e-15 {
812 g[i] = 0.0;
813 continue;
814 }
815 g[i] = -y[i - 1] * h_r / (h_l * h_sum)
816 + y[i] * (h_r - h_l) / (h_l * h_r)
817 + y[i + 1] * h_l / (h_r * h_sum);
818 }
819
820 let h_last = t[n - 1] - t[n - 2];
822 let h_prev = t[n - 1] - t[n - 3];
823 let h_mid = t[n - 2] - t[n - 3];
824 if h_last.abs() > 1e-15 && h_prev.abs() > 1e-15 && h_mid.abs() > 1e-15 {
825 g[n - 1] = y[n - 3] * h_last / (h_mid * h_prev) - y[n - 2] * h_prev / (h_mid * h_last)
826 + y[n - 1] * (h_prev + h_last) / (h_prev * h_last);
827 } else {
828 g[n - 1] = (y[n - 1] - y[n - 2]) / h_last.max(1e-15);
829 }
830
831 g
832}
833
834pub fn gradient(y: &[f64], t: &[f64]) -> Vec<f64> {
840 let n = t.len();
841 if n < 2 {
842 return vec![0.0; y.len()];
843 }
844
845 let h0 = t[1] - t[0];
846 let is_uniform = t
847 .windows(2)
848 .all(|w| ((w[1] - w[0]) - h0).abs() < 1e-12 * h0.abs().max(1.0));
849
850 if is_uniform {
851 gradient_uniform(y, h0)
852 } else {
853 gradient_nonuniform(y, t)
854 }
855}
856
857#[derive(Debug, Clone, PartialEq)]
864#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
865pub enum ExtrapolationPolicy {
866 Boundary,
871 Exception,
876 Fill(f64),
878 Periodic,
884}
885
886pub fn fdata_interpolate_with_policy(
907 data: &crate::matrix::FdMatrix,
908 argvals: &[f64],
909 new_argvals: &[f64],
910 method: InterpolationMethod,
911 policy: ExtrapolationPolicy,
912) -> Result<crate::matrix::FdMatrix, crate::FdarError> {
913 let (n, m) = data.shape();
914 if argvals.len() != m {
915 return Err(crate::FdarError::InvalidDimension {
916 parameter: "argvals",
917 expected: format!("{m}"),
918 actual: format!("{}", argvals.len()),
919 });
920 }
921 let m_new = new_argvals.len();
922 if n == 0 || m < 2 || m_new == 0 {
923 return Ok(crate::matrix::FdMatrix::zeros(n.max(1), m_new.max(1)));
924 }
925 let t_min = argvals[0];
926 let t_max = argvals[m - 1];
927 let domain_len = t_max - t_min;
928
929 if domain_len <= 0.0 && matches!(policy, ExtrapolationPolicy::Periodic) {
933 return Err(crate::FdarError::InvalidParameter {
934 parameter: "argvals",
935 message: "Periodic extrapolation requires a positive domain length \
936 (argvals[0] < argvals[m-1])"
937 .to_string(),
938 });
939 }
940
941 let mut result = crate::matrix::FdMatrix::zeros(n, m_new);
942 for i in 0..n {
943 let y: Vec<f64> = (0..m).map(|j| data[(i, j)]).collect();
944 for (j, &t) in new_argvals.iter().enumerate() {
945 let in_range = t >= t_min && t <= t_max;
946 result[(i, j)] = if in_range {
947 match method {
948 InterpolationMethod::Linear => linear_interp(argvals, &y, t),
949 InterpolationMethod::CubicHermite => cubic_hermite_interp(argvals, &y, t),
950 }
951 } else {
952 match &policy {
953 ExtrapolationPolicy::Boundary => {
954 let t_clamped = t.clamp(t_min, t_max);
955 match method {
956 InterpolationMethod::Linear => linear_interp(argvals, &y, t_clamped),
957 InterpolationMethod::CubicHermite => {
958 cubic_hermite_interp(argvals, &y, t_clamped)
959 }
960 }
961 }
962 ExtrapolationPolicy::Exception => {
963 return Err(crate::FdarError::InvalidParameter {
964 parameter: "new_argvals",
965 message: format!("query {t} is outside domain [{t_min}, {t_max}]"),
966 });
967 }
968 ExtrapolationPolicy::Fill(v) => *v,
969 ExtrapolationPolicy::Periodic => {
970 let wrapped = t_min + ((t - t_min) % domain_len + domain_len) % domain_len;
971 match method {
972 InterpolationMethod::Linear => linear_interp(argvals, &y, wrapped),
973 InterpolationMethod::CubicHermite => {
974 cubic_hermite_interp(argvals, &y, wrapped)
975 }
976 }
977 }
978 }
979 };
980 }
981 }
982 Ok(result)
983}
984
985#[derive(Debug, Clone, PartialEq)]
995#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
996pub enum ImputationMethod {
997 Linear,
1002 Mean,
1004 Constant(f64),
1006}
1007
1008pub fn impute_missing_values(
1025 data: &crate::matrix::FdMatrix,
1026 argvals: &[f64],
1027 method: ImputationMethod,
1028) -> Result<crate::matrix::FdMatrix, crate::FdarError> {
1029 let (n, m) = data.shape();
1030 if argvals.len() != m {
1031 return Err(crate::FdarError::InvalidDimension {
1032 parameter: "argvals",
1033 expected: format!("{m}"),
1034 actual: format!("{}", argvals.len()),
1035 });
1036 }
1037 if m == 0 {
1041 return Err(crate::FdarError::InvalidDimension {
1042 parameter: "data",
1043 expected: "m >= 1".to_string(),
1044 actual: "m=0".to_string(),
1045 });
1046 }
1047 let mut out_data = vec![0.0_f64; n * m]; for i in 0..n {
1049 let row: Vec<f64> = data.row(i);
1050 let valid_count = row.iter().filter(|v| !v.is_nan()).count();
1051 if valid_count == 0 {
1052 return Err(crate::FdarError::InvalidParameter {
1053 parameter: "data",
1054 message: format!("curve {i} contains only NaN values"),
1055 });
1056 }
1057 let imputed = impute_row(&row, argvals, &method);
1058 for j in 0..m {
1059 out_data[i + j * n] = imputed[j]; }
1061 }
1062 crate::matrix::FdMatrix::from_column_major(out_data, n, m)
1063}
1064
1065fn impute_row(row: &[f64], argvals: &[f64], method: &ImputationMethod) -> Vec<f64> {
1067 let mut result = row.to_vec();
1068 match method {
1069 ImputationMethod::Mean => {
1070 let sum: f64 = row.iter().filter(|v| !v.is_nan()).sum();
1071 let count = row.iter().filter(|v| !v.is_nan()).count();
1072 let mean = sum / count as f64;
1073 for v in &mut result {
1074 if v.is_nan() {
1075 *v = mean;
1076 }
1077 }
1078 }
1079 ImputationMethod::Constant(c) => {
1080 for v in &mut result {
1081 if v.is_nan() {
1082 *v = *c;
1083 }
1084 }
1085 }
1086 ImputationMethod::Linear => {
1087 let valid_idxs: Vec<usize> = (0..row.len()).filter(|&j| !row[j].is_nan()).collect();
1088 for j in 0..row.len() {
1089 if result[j].is_nan() {
1090 let left = valid_idxs.iter().rev().find(|&&k| k < j).copied();
1091 let right = valid_idxs.iter().find(|&&k| k > j).copied();
1092 result[j] = match (left, right) {
1093 (Some(l), Some(r)) => {
1094 linear_interp(&[argvals[l], argvals[r]], &[row[l], row[r]], argvals[j])
1095 }
1096 (Some(l), None) => row[l], (None, Some(r)) => row[r], (None, None) => unreachable!(), };
1100 }
1101 }
1102 }
1103 }
1104 result
1105}
1106
1107#[cfg(test)]
1108mod tests {
1109 use super::*;
1110
1111 #[test]
1112 fn test_simpsons_weights_uniform() {
1113 let argvals = vec![0.0, 0.25, 0.5, 0.75, 1.0];
1114 let weights = simpsons_weights(&argvals);
1115 let sum: f64 = weights.iter().sum();
1116 assert!((sum - 1.0).abs() < NUMERICAL_EPS);
1117 }
1118
1119 #[test]
1120 fn test_simpsons_weights_2d() {
1121 let argvals_s = vec![0.0, 0.5, 1.0];
1122 let argvals_t = vec![0.0, 0.5, 1.0];
1123 let weights = simpsons_weights_2d(&argvals_s, &argvals_t);
1124 let sum: f64 = weights.iter().sum();
1125 assert!((sum - 1.0).abs() < NUMERICAL_EPS);
1126 }
1127
1128 #[test]
1129 fn test_extract_curves() {
1130 let data = vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0];
1133 let mat = crate::matrix::FdMatrix::from_column_major(data, 2, 3).unwrap();
1134 let curves = extract_curves(&mat);
1135 assert_eq!(curves.len(), 2);
1136 assert_eq!(curves[0], vec![1.0, 2.0, 3.0]);
1137 assert_eq!(curves[1], vec![4.0, 5.0, 6.0]);
1138 }
1139
1140 #[test]
1141 fn test_l2_distance_identical() {
1142 let curve = vec![1.0, 2.0, 3.0];
1143 let weights = vec![0.25, 0.5, 0.25];
1144 let dist = l2_distance(&curve, &curve, &weights);
1145 assert!(dist.abs() < NUMERICAL_EPS);
1146 }
1147
1148 #[test]
1149 fn test_l2_distance_different() {
1150 let curve1 = vec![0.0, 0.0, 0.0];
1151 let curve2 = vec![1.0, 1.0, 1.0];
1152 let weights = vec![0.25, 0.5, 0.25]; let dist = l2_distance(&curve1, &curve2, &weights);
1154 assert!((dist - 1.0).abs() < NUMERICAL_EPS);
1156 }
1157
1158 #[test]
1159 fn test_n1_weights() {
1160 let w = simpsons_weights(&[0.5]);
1162 assert_eq!(w.len(), 1);
1163 assert!((w[0] - 1.0).abs() < 1e-12);
1164 }
1165
1166 #[test]
1167 fn test_n2_weights() {
1168 let w = simpsons_weights(&[0.0, 1.0]);
1169 assert_eq!(w.len(), 2);
1170 assert!((w[0] - 0.5).abs() < 1e-12);
1172 assert!((w[1] - 0.5).abs() < 1e-12);
1173 }
1174
1175 #[test]
1176 fn test_mismatched_l2_distance() {
1177 let a = vec![1.0, 2.0, 3.0];
1179 let b = vec![1.0, 2.0, 3.0];
1180 let w = vec![0.5, 0.5, 0.5];
1181 let d = l2_distance(&a, &b, &w);
1182 assert!(d.abs() < 1e-12, "Same vectors should have zero distance");
1183 }
1184
1185 #[test]
1188 fn test_trapz_sine() {
1189 let m = 1000;
1191 let x: Vec<f64> = (0..m)
1192 .map(|i| std::f64::consts::PI * i as f64 / (m - 1) as f64)
1193 .collect();
1194 let y: Vec<f64> = x.iter().map(|&xi| xi.sin()).collect();
1195 let result = trapz(&y, &x);
1196 assert!(
1197 (result - 2.0).abs() < 1e-4,
1198 "∫ sin(x) dx over [0,π] should be ~2, got {result}"
1199 );
1200 }
1201
1202 #[test]
1205 fn test_cumulative_trapz_matches_final() {
1206 let m = 100;
1207 let x: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
1208 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi).collect();
1209 let cum = cumulative_trapz(&y, &x);
1210 let total = trapz(&y, &x);
1211 assert!(
1212 (cum[m - 1] - total).abs() < 1e-12,
1213 "Final cumulative value should match trapz"
1214 );
1215 }
1216
1217 #[test]
1220 fn test_linear_interp_boundary_clamp() {
1221 let x = vec![0.0, 0.5, 1.0];
1222 let y = vec![10.0, 20.0, 30.0];
1223 assert!((linear_interp(&x, &y, -1.0) - 10.0).abs() < 1e-12);
1224 assert!((linear_interp(&x, &y, 2.0) - 30.0).abs() < 1e-12);
1225 assert!((linear_interp(&x, &y, 0.25) - 15.0).abs() < 1e-12);
1226 }
1227
1228 #[test]
1231 fn test_gradient_uniform_linear() {
1232 let m = 50;
1234 let h = 1.0 / (m - 1) as f64;
1235 let y: Vec<f64> = (0..m).map(|i| 3.0 * i as f64 * h).collect();
1236 let g = gradient_uniform(&y, h);
1237 for i in 0..m {
1238 assert!(
1239 (g[i] - 3.0).abs() < 1e-10,
1240 "gradient of 3x should be 3 at i={i}, got {}",
1241 g[i]
1242 );
1243 }
1244 }
1245
1246 #[test]
1249 fn test_gaussian_kernel() {
1250 assert!((gaussian_kernel(0.0, 1.0) - 1.0).abs() < 1e-12);
1251 assert!(gaussian_kernel(3.0, 1.0) < 0.02); assert!((gaussian_kernel(1.0, 0.0)).abs() < 1e-12); }
1254
1255 #[test]
1256 fn test_bandwidth_candidates() {
1257 let n = 5;
1258 let mut dists = vec![0.0; n * n];
1259 for i in 0..n {
1260 for j in 0..n {
1261 dists[i * n + j] = (i as f64 - j as f64).abs();
1262 }
1263 }
1264 let cands = bandwidth_candidates_from_dists(&dists, n, 10);
1265 assert!(!cands.is_empty());
1266 assert!(cands.iter().all(|&h| h > 0.0));
1267 for w in cands.windows(2) {
1269 assert!(w[1] >= w[0]);
1270 }
1271 }
1272
1273 #[test]
1274 fn test_quantile_sorted() {
1275 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1276 assert!((quantile_sorted(&data, 0.0) - 1.0).abs() < 1e-12);
1277 assert!((quantile_sorted(&data, 1.0) - 5.0).abs() < 1e-12);
1278 assert!((quantile_sorted(&data, 0.5) - 3.0).abs() < 1e-12);
1279 assert!((quantile_sorted(&data, 0.25) - 2.0).abs() < 1e-12);
1280 }
1281
1282 #[test]
1283 fn test_r_squared_perfect() {
1284 let y = vec![1.0, 2.0, 3.0, 4.0];
1285 let resid = vec![0.0, 0.0, 0.0, 0.0];
1286 assert!((r_squared(&y, &resid) - 1.0).abs() < 1e-12);
1287 }
1288
1289 #[test]
1290 fn test_r_squared_mean_model() {
1291 let y = vec![1.0, 2.0, 3.0, 4.0];
1292 let mean = 2.5;
1293 let resid: Vec<f64> = y.iter().map(|&yi| yi - mean).collect();
1294 assert!(r_squared(&y, &resid).abs() < 1e-12); }
1296
1297 #[test]
1298 fn test_aic_bic() {
1299 let a = aic(100, 50.0, 5);
1300 let b = bic(100, 50.0, 5);
1301 assert!(a.is_finite());
1302 assert!(b.is_finite());
1303 assert!(b > a); }
1305
1306 #[test]
1307 fn fdata_interpolate_linear_identity() {
1308 let t: Vec<f64> = (0..20).map(|i| i as f64 / 19.0).collect();
1309 let vals: Vec<f64> = t.iter().map(|&x| x.sin()).collect();
1310 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1311 let result = fdata_interpolate(&data, &t, &t, InterpolationMethod::Linear);
1312 for j in 0..20 {
1313 assert!((result[(0, j)] - data[(0, j)]).abs() < 1e-12);
1314 }
1315 }
1316
1317 #[test]
1318 fn fdata_interpolate_cubic_hermite_smooth() {
1319 let t: Vec<f64> = (0..20).map(|i| i as f64 / 19.0).collect();
1320 let vals: Vec<f64> = t.iter().map(|&x| x.sin()).collect();
1321 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1322
1323 let t_fine: Vec<f64> = (0..100).map(|i| i as f64 / 99.0).collect();
1324 let result = fdata_interpolate(&data, &t, &t_fine, InterpolationMethod::CubicHermite);
1325
1326 for (j, &tj) in t_fine.iter().enumerate() {
1328 assert!(
1329 (result[(0, j)] - tj.sin()).abs() < 0.02,
1330 "at t={tj:.2}: got {:.4}, expected {:.4}",
1331 result[(0, j)],
1332 tj.sin()
1333 );
1334 }
1335 }
1336
1337 #[test]
1338 fn fdata_interpolate_multiple_curves() {
1339 let t: Vec<f64> = (0..30).map(|i| i as f64 / 29.0).collect();
1340 let n = 5;
1341 let m = 30;
1342 let mut col_major = vec![0.0; n * m];
1344 for i in 0..n {
1345 for j in 0..m {
1346 col_major[i + j * n] = ((i + 1) as f64 * t[j]).sin();
1347 }
1348 }
1349 let data = crate::matrix::FdMatrix::from_column_major(col_major, n, m).unwrap();
1350
1351 let t_new: Vec<f64> = (0..50).map(|i| i as f64 / 49.0).collect();
1352 let result = fdata_interpolate(&data, &t, &t_new, InterpolationMethod::Linear);
1353 assert_eq!(result.shape(), (n, 50));
1354 for i in 0..n {
1356 for j in 0..50 {
1357 assert!(result[(i, j)].is_finite());
1358 }
1359 }
1360 }
1361
1362 #[test]
1365 fn spline_interpolate_reproduces_argvals() {
1366 use crate::test_helpers::uniform_grid;
1367 let t = uniform_grid(20);
1368 let vals: Vec<f64> = t.iter().map(|&x| x.powi(3)).collect();
1369 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1371 let result = spline_interpolate(&data, &t, &t, 4).unwrap();
1372 for j in 0..20 {
1373 assert!(
1374 (result[(0, j)] - data[(0, j)]).abs() < 1e-10,
1375 "at j={j}: got {}, expected {}",
1376 result[(0, j)],
1377 data[(0, j)]
1378 );
1379 }
1380 }
1381
1382 #[test]
1383 fn spline_interpolate_cubic_offgrid() {
1384 use crate::test_helpers::uniform_grid;
1388 let t = uniform_grid(20); let poly = |x: f64| 2.0 * x.powi(3) - x.powi(2) + 0.5 * x - 0.1;
1390 let vals: Vec<f64> = t.iter().map(|&x| poly(x)).collect();
1391 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1392
1393 let q: Vec<f64> = t.windows(2).map(|w| (w[0] + w[1]) / 2.0).collect();
1395 let result = spline_interpolate(&data, &t, &q, 4).unwrap();
1396
1397 for (j, &qj) in q.iter().enumerate() {
1398 let expected = poly(qj);
1399 let got = result[(0, j)];
1400 assert!(
1401 (got - expected).abs() < 1e-10,
1402 "off-grid at q={qj:.4}: got {got}, expected {expected}"
1403 );
1404 }
1405 }
1406
1407 #[test]
1408 fn spline_interpolate_rejects_out_of_range() {
1409 use crate::test_helpers::uniform_grid;
1410 let t = uniform_grid(20);
1411 let vals: Vec<f64> = t.to_vec();
1412 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1413
1414 let q_below = vec![-0.1_f64];
1416 let err = spline_interpolate(&data, &t, &q_below, 4).unwrap_err();
1417 assert!(
1418 matches!(
1419 err,
1420 crate::FdarError::InvalidParameter {
1421 parameter: "query_points",
1422 ..
1423 }
1424 ),
1425 "expected InvalidParameter for query below domain, got {err:?}"
1426 );
1427
1428 let q_above = vec![1.1_f64];
1430 let err2 = spline_interpolate(&data, &t, &q_above, 4).unwrap_err();
1431 assert!(
1432 matches!(
1433 err2,
1434 crate::FdarError::InvalidParameter {
1435 parameter: "query_points",
1436 ..
1437 }
1438 ),
1439 "expected InvalidParameter for query above domain, got {err2:?}"
1440 );
1441 }
1442
1443 #[test]
1444 fn spline_interpolate_rejects_bad_order() {
1445 use crate::test_helpers::uniform_grid;
1446 let t = uniform_grid(20);
1447 let vals: Vec<f64> = t.to_vec();
1448 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1449 let q = vec![0.5_f64];
1450
1451 let err = spline_interpolate(&data, &t, &q, 0).unwrap_err();
1453 assert!(
1454 matches!(
1455 err,
1456 crate::FdarError::InvalidParameter {
1457 parameter: "order",
1458 ..
1459 }
1460 ),
1461 "expected InvalidParameter for order=0, got {err:?}"
1462 );
1463
1464 let err2 = spline_interpolate(&data, &t, &q, 20).unwrap_err();
1466 assert!(
1467 matches!(
1468 err2,
1469 crate::FdarError::InvalidParameter {
1470 parameter: "order",
1471 ..
1472 }
1473 ),
1474 "expected InvalidParameter for order=20 (>=m=20), got {err2:?}"
1475 );
1476 }
1477
1478 #[test]
1479 fn spline_interpolate_rejects_dim_mismatch() {
1480 use crate::test_helpers::uniform_grid;
1481 let t = uniform_grid(20);
1482 let vals: Vec<f64> = t.to_vec();
1483 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1484
1485 let bad_argvals: Vec<f64> = (0..15).map(|i| i as f64 / 14.0).collect();
1487 let q = vec![0.5_f64];
1488 let err = spline_interpolate(&data, &bad_argvals, &q, 4).unwrap_err();
1489 assert!(
1490 matches!(
1491 err,
1492 crate::FdarError::InvalidDimension {
1493 parameter: "argvals",
1494 ..
1495 }
1496 ),
1497 "expected InvalidDimension for argvals mismatch, got {err:?}"
1498 );
1499
1500 let err2 = spline_interpolate(&data, &t, &[], 4).unwrap_err();
1502 assert!(
1503 matches!(
1504 err2,
1505 crate::FdarError::InvalidDimension {
1506 parameter: "query_points",
1507 ..
1508 }
1509 ),
1510 "expected InvalidDimension for empty query_points, got {err2:?}"
1511 );
1512 }
1513
1514 #[test]
1517 fn test_spline_with_policy_in_range_matches_spline() {
1518 use crate::test_helpers::uniform_grid;
1520 let t = uniform_grid(20);
1521 let poly = |x: f64| 2.0 * x.powi(3) - x.powi(2) + 0.5 * x - 0.1;
1522 let vals: Vec<f64> = t.iter().map(|&x| poly(x)).collect();
1523 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1524 let q: Vec<f64> = t.windows(2).map(|w| (w[0] + w[1]) / 2.0).collect();
1526 let expected = spline_interpolate(&data, &t, &q, 4).unwrap();
1527 let actual =
1528 spline_interpolate_with_policy(&data, &t, &q, 4, ExtrapolationPolicy::Boundary)
1529 .unwrap();
1530 for j in 0..q.len() {
1531 assert!(
1532 (actual[(0, j)] - expected[(0, j)]).abs() < 1e-10,
1533 "in-range mismatch at j={j}: policy={} vs plain={}",
1534 actual[(0, j)],
1535 expected[(0, j)]
1536 );
1537 }
1538 }
1539
1540 #[test]
1541 fn test_spline_with_policy_boundary() {
1542 use crate::test_helpers::uniform_grid;
1544 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();
1547 let q = vec![-0.5_f64, 0.5, 1.5];
1549 let result =
1550 spline_interpolate_with_policy(&data, &t, &q, 4, ExtrapolationPolicy::Boundary)
1551 .unwrap();
1552 assert!(
1554 result[(0, 0)].abs() < 1e-9,
1555 "below boundary should clamp, got {}",
1556 result[(0, 0)]
1557 );
1558 assert!(
1560 (result[(0, 2)] - 1.0).abs() < 1e-9,
1561 "above boundary should clamp, got {}",
1562 result[(0, 2)]
1563 );
1564 assert!(
1566 (result[(0, 1)] - 0.25).abs() < 1e-9,
1567 "in-range should be ~0.25, got {}",
1568 result[(0, 1)]
1569 );
1570 }
1571
1572 #[test]
1573 fn test_spline_with_policy_exception() {
1574 use crate::test_helpers::uniform_grid;
1576 let t = uniform_grid(20);
1577 let vals: Vec<f64> = t.to_vec();
1578 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1579 let q_oob = vec![1.5_f64];
1580 let err =
1581 spline_interpolate_with_policy(&data, &t, &q_oob, 4, ExtrapolationPolicy::Exception)
1582 .unwrap_err();
1583 assert!(
1584 matches!(
1585 err,
1586 crate::FdarError::InvalidParameter {
1587 parameter: "query_points",
1588 ..
1589 }
1590 ),
1591 "Exception policy should error on OOB, got {err:?}"
1592 );
1593 let q_ok = vec![0.0_f64, 0.5, 1.0];
1595 let ok =
1596 spline_interpolate_with_policy(&data, &t, &q_ok, 4, ExtrapolationPolicy::Exception);
1597 assert!(
1598 ok.is_ok(),
1599 "Exception policy should succeed for in-range queries"
1600 );
1601 }
1602
1603 #[test]
1604 fn test_spline_with_policy_fill() {
1605 use crate::test_helpers::uniform_grid;
1607 let t = uniform_grid(20); let vals: Vec<f64> = t.iter().map(|&x| x.powi(2)).collect();
1609 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1610 let fill_val = 42.0_f64;
1611 let q = vec![-0.5_f64, 0.5, 2.0];
1612 let result =
1613 spline_interpolate_with_policy(&data, &t, &q, 4, ExtrapolationPolicy::Fill(fill_val))
1614 .unwrap();
1615 assert!(
1616 (result[(0, 0)] - fill_val).abs() < 1e-10,
1617 "OOB below should be fill value, got {}",
1618 result[(0, 0)]
1619 );
1620 assert!(
1621 (result[(0, 2)] - fill_val).abs() < 1e-10,
1622 "OOB above should be fill value, got {}",
1623 result[(0, 2)]
1624 );
1625 assert!(
1627 (result[(0, 1)] - 0.25).abs() < 1e-9,
1628 "in-range should be ~0.25, got {}",
1629 result[(0, 1)]
1630 );
1631 }
1632
1633 #[test]
1634 fn test_spline_with_policy_periodic() {
1635 use crate::test_helpers::uniform_grid;
1638 let t = uniform_grid(20); let vals: Vec<f64> = t.to_vec(); let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
1641 let q = vec![1.3_f64];
1642 let result =
1643 spline_interpolate_with_policy(&data, &t, &q, 4, ExtrapolationPolicy::Periodic)
1644 .unwrap();
1645 assert!(
1647 (result[(0, 0)] - 0.3).abs() < 1e-9,
1648 "Periodic wrap of 1.3 should give ~0.3, got {}",
1649 result[(0, 0)]
1650 );
1651 let q2 = vec![-0.2_f64];
1653 let result2 =
1654 spline_interpolate_with_policy(&data, &t, &q2, 4, ExtrapolationPolicy::Periodic)
1655 .unwrap();
1656 assert!(
1657 (result2[(0, 0)] - 0.8).abs() < 1e-9,
1658 "Periodic wrap of -0.2 should give ~0.8, got {}",
1659 result2[(0, 0)]
1660 );
1661 }
1662
1663 #[test]
1664 fn test_spline_with_policy_periodic_zero_length_domain_errors() {
1665 let argvals = vec![3.0_f64, 3.0, 3.0];
1667 let vals = vec![1.0_f64, 1.0, 1.0];
1668 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 3).unwrap();
1669 let q = vec![4.0_f64]; let err =
1671 spline_interpolate_with_policy(&data, &argvals, &q, 1, ExtrapolationPolicy::Periodic)
1672 .unwrap_err();
1673 assert!(
1674 matches!(
1675 err,
1676 crate::FdarError::InvalidParameter {
1677 parameter: "argvals",
1678 ..
1679 }
1680 ),
1681 "Periodic + zero-length domain should error, got {err:?}"
1682 );
1683 }
1684
1685 fn make_linear_curve(n_pts: usize) -> (crate::matrix::FdMatrix, Vec<f64>) {
1689 use crate::test_helpers::uniform_grid;
1690 let t = uniform_grid(n_pts);
1691 let vals: Vec<f64> = t.to_vec();
1692 let mat = crate::matrix::FdMatrix::from_column_major(vals, 1, n_pts).unwrap();
1693 (mat, t)
1694 }
1695
1696 #[test]
1697 fn test_extrapolation_boundary() {
1698 let (data, t) = make_linear_curve(11); let q = vec![-0.2_f64, 0.5, 1.3];
1701 let result = fdata_interpolate_with_policy(
1702 &data,
1703 &t,
1704 &q,
1705 InterpolationMethod::Linear,
1706 ExtrapolationPolicy::Boundary,
1707 )
1708 .unwrap();
1709 assert!(
1711 (result[(0, 0)] - 0.0).abs() < 1e-10,
1712 "below boundary should clamp to 0"
1713 );
1714 assert!(
1716 (result[(0, 1)] - 0.5).abs() < 1e-10,
1717 "in-range should interpolate correctly"
1718 );
1719 assert!(
1721 (result[(0, 2)] - 1.0).abs() < 1e-10,
1722 "above boundary should clamp to 1"
1723 );
1724 }
1725
1726 #[test]
1727 fn test_extrapolation_exception() {
1728 let (data, t) = make_linear_curve(11);
1729 let q_bad = vec![1.5_f64]; let err = fdata_interpolate_with_policy(
1731 &data,
1732 &t,
1733 &q_bad,
1734 InterpolationMethod::Linear,
1735 ExtrapolationPolicy::Exception,
1736 )
1737 .unwrap_err();
1738 assert!(
1739 matches!(
1740 err,
1741 crate::FdarError::InvalidParameter {
1742 parameter: "new_argvals",
1743 ..
1744 }
1745 ),
1746 "expected InvalidParameter for OOB query, got {err:?}"
1747 );
1748
1749 let q_ok = vec![0.0_f64, 0.5, 1.0];
1751 let result = fdata_interpolate_with_policy(
1752 &data,
1753 &t,
1754 &q_ok,
1755 InterpolationMethod::Linear,
1756 ExtrapolationPolicy::Exception,
1757 )
1758 .unwrap();
1759 assert!((result[(0, 1)] - 0.5).abs() < 1e-10);
1760 }
1761
1762 #[test]
1763 fn test_extrapolation_fill() {
1764 let (data, t) = make_linear_curve(11);
1765 let fill_val = 99.0_f64;
1766 let q = vec![-0.5_f64, 0.5, 2.0];
1767 let result = fdata_interpolate_with_policy(
1768 &data,
1769 &t,
1770 &q,
1771 InterpolationMethod::Linear,
1772 ExtrapolationPolicy::Fill(fill_val),
1773 )
1774 .unwrap();
1775 assert!(
1776 (result[(0, 0)] - fill_val).abs() < 1e-10,
1777 "below range should be fill value"
1778 );
1779 assert!(
1780 (result[(0, 1)] - 0.5).abs() < 1e-10,
1781 "in-range should interpolate"
1782 );
1783 assert!(
1784 (result[(0, 2)] - fill_val).abs() < 1e-10,
1785 "above range should be fill value"
1786 );
1787 }
1788
1789 #[test]
1790 fn test_extrapolation_periodic() {
1791 let (data, t) = make_linear_curve(11); let q = vec![-0.1_f64, 0.5, 1.1];
1794 let result = fdata_interpolate_with_policy(
1795 &data,
1796 &t,
1797 &q,
1798 InterpolationMethod::Linear,
1799 ExtrapolationPolicy::Periodic,
1800 )
1801 .unwrap();
1802 assert!(
1804 (result[(0, 0)] - 0.9).abs() < 1e-9,
1805 "t=-0.1 should wrap to 0.9, got {}",
1806 result[(0, 0)]
1807 );
1808 assert!(
1809 (result[(0, 1)] - 0.5).abs() < 1e-10,
1810 "in-range point unchanged"
1811 );
1812 assert!(
1814 (result[(0, 2)] - 0.1).abs() < 1e-9,
1815 "t=1.1 should wrap to 0.1, got {}",
1816 result[(0, 2)]
1817 );
1818 }
1819
1820 #[test]
1821 fn test_extrapolation_in_range_equivalence() {
1822 let (data, t) = make_linear_curve(21);
1824 let q: Vec<f64> = (0..=10).map(|i| i as f64 / 10.0).collect();
1825 let expected = fdata_interpolate(&data, &t, &q, InterpolationMethod::Linear);
1826 let actual = fdata_interpolate_with_policy(
1827 &data,
1828 &t,
1829 &q,
1830 InterpolationMethod::Linear,
1831 ExtrapolationPolicy::Boundary,
1832 )
1833 .unwrap();
1834 let (_, m_new) = actual.shape();
1835 for j in 0..m_new {
1836 assert!(
1837 (actual[(0, j)] - expected[(0, j)]).abs() < 1e-12,
1838 "in-range mismatch at j={j}: policy={} vs plain={}",
1839 actual[(0, j)],
1840 expected[(0, j)]
1841 );
1842 }
1843 }
1844
1845 #[test]
1846 fn test_extrapolation_policy_dim_guard() {
1847 let (data, _t) = make_linear_curve(11);
1848 let bad_argvals: Vec<f64> = (0..5).map(|i| i as f64 / 4.0).collect(); let q = vec![0.5_f64];
1850 let err = fdata_interpolate_with_policy(
1851 &data,
1852 &bad_argvals,
1853 &q,
1854 InterpolationMethod::Linear,
1855 ExtrapolationPolicy::Boundary,
1856 )
1857 .unwrap_err();
1858 assert!(
1859 matches!(
1860 err,
1861 crate::FdarError::InvalidDimension {
1862 parameter: "argvals",
1863 ..
1864 }
1865 ),
1866 "expected InvalidDimension for argvals mismatch, got {err:?}"
1867 );
1868 }
1869
1870 fn make_curve_with_vals(vals: Vec<f64>) -> (crate::matrix::FdMatrix, Vec<f64>) {
1874 let m = vals.len();
1875 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
1876 let mat = crate::matrix::FdMatrix::from_column_major(vals, 1, m).unwrap();
1877 (mat, argvals)
1878 }
1879
1880 #[test]
1881 fn test_impute_linear() {
1882 let (data, argvals) = make_curve_with_vals(vec![0.0_f64, f64::NAN, 1.0]);
1885 let result = impute_missing_values(&data, &argvals, ImputationMethod::Linear).unwrap();
1886 assert!(
1888 (result[(0, 1)] - 0.5).abs() < 1e-10,
1889 "linear imputation should give 0.5, got {}",
1890 result[(0, 1)]
1891 );
1892 assert!((result[(0, 0)] - 0.0).abs() < 1e-10);
1894 assert!((result[(0, 2)] - 1.0).abs() < 1e-10);
1895 }
1896
1897 #[test]
1898 fn test_impute_mean() {
1899 let (data, argvals) = make_curve_with_vals(vec![1.0_f64, f64::NAN, 3.0]);
1901 let result = impute_missing_values(&data, &argvals, ImputationMethod::Mean).unwrap();
1902 assert!(
1903 (result[(0, 1)] - 2.0).abs() < 1e-10,
1904 "mean imputation should give 2.0, got {}",
1905 result[(0, 1)]
1906 );
1907 }
1908
1909 #[test]
1910 fn test_impute_constant() {
1911 let (data, argvals) = make_curve_with_vals(vec![1.0_f64, f64::NAN, 3.0]);
1913 let result =
1914 impute_missing_values(&data, &argvals, ImputationMethod::Constant(99.0)).unwrap();
1915 assert!(
1916 (result[(0, 1)] - 99.0).abs() < 1e-10,
1917 "constant imputation should give 99.0, got {}",
1918 result[(0, 1)]
1919 );
1920 assert!((result[(0, 0)] - 1.0).abs() < 1e-10);
1922 assert!((result[(0, 2)] - 3.0).abs() < 1e-10);
1923 }
1924
1925 #[test]
1926 fn test_impute_all_nan() {
1927 let (data, argvals) = make_curve_with_vals(vec![f64::NAN, f64::NAN, f64::NAN]);
1929 let err = impute_missing_values(&data, &argvals, ImputationMethod::Linear).unwrap_err();
1930 assert!(
1931 matches!(
1932 err,
1933 crate::FdarError::InvalidParameter {
1934 parameter: "data",
1935 ..
1936 }
1937 ),
1938 "expected InvalidParameter for all-NaN curve, got {err:?}"
1939 );
1940 }
1941
1942 #[test]
1943 fn test_impute_boundary_nan() {
1944 let (data, argvals) = make_curve_with_vals(vec![f64::NAN, 0.5_f64, 1.0]);
1946 let result = impute_missing_values(&data, &argvals, ImputationMethod::Linear).unwrap();
1947 assert!(
1948 (result[(0, 0)] - 0.5).abs() < 1e-10,
1949 "leading NaN should be filled with nearest valid (0.5), got {}",
1950 result[(0, 0)]
1951 );
1952
1953 let (data2, argvals2) = make_curve_with_vals(vec![0.0_f64, 0.5, f64::NAN]);
1955 let result2 = impute_missing_values(&data2, &argvals2, ImputationMethod::Linear).unwrap();
1956 assert!(
1957 (result2[(0, 2)] - 0.5).abs() < 1e-10,
1958 "trailing NaN should be filled with nearest valid (0.5), got {}",
1959 result2[(0, 2)]
1960 );
1961 }
1962
1963 #[test]
1966 fn test_extrapolation_periodic_zero_length_domain_errors() {
1967 let degenerate_argvals = vec![5.0_f64, 5.0, 5.0];
1969 let vals = vec![1.0_f64, 1.0, 1.0];
1970 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 3).unwrap();
1971 let q = vec![6.0_f64]; let err = fdata_interpolate_with_policy(
1974 &data,
1975 °enerate_argvals,
1976 &q,
1977 InterpolationMethod::Linear,
1978 ExtrapolationPolicy::Periodic,
1979 )
1980 .unwrap_err();
1981 assert!(
1982 matches!(
1983 err,
1984 crate::FdarError::InvalidParameter {
1985 parameter: "argvals",
1986 ..
1987 }
1988 ),
1989 "expected InvalidParameter for zero-length domain + Periodic, got {err:?}"
1990 );
1991 }
1992
1993 #[test]
1996 fn test_impute_zero_columns_errors() {
1997 let data = crate::matrix::FdMatrix::zeros(2, 0);
1999 let argvals: Vec<f64> = vec![];
2000 let err = impute_missing_values(&data, &argvals, ImputationMethod::Linear).unwrap_err();
2001 assert!(
2002 matches!(
2003 err,
2004 crate::FdarError::InvalidDimension {
2005 parameter: "data",
2006 ..
2007 }
2008 ),
2009 "expected InvalidDimension for m=0 matrix, got {err:?}"
2010 );
2011 }
2012
2013 #[test]
2014 fn test_impute_dim_mismatch() {
2015 let (data, _argvals) = make_curve_with_vals(vec![1.0, 2.0, 3.0]);
2017 let bad_argvals = vec![0.0_f64, 1.0]; let err = impute_missing_values(&data, &bad_argvals, ImputationMethod::Linear).unwrap_err();
2019 assert!(
2020 matches!(
2021 err,
2022 crate::FdarError::InvalidDimension {
2023 parameter: "argvals",
2024 ..
2025 }
2026 ),
2027 "expected InvalidDimension for argvals mismatch, got {err:?}"
2028 );
2029 }
2030}