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> {
51 let n = argvals.len();
52 if n < 2 {
53 return vec![1.0; n];
54 }
55
56 let mut weights = vec![0.0; n];
57
58 if n == 2 {
59 let h = argvals[1] - argvals[0];
61 weights[0] = h / 2.0;
62 weights[1] = h / 2.0;
63 return weights;
64 }
65
66 for i in 0..n {
68 if i == 0 {
69 weights[i] = (argvals[1] - argvals[0]) / 2.0;
70 } else if i == n - 1 {
71 weights[i] = (argvals[n - 1] - argvals[n - 2]) / 2.0;
72 } else {
73 weights[i] = (argvals[i + 1] - argvals[i - 1]) / 2.0;
74 }
75 }
76
77 weights
78}
79
80pub fn simpsons_weights_2d(argvals_s: &[f64], argvals_t: &[f64]) -> Vec<f64> {
91 let weights_s = simpsons_weights(argvals_s);
92 let weights_t = simpsons_weights(argvals_t);
93 let m1 = argvals_s.len();
94 let m2 = argvals_t.len();
95
96 let mut weights = vec![0.0; m1 * m2];
97 for i in 0..m1 {
98 for j in 0..m2 {
99 weights[i * m2 + j] = weights_s[i] * weights_t[j];
100 }
101 }
102 weights
103}
104
105pub fn linear_interp(x: &[f64], y: &[f64], t: f64) -> f64 {
109 if t <= x[0] {
110 return y[0];
111 }
112 let last = x.len() - 1;
113 if t >= x[last] {
114 return y[last];
115 }
116
117 let idx = match x.binary_search_by(|v| v.partial_cmp(&t).unwrap()) {
118 Ok(i) => return y[i],
119 Err(i) => i,
120 };
121
122 let t0 = x[idx - 1];
123 let t1 = x[idx];
124 let y0 = y[idx - 1];
125 let y1 = y[idx];
126 y0 + (y1 - y0) * (t - t0) / (t1 - t0)
127}
128
129pub fn cumulative_trapz(y: &[f64], x: &[f64]) -> Vec<f64> {
131 let n = y.len();
132 let mut out = vec![0.0; n];
133 for k in 1..n {
134 out[k] = out[k - 1] + 0.5 * (y[k] + y[k - 1]) * (x[k] - x[k - 1]);
135 }
136 out
137}
138
139pub fn trapz(y: &[f64], x: &[f64]) -> f64 {
141 let mut sum = 0.0;
142 for k in 1..y.len() {
143 sum += 0.5 * (y[k] + y[k - 1]) * (x[k] - x[k - 1]);
144 }
145 sum
146}
147
148pub fn gradient_uniform(y: &[f64], h: f64) -> Vec<f64> {
150 let n = y.len();
151 let mut g = vec![0.0; n];
152 if n < 2 {
153 return g;
154 }
155 g[0] = (y[1] - y[0]) / h;
156 for i in 1..(n - 1) {
157 g[i] = (y[i + 1] - y[i - 1]) / (2.0 * h);
158 }
159 g[n - 1] = (y[n - 1] - y[n - 2]) / h;
160 g
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn test_simpsons_weights_uniform() {
169 let argvals = vec![0.0, 0.25, 0.5, 0.75, 1.0];
170 let weights = simpsons_weights(&argvals);
171 let sum: f64 = weights.iter().sum();
172 assert!((sum - 1.0).abs() < NUMERICAL_EPS);
173 }
174
175 #[test]
176 fn test_simpsons_weights_2d() {
177 let argvals_s = vec![0.0, 0.5, 1.0];
178 let argvals_t = vec![0.0, 0.5, 1.0];
179 let weights = simpsons_weights_2d(&argvals_s, &argvals_t);
180 let sum: f64 = weights.iter().sum();
181 assert!((sum - 1.0).abs() < NUMERICAL_EPS);
182 }
183
184 #[test]
185 fn test_extract_curves() {
186 let data = vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0];
189 let mat = crate::matrix::FdMatrix::from_column_major(data, 2, 3).unwrap();
190 let curves = extract_curves(&mat);
191 assert_eq!(curves.len(), 2);
192 assert_eq!(curves[0], vec![1.0, 2.0, 3.0]);
193 assert_eq!(curves[1], vec![4.0, 5.0, 6.0]);
194 }
195
196 #[test]
197 fn test_l2_distance_identical() {
198 let curve = vec![1.0, 2.0, 3.0];
199 let weights = vec![0.25, 0.5, 0.25];
200 let dist = l2_distance(&curve, &curve, &weights);
201 assert!(dist.abs() < NUMERICAL_EPS);
202 }
203
204 #[test]
205 fn test_l2_distance_different() {
206 let curve1 = vec![0.0, 0.0, 0.0];
207 let curve2 = vec![1.0, 1.0, 1.0];
208 let weights = vec![0.25, 0.5, 0.25]; let dist = l2_distance(&curve1, &curve2, &weights);
210 assert!((dist - 1.0).abs() < NUMERICAL_EPS);
212 }
213
214 #[test]
215 fn test_n1_weights() {
216 let w = simpsons_weights(&[0.5]);
218 assert_eq!(w.len(), 1);
219 assert!((w[0] - 1.0).abs() < 1e-12);
220 }
221
222 #[test]
223 fn test_n2_weights() {
224 let w = simpsons_weights(&[0.0, 1.0]);
225 assert_eq!(w.len(), 2);
226 assert!((w[0] - 0.5).abs() < 1e-12);
228 assert!((w[1] - 0.5).abs() < 1e-12);
229 }
230
231 #[test]
232 fn test_mismatched_l2_distance() {
233 let a = vec![1.0, 2.0, 3.0];
235 let b = vec![1.0, 2.0, 3.0];
236 let w = vec![0.5, 0.5, 0.5];
237 let d = l2_distance(&a, &b, &w);
238 assert!(d.abs() < 1e-12, "Same vectors should have zero distance");
239 }
240
241 #[test]
244 fn test_trapz_sine() {
245 let m = 1000;
247 let x: Vec<f64> = (0..m)
248 .map(|i| std::f64::consts::PI * i as f64 / (m - 1) as f64)
249 .collect();
250 let y: Vec<f64> = x.iter().map(|&xi| xi.sin()).collect();
251 let result = trapz(&y, &x);
252 assert!(
253 (result - 2.0).abs() < 1e-4,
254 "∫ sin(x) dx over [0,π] should be ~2, got {result}"
255 );
256 }
257
258 #[test]
261 fn test_cumulative_trapz_matches_final() {
262 let m = 100;
263 let x: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
264 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi).collect();
265 let cum = cumulative_trapz(&y, &x);
266 let total = trapz(&y, &x);
267 assert!(
268 (cum[m - 1] - total).abs() < 1e-12,
269 "Final cumulative value should match trapz"
270 );
271 }
272
273 #[test]
276 fn test_linear_interp_boundary_clamp() {
277 let x = vec![0.0, 0.5, 1.0];
278 let y = vec![10.0, 20.0, 30.0];
279 assert!((linear_interp(&x, &y, -1.0) - 10.0).abs() < 1e-12);
280 assert!((linear_interp(&x, &y, 2.0) - 30.0).abs() < 1e-12);
281 assert!((linear_interp(&x, &y, 0.25) - 15.0).abs() < 1e-12);
282 }
283
284 #[test]
287 fn test_gradient_uniform_linear() {
288 let m = 50;
290 let h = 1.0 / (m - 1) as f64;
291 let y: Vec<f64> = (0..m).map(|i| 3.0 * i as f64 * h).collect();
292 let g = gradient_uniform(&y, h);
293 for i in 0..m {
294 assert!(
295 (g[i] - 3.0).abs() < 1e-10,
296 "gradient of 3x should be 3 at i={i}, got {}",
297 g[i]
298 );
299 }
300 }
301}