1pub const NUMERICAL_EPS: f64 = 1e-10;
5
6pub const DEFAULT_CONVERGENCE_TOL: f64 = 1e-6;
8
9pub fn extract_curves(data: &crate::matrix::FdMatrix) -> Vec<Vec<f64>> {
20 data.rows()
21}
22
23pub fn l2_distance(curve1: &[f64], curve2: &[f64], weights: &[f64]) -> f64 {
33 let mut dist_sq = 0.0;
34 for i in 0..curve1.len() {
35 let diff = curve1[i] - curve2[i];
36 dist_sq += diff * diff * weights[i];
37 }
38 dist_sq.sqrt()
39}
40
41pub fn simpsons_weights(argvals: &[f64]) -> Vec<f64> {
53 let n = argvals.len();
54 if n < 2 {
55 return vec![1.0; n];
56 }
57
58 let mut weights = vec![0.0; n];
59
60 if n == 2 {
61 let h = argvals[1] - argvals[0];
63 weights[0] = h / 2.0;
64 weights[1] = h / 2.0;
65 return weights;
66 }
67
68 let h0 = argvals[1] - argvals[0];
70 let is_uniform = argvals
71 .windows(2)
72 .all(|w| ((w[1] - w[0]) - h0).abs() < 1e-12 * h0.abs());
73
74 if is_uniform {
75 simpsons_weights_uniform(&mut weights, n, h0);
76 } else {
77 simpsons_weights_nonuniform(&mut weights, argvals, n);
78 }
79
80 weights
81}
82
83fn simpsons_weights_uniform(weights: &mut [f64], n: usize, h0: f64) {
85 let n_intervals = n - 1;
86 if n_intervals % 2 == 0 {
87 weights[0] = h0 / 3.0;
89 weights[n - 1] = h0 / 3.0;
90 for i in 1..n - 1 {
91 weights[i] = if i % 2 == 1 {
92 4.0 * h0 / 3.0
93 } else {
94 2.0 * h0 / 3.0
95 };
96 }
97 } else {
98 let n_simp = n - 1;
100 weights[0] = h0 / 3.0;
101 weights[n_simp - 1] = h0 / 3.0;
102 for i in 1..n_simp - 1 {
103 weights[i] = if i % 2 == 1 {
104 4.0 * h0 / 3.0
105 } else {
106 2.0 * h0 / 3.0
107 };
108 }
109 weights[n_simp - 1] += h0 / 2.0;
110 weights[n - 1] += h0 / 2.0;
111 }
112}
113
114fn simpsons_weights_nonuniform(weights: &mut [f64], argvals: &[f64], n: usize) {
116 let n_intervals = n - 1;
117 let n_pairs = n_intervals / 2;
118
119 for k in 0..n_pairs {
120 let i0 = 2 * k;
121 let i1 = i0 + 1;
122 let i2 = i0 + 2;
123 let h1 = argvals[i1] - argvals[i0];
124 let h2 = argvals[i2] - argvals[i1];
125 let h_sum = h1 + h2;
126
127 weights[i0] += (2.0 * h1 - h2) * h_sum / (6.0 * h1);
128 weights[i1] += h_sum * h_sum * h_sum / (6.0 * h1 * h2);
129 weights[i2] += (2.0 * h2 - h1) * h_sum / (6.0 * h2);
130 }
131
132 if n_intervals % 2 == 1 {
133 let h_last = argvals[n - 1] - argvals[n - 2];
134 weights[n - 2] += h_last / 2.0;
135 weights[n - 1] += h_last / 2.0;
136 }
137}
138
139pub fn simpsons_weights_2d(argvals_s: &[f64], argvals_t: &[f64]) -> Vec<f64> {
150 let weights_s = simpsons_weights(argvals_s);
151 let weights_t = simpsons_weights(argvals_t);
152 let m1 = argvals_s.len();
153 let m2 = argvals_t.len();
154
155 let mut weights = vec![0.0; m1 * m2];
156 for i in 0..m1 {
157 for j in 0..m2 {
158 weights[i + j * m1] = weights_s[i] * weights_t[j];
159 }
160 }
161 weights
162}
163
164pub fn linear_interp(x: &[f64], y: &[f64], t: f64) -> f64 {
168 if t <= x[0] {
169 return y[0];
170 }
171 let last = x.len() - 1;
172 if t >= x[last] {
173 return y[last];
174 }
175
176 let idx = match x.binary_search_by(|v| v.partial_cmp(&t).unwrap()) {
177 Ok(i) => return y[i],
178 Err(i) => i,
179 };
180
181 let t0 = x[idx - 1];
182 let t1 = x[idx];
183 let y0 = y[idx - 1];
184 let y1 = y[idx];
185 y0 + (y1 - y0) * (t - t0) / (t1 - t0)
186}
187
188pub fn cumulative_trapz(y: &[f64], x: &[f64]) -> Vec<f64> {
193 let n = y.len();
194 let mut out = vec![0.0; n];
195 if n < 2 {
196 return out;
197 }
198
199 let mut k = 1;
201 while k + 1 < n {
202 let h1 = x[k] - x[k - 1];
203 let h2 = x[k + 1] - x[k];
204 let h_sum = h1 + h2;
205
206 let integral = h_sum / 6.0
208 * (y[k - 1] * (2.0 * h1 - h2) / h1
209 + y[k] * h_sum * h_sum / (h1 * h2)
210 + y[k + 1] * (2.0 * h2 - h1) / h2);
211
212 out[k] = out[k - 1] + {
213 0.5 * (y[k] + y[k - 1]) * h1
215 };
216 out[k + 1] = out[k - 1] + integral;
217 k += 2;
218 }
219
220 if k < n {
222 out[k] = out[k - 1] + 0.5 * (y[k] + y[k - 1]) * (x[k] - x[k - 1]);
223 }
224
225 out
226}
227
228pub fn trapz(y: &[f64], x: &[f64]) -> f64 {
230 let mut sum = 0.0;
231 for k in 1..y.len() {
232 sum += 0.5 * (y[k] + y[k - 1]) * (x[k] - x[k - 1]);
233 }
234 sum
235}
236
237pub fn gradient_uniform(y: &[f64], h: f64) -> Vec<f64> {
244 let n = y.len();
245 let mut g = vec![0.0; n];
246 if n < 2 {
247 return g;
248 }
249 if n == 2 {
250 g[0] = (y[1] - y[0]) / h;
251 g[1] = (y[1] - y[0]) / h;
252 return g;
253 }
254 if n == 3 {
255 g[0] = (-3.0 * y[0] + 4.0 * y[1] - y[2]) / (2.0 * h);
256 g[1] = (y[2] - y[0]) / (2.0 * h);
257 g[2] = (y[0] - 4.0 * y[1] + 3.0 * y[2]) / (2.0 * h);
258 return g;
259 }
260 if n == 4 {
261 g[0] = (-3.0 * y[0] + 4.0 * y[1] - y[2]) / (2.0 * h);
262 g[1] = (y[2] - y[0]) / (2.0 * h);
263 g[2] = (y[3] - y[1]) / (2.0 * h);
264 g[3] = (y[1] - 4.0 * y[2] + 3.0 * y[3]) / (2.0 * h);
265 return g;
266 }
267
268 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);
271 g[1] = (-3.0 * y[0] - 10.0 * y[1] + 18.0 * y[2] - 6.0 * y[3] + y[4]) / (12.0 * h);
272
273 for i in 2..n - 2 {
275 g[i] = (-y[i + 2] + 8.0 * y[i + 1] - 8.0 * y[i - 1] + y[i - 2]) / (12.0 * h);
276 }
277
278 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])
280 / (12.0 * h);
281 g[n - 1] = (3.0 * y[n - 5] - 16.0 * y[n - 4] + 36.0 * y[n - 3] - 48.0 * y[n - 2]
282 + 25.0 * y[n - 1])
283 / (12.0 * h);
284 g
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn test_simpsons_weights_uniform() {
293 let argvals = vec![0.0, 0.25, 0.5, 0.75, 1.0];
294 let weights = simpsons_weights(&argvals);
295 let sum: f64 = weights.iter().sum();
296 assert!((sum - 1.0).abs() < NUMERICAL_EPS);
297 }
298
299 #[test]
300 fn test_simpsons_weights_2d() {
301 let argvals_s = vec![0.0, 0.5, 1.0];
302 let argvals_t = vec![0.0, 0.5, 1.0];
303 let weights = simpsons_weights_2d(&argvals_s, &argvals_t);
304 let sum: f64 = weights.iter().sum();
305 assert!((sum - 1.0).abs() < NUMERICAL_EPS);
306 }
307
308 #[test]
309 fn test_extract_curves() {
310 let data = vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0];
313 let mat = crate::matrix::FdMatrix::from_column_major(data, 2, 3).unwrap();
314 let curves = extract_curves(&mat);
315 assert_eq!(curves.len(), 2);
316 assert_eq!(curves[0], vec![1.0, 2.0, 3.0]);
317 assert_eq!(curves[1], vec![4.0, 5.0, 6.0]);
318 }
319
320 #[test]
321 fn test_l2_distance_identical() {
322 let curve = vec![1.0, 2.0, 3.0];
323 let weights = vec![0.25, 0.5, 0.25];
324 let dist = l2_distance(&curve, &curve, &weights);
325 assert!(dist.abs() < NUMERICAL_EPS);
326 }
327
328 #[test]
329 fn test_l2_distance_different() {
330 let curve1 = vec![0.0, 0.0, 0.0];
331 let curve2 = vec![1.0, 1.0, 1.0];
332 let weights = vec![0.25, 0.5, 0.25]; let dist = l2_distance(&curve1, &curve2, &weights);
334 assert!((dist - 1.0).abs() < NUMERICAL_EPS);
336 }
337
338 #[test]
339 fn test_n1_weights() {
340 let w = simpsons_weights(&[0.5]);
342 assert_eq!(w.len(), 1);
343 assert!((w[0] - 1.0).abs() < 1e-12);
344 }
345
346 #[test]
347 fn test_n2_weights() {
348 let w = simpsons_weights(&[0.0, 1.0]);
349 assert_eq!(w.len(), 2);
350 assert!((w[0] - 0.5).abs() < 1e-12);
352 assert!((w[1] - 0.5).abs() < 1e-12);
353 }
354
355 #[test]
356 fn test_mismatched_l2_distance() {
357 let a = vec![1.0, 2.0, 3.0];
359 let b = vec![1.0, 2.0, 3.0];
360 let w = vec![0.5, 0.5, 0.5];
361 let d = l2_distance(&a, &b, &w);
362 assert!(d.abs() < 1e-12, "Same vectors should have zero distance");
363 }
364
365 #[test]
368 fn test_trapz_sine() {
369 let m = 1000;
371 let x: Vec<f64> = (0..m)
372 .map(|i| std::f64::consts::PI * i as f64 / (m - 1) as f64)
373 .collect();
374 let y: Vec<f64> = x.iter().map(|&xi| xi.sin()).collect();
375 let result = trapz(&y, &x);
376 assert!(
377 (result - 2.0).abs() < 1e-4,
378 "∫ sin(x) dx over [0,π] should be ~2, got {result}"
379 );
380 }
381
382 #[test]
385 fn test_cumulative_trapz_matches_final() {
386 let m = 100;
387 let x: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
388 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi).collect();
389 let cum = cumulative_trapz(&y, &x);
390 let total = trapz(&y, &x);
391 assert!(
392 (cum[m - 1] - total).abs() < 1e-12,
393 "Final cumulative value should match trapz"
394 );
395 }
396
397 #[test]
400 fn test_linear_interp_boundary_clamp() {
401 let x = vec![0.0, 0.5, 1.0];
402 let y = vec![10.0, 20.0, 30.0];
403 assert!((linear_interp(&x, &y, -1.0) - 10.0).abs() < 1e-12);
404 assert!((linear_interp(&x, &y, 2.0) - 30.0).abs() < 1e-12);
405 assert!((linear_interp(&x, &y, 0.25) - 15.0).abs() < 1e-12);
406 }
407
408 #[test]
411 fn test_gradient_uniform_linear() {
412 let m = 50;
414 let h = 1.0 / (m - 1) as f64;
415 let y: Vec<f64> = (0..m).map(|i| 3.0 * i as f64 * h).collect();
416 let g = gradient_uniform(&y, h);
417 for i in 0..m {
418 assert!(
419 (g[i] - 3.0).abs() < 1e-10,
420 "gradient of 3x should be 3 at i={i}, got {}",
421 g[i]
422 );
423 }
424 }
425}