use crate::helpers::{simpsons_weights, NUMERICAL_EPS};
use crate::matrix::FdMatrix;
use crate::FdarError;
fn validate_shapes(
y_true: &FdMatrix,
y_pred: &FdMatrix,
argvals: &[f64],
) -> Result<(usize, usize), FdarError> {
let (n, m) = y_true.shape();
if y_pred.shape() != (n, m) {
return Err(FdarError::InvalidDimension {
parameter: "y_pred",
expected: format!("({n}, {m})"),
actual: format!("{:?}", y_pred.shape()),
});
}
if argvals.len() != m {
return Err(FdarError::InvalidDimension {
parameter: "argvals",
expected: format!("{m}"),
actual: format!("{}", argvals.len()),
});
}
if n == 0 || m < 2 {
return Err(FdarError::InvalidDimension {
parameter: "data",
expected: "n >= 1 and m >= 2".to_string(),
actual: format!("n={n}, m={m}"),
});
}
Ok((n, m))
}
pub fn functional_mae(
y_true: &FdMatrix,
y_pred: &FdMatrix,
argvals: &[f64],
) -> Result<f64, FdarError> {
let (n, m) = validate_shapes(y_true, y_pred, argvals)?;
let weights = simpsons_weights(argvals);
let mut total = 0.0_f64;
for i in 0..n {
for j in 0..m {
total += (y_true[(i, j)] - y_pred[(i, j)]).abs() * weights[j];
}
}
Ok(total / n as f64)
}
pub fn functional_mse(
y_true: &FdMatrix,
y_pred: &FdMatrix,
argvals: &[f64],
) -> Result<f64, FdarError> {
let (n, m) = validate_shapes(y_true, y_pred, argvals)?;
let weights = simpsons_weights(argvals);
let mut total = 0.0_f64;
for i in 0..n {
for j in 0..m {
let diff = y_true[(i, j)] - y_pred[(i, j)];
total += diff * diff * weights[j];
}
}
Ok(total / n as f64)
}
pub fn functional_mape(
y_true: &FdMatrix,
y_pred: &FdMatrix,
argvals: &[f64],
) -> Result<f64, FdarError> {
let (n, m) = validate_shapes(y_true, y_pred, argvals)?;
for i in 0..n {
for j in 0..m {
if y_true[(i, j)].abs() < NUMERICAL_EPS {
return Err(FdarError::InvalidParameter {
parameter: "y_true",
message: format!(
"MAPE is undefined when y_true contains values near zero \
(found |y_true[{i},{j}]| = {} < NUMERICAL_EPS)",
y_true[(i, j)].abs()
),
});
}
}
}
let weights = simpsons_weights(argvals);
let mut total = 0.0_f64;
for i in 0..n {
for j in 0..m {
let pct_err = (y_true[(i, j)] - y_pred[(i, j)]).abs() / y_true[(i, j)].abs();
total += pct_err * weights[j];
}
}
Ok(total / n as f64)
}
pub fn functional_msle(
y_true: &FdMatrix,
y_pred: &FdMatrix,
argvals: &[f64],
) -> Result<f64, FdarError> {
let (n, m) = validate_shapes(y_true, y_pred, argvals)?;
let threshold = -1.0 + NUMERICAL_EPS;
for i in 0..n {
for j in 0..m {
if y_true[(i, j)] <= threshold {
return Err(FdarError::InvalidParameter {
parameter: "y_true",
message: format!(
"MSLE requires y_true > -1; found y_true[{i},{j}] = {}",
y_true[(i, j)]
),
});
}
if y_pred[(i, j)] <= threshold {
return Err(FdarError::InvalidParameter {
parameter: "y_pred",
message: format!(
"MSLE requires y_pred > -1; found y_pred[{i},{j}] = {}",
y_pred[(i, j)]
),
});
}
}
}
let weights = simpsons_weights(argvals);
let mut total = 0.0_f64;
for i in 0..n {
for j in 0..m {
let log_diff = f64::ln_1p(y_true[(i, j)]) - f64::ln_1p(y_pred[(i, j)]);
total += log_diff.powi(2) * weights[j];
}
}
Ok(total / n as f64)
}
pub fn functional_explained_variance(
y_true: &FdMatrix,
y_pred: &FdMatrix,
argvals: &[f64],
) -> Result<f64, FdarError> {
let (n, m) = validate_shapes(y_true, y_pred, argvals)?;
let weights = simpsons_weights(argvals);
let domain_len: f64 = weights.iter().sum();
let mut ev_sum = 0.0_f64;
for i in 0..n {
let mut res_sum = 0.0_f64;
let mut true_sum = 0.0_f64;
for j in 0..m {
let residual = y_true[(i, j)] - y_pred[(i, j)];
res_sum += residual * weights[j];
true_sum += y_true[(i, j)] * weights[j];
}
let mean_res = if domain_len > NUMERICAL_EPS {
res_sum / domain_len
} else {
0.0
};
let mean_true = if domain_len > NUMERICAL_EPS {
true_sum / domain_len
} else {
0.0
};
let mut ss_res = 0.0_f64;
let mut ss_tot = 0.0_f64;
for j in 0..m {
let res_centered = (y_true[(i, j)] - y_pred[(i, j)]) - mean_res;
let true_centered = y_true[(i, j)] - mean_true;
ss_res += res_centered.powi(2) * weights[j];
ss_tot += true_centered.powi(2) * weights[j];
}
let ev_i = if ss_tot < NUMERICAL_EPS {
if ss_res <= ss_tot * (1.0 + 1e-6) {
1.0
} else {
0.0
}
} else {
1.0 - ss_res / ss_tot
};
ev_sum += ev_i;
}
Ok(ev_sum / n as f64)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::uniform_grid;
fn mat_from_rows(rows: &[Vec<f64>]) -> FdMatrix {
let n = rows.len();
let m = rows[0].len();
let mut col_major = vec![0.0_f64; n * m];
for (i, row) in rows.iter().enumerate() {
for (j, &v) in row.iter().enumerate() {
col_major[i + j * n] = v;
}
}
FdMatrix::from_column_major(col_major, n, m).unwrap()
}
#[test]
fn test_functional_mae_constant_error() {
let c = 2.0_f64;
let argvals = uniform_grid(5); let y_true = mat_from_rows(&[vec![0.0; 5]]);
let y_pred = mat_from_rows(&[vec![c; 5]]);
let mae = functional_mae(&y_true, &y_pred, &argvals).unwrap();
assert!((mae - c).abs() < 1e-10, "mae={mae}, expected {c}");
}
#[test]
fn test_functional_mae_multi_curve() {
let argvals = uniform_grid(5);
let y_true = mat_from_rows(&[vec![0.0; 5], vec![0.0; 5]]);
let y_pred = mat_from_rows(&[vec![1.0; 5], vec![2.0; 5]]);
let mae = functional_mae(&y_true, &y_pred, &argvals).unwrap();
assert!((mae - 1.5).abs() < 1e-10, "mae={mae}, expected 1.5");
}
#[test]
fn test_functional_mae_shape_mismatch_y_pred() {
let argvals = uniform_grid(5);
let y_true = mat_from_rows(&[vec![0.0; 5]]);
let y_pred = mat_from_rows(&[vec![0.0; 4]]); let result = functional_mae(&y_true, &y_pred, &argvals);
assert!(matches!(
result,
Err(FdarError::InvalidDimension {
parameter: "y_pred",
..
})
));
}
#[test]
fn test_functional_mae_shape_mismatch_argvals() {
let argvals = uniform_grid(4); let y_true = mat_from_rows(&[vec![0.0; 5]]);
let y_pred = mat_from_rows(&[vec![0.0; 5]]);
let result = functional_mae(&y_true, &y_pred, &argvals);
assert!(matches!(
result,
Err(FdarError::InvalidDimension {
parameter: "argvals",
..
})
));
}
#[test]
fn test_functional_mse_constant_error() {
let c = 3.0_f64;
let argvals = uniform_grid(5);
let y_true = mat_from_rows(&[vec![0.0; 5]]);
let y_pred = mat_from_rows(&[vec![c; 5]]);
let mse = functional_mse(&y_true, &y_pred, &argvals).unwrap();
assert!((mse - c * c).abs() < 1e-10, "mse={mse}, expected {}", c * c);
}
#[test]
fn test_functional_mse_zero_error() {
let argvals = uniform_grid(5);
let y_true = mat_from_rows(&[vec![1.0, 2.0, 3.0, 4.0, 5.0]]);
let y_pred = y_true.clone();
let mse = functional_mse(&y_true, &y_pred, &argvals).unwrap();
assert!(mse.abs() < 1e-14, "mse={mse}, expected 0");
}
#[test]
fn test_functional_mape_constant_error() {
let argvals = uniform_grid(5);
let y_true = mat_from_rows(&[vec![4.0; 5]]);
let y_pred = mat_from_rows(&[vec![5.0; 5]]);
let mape = functional_mape(&y_true, &y_pred, &argvals).unwrap();
assert!((mape - 0.25).abs() < 1e-10, "mape={mape}, expected 0.25");
}
#[test]
fn test_functional_mape_zero_y_true() {
let argvals = uniform_grid(5);
let y_true = mat_from_rows(&[vec![0.0; 5]]);
let y_pred = mat_from_rows(&[vec![1.0; 5]]);
let result = functional_mape(&y_true, &y_pred, &argvals);
assert!(matches!(
result,
Err(FdarError::InvalidParameter {
parameter: "y_true",
..
})
));
}
#[test]
fn test_functional_msle_constant() {
let argvals = uniform_grid(5);
let y_true = mat_from_rows(&[vec![1.0; 5]]);
let y_pred = y_true.clone();
let msle = functional_msle(&y_true, &y_pred, &argvals).unwrap();
assert!(msle.abs() < 1e-14, "msle={msle}, expected 0");
}
#[test]
fn test_functional_msle_hand_computed() {
let argvals = uniform_grid(5);
let y_true = mat_from_rows(&[vec![3.0; 5]]);
let y_pred = mat_from_rows(&[vec![1.0; 5]]);
let msle = functional_msle(&y_true, &y_pred, &argvals).unwrap();
let expected = f64::ln(2.0).powi(2);
assert!(
(msle - expected).abs() < 1e-10,
"msle={msle}, expected={expected}"
);
}
#[test]
fn test_functional_msle_domain_y_true() {
let argvals = uniform_grid(5);
let y_true = mat_from_rows(&[vec![-1.5; 5]]);
let y_pred = mat_from_rows(&[vec![1.0; 5]]);
let result = functional_msle(&y_true, &y_pred, &argvals);
assert!(matches!(
result,
Err(FdarError::InvalidParameter {
parameter: "y_true",
..
})
));
}
#[test]
fn test_functional_msle_domain_y_pred() {
let argvals = uniform_grid(5);
let y_true = mat_from_rows(&[vec![1.0; 5]]);
let y_pred = mat_from_rows(&[vec![-2.0; 5]]);
let result = functional_msle(&y_true, &y_pred, &argvals);
assert!(matches!(
result,
Err(FdarError::InvalidParameter {
parameter: "y_pred",
..
})
));
}
#[test]
fn test_explained_variance_perfect() {
let argvals = uniform_grid(5);
let y_true = mat_from_rows(&[vec![1.0, 2.0, 3.0, 4.0, 5.0]]);
let y_pred = y_true.clone();
let ev = functional_explained_variance(&y_true, &y_pred, &argvals).unwrap();
assert!((ev - 1.0).abs() < 1e-10, "ev={ev}, expected 1.0");
}
#[test]
fn test_explained_variance_constant_true() {
let argvals = uniform_grid(5);
let y_true = mat_from_rows(&[vec![3.0; 5]]);
let y_pred = y_true.clone();
let ev = functional_explained_variance(&y_true, &y_pred, &argvals).unwrap();
assert!((ev - 1.0).abs() < 1e-10, "ev={ev}, expected 1.0");
}
#[test]
fn test_explained_variance_shape_mismatch() {
let argvals = uniform_grid(5);
let y_true = mat_from_rows(&[vec![1.0; 5]]);
let y_pred = mat_from_rows(&[vec![0.0; 4]]); let result = functional_explained_variance(&y_true, &y_pred, &argvals);
assert!(matches!(
result,
Err(FdarError::InvalidDimension {
parameter: "y_pred",
..
})
));
}
#[test]
fn test_explained_variance_constant_true_perturbed_pred() {
let m = 100_usize;
let argvals: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
let y_true_row: Vec<f64> = vec![5.0_f64; m];
let y_pred_row: Vec<f64> = argvals
.iter()
.map(|&t| 5.0 + 1e-6 * (t * std::f64::consts::PI * 2.0).sin())
.collect();
let y_true = mat_from_rows(&[y_true_row]);
let y_pred = mat_from_rows(&[y_pred_row]);
let ev = functional_explained_variance(&y_true, &y_pred, &argvals).unwrap();
assert!(
ev <= 0.0,
"EV for constant true + oscillating pred must be <= 0.0, got {ev}"
);
}
}