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
393fn cubic_hermite_interp(x: &[f64], y: &[f64], t: f64) -> f64 {
397 let n = x.len();
398 if n < 2 {
399 return if n == 1 { y[0] } else { 0.0 };
400 }
401
402 if t <= x[0] {
404 return y[0];
405 }
406 if t >= x[n - 1] {
407 return y[n - 1];
408 }
409
410 let k = match x.binary_search_by(|v| v.partial_cmp(&t).unwrap_or(std::cmp::Ordering::Equal)) {
412 Ok(i) => return y[i],
413 Err(i) => {
414 if i == 0 {
415 0
416 } else {
417 i - 1
418 }
419 }
420 };
421
422 let slopes: Vec<f64> = x
424 .windows(2)
425 .zip(y.windows(2))
426 .map(|(xw, yw)| (yw[1] - yw[0]) / (xw[1] - xw[0]))
427 .collect();
428
429 let mut tangents = vec![0.0; n];
431 tangents[0] = slopes[0];
432 tangents[n - 1] = slopes[n - 2];
433 for i in 1..n - 1 {
434 if slopes[i - 1].signum() != slopes[i].signum() {
435 tangents[i] = 0.0;
436 } else {
437 tangents[i] = (slopes[i - 1] + slopes[i]) / 2.0;
438 }
439 }
440
441 let h = x[k + 1] - x[k];
443 let s = (t - x[k]) / h;
444 let s2 = s * s;
445 let s3 = s2 * s;
446
447 let h00 = 2.0 * s3 - 3.0 * s2 + 1.0;
448 let h10 = s3 - 2.0 * s2 + s;
449 let h01 = -2.0 * s3 + 3.0 * s2;
450 let h11 = s3 - s2;
451
452 h00 * y[k] + h10 * h * tangents[k] + h01 * y[k + 1] + h11 * h * tangents[k + 1]
453}
454
455pub fn gradient_uniform(y: &[f64], h: f64) -> Vec<f64> {
462 let n = y.len();
463 let mut g = vec![0.0; n];
464 if n < 2 {
465 return g;
466 }
467 if n == 2 {
468 g[0] = (y[1] - y[0]) / h;
469 g[1] = (y[1] - y[0]) / h;
470 return g;
471 }
472 if n == 3 {
473 g[0] = (-3.0 * y[0] + 4.0 * y[1] - y[2]) / (2.0 * h);
474 g[1] = (y[2] - y[0]) / (2.0 * h);
475 g[2] = (y[0] - 4.0 * y[1] + 3.0 * y[2]) / (2.0 * h);
476 return g;
477 }
478 if n == 4 {
479 g[0] = (-3.0 * y[0] + 4.0 * y[1] - y[2]) / (2.0 * h);
480 g[1] = (y[2] - y[0]) / (2.0 * h);
481 g[2] = (y[3] - y[1]) / (2.0 * h);
482 g[3] = (y[1] - 4.0 * y[2] + 3.0 * y[3]) / (2.0 * h);
483 return g;
484 }
485
486 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);
489 g[1] = (-3.0 * y[0] - 10.0 * y[1] + 18.0 * y[2] - 6.0 * y[3] + y[4]) / (12.0 * h);
490
491 for i in 2..n - 2 {
493 g[i] = (-y[i + 2] + 8.0 * y[i + 1] - 8.0 * y[i - 1] + y[i - 2]) / (12.0 * h);
494 }
495
496 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])
498 / (12.0 * h);
499 g[n - 1] = (3.0 * y[n - 5] - 16.0 * y[n - 4] + 36.0 * y[n - 3] - 48.0 * y[n - 2]
500 + 25.0 * y[n - 1])
501 / (12.0 * h);
502 g
503}
504
505pub fn gradient_nonuniform(y: &[f64], t: &[f64]) -> Vec<f64> {
513 let n = y.len();
514 assert_eq!(n, t.len(), "y and t must have the same length");
515 let mut g = vec![0.0; n];
516 if n < 2 {
517 return g;
518 }
519 if n == 2 {
520 let h = t[1] - t[0];
521 if h.abs() < 1e-15 {
522 return g;
523 }
524 g[0] = (y[1] - y[0]) / h;
525 g[1] = g[0];
526 return g;
527 }
528
529 let h0 = t[1] - t[0];
531 let h1 = t[2] - t[0];
532 if h0.abs() > 1e-15 && h1.abs() > 1e-15 && (h1 - h0).abs() > 1e-15 {
533 g[0] = y[0] * (-h1 - h0) / (h0 * h1) + y[1] * h1 / (h0 * (h1 - h0))
534 - y[2] * h0 / (h1 * (h1 - h0));
535 } else {
536 g[0] = (y[1] - y[0]) / h0.max(1e-15);
537 }
538
539 for i in 1..n - 1 {
541 let h_l = t[i] - t[i - 1];
542 let h_r = t[i + 1] - t[i];
543 let h_sum = h_l + h_r;
544 if h_l.abs() < 1e-15 || h_r.abs() < 1e-15 || h_sum.abs() < 1e-15 {
545 g[i] = 0.0;
546 continue;
547 }
548 g[i] = -y[i - 1] * h_r / (h_l * h_sum)
549 + y[i] * (h_r - h_l) / (h_l * h_r)
550 + y[i + 1] * h_l / (h_r * h_sum);
551 }
552
553 let h_last = t[n - 1] - t[n - 2];
555 let h_prev = t[n - 1] - t[n - 3];
556 let h_mid = t[n - 2] - t[n - 3];
557 if h_last.abs() > 1e-15 && h_prev.abs() > 1e-15 && h_mid.abs() > 1e-15 {
558 g[n - 1] = y[n - 3] * h_last / (h_mid * h_prev) - y[n - 2] * h_prev / (h_mid * h_last)
559 + y[n - 1] * (h_prev + h_last) / (h_prev * h_last);
560 } else {
561 g[n - 1] = (y[n - 1] - y[n - 2]) / h_last.max(1e-15);
562 }
563
564 g
565}
566
567pub fn gradient(y: &[f64], t: &[f64]) -> Vec<f64> {
573 let n = t.len();
574 if n < 2 {
575 return vec![0.0; y.len()];
576 }
577
578 let h0 = t[1] - t[0];
579 let is_uniform = t
580 .windows(2)
581 .all(|w| ((w[1] - w[0]) - h0).abs() < 1e-12 * h0.abs().max(1.0));
582
583 if is_uniform {
584 gradient_uniform(y, h0)
585 } else {
586 gradient_nonuniform(y, t)
587 }
588}
589
590#[cfg(test)]
591mod tests {
592 use super::*;
593
594 #[test]
595 fn test_simpsons_weights_uniform() {
596 let argvals = vec![0.0, 0.25, 0.5, 0.75, 1.0];
597 let weights = simpsons_weights(&argvals);
598 let sum: f64 = weights.iter().sum();
599 assert!((sum - 1.0).abs() < NUMERICAL_EPS);
600 }
601
602 #[test]
603 fn test_simpsons_weights_2d() {
604 let argvals_s = vec![0.0, 0.5, 1.0];
605 let argvals_t = vec![0.0, 0.5, 1.0];
606 let weights = simpsons_weights_2d(&argvals_s, &argvals_t);
607 let sum: f64 = weights.iter().sum();
608 assert!((sum - 1.0).abs() < NUMERICAL_EPS);
609 }
610
611 #[test]
612 fn test_extract_curves() {
613 let data = vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0];
616 let mat = crate::matrix::FdMatrix::from_column_major(data, 2, 3).unwrap();
617 let curves = extract_curves(&mat);
618 assert_eq!(curves.len(), 2);
619 assert_eq!(curves[0], vec![1.0, 2.0, 3.0]);
620 assert_eq!(curves[1], vec![4.0, 5.0, 6.0]);
621 }
622
623 #[test]
624 fn test_l2_distance_identical() {
625 let curve = vec![1.0, 2.0, 3.0];
626 let weights = vec![0.25, 0.5, 0.25];
627 let dist = l2_distance(&curve, &curve, &weights);
628 assert!(dist.abs() < NUMERICAL_EPS);
629 }
630
631 #[test]
632 fn test_l2_distance_different() {
633 let curve1 = vec![0.0, 0.0, 0.0];
634 let curve2 = vec![1.0, 1.0, 1.0];
635 let weights = vec![0.25, 0.5, 0.25]; let dist = l2_distance(&curve1, &curve2, &weights);
637 assert!((dist - 1.0).abs() < NUMERICAL_EPS);
639 }
640
641 #[test]
642 fn test_n1_weights() {
643 let w = simpsons_weights(&[0.5]);
645 assert_eq!(w.len(), 1);
646 assert!((w[0] - 1.0).abs() < 1e-12);
647 }
648
649 #[test]
650 fn test_n2_weights() {
651 let w = simpsons_weights(&[0.0, 1.0]);
652 assert_eq!(w.len(), 2);
653 assert!((w[0] - 0.5).abs() < 1e-12);
655 assert!((w[1] - 0.5).abs() < 1e-12);
656 }
657
658 #[test]
659 fn test_mismatched_l2_distance() {
660 let a = vec![1.0, 2.0, 3.0];
662 let b = vec![1.0, 2.0, 3.0];
663 let w = vec![0.5, 0.5, 0.5];
664 let d = l2_distance(&a, &b, &w);
665 assert!(d.abs() < 1e-12, "Same vectors should have zero distance");
666 }
667
668 #[test]
671 fn test_trapz_sine() {
672 let m = 1000;
674 let x: Vec<f64> = (0..m)
675 .map(|i| std::f64::consts::PI * i as f64 / (m - 1) as f64)
676 .collect();
677 let y: Vec<f64> = x.iter().map(|&xi| xi.sin()).collect();
678 let result = trapz(&y, &x);
679 assert!(
680 (result - 2.0).abs() < 1e-4,
681 "∫ sin(x) dx over [0,π] should be ~2, got {result}"
682 );
683 }
684
685 #[test]
688 fn test_cumulative_trapz_matches_final() {
689 let m = 100;
690 let x: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
691 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi).collect();
692 let cum = cumulative_trapz(&y, &x);
693 let total = trapz(&y, &x);
694 assert!(
695 (cum[m - 1] - total).abs() < 1e-12,
696 "Final cumulative value should match trapz"
697 );
698 }
699
700 #[test]
703 fn test_linear_interp_boundary_clamp() {
704 let x = vec![0.0, 0.5, 1.0];
705 let y = vec![10.0, 20.0, 30.0];
706 assert!((linear_interp(&x, &y, -1.0) - 10.0).abs() < 1e-12);
707 assert!((linear_interp(&x, &y, 2.0) - 30.0).abs() < 1e-12);
708 assert!((linear_interp(&x, &y, 0.25) - 15.0).abs() < 1e-12);
709 }
710
711 #[test]
714 fn test_gradient_uniform_linear() {
715 let m = 50;
717 let h = 1.0 / (m - 1) as f64;
718 let y: Vec<f64> = (0..m).map(|i| 3.0 * i as f64 * h).collect();
719 let g = gradient_uniform(&y, h);
720 for i in 0..m {
721 assert!(
722 (g[i] - 3.0).abs() < 1e-10,
723 "gradient of 3x should be 3 at i={i}, got {}",
724 g[i]
725 );
726 }
727 }
728
729 #[test]
732 fn test_gaussian_kernel() {
733 assert!((gaussian_kernel(0.0, 1.0) - 1.0).abs() < 1e-12);
734 assert!(gaussian_kernel(3.0, 1.0) < 0.02); assert!((gaussian_kernel(1.0, 0.0)).abs() < 1e-12); }
737
738 #[test]
739 fn test_bandwidth_candidates() {
740 let n = 5;
741 let mut dists = vec![0.0; n * n];
742 for i in 0..n {
743 for j in 0..n {
744 dists[i * n + j] = (i as f64 - j as f64).abs();
745 }
746 }
747 let cands = bandwidth_candidates_from_dists(&dists, n, 10);
748 assert!(!cands.is_empty());
749 assert!(cands.iter().all(|&h| h > 0.0));
750 for w in cands.windows(2) {
752 assert!(w[1] >= w[0]);
753 }
754 }
755
756 #[test]
757 fn test_quantile_sorted() {
758 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
759 assert!((quantile_sorted(&data, 0.0) - 1.0).abs() < 1e-12);
760 assert!((quantile_sorted(&data, 1.0) - 5.0).abs() < 1e-12);
761 assert!((quantile_sorted(&data, 0.5) - 3.0).abs() < 1e-12);
762 assert!((quantile_sorted(&data, 0.25) - 2.0).abs() < 1e-12);
763 }
764
765 #[test]
766 fn test_r_squared_perfect() {
767 let y = vec![1.0, 2.0, 3.0, 4.0];
768 let resid = vec![0.0, 0.0, 0.0, 0.0];
769 assert!((r_squared(&y, &resid) - 1.0).abs() < 1e-12);
770 }
771
772 #[test]
773 fn test_r_squared_mean_model() {
774 let y = vec![1.0, 2.0, 3.0, 4.0];
775 let mean = 2.5;
776 let resid: Vec<f64> = y.iter().map(|&yi| yi - mean).collect();
777 assert!(r_squared(&y, &resid).abs() < 1e-12); }
779
780 #[test]
781 fn test_aic_bic() {
782 let a = aic(100, 50.0, 5);
783 let b = bic(100, 50.0, 5);
784 assert!(a.is_finite());
785 assert!(b.is_finite());
786 assert!(b > a); }
788
789 #[test]
790 fn fdata_interpolate_linear_identity() {
791 let t: Vec<f64> = (0..20).map(|i| i as f64 / 19.0).collect();
792 let vals: Vec<f64> = t.iter().map(|&x| x.sin()).collect();
793 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
794 let result = fdata_interpolate(&data, &t, &t, InterpolationMethod::Linear);
795 for j in 0..20 {
796 assert!((result[(0, j)] - data[(0, j)]).abs() < 1e-12);
797 }
798 }
799
800 #[test]
801 fn fdata_interpolate_cubic_hermite_smooth() {
802 let t: Vec<f64> = (0..20).map(|i| i as f64 / 19.0).collect();
803 let vals: Vec<f64> = t.iter().map(|&x| x.sin()).collect();
804 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
805
806 let t_fine: Vec<f64> = (0..100).map(|i| i as f64 / 99.0).collect();
807 let result = fdata_interpolate(&data, &t, &t_fine, InterpolationMethod::CubicHermite);
808
809 for (j, &tj) in t_fine.iter().enumerate() {
811 assert!(
812 (result[(0, j)] - tj.sin()).abs() < 0.02,
813 "at t={tj:.2}: got {:.4}, expected {:.4}",
814 result[(0, j)],
815 tj.sin()
816 );
817 }
818 }
819
820 #[test]
821 fn fdata_interpolate_multiple_curves() {
822 let t: Vec<f64> = (0..30).map(|i| i as f64 / 29.0).collect();
823 let n = 5;
824 let m = 30;
825 let mut col_major = vec![0.0; n * m];
827 for i in 0..n {
828 for j in 0..m {
829 col_major[i + j * n] = ((i + 1) as f64 * t[j]).sin();
830 }
831 }
832 let data = crate::matrix::FdMatrix::from_column_major(col_major, n, m).unwrap();
833
834 let t_new: Vec<f64> = (0..50).map(|i| i as f64 / 49.0).collect();
835 let result = fdata_interpolate(&data, &t, &t_new, InterpolationMethod::Linear);
836 assert_eq!(result.shape(), (n, 50));
837 for i in 0..n {
839 for j in 0..50 {
840 assert!(result[(i, j)].is_finite());
841 }
842 }
843 }
844}