1use crate::error::{InterpolateError, InterpolateResult};
31
32#[inline]
37fn phi(r: f64, order: usize) -> f64 {
38 if r < f64::EPSILON {
39 return 0.0;
40 }
41 let rk = r.powi(order as i32);
42 if order % 2 == 0 {
43 rk * r.ln()
44 } else {
45 rk
46 }
47}
48
49#[inline]
51fn euclidean_distance(a: &[f64], b: &[f64]) -> f64 {
52 let mut sq = 0.0;
53 for (ai, bi) in a.iter().zip(b.iter()) {
54 let d = ai - bi;
55 sq += d * d;
56 }
57 sq.sqrt()
58}
59
60pub struct PolyharmonicSpline {
88 points: Vec<Vec<f64>>,
90 dim: usize,
92 order: usize,
94 weights: Vec<f64>,
96 poly_coeffs: Vec<f64>,
98}
99
100fn poly_term_count(dim: usize, deg: usize) -> usize {
103 let mut num = 1usize;
105 let mut den = 1usize;
106 for i in 1..=deg {
107 num *= dim + i;
108 den *= i;
109 }
110 num / den
111}
112
113fn poly_basis(x: &[f64], deg: usize) -> Vec<f64> {
116 let dim = x.len();
117 let count = poly_term_count(dim, deg);
118 let mut basis = Vec::with_capacity(count);
119
120 let mut multi_index = vec![0usize; dim];
123 loop {
124 let total_deg: usize = multi_index.iter().sum();
125 if total_deg <= deg {
126 let mut val = 1.0;
127 for (i, &exp) in multi_index.iter().enumerate() {
128 val *= x[i].powi(exp as i32);
129 }
130 basis.push(val);
131 }
132
133 if !increment_multi_index(&mut multi_index, deg) {
135 break;
136 }
137 }
138
139 basis
140}
141
142fn increment_multi_index(idx: &mut [usize], max_total: usize) -> bool {
145 let d = idx.len();
146 if d == 0 {
147 return false;
148 }
149
150 let last = d - 1;
152 idx[last] += 1;
153 if idx.iter().sum::<usize>() <= max_total {
154 return true;
155 }
156
157 idx[last] = 0;
159 for i in (0..last).rev() {
160 idx[i] += 1;
161 if idx.iter().sum::<usize>() <= max_total {
162 return true;
163 }
164 idx[i] = 0;
165 }
166 false
167}
168
169fn solve_linear_system(a: &[Vec<f64>], b: &[f64]) -> InterpolateResult<Vec<f64>> {
172 let n = b.len();
173 if a.len() != n {
174 return Err(InterpolateError::DimensionMismatch(
175 "matrix row count must match right-hand side length".to_string(),
176 ));
177 }
178 for row in a.iter() {
179 if row.len() != n {
180 return Err(InterpolateError::DimensionMismatch(
181 "matrix must be square".to_string(),
182 ));
183 }
184 }
185
186 let mut aug: Vec<Vec<f64>> = Vec::with_capacity(n);
188 for i in 0..n {
189 let mut row = Vec::with_capacity(n + 1);
190 row.extend_from_slice(&a[i]);
191 row.push(b[i]);
192 aug.push(row);
193 }
194
195 for col in 0..n {
197 let mut max_abs = aug[col][col].abs();
199 let mut max_row = col;
200 for row in (col + 1)..n {
201 let abs_val = aug[row][col].abs();
202 if abs_val > max_abs {
203 max_abs = abs_val;
204 max_row = row;
205 }
206 }
207
208 if max_abs < 1e-15 {
209 return Err(InterpolateError::LinalgError(
210 "singular or near-singular matrix in polyharmonic spline system".to_string(),
211 ));
212 }
213
214 if max_row != col {
215 aug.swap(col, max_row);
216 }
217
218 let pivot = aug[col][col];
219 for row in (col + 1)..n {
220 let factor = aug[row][col] / pivot;
221 for j in col..=n {
222 let val = aug[col][j];
223 aug[row][j] -= factor * val;
224 }
225 }
226 }
227
228 let mut x = vec![0.0; n];
230 for i in (0..n).rev() {
231 let mut sum = aug[i][n];
232 for j in (i + 1)..n {
233 sum -= aug[i][j] * x[j];
234 }
235 if aug[i][i].abs() < 1e-15 {
236 return Err(InterpolateError::LinalgError(
237 "zero pivot in back substitution".to_string(),
238 ));
239 }
240 x[i] = sum / aug[i][i];
241 }
242
243 Ok(x)
244}
245
246impl PolyharmonicSpline {
247 pub fn fit(
267 points: &[Vec<f64>],
268 values: &[f64],
269 order: usize,
270 smoothing: f64,
271 ) -> InterpolateResult<Self> {
272 let n = points.len();
273 if n == 0 {
274 return Err(InterpolateError::InsufficientData(
275 "at least one data point is required for polyharmonic spline".to_string(),
276 ));
277 }
278 if n != values.len() {
279 return Err(InterpolateError::DimensionMismatch(format!(
280 "points count ({}) must match values count ({})",
281 n,
282 values.len()
283 )));
284 }
285 if order == 0 {
286 return Err(InterpolateError::InvalidValue(
287 "order must be >= 1 for polyharmonic spline".to_string(),
288 ));
289 }
290
291 let dim = points[0].len();
292 if dim == 0 {
293 return Err(InterpolateError::InvalidValue(
294 "point dimension must be >= 1".to_string(),
295 ));
296 }
297 for (i, pt) in points.iter().enumerate() {
298 if pt.len() != dim {
299 return Err(InterpolateError::DimensionMismatch(format!(
300 "point {} has dimension {} but expected {}",
301 i,
302 pt.len(),
303 dim
304 )));
305 }
306 }
307
308 let poly_deg = order - 1;
310 let m = poly_term_count(dim, poly_deg);
311 let total = n + m;
312
313 let mut sys: Vec<Vec<f64>> = vec![vec![0.0; total]; total];
317 let mut rhs = vec![0.0; total];
318
319 for i in 0..n {
321 for j in 0..n {
322 let r = euclidean_distance(&points[i], &points[j]);
323 sys[i][j] = phi(r, order);
324 }
325 if smoothing > 0.0 {
327 sys[i][i] += smoothing;
328 }
329 }
330
331 for i in 0..n {
333 let basis = poly_basis(&points[i], poly_deg);
334 for (j, &b) in basis.iter().enumerate() {
335 sys[i][n + j] = b;
336 sys[n + j][i] = b;
337 }
338 }
339
340 for i in 0..n {
342 rhs[i] = values[i];
343 }
344
345 let solution = solve_linear_system(&sys, &rhs)?;
347
348 let weights = solution[..n].to_vec();
349 let poly_coeffs = solution[n..].to_vec();
350
351 Ok(PolyharmonicSpline {
352 points: points.to_vec(),
353 dim,
354 order,
355 weights,
356 poly_coeffs,
357 })
358 }
359
360 pub fn thin_plate_spline(
364 points: &[Vec<f64>],
365 values: &[f64],
366 smoothing: f64,
367 ) -> InterpolateResult<Self> {
368 Self::fit(points, values, 2, smoothing)
369 }
370
371 pub fn evaluate(&self, query: &[f64]) -> InterpolateResult<f64> {
378 if query.len() != self.dim {
379 return Err(InterpolateError::DimensionMismatch(format!(
380 "query dimension ({}) must match data dimension ({})",
381 query.len(),
382 self.dim
383 )));
384 }
385
386 let mut result = 0.0;
387
388 for (i, pt) in self.points.iter().enumerate() {
390 let r = euclidean_distance(query, pt);
391 result += self.weights[i] * phi(r, self.order);
392 }
393
394 let poly_deg = self.order - 1;
396 let basis = poly_basis(query, poly_deg);
397 for (j, &b) in basis.iter().enumerate() {
398 result += self.poly_coeffs[j] * b;
399 }
400
401 Ok(result)
402 }
403
404 pub fn evaluate_batch(&self, queries: &[Vec<f64>]) -> InterpolateResult<Vec<f64>> {
408 let mut results = Vec::with_capacity(queries.len());
409 for q in queries {
410 results.push(self.evaluate(q)?);
411 }
412 Ok(results)
413 }
414
415 pub fn order(&self) -> usize {
417 self.order
418 }
419
420 pub fn dim(&self) -> usize {
422 self.dim
423 }
424
425 pub fn num_points(&self) -> usize {
427 self.points.len()
428 }
429
430 pub fn weights(&self) -> &[f64] {
432 &self.weights
433 }
434
435 pub fn poly_coefficients(&self) -> &[f64] {
437 &self.poly_coeffs
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444
445 #[test]
446 fn test_tps_interpolates_exactly() {
447 let points = vec![
449 vec![0.0, 0.0],
450 vec![1.0, 0.0],
451 vec![0.0, 1.0],
452 vec![1.0, 1.0],
453 vec![0.5, 0.5],
454 ];
455 let values = vec![0.0, 1.0, 2.0, 3.0, 1.5];
456
457 let spline = PolyharmonicSpline::thin_plate_spline(&points, &values, 0.0)
458 .expect("test: fit should succeed");
459
460 for (pt, &expected) in points.iter().zip(values.iter()) {
461 let val = spline.evaluate(pt).expect("test: evaluate should succeed");
462 assert!(
463 (val - expected).abs() < 1e-8,
464 "TPS should interpolate exactly: expected {}, got {} at {:?}",
465 expected,
466 val,
467 pt
468 );
469 }
470 }
471
472 #[test]
473 fn test_tps_smooth_between_points() {
474 let points = vec![
475 vec![0.0, 0.0],
476 vec![1.0, 0.0],
477 vec![0.0, 1.0],
478 vec![1.0, 1.0],
479 ];
480 let values = vec![0.0, 1.0, 1.0, 2.0];
481
482 let spline = PolyharmonicSpline::thin_plate_spline(&points, &values, 0.0)
483 .expect("test: fit should succeed");
484
485 let center_val = spline
487 .evaluate(&[0.5, 0.5])
488 .expect("test: evaluate should succeed");
489 assert!(
490 (center_val - 1.0).abs() < 1e-6,
491 "TPS at center should be ~1.0, got {}",
492 center_val
493 );
494 }
495
496 #[test]
497 fn test_regularization_produces_smoother_surface() {
498 let points = vec![vec![0.0], vec![0.25], vec![0.5], vec![0.75], vec![1.0]];
499 let values_noisy = vec![0.0, 0.3, 0.45, 0.8, 1.0];
501
502 let spline_exact = PolyharmonicSpline::fit(&points, &values_noisy, 2, 0.0)
503 .expect("test: fit should succeed");
504 let spline_smooth = PolyharmonicSpline::fit(&points, &values_noisy, 2, 1.0)
505 .expect("test: fit should succeed");
506
507 let mut exact_residual = 0.0;
510 let mut smooth_residual = 0.0;
511 for (pt, &v) in points.iter().zip(values_noisy.iter()) {
512 let e = (spline_exact
513 .evaluate(pt)
514 .expect("test: evaluate should succeed")
515 - v)
516 .abs();
517 let s = (spline_smooth
518 .evaluate(pt)
519 .expect("test: evaluate should succeed")
520 - v)
521 .abs();
522 exact_residual += e;
523 smooth_residual += s;
524 }
525
526 assert!(
528 exact_residual < 1e-8,
529 "exact spline should interpolate exactly"
530 );
531 assert!(
533 smooth_residual > 1e-6,
534 "smoothed spline should not interpolate exactly"
535 );
536 }
537
538 #[test]
539 fn test_different_orders_produce_different_results() {
540 let points: Vec<Vec<f64>> = vec![
543 vec![0.0],
544 vec![0.2],
545 vec![0.4],
546 vec![0.6],
547 vec![0.8],
548 vec![1.0],
549 ];
550 let values = vec![0.0, 0.3, 0.5, 0.45, 0.8, 1.0];
551
552 let spline_o1 = PolyharmonicSpline::fit(&points, &values, 1, 0.0)
553 .expect("test: fit order 1 should succeed");
554 let spline_o2 = PolyharmonicSpline::fit(&points, &values, 2, 0.0)
555 .expect("test: fit order 2 should succeed");
556 let spline_o3 = PolyharmonicSpline::fit(&points, &values, 3, 0.0)
557 .expect("test: fit order 3 should succeed");
558
559 let test_pt = vec![0.35];
560 let v1 = spline_o1
561 .evaluate(&test_pt)
562 .expect("test: evaluate should succeed");
563 let v2 = spline_o2
564 .evaluate(&test_pt)
565 .expect("test: evaluate should succeed");
566 let v3 = spline_o3
567 .evaluate(&test_pt)
568 .expect("test: evaluate should succeed");
569
570 let diff_12 = (v1 - v2).abs();
572 let diff_13 = (v1 - v3).abs();
573 let diff_23 = (v2 - v3).abs();
574 let max_diff = diff_12.max(diff_13).max(diff_23);
575 assert!(
576 max_diff > 1e-6,
577 "different orders should produce different results: v1={}, v2={}, v3={}",
578 v1,
579 v2,
580 v3
581 );
582 }
583
584 #[test]
585 fn test_1d_polyharmonic() {
586 let points: Vec<Vec<f64>> = vec![vec![0.0], vec![1.0], vec![2.0], vec![3.0], vec![4.0]];
588 let values: Vec<f64> = points.iter().map(|p| p[0] * p[0]).collect();
589
590 let spline =
591 PolyharmonicSpline::fit(&points, &values, 3, 0.0).expect("test: fit should succeed");
592
593 for (pt, &expected) in points.iter().zip(values.iter()) {
595 let val = spline.evaluate(pt).expect("test: evaluate should succeed");
596 assert!(
597 (val - expected).abs() < 1e-6,
598 "1D polyharmonic should interpolate x^2 exactly at data points"
599 );
600 }
601 }
602
603 #[test]
604 fn test_3d_polyharmonic() {
605 let points = vec![
607 vec![0.0, 0.0, 0.0],
608 vec![1.0, 0.0, 0.0],
609 vec![0.0, 1.0, 0.0],
610 vec![0.0, 0.0, 1.0],
611 vec![1.0, 1.0, 1.0],
612 ];
613 let values: Vec<f64> = points.iter().map(|p| p[0] + p[1] + p[2]).collect();
615
616 let spline =
617 PolyharmonicSpline::fit(&points, &values, 1, 0.0).expect("test: fit should succeed");
618
619 for (pt, &expected) in points.iter().zip(values.iter()) {
620 let val = spline.evaluate(pt).expect("test: evaluate should succeed");
621 assert!(
622 (val - expected).abs() < 1e-6,
623 "3D polyharmonic should interpolate linear function exactly"
624 );
625 }
626 }
627
628 #[test]
629 fn test_batch_evaluation() {
630 let points = vec![vec![0.0], vec![1.0], vec![2.0]];
631 let values = vec![0.0, 1.0, 4.0];
632
633 let spline =
634 PolyharmonicSpline::fit(&points, &values, 2, 0.0).expect("test: fit should succeed");
635
636 let queries = vec![vec![0.0], vec![1.0], vec![2.0]];
637 let results = spline
638 .evaluate_batch(&queries)
639 .expect("test: batch evaluate should succeed");
640
641 for (res, &expected) in results.iter().zip(values.iter()) {
642 assert!(
643 (res - expected).abs() < 1e-8,
644 "batch evaluation should match single evaluation"
645 );
646 }
647 }
648
649 #[test]
650 fn test_error_on_empty_data() {
651 let points: Vec<Vec<f64>> = vec![];
652 let values: Vec<f64> = vec![];
653 let result = PolyharmonicSpline::fit(&points, &values, 2, 0.0);
654 assert!(result.is_err());
655 }
656
657 #[test]
658 fn test_error_on_dimension_mismatch() {
659 let points = vec![
662 vec![0.0, 0.0],
663 vec![1.0, 0.0],
664 vec![0.0, 1.0],
665 vec![1.0, 1.0],
666 ];
667 let values = vec![0.0, 1.0, 1.0, 2.0];
668
669 let spline =
670 PolyharmonicSpline::fit(&points, &values, 2, 0.0).expect("test: fit should succeed");
671
672 let result = spline.evaluate(&[0.5]);
674 assert!(result.is_err());
675 }
676}