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 let n_intervals = n - 1;
77 if n_intervals % 2 == 0 {
78 weights[0] = h0 / 3.0;
81 weights[n - 1] = h0 / 3.0;
82 for i in 1..n - 1 {
83 weights[i] = if i % 2 == 1 {
84 4.0 * h0 / 3.0
85 } else {
86 2.0 * h0 / 3.0
87 };
88 }
89 } else {
90 let n_simp = n - 1; weights[0] = h0 / 3.0;
94 weights[n_simp - 1] = h0 / 3.0;
95 for i in 1..n_simp - 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 weights[n_simp - 1] += h0 / 2.0;
104 weights[n - 1] += h0 / 2.0;
105 }
106 } else {
107 let n_intervals = n - 1;
109 let n_pairs = n_intervals / 2;
110
111 for k in 0..n_pairs {
112 let i0 = 2 * k;
113 let i1 = i0 + 1;
114 let i2 = i0 + 2;
115 let h1 = argvals[i1] - argvals[i0];
116 let h2 = argvals[i2] - argvals[i1];
117 let h_sum = h1 + h2;
118
119 weights[i0] += (2.0 * h1 - h2) * h_sum / (6.0 * h1);
121 weights[i1] += h_sum * h_sum * h_sum / (6.0 * h1 * h2);
122 weights[i2] += (2.0 * h2 - h1) * h_sum / (6.0 * h2);
123 }
124
125 if n_intervals % 2 == 1 {
127 let h_last = argvals[n - 1] - argvals[n - 2];
128 weights[n - 2] += h_last / 2.0;
129 weights[n - 1] += h_last / 2.0;
130 }
131 }
132
133 weights
134}
135
136pub fn simpsons_weights_2d(argvals_s: &[f64], argvals_t: &[f64]) -> Vec<f64> {
147 let weights_s = simpsons_weights(argvals_s);
148 let weights_t = simpsons_weights(argvals_t);
149 let m1 = argvals_s.len();
150 let m2 = argvals_t.len();
151
152 let mut weights = vec![0.0; m1 * m2];
153 for i in 0..m1 {
154 for j in 0..m2 {
155 weights[i * m2 + j] = weights_s[i] * weights_t[j];
156 }
157 }
158 weights
159}
160
161pub fn linear_interp(x: &[f64], y: &[f64], t: f64) -> f64 {
165 if t <= x[0] {
166 return y[0];
167 }
168 let last = x.len() - 1;
169 if t >= x[last] {
170 return y[last];
171 }
172
173 let idx = match x.binary_search_by(|v| v.partial_cmp(&t).unwrap()) {
174 Ok(i) => return y[i],
175 Err(i) => i,
176 };
177
178 let t0 = x[idx - 1];
179 let t1 = x[idx];
180 let y0 = y[idx - 1];
181 let y1 = y[idx];
182 y0 + (y1 - y0) * (t - t0) / (t1 - t0)
183}
184
185pub fn cumulative_trapz(y: &[f64], x: &[f64]) -> Vec<f64> {
190 let n = y.len();
191 let mut out = vec![0.0; n];
192 if n < 2 {
193 return out;
194 }
195
196 let mut k = 1;
198 while k + 1 < n {
199 let h1 = x[k] - x[k - 1];
200 let h2 = x[k + 1] - x[k];
201 let h_sum = h1 + h2;
202
203 let integral = h_sum / 6.0
205 * (y[k - 1] * (2.0 * h1 - h2) / h1
206 + y[k] * h_sum * h_sum / (h1 * h2)
207 + y[k + 1] * (2.0 * h2 - h1) / h2);
208
209 out[k] = out[k - 1] + {
210 0.5 * (y[k] + y[k - 1]) * h1
212 };
213 out[k + 1] = out[k - 1] + integral;
214 k += 2;
215 }
216
217 if k < n {
219 out[k] = out[k - 1] + 0.5 * (y[k] + y[k - 1]) * (x[k] - x[k - 1]);
220 }
221
222 out
223}
224
225pub fn trapz(y: &[f64], x: &[f64]) -> f64 {
227 let mut sum = 0.0;
228 for k in 1..y.len() {
229 sum += 0.5 * (y[k] + y[k - 1]) * (x[k] - x[k - 1]);
230 }
231 sum
232}
233
234pub fn gradient_uniform(y: &[f64], h: f64) -> Vec<f64> {
241 let n = y.len();
242 let mut g = vec![0.0; n];
243 if n < 2 {
244 return g;
245 }
246 if n == 2 {
247 g[0] = (y[1] - y[0]) / h;
248 g[1] = (y[1] - y[0]) / h;
249 return g;
250 }
251 if n == 3 {
252 g[0] = (-3.0 * y[0] + 4.0 * y[1] - y[2]) / (2.0 * h);
253 g[1] = (y[2] - y[0]) / (2.0 * h);
254 g[2] = (y[0] - 4.0 * y[1] + 3.0 * y[2]) / (2.0 * h);
255 return g;
256 }
257 if n == 4 {
258 g[0] = (-3.0 * y[0] + 4.0 * y[1] - y[2]) / (2.0 * h);
259 g[1] = (y[2] - y[0]) / (2.0 * h);
260 g[2] = (y[3] - y[1]) / (2.0 * h);
261 g[3] = (y[1] - 4.0 * y[2] + 3.0 * y[3]) / (2.0 * h);
262 return g;
263 }
264
265 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);
268 g[1] = (-3.0 * y[0] - 10.0 * y[1] + 18.0 * y[2] - 6.0 * y[3] + y[4]) / (12.0 * h);
269
270 for i in 2..n - 2 {
272 g[i] = (-y[i + 2] + 8.0 * y[i + 1] - 8.0 * y[i - 1] + y[i - 2]) / (12.0 * h);
273 }
274
275 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])
277 / (12.0 * h);
278 g[n - 1] = (3.0 * y[n - 5] - 16.0 * y[n - 4] + 36.0 * y[n - 3] - 48.0 * y[n - 2]
279 + 25.0 * y[n - 1])
280 / (12.0 * h);
281 g
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn test_simpsons_weights_uniform() {
290 let argvals = vec![0.0, 0.25, 0.5, 0.75, 1.0];
291 let weights = simpsons_weights(&argvals);
292 let sum: f64 = weights.iter().sum();
293 assert!((sum - 1.0).abs() < NUMERICAL_EPS);
294 }
295
296 #[test]
297 fn test_simpsons_weights_2d() {
298 let argvals_s = vec![0.0, 0.5, 1.0];
299 let argvals_t = vec![0.0, 0.5, 1.0];
300 let weights = simpsons_weights_2d(&argvals_s, &argvals_t);
301 let sum: f64 = weights.iter().sum();
302 assert!((sum - 1.0).abs() < NUMERICAL_EPS);
303 }
304
305 #[test]
306 fn test_extract_curves() {
307 let data = vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0];
310 let mat = crate::matrix::FdMatrix::from_column_major(data, 2, 3).unwrap();
311 let curves = extract_curves(&mat);
312 assert_eq!(curves.len(), 2);
313 assert_eq!(curves[0], vec![1.0, 2.0, 3.0]);
314 assert_eq!(curves[1], vec![4.0, 5.0, 6.0]);
315 }
316
317 #[test]
318 fn test_l2_distance_identical() {
319 let curve = vec![1.0, 2.0, 3.0];
320 let weights = vec![0.25, 0.5, 0.25];
321 let dist = l2_distance(&curve, &curve, &weights);
322 assert!(dist.abs() < NUMERICAL_EPS);
323 }
324
325 #[test]
326 fn test_l2_distance_different() {
327 let curve1 = vec![0.0, 0.0, 0.0];
328 let curve2 = vec![1.0, 1.0, 1.0];
329 let weights = vec![0.25, 0.5, 0.25]; let dist = l2_distance(&curve1, &curve2, &weights);
331 assert!((dist - 1.0).abs() < NUMERICAL_EPS);
333 }
334
335 #[test]
336 fn test_n1_weights() {
337 let w = simpsons_weights(&[0.5]);
339 assert_eq!(w.len(), 1);
340 assert!((w[0] - 1.0).abs() < 1e-12);
341 }
342
343 #[test]
344 fn test_n2_weights() {
345 let w = simpsons_weights(&[0.0, 1.0]);
346 assert_eq!(w.len(), 2);
347 assert!((w[0] - 0.5).abs() < 1e-12);
349 assert!((w[1] - 0.5).abs() < 1e-12);
350 }
351
352 #[test]
353 fn test_mismatched_l2_distance() {
354 let a = vec![1.0, 2.0, 3.0];
356 let b = vec![1.0, 2.0, 3.0];
357 let w = vec![0.5, 0.5, 0.5];
358 let d = l2_distance(&a, &b, &w);
359 assert!(d.abs() < 1e-12, "Same vectors should have zero distance");
360 }
361
362 #[test]
365 fn test_trapz_sine() {
366 let m = 1000;
368 let x: Vec<f64> = (0..m)
369 .map(|i| std::f64::consts::PI * i as f64 / (m - 1) as f64)
370 .collect();
371 let y: Vec<f64> = x.iter().map(|&xi| xi.sin()).collect();
372 let result = trapz(&y, &x);
373 assert!(
374 (result - 2.0).abs() < 1e-4,
375 "∫ sin(x) dx over [0,π] should be ~2, got {result}"
376 );
377 }
378
379 #[test]
382 fn test_cumulative_trapz_matches_final() {
383 let m = 100;
384 let x: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
385 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi).collect();
386 let cum = cumulative_trapz(&y, &x);
387 let total = trapz(&y, &x);
388 assert!(
389 (cum[m - 1] - total).abs() < 1e-12,
390 "Final cumulative value should match trapz"
391 );
392 }
393
394 #[test]
397 fn test_linear_interp_boundary_clamp() {
398 let x = vec![0.0, 0.5, 1.0];
399 let y = vec![10.0, 20.0, 30.0];
400 assert!((linear_interp(&x, &y, -1.0) - 10.0).abs() < 1e-12);
401 assert!((linear_interp(&x, &y, 2.0) - 30.0).abs() < 1e-12);
402 assert!((linear_interp(&x, &y, 0.25) - 15.0).abs() < 1e-12);
403 }
404
405 #[test]
408 fn test_gradient_uniform_linear() {
409 let m = 50;
411 let h = 1.0 / (m - 1) as f64;
412 let y: Vec<f64> = (0..m).map(|i| 3.0 * i as f64 * h).collect();
413 let g = gradient_uniform(&y, h);
414 for i in 0..m {
415 assert!(
416 (g[i] - 3.0).abs() < 1e-10,
417 "gradient of 3x should be 3 at i={i}, got {}",
418 g[i]
419 );
420 }
421 }
422}