1pub mod grid_spec;
12pub use grid_spec::{resample_scattered_to_grid, Aggregator, GridSpec, ResampleStrategy};
13
14use crate::error::InterpolateError;
15
16#[non_exhaustive]
20#[derive(Debug, Clone, PartialEq)]
21pub enum ExtrapolationMode {
22 Nearest,
24 Linear,
26 Polynomial(usize),
28 Reflection,
30 Periodic,
32 Zero,
34 Constant(f64),
36}
37
38#[non_exhaustive]
42#[derive(Debug, Clone, PartialEq)]
43pub enum ResamplingMethod {
44 Linear,
46 CubicSpline,
48 Nearest,
50 Lanczos(usize),
52}
53
54#[derive(Debug, Clone)]
58pub struct ResamplingConfig {
59 pub method: ResamplingMethod,
61 pub extrapolation: ExtrapolationMode,
63}
64
65impl Default for ResamplingConfig {
66 fn default() -> Self {
67 Self {
68 method: ResamplingMethod::Linear,
69 extrapolation: ExtrapolationMode::Nearest,
70 }
71 }
72}
73
74pub fn resample_1d(
80 x_in: &[f64],
81 y_in: &[f64],
82 x_out: &[f64],
83 config: &ResamplingConfig,
84) -> Result<Vec<f64>, InterpolateError> {
85 let n = x_in.len();
86 if n < 2 {
87 return Err(InterpolateError::InsufficientData(
88 "resample_1d requires at least 2 input points".to_string(),
89 ));
90 }
91 if n != y_in.len() {
92 return Err(InterpolateError::DimensionMismatch(format!(
93 "x_in length {} != y_in length {}",
94 n,
95 y_in.len()
96 )));
97 }
98
99 for i in 1..n {
101 if x_in[i] <= x_in[i - 1] {
102 return Err(InterpolateError::InvalidInput {
103 message: "x_in must be strictly increasing".to_string(),
104 });
105 }
106 }
107
108 let spline_coeffs: Option<Vec<[f64; 4]>> = match config.method {
110 ResamplingMethod::CubicSpline => Some(natural_cubic_spline_coeffs(x_in, y_in)?),
111 _ => None,
112 };
113
114 let x_min = x_in[0];
115 let x_max = x_in[n - 1];
116
117 let result: Result<Vec<f64>, InterpolateError> = x_out
118 .iter()
119 .map(|&xq| {
120 let xq_mapped = resolve_query(xq, x_min, x_max, &config.extrapolation);
122
123 match xq_mapped {
124 ResolvedQuery::InDomain(xr) => {
125 interpolate_1d(x_in, y_in, xr, config, &spline_coeffs)
126 }
127 ResolvedQuery::Extrapolated(val) => Ok(val),
128 ResolvedQuery::ExtrapLinear(xr) => {
129 interpolate_1d_linear_extrap(x_in, y_in, xr)
131 }
132 ResolvedQuery::ExtrapPolynomial(xr, deg) => {
133 interpolate_1d_poly_extrap(x_in, y_in, xr, deg)
134 }
135 }
136 })
137 .collect();
138
139 result
140}
141
142enum ResolvedQuery {
145 InDomain(f64),
146 Extrapolated(f64),
147 ExtrapLinear(f64),
148 ExtrapPolynomial(f64, usize),
149}
150
151fn resolve_query(xq: f64, x_min: f64, x_max: f64, mode: &ExtrapolationMode) -> ResolvedQuery {
152 if xq >= x_min && xq <= x_max {
153 return ResolvedQuery::InDomain(xq);
154 }
155
156 match mode {
157 ExtrapolationMode::Nearest => ResolvedQuery::InDomain(xq.clamp(x_min, x_max)),
158 ExtrapolationMode::Linear => ResolvedQuery::ExtrapLinear(xq),
159 ExtrapolationMode::Polynomial(deg) => ResolvedQuery::ExtrapPolynomial(xq, *deg),
160 ExtrapolationMode::Reflection => {
161 let range = x_max - x_min;
162 if range < 1e-300 {
163 return ResolvedQuery::InDomain(x_min);
164 }
165 let shifted = xq - x_min;
167 let period = 2.0 * range;
168 let t = shifted - (shifted / period).floor() * period;
169 let reflected = if t <= range { t } else { period - t };
170 ResolvedQuery::InDomain(x_min + reflected.clamp(0.0, range))
171 }
172 ExtrapolationMode::Periodic => {
173 let range = x_max - x_min;
174 if range < 1e-300 {
175 return ResolvedQuery::InDomain(x_min);
176 }
177 let shifted = xq - x_min;
178 let t = shifted - (shifted / range).floor() * range;
179 ResolvedQuery::InDomain(x_min + t.clamp(0.0, range))
180 }
181 ExtrapolationMode::Zero => ResolvedQuery::Extrapolated(0.0),
182 ExtrapolationMode::Constant(c) => ResolvedQuery::Extrapolated(*c),
183 }
184}
185
186fn interpolate_1d(
189 x_in: &[f64],
190 y_in: &[f64],
191 xq: f64,
192 config: &ResamplingConfig,
193 spline_coeffs: &Option<Vec<[f64; 4]>>,
194) -> Result<f64, InterpolateError> {
195 let n = x_in.len();
196 let idx = binary_search_floor(x_in, xq);
197 let i = idx.min(n - 2);
198
199 match &config.method {
200 ResamplingMethod::Linear => {
201 let t = (xq - x_in[i]) / (x_in[i + 1] - x_in[i]);
202 Ok(y_in[i] * (1.0 - t) + y_in[i + 1] * t)
203 }
204 ResamplingMethod::Nearest => {
205 let i_near = if (xq - x_in[i]).abs() < (xq - x_in[(i + 1).min(n - 1)]).abs() {
206 i
207 } else {
208 (i + 1).min(n - 1)
209 };
210 Ok(y_in[i_near])
211 }
212 ResamplingMethod::CubicSpline => {
213 if let Some(coeffs) = spline_coeffs {
214 let dx = xq - x_in[i];
215 let [a, b, c, d] = coeffs[i];
216 Ok(a + b * dx + c * dx * dx + d * dx * dx * dx)
217 } else {
218 let t = (xq - x_in[i]) / (x_in[i + 1] - x_in[i]);
220 Ok(y_in[i] * (1.0 - t) + y_in[i + 1] * t)
221 }
222 }
223 ResamplingMethod::Lanczos(a) => Ok(lanczos_interp(x_in, y_in, xq, *a)),
224 }
225}
226
227fn interpolate_1d_linear_extrap(
228 x_in: &[f64],
229 y_in: &[f64],
230 xq: f64,
231) -> Result<f64, InterpolateError> {
232 let n = x_in.len();
233 let x_min = x_in[0];
234 let x_max = x_in[n - 1];
235 if xq < x_min {
236 let slope = (y_in[1] - y_in[0]) / (x_in[1] - x_in[0]);
238 Ok(y_in[0] + slope * (xq - x_min))
239 } else {
240 let slope = (y_in[n - 1] - y_in[n - 2]) / (x_in[n - 1] - x_in[n - 2]);
242 Ok(y_in[n - 1] + slope * (xq - x_max))
243 }
244}
245
246fn interpolate_1d_poly_extrap(
247 x_in: &[f64],
248 y_in: &[f64],
249 xq: f64,
250 deg: usize,
251) -> Result<f64, InterpolateError> {
252 let n = x_in.len();
253 let x_min = x_in[0];
254 let pts = deg + 1;
255 let (px, py): (Vec<f64>, Vec<f64>) = if xq < x_min {
257 let end = pts.min(n);
259 (x_in[..end].to_vec(), y_in[..end].to_vec())
260 } else {
261 let start = n.saturating_sub(pts);
263 (x_in[start..].to_vec(), y_in[start..].to_vec())
264 };
265
266 Ok(lagrange_eval(&px, &py, xq))
268}
269
270fn natural_cubic_spline_coeffs(x: &[f64], y: &[f64]) -> Result<Vec<[f64; 4]>, InterpolateError> {
276 let n = x.len();
277 if n < 2 {
278 return Err(InterpolateError::InsufficientData(
279 "Need at least 2 points for spline".to_string(),
280 ));
281 }
282 let m = n - 1;
283 let mut h = vec![0.0f64; m];
284 for i in 0..m {
285 h[i] = x[i + 1] - x[i];
286 if h[i] <= 0.0 {
287 return Err(InterpolateError::InvalidInput {
288 message: "x must be strictly increasing".to_string(),
289 });
290 }
291 }
292
293 if n == 2 {
294 let b = (y[1] - y[0]) / h[0];
295 return Ok(vec![[y[0], b, 0.0, 0.0]]);
296 }
297
298 let mut alpha = vec![0.0f64; n];
300 for i in 1..m {
301 alpha[i] = 3.0 * ((y[i + 1] - y[i]) / h[i] - (y[i] - y[i - 1]) / h[i - 1]);
302 }
303
304 let mut l = vec![1.0f64; n];
306 let mut mu = vec![0.0f64; n];
307 let mut z = vec![0.0f64; n];
308
309 for i in 1..m {
310 l[i] = 2.0 * (x[i + 1] - x[i - 1]) - h[i - 1] * mu[i - 1];
311 if l[i].abs() < 1e-300 {
312 l[i] = 1e-300;
313 }
314 mu[i] = h[i] / l[i];
315 z[i] = (alpha[i] - h[i - 1] * z[i - 1]) / l[i];
316 }
317
318 let mut sigma = vec![0.0f64; n]; for i in (1..m).rev() {
320 sigma[i] = z[i] - mu[i] * sigma[i + 1];
321 }
322
323 let mut coeffs = Vec::with_capacity(m);
325 for i in 0..m {
326 let a = y[i];
327 let b = (y[i + 1] - y[i]) / h[i] - h[i] * (2.0 * sigma[i] + sigma[i + 1]) / 3.0;
328 let c = sigma[i];
329 let d = (sigma[i + 1] - sigma[i]) / (3.0 * h[i]);
330 coeffs.push([a, b, c, d]);
331 }
332
333 Ok(coeffs)
334}
335
336fn sinc(x: f64) -> f64 {
339 if x.abs() < 1e-12 {
340 1.0
341 } else {
342 let px = std::f64::consts::PI * x;
343 px.sin() / px
344 }
345}
346
347fn lanczos_kernel(x: f64, a: usize) -> f64 {
348 let af = a as f64;
349 if x.abs() >= af {
350 0.0
351 } else {
352 sinc(x) * sinc(x / af)
353 }
354}
355
356fn lanczos_interp(x_in: &[f64], y_in: &[f64], xq: f64, a: usize) -> f64 {
357 let n = x_in.len();
358 if n < 2 {
359 return y_in.first().copied().unwrap_or(0.0);
360 }
361 let i0 = binary_search_floor(x_in, xq);
364 let h = x_in[1] - x_in[0]; if h.abs() < 1e-300 {
366 return y_in[i0.min(n - 1)];
367 }
368 let frac = (xq - x_in[i0.min(n - 1)]) / h;
369 let fi = i0 as f64 + frac;
370
371 let mut numer = 0.0f64;
372 let mut denom = 0.0f64;
373 let start = (fi as isize - a as isize).max(0) as usize;
374 let end = ((fi as isize + a as isize + 1) as usize).min(n);
375
376 for k in start..end {
377 let w = lanczos_kernel(fi - k as f64, a);
378 numer += w * y_in[k];
379 denom += w;
380 }
381
382 if denom.abs() < 1e-300 {
383 y_in[i0.min(n - 1)]
384 } else {
385 numer / denom
386 }
387}
388
389fn lagrange_eval(px: &[f64], py: &[f64], xq: f64) -> f64 {
392 let n = px.len();
393 let mut result = 0.0f64;
394 for i in 0..n {
395 let mut li = 1.0f64;
396 for j in 0..n {
397 if i != j {
398 let denom = px[i] - px[j];
399 if denom.abs() < 1e-300 {
400 continue;
401 }
402 li *= (xq - px[j]) / denom;
403 }
404 }
405 result += py[i] * li;
406 }
407 result
408}
409
410fn binary_search_floor(x_in: &[f64], xq: f64) -> usize {
415 let n = x_in.len();
416 if n == 0 {
417 return 0;
418 }
419 let mut lo = 0usize;
420 let mut hi = n - 1;
421 while lo + 1 < hi {
422 let mid = (lo + hi) / 2;
423 if x_in[mid] <= xq {
424 lo = mid;
425 } else {
426 hi = mid;
427 }
428 }
429 lo.min(n.saturating_sub(2))
430}
431
432pub fn resample_2d(
438 grid: &[Vec<f64>],
439 x_in: &[f64],
440 y_in: &[f64],
441 x_out: &[f64],
442 y_out: &[f64],
443 config: &ResamplingConfig,
444) -> Result<Vec<Vec<f64>>, InterpolateError> {
445 let ny_in = y_in.len();
446 let nx_in = x_in.len();
447 if grid.len() != ny_in {
448 return Err(InterpolateError::DimensionMismatch(format!(
449 "grid has {} rows but y_in has {} elements",
450 grid.len(),
451 ny_in
452 )));
453 }
454 for (row_idx, row) in grid.iter().enumerate() {
455 if row.len() != nx_in {
456 return Err(InterpolateError::DimensionMismatch(format!(
457 "grid row {} has {} columns but x_in has {} elements",
458 row_idx,
459 row.len(),
460 nx_in
461 )));
462 }
463 }
464
465 let mut intermediate: Vec<Vec<f64>> = Vec::with_capacity(ny_in);
468 for row in grid.iter() {
469 let resampled_row = resample_1d(x_in, row, x_out, config)?;
470 intermediate.push(resampled_row);
471 }
472
473 let nx_out = x_out.len();
475 let ny_out = y_out.len();
476 let mut output = vec![vec![0.0f64; nx_out]; ny_out];
477
478 for ix in 0..nx_out {
479 let col: Vec<f64> = intermediate.iter().map(|row| row[ix]).collect();
481 let resampled_col = resample_1d(y_in, &col, y_out, config)?;
482 for iy in 0..ny_out {
483 output[iy][ix] = resampled_col[iy];
484 }
485 }
486
487 Ok(output)
488}
489
490pub fn scattered_to_grid(
497 x: &[Vec<f64>],
498 y: &[f64],
499 grid_ranges: &[(f64, f64, usize)],
500 _config: &ResamplingConfig,
501) -> Result<Vec<f64>, InterpolateError> {
502 if x.is_empty() {
503 return Err(InterpolateError::InsufficientData(
504 "No scattered data points".to_string(),
505 ));
506 }
507 if x.len() != y.len() {
508 return Err(InterpolateError::DimensionMismatch(format!(
509 "x has {} rows but y has {} elements",
510 x.len(),
511 y.len()
512 )));
513 }
514 if grid_ranges.is_empty() {
515 return Err(InterpolateError::InvalidInput {
516 message: "grid_ranges must not be empty".to_string(),
517 });
518 }
519
520 let n_dims = grid_ranges.len();
521 let input_dims = x[0].len();
522 if input_dims != n_dims {
523 return Err(InterpolateError::DimensionMismatch(format!(
524 "x has {} dimensions but grid_ranges specifies {} dimensions",
525 input_dims, n_dims
526 )));
527 }
528
529 let axes: Vec<Vec<f64>> = grid_ranges
531 .iter()
532 .map(|&(lo, hi, n)| {
533 if n <= 1 {
534 vec![lo]
535 } else {
536 (0..n)
537 .map(|i| lo + (hi - lo) * i as f64 / (n - 1) as f64)
538 .collect()
539 }
540 })
541 .collect();
542
543 let total: usize = axes.iter().map(|a| a.len()).product();
545 let mut result = vec![0.0f64; total];
546
547 let shapes: Vec<usize> = axes.iter().map(|a| a.len()).collect();
549 let mut flat_idx = 0usize;
550
551 let mut multi = vec![0usize; n_dims];
552 loop {
553 let gp: Vec<f64> = (0..n_dims).map(|d| axes[d][multi[d]]).collect();
555
556 let mut numer = 0.0f64;
558 let mut denom = 0.0f64;
559 for (xi, &yi) in x.iter().zip(y.iter()) {
560 let dist2: f64 = xi.iter().zip(gp.iter()).map(|(a, b)| (a - b).powi(2)).sum();
561 if dist2 < 1e-28 {
562 numer = yi;
564 denom = 1.0;
565 break;
566 }
567 let w = 1.0 / dist2;
568 numer += w * yi;
569 denom += w;
570 }
571 result[flat_idx] = if denom > 1e-300 { numer / denom } else { 0.0 };
572
573 flat_idx += 1;
575 let mut carry = true;
576 for d in (0..n_dims).rev() {
577 if carry {
578 multi[d] += 1;
579 if multi[d] >= shapes[d] {
580 multi[d] = 0;
581 } else {
582 carry = false;
583 }
584 }
585 }
586 if carry {
587 break; }
589 }
590
591 Ok(result)
592}
593
594#[derive(Debug, Clone)]
602pub struct SplineDerivative {
603 pub coefficients: Vec<Vec<f64>>,
605 pub knots: Vec<f64>,
607 pub degree: usize,
609}
610
611impl SplineDerivative {
612 pub fn new(
614 coefficients: Vec<Vec<f64>>,
615 knots: Vec<f64>,
616 degree: usize,
617 ) -> Result<Self, InterpolateError> {
618 if knots.len() < 2 {
619 return Err(InterpolateError::InsufficientData(
620 "SplineDerivative needs at least 2 knots".to_string(),
621 ));
622 }
623 let n_seg = knots.len() - 1;
624 if coefficients.len() != n_seg {
625 return Err(InterpolateError::DimensionMismatch(format!(
626 "Expected {} coefficient vectors for {} segments, got {}",
627 n_seg,
628 n_seg,
629 coefficients.len()
630 )));
631 }
632 Ok(Self {
633 coefficients,
634 knots,
635 degree,
636 })
637 }
638
639 pub fn differentiate(spline: &SplineDerivative) -> Result<Self, InterpolateError> {
641 if spline.degree == 0 {
642 return Err(InterpolateError::InvalidOperation(
643 "Cannot differentiate a degree-0 spline".to_string(),
644 ));
645 }
646 let new_degree = spline.degree - 1;
647 let new_coeffs: Vec<Vec<f64>> = spline
648 .coefficients
649 .iter()
650 .map(|seg_coeffs| {
651 let n = seg_coeffs.len().min(spline.degree + 1);
654 (1..n)
655 .map(|k| k as f64 * seg_coeffs[k])
656 .collect::<Vec<f64>>()
657 })
658 .collect();
659
660 Self::new(new_coeffs, spline.knots.clone(), new_degree)
661 }
662
663 pub fn evaluate(&self, x: f64) -> Result<f64, InterpolateError> {
665 let n = self.knots.len();
666 if n < 2 {
667 return Err(InterpolateError::InsufficientData(
668 "No segments to evaluate".to_string(),
669 ));
670 }
671
672 let seg = if x <= self.knots[0] {
674 0
675 } else if x >= self.knots[n - 1] {
676 n - 2
677 } else {
678 binary_search_floor(&self.knots, x)
679 };
680
681 let dx = x - self.knots[seg];
682 let coeffs = &self.coefficients[seg];
683 let mut val = 0.0f64;
685 for &c in coeffs.iter().rev() {
686 val = val * dx + c;
687 }
688 Ok(val)
689 }
690}
691
692pub fn resample_to_regular(
699 scattered_x: &[f64],
700 scattered_y: &[f64],
701 n_grid_points: usize,
702 config: &ResamplingConfig,
703) -> Result<(Vec<f64>, Vec<f64>), InterpolateError> {
704 if scattered_x.len() < 2 {
705 return Err(InterpolateError::InsufficientData(
706 "resample_to_regular requires at least 2 input points".to_string(),
707 ));
708 }
709 if scattered_x.len() != scattered_y.len() {
710 return Err(InterpolateError::DimensionMismatch(format!(
711 "scattered_x len {} != scattered_y len {}",
712 scattered_x.len(),
713 scattered_y.len()
714 )));
715 }
716 if n_grid_points < 2 {
717 return Err(InterpolateError::InvalidInput {
718 message: "n_grid_points must be >= 2".to_string(),
719 });
720 }
721
722 let mut pairs: Vec<(f64, f64)> = scattered_x
724 .iter()
725 .copied()
726 .zip(scattered_y.iter().copied())
727 .collect();
728 pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
729
730 let mut sorted_x: Vec<f64> = Vec::with_capacity(pairs.len());
732 let mut sorted_y: Vec<f64> = Vec::with_capacity(pairs.len());
733 for &(px, py) in &pairs {
734 if let Some(&last_x) = sorted_x.last() {
735 if (px - last_x).abs() < 1e-15_f64 {
736 if let Some(ly) = sorted_y.last_mut() {
738 *ly = py;
739 }
740 continue;
741 }
742 }
743 sorted_x.push(px);
744 sorted_y.push(py);
745 }
746
747 if sorted_x.len() < 2 {
748 return Err(InterpolateError::InsufficientData(
749 "After deduplication, fewer than 2 unique x values remain".to_string(),
750 ));
751 }
752
753 let x_min = sorted_x[0];
754 let x_max = sorted_x[sorted_x.len() - 1];
755 let step = (x_max - x_min) / (n_grid_points - 1) as f64;
756 let grid_x: Vec<f64> = (0..n_grid_points)
757 .map(|i| x_min + i as f64 * step)
758 .collect();
759
760 let grid_y = resample_1d(&sorted_x, &sorted_y, &grid_x, config)?;
761 Ok((grid_x, grid_y))
762}
763
764pub fn resample_to_irregular(
768 data_x: &[f64],
769 data_y: &[f64],
770 target_x: &[f64],
771 config: &ResamplingConfig,
772) -> Result<Vec<f64>, InterpolateError> {
773 if data_x.len() < 2 {
774 return Err(InterpolateError::InsufficientData(
775 "resample_to_irregular requires at least 2 input points".to_string(),
776 ));
777 }
778 if data_x.len() != data_y.len() {
779 return Err(InterpolateError::DimensionMismatch(format!(
780 "data_x len {} != data_y len {}",
781 data_x.len(),
782 data_y.len()
783 )));
784 }
785
786 let mut pairs: Vec<(f64, f64)> = data_x.iter().copied().zip(data_y.iter().copied()).collect();
788 pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
789
790 let sorted_x: Vec<f64> = pairs.iter().map(|p| p.0).collect();
791 let sorted_y: Vec<f64> = pairs.iter().map(|p| p.1).collect();
792
793 resample_1d(&sorted_x, &sorted_y, target_x, config)
794}
795
796pub fn resample_scattered_2d(
803 scattered_xy: &[(f64, f64)],
804 values: &[f64],
805 grid_nx: usize,
806 grid_ny: usize,
807) -> Result<Vec<Vec<f64>>, InterpolateError> {
808 if scattered_xy.is_empty() {
809 return Err(InterpolateError::InsufficientData(
810 "No scattered data points for 2D resampling".to_string(),
811 ));
812 }
813 if scattered_xy.len() != values.len() {
814 return Err(InterpolateError::DimensionMismatch(format!(
815 "scattered_xy len {} != values len {}",
816 scattered_xy.len(),
817 values.len()
818 )));
819 }
820 if grid_nx < 2 || grid_ny < 2 {
821 return Err(InterpolateError::InvalidInput {
822 message: "grid_nx and grid_ny must each be >= 2".to_string(),
823 });
824 }
825
826 let x_vals: Vec<f64> = scattered_xy.iter().map(|p| p.0).collect();
827 let y_vals: Vec<f64> = scattered_xy.iter().map(|p| p.1).collect();
828
829 let x_min = x_vals.iter().copied().fold(f64::INFINITY, f64::min);
830 let x_max = x_vals.iter().copied().fold(f64::NEG_INFINITY, f64::max);
831 let y_min = y_vals.iter().copied().fold(f64::INFINITY, f64::min);
832 let y_max = y_vals.iter().copied().fold(f64::NEG_INFINITY, f64::max);
833
834 let dx = if (x_max - x_min).abs() < 1e-15 {
836 1.0
837 } else {
838 (x_max - x_min) / (grid_nx - 1) as f64
839 };
840 let dy = if (y_max - y_min).abs() < 1e-15 {
841 1.0
842 } else {
843 (y_max - y_min) / (grid_ny - 1) as f64
844 };
845
846 let mut grid = vec![vec![0.0f64; grid_nx]; grid_ny];
847
848 for iy in 0..grid_ny {
849 let gy = y_min + iy as f64 * dy;
850 for ix in 0..grid_nx {
851 let gx = x_min + ix as f64 * dx;
852
853 let mut numer = 0.0_f64;
855 let mut denom = 0.0_f64;
856 let mut exact_hit = false;
857 for (idx, &(sx, sy)) in scattered_xy.iter().enumerate() {
858 let dist2 = (sx - gx).powi(2) + (sy - gy).powi(2);
859 if dist2 < 1e-28 {
860 grid[iy][ix] = values[idx];
861 exact_hit = true;
862 break;
863 }
864 let w = 1.0 / dist2;
865 numer += w * values[idx];
866 denom += w;
867 }
868 if !exact_hit {
869 grid[iy][ix] = if denom > 1e-300 { numer / denom } else { 0.0 };
870 }
871 }
872 }
873
874 Ok(grid)
875}
876
877pub fn downsample(
881 x: &[f64],
882 y: &[f64],
883 factor: usize,
884) -> Result<(Vec<f64>, Vec<f64>), InterpolateError> {
885 if x.len() != y.len() {
886 return Err(InterpolateError::DimensionMismatch(format!(
887 "x len {} != y len {}",
888 x.len(),
889 y.len()
890 )));
891 }
892 if factor == 0 {
893 return Err(InterpolateError::InvalidInput {
894 message: "downsample factor must be >= 1".to_string(),
895 });
896 }
897 if x.is_empty() {
898 return Ok((Vec::new(), Vec::new()));
899 }
900
901 let x_out: Vec<f64> = x.iter().copied().step_by(factor).collect();
902 let y_out: Vec<f64> = y.iter().copied().step_by(factor).collect();
903 Ok((x_out, y_out))
904}
905
906pub fn upsample(
911 x: &[f64],
912 y: &[f64],
913 factor: usize,
914 config: &ResamplingConfig,
915) -> Result<(Vec<f64>, Vec<f64>), InterpolateError> {
916 if x.len() != y.len() {
917 return Err(InterpolateError::DimensionMismatch(format!(
918 "x len {} != y len {}",
919 x.len(),
920 y.len()
921 )));
922 }
923 if factor == 0 {
924 return Err(InterpolateError::InvalidInput {
925 message: "upsample factor must be >= 1".to_string(),
926 });
927 }
928 if x.len() < 2 {
929 return Ok((x.to_vec(), y.to_vec()));
930 }
931
932 let mut pairs: Vec<(f64, f64)> = x.iter().copied().zip(y.iter().copied()).collect();
934 pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
935
936 let sorted_x: Vec<f64> = pairs.iter().map(|p| p.0).collect();
937 let sorted_y: Vec<f64> = pairs.iter().map(|p| p.1).collect();
938
939 let n = sorted_x.len();
940 let n_out = (n - 1) * factor + 1;
942 let mut x_out = Vec::with_capacity(n_out);
943
944 for i in 0..(n - 1) {
945 let x0 = sorted_x[i];
946 let x1 = sorted_x[i + 1];
947 for j in 0..factor {
948 let t = j as f64 / factor as f64;
949 x_out.push(x0 + t * (x1 - x0));
950 }
951 }
952 x_out.push(sorted_x[n - 1]);
954
955 let y_out = resample_1d(&sorted_x, &sorted_y, &x_out, config)?;
956 Ok((x_out, y_out))
957}
958
959#[cfg(test)]
962mod tests {
963 use super::*;
964
965 #[test]
966 fn test_resample_1d_linear_identity() {
967 let x: Vec<f64> = (0..10).map(|i| i as f64).collect();
968 let y: Vec<f64> = x.clone();
969 let config = ResamplingConfig {
970 method: ResamplingMethod::Linear,
971 extrapolation: ExtrapolationMode::Nearest,
972 };
973 let x_out: Vec<f64> = (0..10).map(|i| i as f64 * 0.5 + 0.5).collect();
974 let result = resample_1d(&x, &y, &x_out, &config).expect("resample");
975 for (got, expected) in result.iter().zip(x_out.iter()) {
976 let x_clamped = expected.clamp(x[0], x[x.len() - 1]);
978 assert!(
979 (got - x_clamped).abs() < 1e-10,
980 "Linear identity failed: got={got}, expected={x_clamped}"
981 );
982 }
983 }
984
985 #[test]
986 fn test_extrapolation_nearest_boundary() {
987 let x: Vec<f64> = vec![0.0, 1.0, 2.0, 3.0];
988 let y: Vec<f64> = vec![10.0, 20.0, 30.0, 40.0];
989 let config = ResamplingConfig {
990 method: ResamplingMethod::Linear,
991 extrapolation: ExtrapolationMode::Nearest,
992 };
993 let x_out = vec![-1.0, 5.0];
994 let result = resample_1d(&x, &y, &x_out, &config).expect("resample");
995 assert!(
996 (result[0] - 10.0).abs() < 1e-10,
997 "Left boundary clamped: {}",
998 result[0]
999 );
1000 assert!(
1001 (result[1] - 40.0).abs() < 1e-10,
1002 "Right boundary clamped: {}",
1003 result[1]
1004 );
1005 }
1006
1007 #[test]
1008 fn test_extrapolation_zero() {
1009 let x: Vec<f64> = vec![0.0, 1.0, 2.0];
1010 let y: Vec<f64> = vec![1.0, 2.0, 3.0];
1011 let config = ResamplingConfig {
1012 method: ResamplingMethod::Linear,
1013 extrapolation: ExtrapolationMode::Zero,
1014 };
1015 let x_out = vec![-1.0, 5.0];
1016 let result = resample_1d(&x, &y, &x_out, &config).expect("resample");
1017 assert!((result[0] - 0.0).abs() < 1e-10);
1018 assert!((result[1] - 0.0).abs() < 1e-10);
1019 }
1020
1021 #[test]
1022 fn test_extrapolation_constant() {
1023 let x: Vec<f64> = vec![0.0, 1.0, 2.0];
1024 let y: Vec<f64> = vec![1.0, 2.0, 3.0];
1025 let config = ResamplingConfig {
1026 method: ResamplingMethod::Linear,
1027 extrapolation: ExtrapolationMode::Constant(99.0),
1028 };
1029 let x_out = vec![-5.0, 10.0];
1030 let result = resample_1d(&x, &y, &x_out, &config).expect("resample");
1031 assert!((result[0] - 99.0).abs() < 1e-10);
1032 assert!((result[1] - 99.0).abs() < 1e-10);
1033 }
1034
1035 #[test]
1036 fn test_extrapolation_periodic() {
1037 let x: Vec<f64> = vec![0.0, 1.0, 2.0, 3.0];
1038 let y: Vec<f64> = vec![0.0, 1.0, 2.0, 3.0]; let config = ResamplingConfig {
1040 method: ResamplingMethod::Linear,
1041 extrapolation: ExtrapolationMode::Periodic,
1042 };
1043 let result = resample_1d(&x, &y, &[3.5], &config).expect("periodic");
1045 assert!(
1046 (result[0] - 0.5).abs() < 0.2,
1047 "Periodic wrap: got {} expected ~0.5",
1048 result[0]
1049 );
1050 }
1051
1052 #[test]
1053 fn test_extrapolation_linear() {
1054 let x: Vec<f64> = vec![0.0, 1.0, 2.0, 3.0];
1055 let y: Vec<f64> = vec![0.0, 2.0, 4.0, 6.0]; let config = ResamplingConfig {
1057 method: ResamplingMethod::Linear,
1058 extrapolation: ExtrapolationMode::Linear,
1059 };
1060 let result = resample_1d(&x, &y, &[4.0], &config).expect("linear extrap");
1062 assert!(
1063 (result[0] - 8.0).abs() < 1e-8,
1064 "Linear extrapolation: got {} expected 8.0",
1065 result[0]
1066 );
1067 }
1068
1069 #[test]
1070 fn test_cubic_spline_resample() {
1071 let x: Vec<f64> = (0..6).map(|i| i as f64).collect();
1072 let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect(); let config = ResamplingConfig {
1074 method: ResamplingMethod::CubicSpline,
1075 extrapolation: ExtrapolationMode::Nearest,
1076 };
1077 let x_out = vec![0.5, 1.5, 2.5, 3.5];
1078 let result = resample_1d(&x, &y, &x_out, &config).expect("cubic");
1079 for (xq, &yq) in x_out.iter().zip(result.iter()) {
1080 let exact = xq * xq;
1081 let tol = if *xq < 1.0 || *xq > 4.0 { 0.2 } else { 0.05 };
1085 assert!(
1086 (yq - exact).abs() < tol,
1087 "Cubic spline on y=x² at x={xq}: got {yq}, expected {exact}"
1088 );
1089 }
1090 }
1091
1092 #[test]
1093 fn test_nearest_method() {
1094 let x: Vec<f64> = vec![0.0, 1.0, 2.0];
1095 let y: Vec<f64> = vec![10.0, 20.0, 30.0];
1096 let config = ResamplingConfig {
1097 method: ResamplingMethod::Nearest,
1098 extrapolation: ExtrapolationMode::Nearest,
1099 };
1100 let result = resample_1d(&x, &y, &[0.3, 0.7], &config).expect("nearest");
1101 assert!(
1102 (result[0] - 10.0).abs() < 1e-10,
1103 "Nearest left: {}",
1104 result[0]
1105 );
1106 assert!(
1107 (result[1] - 20.0).abs() < 1e-10,
1108 "Nearest right: {}",
1109 result[1]
1110 );
1111 }
1112
1113 #[test]
1114 fn test_scattered_to_grid_2d() {
1115 let x: Vec<Vec<f64>> = vec![
1117 vec![0.0, 0.0],
1118 vec![1.0, 0.0],
1119 vec![0.0, 1.0],
1120 vec![1.0, 1.0],
1121 vec![0.5, 0.5],
1122 ];
1123 let y: Vec<f64> = x.iter().map(|xi| xi[0] + xi[1]).collect();
1124 let grid_ranges = vec![(0.0, 1.0, 3), (0.0, 1.0, 3)];
1125 let config = ResamplingConfig::default();
1126 let result = scattered_to_grid(&x, &y, &grid_ranges, &config).expect("scattered_to_grid");
1127 assert_eq!(result.len(), 9); for &v in &result {
1130 assert!(v >= -0.1 && v <= 2.1, "Value out of expected range: {v}");
1131 }
1132 }
1133
1134 #[test]
1135 fn test_spline_derivative_differentiation() {
1136 let spline = SplineDerivative::new(vec![vec![1.0, 2.0, 3.0]], vec![0.0, 2.0], 2)
1139 .expect("create spline");
1140
1141 let deriv = SplineDerivative::differentiate(&spline).expect("differentiate");
1143 assert_eq!(deriv.degree, 1);
1144
1145 let val = deriv.evaluate(1.0).expect("evaluate");
1147 assert!(
1148 (val - 8.0).abs() < 1e-10,
1149 "Derivative at x=1: got {val}, expected 8.0"
1150 );
1151 }
1152
1153 #[test]
1154 fn test_spline_evaluate() {
1155 let spline =
1157 SplineDerivative::new(vec![vec![1.0, 2.0], vec![3.0, 0.0]], vec![0.0, 1.0, 2.0], 1)
1158 .expect("create spline");
1159 let v0 = spline.evaluate(0.5).expect("eval");
1160 assert!((v0 - 2.0).abs() < 1e-10, "Got {v0}"); }
1162
1163 #[test]
1164 fn test_resample_2d() {
1165 let grid: Vec<Vec<f64>> = (0..3)
1167 .map(|i| (0..3).map(|j| (i + j) as f64).collect())
1168 .collect();
1169 let x_in: Vec<f64> = vec![0.0, 1.0, 2.0];
1170 let y_in: Vec<f64> = vec![0.0, 1.0, 2.0];
1171 let x_out: Vec<f64> = vec![0.5, 1.0, 1.5];
1172 let y_out: Vec<f64> = vec![0.5, 1.0, 1.5];
1173 let config = ResamplingConfig::default();
1174 let result =
1175 resample_2d(&grid, &x_in, &y_in, &x_out, &y_out, &config).expect("resample_2d");
1176 assert_eq!(result.len(), 3);
1177 assert_eq!(result[0].len(), 3);
1178 assert!(
1180 (result[0][0] - 1.0).abs() < 0.1,
1181 "2D resample at (0.5,0.5): got {}",
1182 result[0][0]
1183 );
1184 }
1185
1186 #[test]
1187 fn test_reflection_extrapolation() {
1188 let x: Vec<f64> = vec![0.0, 1.0, 2.0, 3.0];
1189 let y: Vec<f64> = vec![0.0, 1.0, 4.0, 9.0];
1190 let config = ResamplingConfig {
1191 method: ResamplingMethod::Linear,
1192 extrapolation: ExtrapolationMode::Reflection,
1193 };
1194 let result = resample_1d(&x, &y, &[-0.5], &config).expect("reflection");
1196 assert!(
1198 result[0].is_finite(),
1199 "Reflection should produce finite value"
1200 );
1201 }
1202
1203 #[test]
1206 fn test_resample_to_regular_roundtrip() {
1207 let x: Vec<f64> = vec![0.0, 1.0, 2.0, 3.0, 4.0];
1209 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi).collect();
1210 let config = ResamplingConfig::default();
1211
1212 let (grid_x, grid_y) =
1213 resample_to_regular(&x, &y, 9, &config).expect("resample_to_regular");
1214 assert_eq!(grid_x.len(), 9);
1215 assert_eq!(grid_y.len(), 9);
1216
1217 for (gx, gy) in grid_x.iter().zip(grid_y.iter()) {
1219 let expected = 2.0 * gx;
1220 assert!(
1221 (gy - expected).abs() < 0.1,
1222 "resample_to_regular roundtrip: at x={gx} got {gy}, expected {expected}"
1223 );
1224 }
1225 }
1226
1227 #[test]
1228 fn test_resample_to_irregular() {
1229 let x: Vec<f64> = vec![0.0, 1.0, 2.0, 3.0, 4.0];
1230 let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect(); let config = ResamplingConfig::default();
1232
1233 let targets = vec![0.5, 1.5, 2.5, 3.5];
1234 let result =
1235 resample_to_irregular(&x, &y, &targets, &config).expect("resample_to_irregular");
1236 assert_eq!(result.len(), 4);
1237
1238 for (i, &tgt) in targets.iter().enumerate() {
1240 let exact = tgt * tgt;
1241 assert!(
1243 (result[i] - exact).abs() < 1.0,
1244 "resample_to_irregular at x={tgt}: got {}, expected ~{exact}",
1245 result[i]
1246 );
1247 }
1248 }
1249
1250 #[test]
1251 fn test_resample_scattered_2d_grid_covers_domain() {
1252 let scattered: Vec<(f64, f64)> =
1253 vec![(0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.5, 0.5)];
1254 let values: Vec<f64> = scattered.iter().map(|&(x, y)| x + y).collect();
1255
1256 let grid = resample_scattered_2d(&scattered, &values, 3, 3).expect("scattered 2d");
1257 assert_eq!(grid.len(), 3);
1258 assert_eq!(grid[0].len(), 3);
1259
1260 for row in &grid {
1262 for &v in row {
1263 assert!(v >= -0.1 && v <= 2.1, "2D grid value out of range: {v}");
1264 }
1265 }
1266 }
1267
1268 #[test]
1269 fn test_downsample() {
1270 let x: Vec<f64> = (0..10).map(|i| i as f64).collect();
1271 let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
1272
1273 let (dx, dy) = downsample(&x, &y, 3).expect("downsample");
1274 assert_eq!(dx.len(), 4);
1276 assert!((dx[0] - 0.0).abs() < 1e-12);
1277 assert!((dx[1] - 3.0).abs() < 1e-12);
1278 assert!((dx[2] - 6.0).abs() < 1e-12);
1279 assert!((dx[3] - 9.0).abs() < 1e-12);
1280 assert!((dy[1] - 9.0).abs() < 1e-12);
1282 }
1283
1284 #[test]
1285 fn test_upsample_preserves_function() {
1286 let x: Vec<f64> = vec![0.0, 1.0, 2.0, 3.0];
1287 let y: Vec<f64> = vec![0.0, 2.0, 4.0, 6.0]; let config = ResamplingConfig::default();
1289
1290 let (ux, uy) = upsample(&x, &y, 3, &config).expect("upsample");
1291 assert_eq!(ux.len(), 10);
1293 assert_eq!(uy.len(), 10);
1294
1295 for (xi, yi) in ux.iter().zip(uy.iter()) {
1297 let expected = 2.0 * xi;
1298 assert!(
1299 (yi - expected).abs() < 0.1,
1300 "upsample: at x={xi} got {yi}, expected {expected}"
1301 );
1302 }
1303 }
1304
1305 #[test]
1306 fn test_downsample_factor_1_identity() {
1307 let x: Vec<f64> = vec![0.0, 1.0, 2.0];
1308 let y: Vec<f64> = vec![1.0, 2.0, 3.0];
1309 let (dx, dy) = downsample(&x, &y, 1).expect("factor 1");
1310 assert_eq!(dx.len(), 3);
1311 assert_eq!(dy.len(), 3);
1312 }
1313
1314 #[test]
1315 fn test_downsample_factor_zero_error() {
1316 let result = downsample(&[1.0], &[1.0], 0);
1317 assert!(result.is_err());
1318 }
1319
1320 #[test]
1321 fn test_upsample_factor_zero_error() {
1322 let config = ResamplingConfig::default();
1323 let result = upsample(&[1.0, 2.0], &[1.0, 2.0], 0, &config);
1324 assert!(result.is_err());
1325 }
1326
1327 #[test]
1328 fn test_resample_to_regular_too_few_points() {
1329 let config = ResamplingConfig::default();
1330 let result = resample_to_regular(&[1.0], &[1.0], 5, &config);
1331 assert!(result.is_err());
1332 }
1333}