use leto::application::reduction::{
median_all, median_axis, quantile_all, quantile_axis, Interpolation,
};
use leto::{Array2, LetoError, Storage};
#[track_caller]
fn assert_close(actual: f64, expected: f64) {
assert!(
(actual - expected).abs() <= 1.0e-12 * expected.abs().max(1.0),
"actual {actual} expected {expected}"
);
}
fn leto(shape: [usize; 2], data: Vec<f64>) -> Array2<f64> {
Array2::from_shape_vec(shape, data).unwrap()
}
#[track_caller]
fn assert_storage_reason(result: leto::Result<f64>, expected: &str) {
match result {
Err(LetoError::StorageError { reason }) => assert_eq!(reason, expected),
other => panic!("expected storage error {expected:?}, got {other:?}"),
}
}
#[test]
fn quantile_all_linear_closed_form() {
let a = leto([1, 4], vec![1.0, 2.0, 3.0, 4.0]);
assert_close(quantile_all(&a, 0.5, Interpolation::Linear).unwrap(), 2.5);
assert_close(quantile_all(&a, 0.25, Interpolation::Linear).unwrap(), 1.75);
assert_close(quantile_all(&a, 0.0, Interpolation::Linear).unwrap(), 1.0);
assert_close(quantile_all(&a, 1.0, Interpolation::Linear).unwrap(), 4.0);
}
#[test]
fn quantile_all_methods_on_tie() {
let a = leto([1, 4], vec![1.0, 2.0, 3.0, 4.0]);
assert_close(quantile_all(&a, 0.5, Interpolation::Lower).unwrap(), 2.0);
assert_close(quantile_all(&a, 0.5, Interpolation::Higher).unwrap(), 3.0);
assert_close(quantile_all(&a, 0.5, Interpolation::Midpoint).unwrap(), 2.5);
assert_close(quantile_all(&a, 0.5, Interpolation::Nearest).unwrap(), 3.0);
}
#[test]
fn median_all_odd_and_even() {
let even = leto([1, 4], vec![1.0, 2.0, 3.0, 4.0]);
assert_close(median_all(&even).unwrap(), 2.5);
let odd = leto([1, 5], vec![5.0, 1.0, 3.0, 2.0, 4.0]);
assert_close(median_all(&odd).unwrap(), 3.0);
}
#[test]
fn quantile_unsorted_input() {
let a = leto([1, 5], vec![4.0, 1.0, 5.0, 2.0, 3.0]);
assert_close(quantile_all(&a, 0.25, Interpolation::Linear).unwrap(), 2.0);
}
#[test]
fn median_axis_closed_form() {
let a = leto([2, 2], vec![1.0, 2.0, 3.0, 4.0]);
let m0 = median_axis::<f64, _, 2, 1>(&a, 0).unwrap();
assert_close(m0.storage().as_slice()[0], 2.0);
assert_close(m0.storage().as_slice()[1], 3.0);
let m1 = median_axis::<f64, _, 2, 1>(&a, 1).unwrap();
assert_close(m1.storage().as_slice()[0], 1.5);
assert_close(m1.storage().as_slice()[1], 3.5);
}
#[test]
fn quantile_axis_matches_quantile_all_per_lane() {
let data: Vec<f64> = (0..12).map(f64::from).collect();
let a = leto([3, 4], data);
let q = 0.4;
let axis_q = quantile_axis::<f64, _, 2, 1>(&a, 1, q, Interpolation::Linear).unwrap();
for row in 0..3usize {
let row_data: Vec<f64> = (0..4).map(|c| f64::from((row * 4 + c) as i32)).collect();
let row_arr = leto([1, 4], row_data);
let expected = quantile_all(&row_arr, q, Interpolation::Linear).unwrap();
assert_close(axis_q.storage().as_slice()[row], expected);
}
}
#[test]
fn quantile_rejects_empty_q_range_and_nan() {
let a = leto([1, 3], vec![1.0, 2.0, 3.0]);
assert_storage_reason(
quantile_all(&a, -0.1, Interpolation::Linear),
"quantile q must be finite and within [0, 1]",
);
assert_storage_reason(
quantile_all(&a, 1.5, Interpolation::Linear),
"quantile q must be finite and within [0, 1]",
);
assert_storage_reason(
quantile_all(&a, f64::NAN, Interpolation::Linear),
"quantile q must be finite and within [0, 1]",
);
let with_nan = leto([1, 3], vec![1.0, f64::NAN, 3.0]);
assert_storage_reason(
quantile_all(&with_nan, 0.5, Interpolation::Linear),
"quantile over data containing NaN is undefined",
);
let empty: Array2<f64> = Array2::from_shape_vec([0, 0], vec![]).unwrap();
assert_storage_reason(
quantile_all(&empty, 0.5, Interpolation::Linear),
"quantile over empty array is undefined",
);
assert_storage_reason(median_all(&empty), "quantile over empty array is undefined");
}