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
242#[derive(Debug, Clone, Copy, PartialEq)]
244#[non_exhaustive]
245pub enum InterpolationMethod {
246 Linear,
248 CubicHermite,
250}
251
252#[must_use]
266pub fn fdata_interpolate(
267 data: &crate::matrix::FdMatrix,
268 argvals: &[f64],
269 new_argvals: &[f64],
270 method: InterpolationMethod,
271) -> crate::matrix::FdMatrix {
272 let (n, m) = data.shape();
273 let m_new = new_argvals.len();
274 if n == 0 || m < 2 || m_new == 0 {
275 return crate::matrix::FdMatrix::zeros(n.max(1), m_new.max(1));
276 }
277
278 let mut result = crate::matrix::FdMatrix::zeros(n, m_new);
279
280 for i in 0..n {
281 let y: Vec<f64> = (0..m).map(|j| data[(i, j)]).collect();
282 for (j, &t) in new_argvals.iter().enumerate() {
283 result[(i, j)] = match method {
284 InterpolationMethod::Linear => linear_interp(argvals, &y, t),
285 InterpolationMethod::CubicHermite => cubic_hermite_interp(argvals, &y, t),
286 };
287 }
288 }
289
290 result
291}
292
293fn cubic_hermite_interp(x: &[f64], y: &[f64], t: f64) -> f64 {
297 let n = x.len();
298 if n < 2 {
299 return if n == 1 { y[0] } else { 0.0 };
300 }
301
302 if t <= x[0] {
304 return y[0];
305 }
306 if t >= x[n - 1] {
307 return y[n - 1];
308 }
309
310 let k = match x.binary_search_by(|v| v.partial_cmp(&t).unwrap_or(std::cmp::Ordering::Equal)) {
312 Ok(i) => return y[i],
313 Err(i) => {
314 if i == 0 {
315 0
316 } else {
317 i - 1
318 }
319 }
320 };
321
322 let slopes: Vec<f64> = x
324 .windows(2)
325 .zip(y.windows(2))
326 .map(|(xw, yw)| (yw[1] - yw[0]) / (xw[1] - xw[0]))
327 .collect();
328
329 let mut tangents = vec![0.0; n];
331 tangents[0] = slopes[0];
332 tangents[n - 1] = slopes[n - 2];
333 for i in 1..n - 1 {
334 if slopes[i - 1].signum() != slopes[i].signum() {
335 tangents[i] = 0.0;
336 } else {
337 tangents[i] = (slopes[i - 1] + slopes[i]) / 2.0;
338 }
339 }
340
341 let h = x[k + 1] - x[k];
343 let s = (t - x[k]) / h;
344 let s2 = s * s;
345 let s3 = s2 * s;
346
347 let h00 = 2.0 * s3 - 3.0 * s2 + 1.0;
348 let h10 = s3 - 2.0 * s2 + s;
349 let h01 = -2.0 * s3 + 3.0 * s2;
350 let h11 = s3 - s2;
351
352 h00 * y[k] + h10 * h * tangents[k] + h01 * y[k + 1] + h11 * h * tangents[k + 1]
353}
354
355pub fn gradient_uniform(y: &[f64], h: f64) -> Vec<f64> {
362 let n = y.len();
363 let mut g = vec![0.0; n];
364 if n < 2 {
365 return g;
366 }
367 if n == 2 {
368 g[0] = (y[1] - y[0]) / h;
369 g[1] = (y[1] - y[0]) / h;
370 return g;
371 }
372 if n == 3 {
373 g[0] = (-3.0 * y[0] + 4.0 * y[1] - y[2]) / (2.0 * h);
374 g[1] = (y[2] - y[0]) / (2.0 * h);
375 g[2] = (y[0] - 4.0 * y[1] + 3.0 * y[2]) / (2.0 * h);
376 return g;
377 }
378 if n == 4 {
379 g[0] = (-3.0 * y[0] + 4.0 * y[1] - y[2]) / (2.0 * h);
380 g[1] = (y[2] - y[0]) / (2.0 * h);
381 g[2] = (y[3] - y[1]) / (2.0 * h);
382 g[3] = (y[1] - 4.0 * y[2] + 3.0 * y[3]) / (2.0 * h);
383 return g;
384 }
385
386 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);
389 g[1] = (-3.0 * y[0] - 10.0 * y[1] + 18.0 * y[2] - 6.0 * y[3] + y[4]) / (12.0 * h);
390
391 for i in 2..n - 2 {
393 g[i] = (-y[i + 2] + 8.0 * y[i + 1] - 8.0 * y[i - 1] + y[i - 2]) / (12.0 * h);
394 }
395
396 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])
398 / (12.0 * h);
399 g[n - 1] = (3.0 * y[n - 5] - 16.0 * y[n - 4] + 36.0 * y[n - 3] - 48.0 * y[n - 2]
400 + 25.0 * y[n - 1])
401 / (12.0 * h);
402 g
403}
404
405pub fn gradient_nonuniform(y: &[f64], t: &[f64]) -> Vec<f64> {
413 let n = y.len();
414 assert_eq!(n, t.len(), "y and t must have the same length");
415 let mut g = vec![0.0; n];
416 if n < 2 {
417 return g;
418 }
419 if n == 2 {
420 let h = t[1] - t[0];
421 if h.abs() < 1e-15 {
422 return g;
423 }
424 g[0] = (y[1] - y[0]) / h;
425 g[1] = g[0];
426 return g;
427 }
428
429 let h0 = t[1] - t[0];
431 let h1 = t[2] - t[0];
432 if h0.abs() > 1e-15 && h1.abs() > 1e-15 && (h1 - h0).abs() > 1e-15 {
433 g[0] = y[0] * (-h1 - h0) / (h0 * h1) + y[1] * h1 / (h0 * (h1 - h0))
434 - y[2] * h0 / (h1 * (h1 - h0));
435 } else {
436 g[0] = (y[1] - y[0]) / h0.max(1e-15);
437 }
438
439 for i in 1..n - 1 {
441 let h_l = t[i] - t[i - 1];
442 let h_r = t[i + 1] - t[i];
443 let h_sum = h_l + h_r;
444 if h_l.abs() < 1e-15 || h_r.abs() < 1e-15 || h_sum.abs() < 1e-15 {
445 g[i] = 0.0;
446 continue;
447 }
448 g[i] = -y[i - 1] * h_r / (h_l * h_sum)
449 + y[i] * (h_r - h_l) / (h_l * h_r)
450 + y[i + 1] * h_l / (h_r * h_sum);
451 }
452
453 let h_last = t[n - 1] - t[n - 2];
455 let h_prev = t[n - 1] - t[n - 3];
456 let h_mid = t[n - 2] - t[n - 3];
457 if h_last.abs() > 1e-15 && h_prev.abs() > 1e-15 && h_mid.abs() > 1e-15 {
458 g[n - 1] = y[n - 3] * h_last / (h_mid * h_prev) - y[n - 2] * h_prev / (h_mid * h_last)
459 + y[n - 1] * (h_prev + h_last) / (h_prev * h_last);
460 } else {
461 g[n - 1] = (y[n - 1] - y[n - 2]) / h_last.max(1e-15);
462 }
463
464 g
465}
466
467pub fn gradient(y: &[f64], t: &[f64]) -> Vec<f64> {
473 let n = t.len();
474 if n < 2 {
475 return vec![0.0; y.len()];
476 }
477
478 let h0 = t[1] - t[0];
479 let is_uniform = t
480 .windows(2)
481 .all(|w| ((w[1] - w[0]) - h0).abs() < 1e-12 * h0.abs().max(1.0));
482
483 if is_uniform {
484 gradient_uniform(y, h0)
485 } else {
486 gradient_nonuniform(y, t)
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493
494 #[test]
495 fn test_simpsons_weights_uniform() {
496 let argvals = vec![0.0, 0.25, 0.5, 0.75, 1.0];
497 let weights = simpsons_weights(&argvals);
498 let sum: f64 = weights.iter().sum();
499 assert!((sum - 1.0).abs() < NUMERICAL_EPS);
500 }
501
502 #[test]
503 fn test_simpsons_weights_2d() {
504 let argvals_s = vec![0.0, 0.5, 1.0];
505 let argvals_t = vec![0.0, 0.5, 1.0];
506 let weights = simpsons_weights_2d(&argvals_s, &argvals_t);
507 let sum: f64 = weights.iter().sum();
508 assert!((sum - 1.0).abs() < NUMERICAL_EPS);
509 }
510
511 #[test]
512 fn test_extract_curves() {
513 let data = vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0];
516 let mat = crate::matrix::FdMatrix::from_column_major(data, 2, 3).unwrap();
517 let curves = extract_curves(&mat);
518 assert_eq!(curves.len(), 2);
519 assert_eq!(curves[0], vec![1.0, 2.0, 3.0]);
520 assert_eq!(curves[1], vec![4.0, 5.0, 6.0]);
521 }
522
523 #[test]
524 fn test_l2_distance_identical() {
525 let curve = vec![1.0, 2.0, 3.0];
526 let weights = vec![0.25, 0.5, 0.25];
527 let dist = l2_distance(&curve, &curve, &weights);
528 assert!(dist.abs() < NUMERICAL_EPS);
529 }
530
531 #[test]
532 fn test_l2_distance_different() {
533 let curve1 = vec![0.0, 0.0, 0.0];
534 let curve2 = vec![1.0, 1.0, 1.0];
535 let weights = vec![0.25, 0.5, 0.25]; let dist = l2_distance(&curve1, &curve2, &weights);
537 assert!((dist - 1.0).abs() < NUMERICAL_EPS);
539 }
540
541 #[test]
542 fn test_n1_weights() {
543 let w = simpsons_weights(&[0.5]);
545 assert_eq!(w.len(), 1);
546 assert!((w[0] - 1.0).abs() < 1e-12);
547 }
548
549 #[test]
550 fn test_n2_weights() {
551 let w = simpsons_weights(&[0.0, 1.0]);
552 assert_eq!(w.len(), 2);
553 assert!((w[0] - 0.5).abs() < 1e-12);
555 assert!((w[1] - 0.5).abs() < 1e-12);
556 }
557
558 #[test]
559 fn test_mismatched_l2_distance() {
560 let a = vec![1.0, 2.0, 3.0];
562 let b = vec![1.0, 2.0, 3.0];
563 let w = vec![0.5, 0.5, 0.5];
564 let d = l2_distance(&a, &b, &w);
565 assert!(d.abs() < 1e-12, "Same vectors should have zero distance");
566 }
567
568 #[test]
571 fn test_trapz_sine() {
572 let m = 1000;
574 let x: Vec<f64> = (0..m)
575 .map(|i| std::f64::consts::PI * i as f64 / (m - 1) as f64)
576 .collect();
577 let y: Vec<f64> = x.iter().map(|&xi| xi.sin()).collect();
578 let result = trapz(&y, &x);
579 assert!(
580 (result - 2.0).abs() < 1e-4,
581 "∫ sin(x) dx over [0,π] should be ~2, got {result}"
582 );
583 }
584
585 #[test]
588 fn test_cumulative_trapz_matches_final() {
589 let m = 100;
590 let x: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
591 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi).collect();
592 let cum = cumulative_trapz(&y, &x);
593 let total = trapz(&y, &x);
594 assert!(
595 (cum[m - 1] - total).abs() < 1e-12,
596 "Final cumulative value should match trapz"
597 );
598 }
599
600 #[test]
603 fn test_linear_interp_boundary_clamp() {
604 let x = vec![0.0, 0.5, 1.0];
605 let y = vec![10.0, 20.0, 30.0];
606 assert!((linear_interp(&x, &y, -1.0) - 10.0).abs() < 1e-12);
607 assert!((linear_interp(&x, &y, 2.0) - 30.0).abs() < 1e-12);
608 assert!((linear_interp(&x, &y, 0.25) - 15.0).abs() < 1e-12);
609 }
610
611 #[test]
614 fn test_gradient_uniform_linear() {
615 let m = 50;
617 let h = 1.0 / (m - 1) as f64;
618 let y: Vec<f64> = (0..m).map(|i| 3.0 * i as f64 * h).collect();
619 let g = gradient_uniform(&y, h);
620 for i in 0..m {
621 assert!(
622 (g[i] - 3.0).abs() < 1e-10,
623 "gradient of 3x should be 3 at i={i}, got {}",
624 g[i]
625 );
626 }
627 }
628
629 #[test]
632 fn fdata_interpolate_linear_identity() {
633 let t: Vec<f64> = (0..20).map(|i| i as f64 / 19.0).collect();
634 let vals: Vec<f64> = t.iter().map(|&x| x.sin()).collect();
635 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
636 let result = fdata_interpolate(&data, &t, &t, InterpolationMethod::Linear);
637 for j in 0..20 {
638 assert!((result[(0, j)] - data[(0, j)]).abs() < 1e-12);
639 }
640 }
641
642 #[test]
643 fn fdata_interpolate_cubic_hermite_smooth() {
644 let t: Vec<f64> = (0..20).map(|i| i as f64 / 19.0).collect();
645 let vals: Vec<f64> = t.iter().map(|&x| x.sin()).collect();
646 let data = crate::matrix::FdMatrix::from_column_major(vals, 1, 20).unwrap();
647
648 let t_fine: Vec<f64> = (0..100).map(|i| i as f64 / 99.0).collect();
649 let result = fdata_interpolate(&data, &t, &t_fine, InterpolationMethod::CubicHermite);
650
651 for (j, &tj) in t_fine.iter().enumerate() {
653 assert!(
654 (result[(0, j)] - tj.sin()).abs() < 0.02,
655 "at t={tj:.2}: got {:.4}, expected {:.4}",
656 result[(0, j)],
657 tj.sin()
658 );
659 }
660 }
661
662 #[test]
663 fn fdata_interpolate_multiple_curves() {
664 let t: Vec<f64> = (0..30).map(|i| i as f64 / 29.0).collect();
665 let n = 5;
666 let m = 30;
667 let mut col_major = vec![0.0; n * m];
669 for i in 0..n {
670 for j in 0..m {
671 col_major[i + j * n] = ((i + 1) as f64 * t[j]).sin();
672 }
673 }
674 let data = crate::matrix::FdMatrix::from_column_major(col_major, n, m).unwrap();
675
676 let t_new: Vec<f64> = (0..50).map(|i| i as f64 / 49.0).collect();
677 let result = fdata_interpolate(&data, &t, &t_new, InterpolationMethod::Linear);
678 assert_eq!(result.shape(), (n, 50));
679 for i in 0..n {
681 for j in 0..50 {
682 assert!(result[(i, j)].is_finite());
683 }
684 }
685 }
686}