1use rayon::prelude::*;
15
16use crate::numeric::stats::quantile_type7;
17use crate::prelude::*;
18
19const MIN_SPLINE_KNOTS: usize = 2;
27
28const MIN_BASIS_DF: usize = 2;
32
33const MIN_INTERP_POINTS: usize = 2;
37
38fn validate_knots(x: &[f64]) -> Result<(), EdgeErrors> {
53 if x.len() < MIN_SPLINE_KNOTS {
54 return Err(EdgeErrors::InvalidArgument(format!(
55 "a spline needs at least {MIN_SPLINE_KNOTS} knots; got {}",
56 x.len()
57 )));
58 }
59 strictly_increasing(x, "knots")
60}
61
62fn validate_interp_knots(xp: &[f64], fp: &[f64]) -> Result<(), EdgeErrors> {
73 if xp.len() < MIN_INTERP_POINTS {
74 return Err(EdgeErrors::InvalidArgument(format!(
75 "linear interpolation needs at least {MIN_INTERP_POINTS} points; got {}",
76 xp.len()
77 )));
78 }
79 if fp.len() != xp.len() {
80 return Err(EdgeErrors::LengthMismatch {
81 name: "fp",
82 expected: xp.len(),
83 got: fp.len(),
84 });
85 }
86 strictly_increasing(xp, "xp")
87}
88
89fn strictly_increasing(x: &[f64], name: &str) -> Result<(), EdgeErrors> {
102 for (i, pair) in x.windows(2).enumerate() {
103 if pair[1] <= pair[0] || pair[0].is_nan() || pair[1].is_nan() {
105 return Err(EdgeErrors::InvalidArgument(format!(
106 "{name} must be strictly increasing; {} at index {} is not above {} at index {i}",
107 pair[1],
108 i + 1,
109 pair[0],
110 )));
111 }
112 }
113 Ok(())
114}
115
116#[derive(Clone, Debug)]
137pub struct FmmSpline {
138 pub x: Vec<f64>,
140 pub y: Vec<f64>,
142 pub b: Vec<f64>,
144 pub c: Vec<f64>,
146 pub d: Vec<f64>,
148}
149
150pub fn fmm_spline(x: &[f64], y: &[f64]) -> Result<FmmSpline, EdgeErrors> {
167 validate_knots(x)?;
168 if y.len() != x.len() {
169 return Err(EdgeErrors::LengthMismatch {
170 name: "y",
171 expected: x.len(),
172 got: y.len(),
173 });
174 }
175
176 let n = x.len();
177 let mut b = vec![0.0; n];
178 let mut c = vec![0.0; n];
179 let mut d = vec![0.0; n];
180 fmm_coefficients(x, y, &mut b, &mut c, &mut d);
181
182 Ok(FmmSpline {
183 x: x.to_vec(),
184 y: y.to_vec(),
185 b,
186 c,
187 d,
188 })
189}
190
191impl FmmSpline {
192 pub fn eval(&self, x: f64) -> f64 {
206 let i = self.x.partition_point(|&knot| knot <= x).saturating_sub(1);
209 let i = i.min(self.x.len() - 1);
210 let dx = x - self.x[i];
211 self.y[i] + dx * (self.b[i] + dx * (self.c[i] + dx * self.d[i]))
212 }
213}
214
215fn fmm_coefficients(x: &[f64], y: &[f64], b: &mut [f64], c: &mut [f64], d: &mut [f64]) {
231 let n = x.len();
232
233 if n < 3 {
234 let t = (y[1] - y[0]) / (x[1] - x[0]);
236 b[0] = t;
237 b[1] = t;
238 c[0] = 0.0;
239 c[1] = 0.0;
240 d[0] = 0.0;
241 d[1] = 0.0;
242 return;
243 }
244
245 let nm1 = n - 1;
246
247 d[0] = x[1] - x[0];
249 c[1] = (y[1] - y[0]) / d[0];
250 for i in 1..nm1 {
251 d[i] = x[i + 1] - x[i];
252 b[i] = 2.0 * (d[i - 1] + d[i]);
253 c[i + 1] = (y[i + 1] - y[i]) / d[i];
254 c[i] = c[i + 1] - c[i];
255 }
256
257 b[0] = -d[0];
261 b[nm1] = -d[nm1 - 1];
262 c[0] = 0.0;
263 c[nm1] = 0.0;
264 if n > 3 {
265 c[0] = c[2] / (x[3] - x[1]) - c[1] / (x[2] - x[0]);
266 c[nm1] = c[nm1 - 1] / (x[nm1] - x[nm1 - 2]) - c[nm1 - 2] / (x[nm1 - 1] - x[nm1 - 3]);
267 c[0] = c[0] * d[0] * d[0] / (x[3] - x[0]);
268 c[nm1] = -c[nm1] * d[nm1 - 1] * d[nm1 - 1] / (x[nm1] - x[nm1 - 3]);
269 }
270
271 for i in 1..n {
273 let t = d[i - 1] / b[i - 1];
274 b[i] -= t * d[i - 1];
275 c[i] -= t * c[i - 1];
276 }
277
278 c[nm1] /= b[nm1];
280 for i in (0..nm1).rev() {
281 c[i] = (c[i] - d[i] * c[i + 1]) / b[i];
282 }
283
284 b[nm1] = (y[nm1] - y[nm1 - 1]) / d[nm1 - 1] + d[nm1 - 1] * (c[nm1 - 1] + 2.0 * c[nm1]);
286 for i in 0..nm1 {
287 b[i] = (y[i + 1] - y[i]) / d[i] - d[i] * (c[i + 1] + 2.0 * c[i]);
288 d[i] = (c[i + 1] - c[i]) / d[i];
289 c[i] *= 3.0;
290 }
291 c[nm1] *= 3.0;
292 d[nm1] = d[nm1 - 1];
293}
294
295pub fn maximize_interpolant(x: &[f64], y: &[f64]) -> Result<f64, EdgeErrors> {
319 validate_knots(x)?;
320 if y.len() != x.len() {
321 return Err(EdgeErrors::LengthMismatch {
322 name: "y",
323 expected: x.len(),
324 got: y.len(),
325 });
326 }
327
328 let n = x.len();
329 let mut b = vec![0.0; n];
330 let mut c = vec![0.0; n];
331 let mut d = vec![0.0; n];
332 Ok(find_max(x, y, &mut b, &mut c, &mut d))
333}
334
335pub fn maximize_interpolant_many(
351 x: &[f64],
352 y: &[f64],
353 n_genes: usize,
354) -> Result<Vec<f64>, EdgeErrors> {
355 validate_knots(x)?;
356 if n_genes == 0 {
357 return Err(EdgeErrors::MustBePositive("n_genes".to_string()));
358 }
359 let npts = x.len();
360 if y.len() != n_genes * npts {
361 return Err(EdgeErrors::LengthMismatch {
362 name: "y",
363 expected: n_genes * npts,
364 got: y.len(),
365 });
366 }
367
368 let chunk = n_genes.div_ceil(rayon::current_num_threads().max(1)).max(1);
369 let mut out = vec![0.0; n_genes];
370 out.par_chunks_mut(chunk)
371 .zip(y.par_chunks(chunk * npts))
372 .for_each(|(out_chunk, y_chunk)| {
373 let mut b = vec![0.0; npts];
374 let mut c = vec![0.0; npts];
375 let mut d = vec![0.0; npts];
376 for (slot, row) in out_chunk.iter_mut().zip(y_chunk.chunks_exact(npts)) {
377 *slot = find_max(x, row, &mut b, &mut c, &mut d);
378 }
379 });
380
381 Ok(out)
382}
383
384fn find_max(x: &[f64], y: &[f64], b: &mut [f64], c: &mut [f64], d: &mut [f64]) -> f64 {
398 let n = x.len();
399
400 let mut maxed = y[0];
401 let mut maxed_at = 0;
402 for (i, &yi) in y.iter().enumerate().skip(1) {
403 if yi > maxed {
404 maxed = yi;
405 maxed_at = i;
406 }
407 }
408 let mut x_max = x[maxed_at];
409
410 fmm_coefficients(x, y, b, c, d);
411
412 if maxed_at > 0 {
413 refine_on_segment(maxed_at - 1, x, y, b, c, d, &mut maxed, &mut x_max);
414 }
415 if maxed_at + 1 < n {
416 refine_on_segment(maxed_at, x, y, b, c, d, &mut maxed, &mut x_max);
417 }
418
419 x_max
420}
421
422#[allow(clippy::too_many_arguments)]
440fn refine_on_segment(
441 seg: usize,
442 x: &[f64],
443 y: &[f64],
444 b: &[f64],
445 c: &[f64],
446 d: &[f64],
447 maxed: &mut f64,
448 x_max: &mut f64,
449) {
450 let (sb, sc, sd) = (b[seg], c[seg], d[seg]);
451
452 let delta = sc * sc - 3.0 * sd * sb;
453 if delta < 0.0 {
454 return;
455 }
456 if sd == 0.0 {
457 return;
458 }
459
460 let root = (-sc - delta.sqrt()) / (3.0 * sd);
461 let width = x[seg + 1] - x[seg];
462 if root <= 0.0 || root >= width {
463 return;
464 }
465
466 let value = ((sd * root + sc) * root + sb) * root + y[seg];
467 if value > *maxed {
468 *maxed = value;
469 *x_max = root + x[seg];
470 }
471}
472
473pub fn natural_spline_basis(x: &[f64], df: usize) -> Result<(Vec<f64>, usize), EdgeErrors> {
511 if x.is_empty() {
512 return Err(EdgeErrors::InvalidArgument(
513 "natural_spline_basis needs at least one covariate value".to_string(),
514 ));
515 }
516 if df < MIN_BASIS_DF {
517 return Err(EdgeErrors::InvalidArgument(format!(
518 "natural_spline_basis needs df >= {MIN_BASIS_DF}; got {df}"
519 )));
520 }
521
522 let n = x.len();
523 let mut sorted = x.to_vec();
524 sorted.sort_by(f64::total_cmp);
525 let lower = sorted[0];
526 let upper = sorted[n - 1];
527
528 let n_internal = df - MIN_BASIS_DF;
529 if n_internal == 0 || lower == upper {
530 let mut basis = vec![0.0; n * MIN_BASIS_DF];
531 for (row, &xi) in basis.chunks_exact_mut(MIN_BASIS_DF).zip(x) {
532 row[0] = 1.0;
533 row[1] = xi;
534 }
535 return Ok((basis, MIN_BASIS_DF));
536 }
537
538 let mut knots = Vec::with_capacity(df);
540 knots.push(lower);
541 for j in 1..=n_internal {
542 knots.push(quantile_type7(&sorted, j as f64 / (n_internal + 1) as f64)?);
543 }
544 knots.push(upper);
545 knots.sort_by(f64::total_cmp);
546
547 let n_knots = knots.len();
548 let xi_last = knots[n_knots - 1];
549 let xi_prev = knots[n_knots - 2];
550
551 let mut basis = vec![0.0; n * df];
552 for (row, &xi) in basis.chunks_exact_mut(df).zip(x) {
553 row[0] = 1.0;
554 row[1] = xi;
555 let tail = truncated_cube(xi, xi_last);
556 let d_prev = truncated_power_term(xi, xi_prev, xi_last, tail);
557 for j in 0..n_knots - 2 {
558 row[2 + j] = truncated_power_term(xi, knots[j], xi_last, tail) - d_prev;
559 }
560 }
561
562 Ok((basis, df))
563}
564
565#[inline]
576fn truncated_cube(x: f64, knot: f64) -> f64 {
577 let t = (x - knot).max(0.0);
578 t * t * t
579}
580
581#[inline]
598fn truncated_power_term(x: f64, knot: f64, xi_last: f64, tail: f64) -> f64 {
599 let span = xi_last - knot;
600 if span == 0.0 {
601 return 0.0;
602 }
603 (truncated_cube(x, knot) - tail) / span
604}
605
606pub fn interp_linear_extrap(x: &[f64], xp: &[f64], fp: &[f64]) -> Result<Vec<f64>, EdgeErrors> {
624 validate_interp_knots(xp, fp)?;
625 Ok(x.iter().map(|&xi| interp_one(xi, xp, fp)).collect())
626}
627
628#[inline]
640fn interp_one(x: f64, xp: &[f64], fp: &[f64]) -> f64 {
641 let last = xp.len() - 1;
642 if x <= xp[0] {
643 return fp[0];
644 }
645 if x >= xp[last] {
646 return fp[last];
647 }
648 let hi = xp.partition_point(|&knot| knot <= x);
650 let lo = hi - 1;
651 let t = (x - xp[lo]) / (xp[hi] - xp[lo]);
652 fp[lo] + t * (fp[hi] - fp[lo])
653}
654
655#[cfg(test)]
660mod tests {
661 use super::*;
662 use approx::assert_relative_eq;
663
664 const R_FMM_FIVE: [f64; 3] = [4.15972222222222, 2.36805555555556, 4.74305555555556];
667
668 const R_FMM_OUTSIDE: [f64; 2] = [-7.10416666666667, -10.2291666666667];
671
672 const R_FMM_THREE: [f64; 2] = [0.625, 1.0];
675
676 const R_FMM_TWO: [f64; 3] = [2.0, 4.0, 7.0];
679
680 const MI_GRID: [f64; 5] = [0.0, 1.0, 2.0, 3.0, 4.0];
682
683 const R_MI_INTERIOR: f64 = 2.3887669447483653;
687
688 const R_MI_INTERIOR_EARLY: f64 = 1.4164937870948595;
692
693 const R_MI_NONUNIFORM: f64 = 0.6884019949364506;
697
698 const R_NS_DF5: [f64; 50] = [
701 -0.267261241912424,
702 0.243465876939416,
703 0.563319734782542,
704 0.534154130643652,
705 0.282669497191026,
706 0.0747677460437111,
707 0.00588550770606472,
708 0.0,
709 0.0,
710 0.0,
711 0.0,
712 0.0146319158664838,
713 0.11705532693187,
714 0.37037037037037,
715 0.622770919067215,
716 0.622770919067215,
717 0.37037037037037,
718 0.11705532693187,
719 0.0146319158664838,
720 0.0,
721 -0.214285714285714,
722 -0.137678080611314,
723 -0.0733299370748607,
724 -0.0262752385762756,
725 0.0613409777969527,
726 0.272581217827764,
727 0.525783731386229,
728 0.575413155660069,
729 0.304069501600366,
730 -0.142857142857143,
731 0.642857142857143,
732 0.413034241833943,
733 0.219989811224582,
734 0.0973442342473452,
735 0.0512308388999513,
736 0.0732028485743209,
737 0.144871028063535,
738 0.238095238095238,
739 0.333333333333333,
740 0.428571428571429,
741 -0.428571428571429,
742 -0.275356161222629,
743 -0.146659874149721,
744 -0.0648961561648968,
745 -0.0341538925999675,
746 -0.0469729095662368,
747 -0.047197969326307,
748 0.0694362793128225,
749 0.347965249199817,
750 0.714285714285714,
751 ];
752
753 const NUMPY_TP_DF5: [f64; 50] = [
756 1.0,
757 1.0,
758 0.0,
759 0.0,
760 0.0,
761 1.0,
762 2.0,
763 0.1111111111111111,
764 0.0,
765 0.0,
766 1.0,
767 3.0,
768 0.8888888888888888,
769 0.0,
770 0.0,
771 1.0,
772 4.0,
773 3.0,
774 0.0625,
775 0.0,
776 1.0,
777 5.0,
778 7.111111111111111,
779 0.7939814814814815,
780 0.0,
781 1.0,
782 6.0,
783 13.88888888888889,
784 3.0810185185185186,
785 0.027777777777777776,
786 1.0,
787 7.0,
788 24.0,
789 7.8125,
790 0.75,
791 1.0,
792 8.0,
793 38.10416666666667,
794 15.87037037037037,
795 3.4652777777777777,
796 1.0,
797 9.0,
798 56.02083333333333,
799 27.296296296296294,
800 8.659722222222223,
801 1.0,
802 10.0,
803 75.9375,
804 40.5,
805 15.1875,
806 ];
807
808 const NP_INTERP_INSIDE: [f64; 5] = [1.0, 2.0, 2.5, 1.0, -0.9980000000000002];
811
812 const NP_INTERP_OUTSIDE: [f64; 4] = [0.0, 0.0, -1.0, -1.0];
815
816 #[test]
821 fn test_fmm_spline_matches_r_at_interior_points() {
822 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
823 let y = [1.0, 4.0, 2.0, 8.0, 3.0];
824 let s = fmm_spline(&x, &y).unwrap();
825 for (&at, &want) in [1.5, 2.5, 3.5].iter().zip(&R_FMM_FIVE) {
826 assert_relative_eq!(s.eval(at), want, epsilon = 1e-12);
827 }
828 }
829
830 #[test]
831 fn test_fmm_spline_matches_r_outside_the_knots() {
832 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
833 let y = [1.0, 4.0, 2.0, 8.0, 3.0];
834 let s = fmm_spline(&x, &y).unwrap();
835 for (&at, &want) in [0.5, 5.5].iter().zip(&R_FMM_OUTSIDE) {
836 assert_relative_eq!(s.eval(at), want, epsilon = 1e-11);
837 }
838 }
839
840 #[test]
841 fn test_fmm_spline_three_knots_is_a_parabola() {
842 let x = [0.0, 1.0, 3.0];
843 let y = [2.0, 0.0, 5.0];
844 let s = fmm_spline(&x, &y).unwrap();
845 for (&at, &want) in [0.5, 2.0].iter().zip(&R_FMM_THREE) {
846 assert_relative_eq!(s.eval(at), want, epsilon = 1e-12);
847 }
848 assert!(s.d.iter().all(|&di| di.abs() < 1e-12));
850 }
851
852 #[test]
853 fn test_fmm_spline_two_knots_is_a_line() {
854 let x = [0.0, 2.0];
855 let y = [1.0, 5.0];
856 let s = fmm_spline(&x, &y).unwrap();
857 for (&at, &want) in [0.5, 1.5, 3.0].iter().zip(&R_FMM_TWO) {
858 assert_relative_eq!(s.eval(at), want, epsilon = 1e-12);
859 }
860 }
861
862 #[test]
863 fn test_fmm_spline_passes_through_its_knots() {
864 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
865 let y = [1.0, 4.0, 2.0, 8.0, 3.0];
866 let s = fmm_spline(&x, &y).unwrap();
867 for (&at, &want) in x.iter().zip(&y) {
868 assert_relative_eq!(s.eval(at), want, epsilon = 1e-12);
869 }
870 }
871
872 #[test]
877 fn test_maximize_interpolant_finds_max_between_grid_points() {
878 let y = [-4.0, -1.0, 0.5, 0.2, -3.0];
879 let got = maximize_interpolant(&MI_GRID, &y).unwrap();
880 assert_relative_eq!(got, R_MI_INTERIOR, epsilon = 1e-10);
881 assert!((got - 2.0).abs() > 0.3);
883 let s = fmm_spline(&MI_GRID, &y).unwrap();
885 assert!(s.eval(got) > s.eval(2.0));
886 }
887
888 #[test]
889 fn test_maximize_interpolant_at_first_grid_point() {
890 let y = [5.0, 3.0, 1.0, -2.0, -6.0];
891 assert_relative_eq!(
893 maximize_interpolant(&MI_GRID, &y).unwrap(),
894 0.0,
895 epsilon = 1e-12
896 );
897 }
898
899 #[test]
900 fn test_maximize_interpolant_at_last_grid_point() {
901 let y = [-6.0, -2.0, 1.0, 3.0, 5.0];
902 assert_relative_eq!(
904 maximize_interpolant(&MI_GRID, &y).unwrap(),
905 4.0,
906 epsilon = 1e-12
907 );
908 }
909
910 #[test]
911 fn test_maximize_interpolant_on_a_non_uniform_grid() {
912 let x = [-2.0, -0.5, 0.25, 1.5, 4.0];
913 let y = [-3.0, 0.4, 1.1, 0.9, -2.2];
914 assert_relative_eq!(
915 maximize_interpolant(&x, &y).unwrap(),
916 R_MI_NONUNIFORM,
917 epsilon = 1e-10
918 );
919 }
920
921 #[test]
922 fn test_maximize_interpolant_many_matches_row_by_row() {
923 let rows: [[f64; 5]; 4] = [
924 [-4.0, -1.0, 0.5, 0.2, -3.0],
925 [5.0, 3.0, 1.0, -2.0, -6.0],
926 [-6.0, -2.0, 1.0, 3.0, 5.0],
927 [0.0, 2.5, 2.4, 1.0, -1.0],
928 ];
929 let flat: Vec<f64> = rows.iter().flatten().copied().collect();
930 let many = maximize_interpolant_many(&MI_GRID, &flat, rows.len()).unwrap();
931
932 for (got, row) in many.iter().zip(&rows) {
933 assert_relative_eq!(
934 *got,
935 maximize_interpolant(&MI_GRID, row).unwrap(),
936 epsilon = 1e-14
937 );
938 }
939 let want = [R_MI_INTERIOR, 0.0, 4.0, R_MI_INTERIOR_EARLY];
941 for (got, wanted) in many.iter().zip(&want) {
942 assert_relative_eq!(*got, *wanted, epsilon = 1e-10);
943 }
944 }
945
946 #[test]
947 fn test_maximize_interpolant_many_over_many_genes() {
948 let n_genes = 500;
950 let mut flat = Vec::with_capacity(n_genes * MI_GRID.len());
951 for g in 0..n_genes {
952 let peak = 0.5 + 3.0 * (g as f64) / (n_genes as f64);
953 for &xi in &MI_GRID {
954 flat.push(-(xi - peak) * (xi - peak));
955 }
956 }
957 let many = maximize_interpolant_many(&MI_GRID, &flat, n_genes).unwrap();
958 for (g, got) in many.iter().enumerate() {
959 let row = &flat[g * MI_GRID.len()..(g + 1) * MI_GRID.len()];
960 assert_relative_eq!(*got, maximize_interpolant(&MI_GRID, row).unwrap());
961 }
962 }
963
964 #[test]
969 fn test_natural_spline_basis_matches_truncated_power_reference() {
970 let x: Vec<f64> = (1..=10).map(|i| i as f64).collect();
971 let (basis, ncols) = natural_spline_basis(&x, 5).unwrap();
972 assert_eq!(ncols, 5);
973 assert_eq!(basis.len(), 50);
974 for (got, want) in basis.iter().zip(&NUMPY_TP_DF5) {
975 assert_relative_eq!(*got, *want, epsilon = 1e-10);
976 }
977 }
978
979 #[test]
980 fn test_natural_spline_basis_spans_the_same_space_as_r_ns() {
981 let n = 10;
984 let df = 5;
985 let x: Vec<f64> = (1..=n).map(|i| i as f64).collect();
986 let (basis, ncols) = natural_spline_basis(&x, df).unwrap();
987 assert_eq!(ncols, df);
988
989 let mut r_basis = vec![0.0; n * df];
991 for j in 0..df {
992 for i in 0..n {
993 r_basis[i * df + j] = R_NS_DF5[j * n + i];
994 }
995 }
996
997 let gram = gram_matrix(&basis, n, df);
998 for j in 0..df {
999 let target: Vec<f64> = (0..n).map(|i| r_basis[i * df + j]).collect();
1000 let rhs = cross_product(&basis, n, df, &target);
1001 let coef = solve_spd(&gram, df, &rhs);
1002 for i in 0..n {
1003 let fitted: f64 = (0..df).map(|k| basis[i * df + k] * coef[k]).sum();
1004 assert_relative_eq!(fitted, target[i], epsilon = 1e-9);
1005 }
1006 }
1007 }
1008
1009 #[test]
1010 fn test_natural_spline_basis_degenerates_when_x_is_constant() {
1011 let x = [3.0; 6];
1012 let (basis, ncols) = natural_spline_basis(&x, 5).unwrap();
1013 assert_eq!(ncols, 2);
1014 assert_eq!(
1015 basis,
1016 vec![1.0, 3.0, 1.0, 3.0, 1.0, 3.0, 1.0, 3.0, 1.0, 3.0, 1.0, 3.0]
1017 );
1018 }
1019
1020 #[test]
1021 fn test_natural_spline_basis_df_two_is_linear() {
1022 let x = [1.0, 4.0, 9.0];
1023 let (basis, ncols) = natural_spline_basis(&x, 2).unwrap();
1024 assert_eq!(ncols, 2);
1025 assert_eq!(basis, vec![1.0, 1.0, 1.0, 4.0, 1.0, 9.0]);
1026 }
1027
1028 #[test]
1029 fn test_natural_spline_basis_ignores_input_order() {
1030 let ordered: Vec<f64> = (1..=10).map(|i| i as f64).collect();
1031 let shuffled: Vec<f64> = vec![7.0, 1.0, 10.0, 4.0, 2.0, 9.0, 3.0, 6.0, 8.0, 5.0];
1032 let (a, df) = natural_spline_basis(&ordered, 5).unwrap();
1033 let (b, _) = natural_spline_basis(&shuffled, 5).unwrap();
1034 for (i, &xi) in shuffled.iter().enumerate() {
1035 let src = (xi as usize) - 1;
1036 for k in 0..df {
1037 assert_relative_eq!(b[i * df + k], a[src * df + k], epsilon = 1e-12);
1038 }
1039 }
1040 }
1041
1042 #[test]
1047 fn test_interp_linear_extrap_inside_the_range() {
1048 let xp = [0.0, 1.0, 2.0, 4.0];
1049 let fp = [0.0, 2.0, 3.0, -1.0];
1050 let q = [0.5, 1.0, 1.5, 3.0, 3.999];
1051 let got = interp_linear_extrap(&q, &xp, &fp).unwrap();
1052 for (g, w) in got.iter().zip(&NP_INTERP_INSIDE) {
1053 assert_relative_eq!(*g, *w, epsilon = 1e-12);
1054 }
1055 }
1056
1057 #[test]
1058 fn test_interp_linear_extrap_clamps_outside_the_range() {
1059 let xp = [0.0, 1.0, 2.0, 4.0];
1060 let fp = [0.0, 2.0, 3.0, -1.0];
1061 let q = [-3.0, -0.001, 4.5, 10.0];
1062 let got = interp_linear_extrap(&q, &xp, &fp).unwrap();
1063 for (g, w) in got.iter().zip(&NP_INTERP_OUTSIDE) {
1064 assert_relative_eq!(*g, *w, epsilon = 1e-12);
1065 }
1066 }
1067
1068 #[test]
1069 fn test_interp_linear_extrap_hits_the_knots_exactly() {
1070 let xp = [0.0, 1.0, 2.0, 4.0];
1071 let fp = [0.0, 2.0, 3.0, -1.0];
1072 assert_eq!(interp_linear_extrap(&xp, &xp, &fp).unwrap(), fp.to_vec());
1073 }
1074
1075 #[test]
1076 fn test_interp_linear_extrap_empty_query_is_empty() {
1077 let xp = [0.0, 1.0];
1078 let fp = [0.0, 2.0];
1079 assert!(interp_linear_extrap(&[], &xp, &fp).unwrap().is_empty());
1080 }
1081
1082 #[test]
1087 fn test_fmm_spline_rejects_empty_input() {
1088 assert!(matches!(
1089 fmm_spline(&[], &[]).unwrap_err(),
1090 EdgeErrors::InvalidArgument(_)
1091 ));
1092 }
1093
1094 #[test]
1095 fn test_fmm_spline_rejects_a_single_knot() {
1096 assert!(matches!(
1097 fmm_spline(&[1.0], &[2.0]).unwrap_err(),
1098 EdgeErrors::InvalidArgument(_)
1099 ));
1100 }
1101
1102 #[test]
1103 fn test_fmm_spline_rejects_unsorted_knots() {
1104 assert!(matches!(
1105 fmm_spline(&[0.0, 2.0, 1.0], &[1.0, 2.0, 3.0]).unwrap_err(),
1106 EdgeErrors::InvalidArgument(_)
1107 ));
1108 }
1109
1110 #[test]
1111 fn test_fmm_spline_rejects_duplicated_knots() {
1112 assert!(matches!(
1113 fmm_spline(&[0.0, 1.0, 1.0], &[1.0, 2.0, 3.0]).unwrap_err(),
1114 EdgeErrors::InvalidArgument(_)
1115 ));
1116 }
1117
1118 #[test]
1119 fn test_fmm_spline_rejects_mismatched_y() {
1120 assert!(matches!(
1121 fmm_spline(&[0.0, 1.0, 2.0], &[1.0, 2.0]).unwrap_err(),
1122 EdgeErrors::LengthMismatch {
1123 name: "y",
1124 expected: 3,
1125 got: 2
1126 }
1127 ));
1128 }
1129
1130 #[test]
1131 fn test_maximize_interpolant_rejects_mismatched_y() {
1132 assert!(matches!(
1133 maximize_interpolant(&[0.0, 1.0, 2.0], &[1.0]).unwrap_err(),
1134 EdgeErrors::LengthMismatch { .. }
1135 ));
1136 }
1137
1138 #[test]
1139 fn test_maximize_interpolant_many_rejects_zero_genes() {
1140 assert!(matches!(
1141 maximize_interpolant_many(&[0.0, 1.0], &[], 0).unwrap_err(),
1142 EdgeErrors::MustBePositive(_)
1143 ));
1144 }
1145
1146 #[test]
1147 fn test_maximize_interpolant_many_rejects_wrong_matrix_size() {
1148 assert!(matches!(
1149 maximize_interpolant_many(&[0.0, 1.0, 2.0], &[1.0, 2.0, 3.0, 4.0], 2).unwrap_err(),
1150 EdgeErrors::LengthMismatch {
1151 expected: 6,
1152 got: 4,
1153 ..
1154 }
1155 ));
1156 }
1157
1158 #[test]
1159 fn test_maximize_interpolant_many_rejects_bad_grid() {
1160 assert!(matches!(
1161 maximize_interpolant_many(&[1.0], &[1.0], 1).unwrap_err(),
1162 EdgeErrors::InvalidArgument(_)
1163 ));
1164 }
1165
1166 #[test]
1167 fn test_natural_spline_basis_rejects_empty_x() {
1168 assert!(matches!(
1169 natural_spline_basis(&[], 5).unwrap_err(),
1170 EdgeErrors::InvalidArgument(_)
1171 ));
1172 }
1173
1174 #[test]
1175 fn test_natural_spline_basis_rejects_df_below_two() {
1176 for df in [0, 1] {
1177 assert!(matches!(
1178 natural_spline_basis(&[1.0, 2.0, 3.0], df).unwrap_err(),
1179 EdgeErrors::InvalidArgument(_)
1180 ));
1181 }
1182 }
1183
1184 #[test]
1185 fn test_interp_linear_rejects_too_few_points() {
1186 assert!(matches!(
1187 interp_linear_extrap(&[0.5], &[], &[]).unwrap_err(),
1188 EdgeErrors::InvalidArgument(_)
1189 ));
1190 }
1191
1192 #[test]
1193 fn test_interp_linear_rejects_mismatched_fp() {
1194 assert!(matches!(
1195 interp_linear_extrap(&[0.5], &[0.0, 1.0, 2.0], &[1.0, 2.0]).unwrap_err(),
1196 EdgeErrors::LengthMismatch { name: "fp", .. }
1197 ));
1198 }
1199
1200 #[test]
1201 fn test_interp_linear_rejects_unsorted_xp() {
1202 assert!(matches!(
1203 interp_linear_extrap(&[0.5], &[0.0, 2.0, 1.0], &[1.0, 2.0, 3.0]).unwrap_err(),
1204 EdgeErrors::InvalidArgument(_)
1205 ));
1206 assert!(matches!(
1207 interp_linear_extrap(&[0.5], &[0.0, 1.0, 1.0], &[1.0, 2.0, 3.0]).unwrap_err(),
1208 EdgeErrors::InvalidArgument(_)
1209 ));
1210 }
1211
1212 fn gram_matrix(a: &[f64], n: usize, p: usize) -> Vec<f64> {
1218 let mut g = vec![0.0; p * p];
1219 for i in 0..n {
1220 for j in 0..p {
1221 for k in 0..p {
1222 g[j * p + k] += a[i * p + j] * a[i * p + k];
1223 }
1224 }
1225 }
1226 g
1227 }
1228
1229 fn cross_product(a: &[f64], n: usize, p: usize, v: &[f64]) -> Vec<f64> {
1231 let mut out = vec![0.0; p];
1232 for i in 0..n {
1233 for j in 0..p {
1234 out[j] += a[i * p + j] * v[i];
1235 }
1236 }
1237 out
1238 }
1239
1240 fn solve_spd(a: &[f64], p: usize, rhs: &[f64]) -> Vec<f64> {
1242 let mut m = a.to_vec();
1243 let mut b = rhs.to_vec();
1244 for col in 0..p {
1245 let pivot = (col..p)
1246 .max_by(|&r1, &r2| m[r1 * p + col].abs().total_cmp(&m[r2 * p + col].abs()))
1247 .unwrap();
1248 if pivot != col {
1249 for k in 0..p {
1250 m.swap(col * p + k, pivot * p + k);
1251 }
1252 b.swap(col, pivot);
1253 }
1254 let d = m[col * p + col];
1255 for row in (col + 1)..p {
1256 let f = m[row * p + col] / d;
1257 for k in col..p {
1258 m[row * p + k] -= f * m[col * p + k];
1259 }
1260 b[row] -= f * b[col];
1261 }
1262 }
1263 let mut out = vec![0.0; p];
1264 for col in (0..p).rev() {
1265 let mut s = b[col];
1266 for k in (col + 1)..p {
1267 s -= m[col * p + k] * out[k];
1268 }
1269 out[col] = s / m[col * p + col];
1270 }
1271 out
1272 }
1273}