1use crate::helpers::{simpsons_weights, NUMERICAL_EPS};
14use crate::matrix::FdMatrix;
15use crate::FdarError;
16
17fn validate_shapes(
21 y_true: &FdMatrix,
22 y_pred: &FdMatrix,
23 argvals: &[f64],
24) -> Result<(usize, usize), FdarError> {
25 let (n, m) = y_true.shape();
26 if y_pred.shape() != (n, m) {
27 return Err(FdarError::InvalidDimension {
28 parameter: "y_pred",
29 expected: format!("({n}, {m})"),
30 actual: format!("{:?}", y_pred.shape()),
31 });
32 }
33 if argvals.len() != m {
34 return Err(FdarError::InvalidDimension {
35 parameter: "argvals",
36 expected: format!("{m}"),
37 actual: format!("{}", argvals.len()),
38 });
39 }
40 if n == 0 || m < 2 {
41 return Err(FdarError::InvalidDimension {
42 parameter: "data",
43 expected: "n >= 1 and m >= 2".to_string(),
44 actual: format!("n={n}, m={m}"),
45 });
46 }
47 Ok((n, m))
48}
49
50pub fn functional_mae(
60 y_true: &FdMatrix,
61 y_pred: &FdMatrix,
62 argvals: &[f64],
63) -> Result<f64, FdarError> {
64 let (n, m) = validate_shapes(y_true, y_pred, argvals)?;
65 let weights = simpsons_weights(argvals);
66 let mut total = 0.0_f64;
67 for i in 0..n {
68 for j in 0..m {
69 total += (y_true[(i, j)] - y_pred[(i, j)]).abs() * weights[j];
70 }
71 }
72 Ok(total / n as f64)
73}
74
75pub fn functional_mse(
85 y_true: &FdMatrix,
86 y_pred: &FdMatrix,
87 argvals: &[f64],
88) -> Result<f64, FdarError> {
89 let (n, m) = validate_shapes(y_true, y_pred, argvals)?;
90 let weights = simpsons_weights(argvals);
91 let mut total = 0.0_f64;
92 for i in 0..n {
93 for j in 0..m {
94 let diff = y_true[(i, j)] - y_pred[(i, j)];
95 total += diff * diff * weights[j];
96 }
97 }
98 Ok(total / n as f64)
99}
100
101pub fn functional_mape(
112 y_true: &FdMatrix,
113 y_pred: &FdMatrix,
114 argvals: &[f64],
115) -> Result<f64, FdarError> {
116 let (n, m) = validate_shapes(y_true, y_pred, argvals)?;
117 for i in 0..n {
119 for j in 0..m {
120 if y_true[(i, j)].abs() < NUMERICAL_EPS {
121 return Err(FdarError::InvalidParameter {
122 parameter: "y_true",
123 message: format!(
124 "MAPE is undefined when y_true contains values near zero \
125 (found |y_true[{i},{j}]| = {} < NUMERICAL_EPS)",
126 y_true[(i, j)].abs()
127 ),
128 });
129 }
130 }
131 }
132 let weights = simpsons_weights(argvals);
133 let mut total = 0.0_f64;
134 for i in 0..n {
135 for j in 0..m {
136 let pct_err = (y_true[(i, j)] - y_pred[(i, j)]).abs() / y_true[(i, j)].abs();
137 total += pct_err * weights[j];
138 }
139 }
140 Ok(total / n as f64)
141}
142
143pub fn functional_msle(
157 y_true: &FdMatrix,
158 y_pred: &FdMatrix,
159 argvals: &[f64],
160) -> Result<f64, FdarError> {
161 let (n, m) = validate_shapes(y_true, y_pred, argvals)?;
162 let threshold = -1.0 + NUMERICAL_EPS;
164 for i in 0..n {
165 for j in 0..m {
166 if y_true[(i, j)] <= threshold {
167 return Err(FdarError::InvalidParameter {
168 parameter: "y_true",
169 message: format!(
170 "MSLE requires y_true > -1; found y_true[{i},{j}] = {}",
171 y_true[(i, j)]
172 ),
173 });
174 }
175 if y_pred[(i, j)] <= threshold {
176 return Err(FdarError::InvalidParameter {
177 parameter: "y_pred",
178 message: format!(
179 "MSLE requires y_pred > -1; found y_pred[{i},{j}] = {}",
180 y_pred[(i, j)]
181 ),
182 });
183 }
184 }
185 }
186 let weights = simpsons_weights(argvals);
187 let mut total = 0.0_f64;
188 for i in 0..n {
189 for j in 0..m {
190 let log_diff = f64::ln_1p(y_true[(i, j)]) - f64::ln_1p(y_pred[(i, j)]);
191 total += log_diff.powi(2) * weights[j];
192 }
193 }
194 Ok(total / n as f64)
195}
196
197pub fn functional_explained_variance(
219 y_true: &FdMatrix,
220 y_pred: &FdMatrix,
221 argvals: &[f64],
222) -> Result<f64, FdarError> {
223 let (n, m) = validate_shapes(y_true, y_pred, argvals)?;
224 let weights = simpsons_weights(argvals);
225 let domain_len: f64 = weights.iter().sum();
226
227 let mut ev_sum = 0.0_f64;
228 for i in 0..n {
229 let mut res_sum = 0.0_f64;
231 let mut true_sum = 0.0_f64;
232 for j in 0..m {
233 let residual = y_true[(i, j)] - y_pred[(i, j)];
234 res_sum += residual * weights[j];
235 true_sum += y_true[(i, j)] * weights[j];
236 }
237 let mean_res = if domain_len > NUMERICAL_EPS {
239 res_sum / domain_len
240 } else {
241 0.0
242 };
243 let mean_true = if domain_len > NUMERICAL_EPS {
244 true_sum / domain_len
245 } else {
246 0.0
247 };
248 let mut ss_res = 0.0_f64;
250 let mut ss_tot = 0.0_f64;
251 for j in 0..m {
252 let res_centered = (y_true[(i, j)] - y_pred[(i, j)]) - mean_res;
253 let true_centered = y_true[(i, j)] - mean_true;
254 ss_res += res_centered.powi(2) * weights[j];
255 ss_tot += true_centered.powi(2) * weights[j];
256 }
257 let ev_i = if ss_tot < NUMERICAL_EPS {
263 if ss_res <= ss_tot * (1.0 + 1e-6) {
264 1.0
265 } else {
266 0.0
267 }
268 } else {
269 1.0 - ss_res / ss_tot
270 };
271 ev_sum += ev_i;
272 }
273 Ok(ev_sum / n as f64)
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 use crate::test_helpers::uniform_grid;
281
282 fn mat_from_rows(rows: &[Vec<f64>]) -> FdMatrix {
284 let n = rows.len();
285 let m = rows[0].len();
286 let mut col_major = vec![0.0_f64; n * m];
287 for (i, row) in rows.iter().enumerate() {
288 for (j, &v) in row.iter().enumerate() {
289 col_major[i + j * n] = v;
290 }
291 }
292 FdMatrix::from_column_major(col_major, n, m).unwrap()
293 }
294
295 #[test]
298 fn test_functional_mae_constant_error() {
299 let c = 2.0_f64;
303 let argvals = uniform_grid(5); let y_true = mat_from_rows(&[vec![0.0; 5]]);
305 let y_pred = mat_from_rows(&[vec![c; 5]]);
306 let mae = functional_mae(&y_true, &y_pred, &argvals).unwrap();
307 assert!((mae - c).abs() < 1e-10, "mae={mae}, expected {c}");
309 }
310
311 #[test]
312 fn test_functional_mae_multi_curve() {
313 let argvals = uniform_grid(5);
315 let y_true = mat_from_rows(&[vec![0.0; 5], vec![0.0; 5]]);
316 let y_pred = mat_from_rows(&[vec![1.0; 5], vec![2.0; 5]]);
317 let mae = functional_mae(&y_true, &y_pred, &argvals).unwrap();
318 assert!((mae - 1.5).abs() < 1e-10, "mae={mae}, expected 1.5");
320 }
321
322 #[test]
323 fn test_functional_mae_shape_mismatch_y_pred() {
324 let argvals = uniform_grid(5);
325 let y_true = mat_from_rows(&[vec![0.0; 5]]);
326 let y_pred = mat_from_rows(&[vec![0.0; 4]]); let result = functional_mae(&y_true, &y_pred, &argvals);
328 assert!(matches!(
329 result,
330 Err(FdarError::InvalidDimension {
331 parameter: "y_pred",
332 ..
333 })
334 ));
335 }
336
337 #[test]
338 fn test_functional_mae_shape_mismatch_argvals() {
339 let argvals = uniform_grid(4); let y_true = mat_from_rows(&[vec![0.0; 5]]);
341 let y_pred = mat_from_rows(&[vec![0.0; 5]]);
342 let result = functional_mae(&y_true, &y_pred, &argvals);
343 assert!(matches!(
344 result,
345 Err(FdarError::InvalidDimension {
346 parameter: "argvals",
347 ..
348 })
349 ));
350 }
351
352 #[test]
355 fn test_functional_mse_constant_error() {
356 let c = 3.0_f64;
359 let argvals = uniform_grid(5);
360 let y_true = mat_from_rows(&[vec![0.0; 5]]);
361 let y_pred = mat_from_rows(&[vec![c; 5]]);
362 let mse = functional_mse(&y_true, &y_pred, &argvals).unwrap();
363 assert!((mse - c * c).abs() < 1e-10, "mse={mse}, expected {}", c * c);
364 }
365
366 #[test]
367 fn test_functional_mse_zero_error() {
368 let argvals = uniform_grid(5);
370 let y_true = mat_from_rows(&[vec![1.0, 2.0, 3.0, 4.0, 5.0]]);
371 let y_pred = y_true.clone();
372 let mse = functional_mse(&y_true, &y_pred, &argvals).unwrap();
373 assert!(mse.abs() < 1e-14, "mse={mse}, expected 0");
374 }
375
376 #[test]
379 fn test_functional_mape_constant_error() {
380 let argvals = uniform_grid(5);
383 let y_true = mat_from_rows(&[vec![4.0; 5]]);
384 let y_pred = mat_from_rows(&[vec![5.0; 5]]);
385 let mape = functional_mape(&y_true, &y_pred, &argvals).unwrap();
386 assert!((mape - 0.25).abs() < 1e-10, "mape={mape}, expected 0.25");
387 }
388
389 #[test]
390 fn test_functional_mape_zero_y_true() {
391 let argvals = uniform_grid(5);
393 let y_true = mat_from_rows(&[vec![0.0; 5]]);
394 let y_pred = mat_from_rows(&[vec![1.0; 5]]);
395 let result = functional_mape(&y_true, &y_pred, &argvals);
396 assert!(matches!(
397 result,
398 Err(FdarError::InvalidParameter {
399 parameter: "y_true",
400 ..
401 })
402 ));
403 }
404
405 #[test]
408 fn test_functional_msle_constant() {
409 let argvals = uniform_grid(5);
411 let y_true = mat_from_rows(&[vec![1.0; 5]]);
412 let y_pred = y_true.clone();
413 let msle = functional_msle(&y_true, &y_pred, &argvals).unwrap();
414 assert!(msle.abs() < 1e-14, "msle={msle}, expected 0");
415 }
416
417 #[test]
418 fn test_functional_msle_hand_computed() {
419 let argvals = uniform_grid(5);
423 let y_true = mat_from_rows(&[vec![3.0; 5]]);
424 let y_pred = mat_from_rows(&[vec![1.0; 5]]);
425 let msle = functional_msle(&y_true, &y_pred, &argvals).unwrap();
426 let expected = f64::ln(2.0).powi(2);
427 assert!(
428 (msle - expected).abs() < 1e-10,
429 "msle={msle}, expected={expected}"
430 );
431 }
432
433 #[test]
434 fn test_functional_msle_domain_y_true() {
435 let argvals = uniform_grid(5);
437 let y_true = mat_from_rows(&[vec![-1.5; 5]]);
438 let y_pred = mat_from_rows(&[vec![1.0; 5]]);
439 let result = functional_msle(&y_true, &y_pred, &argvals);
440 assert!(matches!(
441 result,
442 Err(FdarError::InvalidParameter {
443 parameter: "y_true",
444 ..
445 })
446 ));
447 }
448
449 #[test]
450 fn test_functional_msle_domain_y_pred() {
451 let argvals = uniform_grid(5);
453 let y_true = mat_from_rows(&[vec![1.0; 5]]);
454 let y_pred = mat_from_rows(&[vec![-2.0; 5]]);
455 let result = functional_msle(&y_true, &y_pred, &argvals);
456 assert!(matches!(
457 result,
458 Err(FdarError::InvalidParameter {
459 parameter: "y_pred",
460 ..
461 })
462 ));
463 }
464
465 #[test]
468 fn test_explained_variance_perfect() {
469 let argvals = uniform_grid(5);
471 let y_true = mat_from_rows(&[vec![1.0, 2.0, 3.0, 4.0, 5.0]]);
472 let y_pred = y_true.clone();
473 let ev = functional_explained_variance(&y_true, &y_pred, &argvals).unwrap();
474 assert!((ev - 1.0).abs() < 1e-10, "ev={ev}, expected 1.0");
475 }
476
477 #[test]
478 fn test_explained_variance_constant_true() {
479 let argvals = uniform_grid(5);
481 let y_true = mat_from_rows(&[vec![3.0; 5]]);
482 let y_pred = y_true.clone();
483 let ev = functional_explained_variance(&y_true, &y_pred, &argvals).unwrap();
484 assert!((ev - 1.0).abs() < 1e-10, "ev={ev}, expected 1.0");
485 }
486
487 #[test]
488 fn test_explained_variance_shape_mismatch() {
489 let argvals = uniform_grid(5);
490 let y_true = mat_from_rows(&[vec![1.0; 5]]);
491 let y_pred = mat_from_rows(&[vec![0.0; 4]]); let result = functional_explained_variance(&y_true, &y_pred, &argvals);
493 assert!(matches!(
494 result,
495 Err(FdarError::InvalidDimension {
496 parameter: "y_pred",
497 ..
498 })
499 ));
500 }
501
502 #[test]
507 fn test_explained_variance_constant_true_perturbed_pred() {
508 let m = 100_usize;
512 let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
513 let y_true_row: Vec<f64> = vec![5.0_f64; m];
514 let y_pred_row: Vec<f64> = argvals
515 .iter()
516 .map(|&t| 5.0 + 1e-6 * (t * std::f64::consts::PI * 2.0).sin())
517 .collect();
518 let y_true = mat_from_rows(&[y_true_row]);
519 let y_pred = mat_from_rows(&[y_pred_row]);
520 let ev = functional_explained_variance(&y_true, &y_pred, &argvals).unwrap();
521 assert!(
522 ev <= 0.0,
523 "EV for constant true + oscillating pred must be <= 0.0, got {ev}"
524 );
525 }
526}