use super::{masked_mean_pool, OpError};
use crate::autograd::{self, Tensor};
const FD_EPS: f32 = 1e-3;
const TOL: f32 = 2e-2;
fn coeff(n: usize) -> Vec<f32> {
(0..n).map(|i| 0.37 + 0.13 * (i as f32)).collect()
}
fn scalar_loss(output: &Tensor, c: &[f32]) -> Tensor {
let ct = Tensor::new(c, output.shape());
output.mul(&ct).sum()
}
fn perturbed_loss<F>(
x_data: &[f32],
x_shape: &[usize],
flat_idx: usize,
delta: f32,
fwd: &F,
c: &[f32],
) -> f32
where
F: Fn(&Tensor) -> Tensor,
{
autograd::no_grad(|| {
let mut xd = x_data.to_vec();
xd[flat_idx] += delta;
let x = Tensor::new(&xd, x_shape);
let y = fwd(&x);
scalar_loss(&y, c).item()
})
}
fn assert_close(analytic: f32, numeric: f32, what: &str) {
let denom = analytic.abs().max(numeric.abs()).max(1.0);
let rel = (analytic - numeric).abs() / denom;
assert!(
rel < TOL,
"{what}: analytic grad {analytic} != finite-diff {numeric} (rel err {rel})"
);
}
fn gradcheck_input<F>(name: &str, x_data: &[f32], x_shape: &[usize], fwd: F)
where
F: Fn(&Tensor) -> Tensor,
{
autograd::clear_graph();
let x = Tensor::new(x_data, x_shape).requires_grad();
let xid = x.id();
let y = fwd(&x);
let c = coeff(y.numel());
let loss = scalar_loss(&y, &c);
loss.backward();
let grad = autograd::get_grad(xid)
.unwrap_or_else(|| panic!("{name}: input received NO gradient — autograd graph severed"));
assert_eq!(grad.shape(), x_shape, "{name}: grad shape mismatch");
assert!(
grad.data().iter().all(|v| v.is_finite()),
"{name}: non-finite grad"
);
assert!(
grad.data().iter().any(|&v| v.abs() > 1e-9),
"{name}: all-zero grad"
);
for i in 0..x_data.len() {
let num = (perturbed_loss(x_data, x_shape, i, FD_EPS, &fwd, &c)
- perturbed_loss(x_data, x_shape, i, -FD_EPS, &fwd, &c))
/ (2.0 * FD_EPS);
assert_close(grad.data()[i], num, &format!("{name} dL/dx[{i}]"));
}
}
#[test]
fn masked_mean_pool_matches_hand_computed_2x3x4_case() {
let x: Vec<f32> = (0..24).map(|i| i as f32).collect();
let hidden = Tensor::new(&x, &[2, 3, 4]);
let mask = [1u8, 1, 0, 1, 1, 1];
let out = masked_mean_pool(&hidden, &mask).expect("pool must succeed");
assert_eq!(out.shape(), &[2, 4], "pooling reduces [B,S,H] to [B,H]");
for h in 0..4 {
let want = (x[h] + x[4 + h]) / 2.0;
assert!(
(out.data()[h] - want).abs() < 1e-6,
"row 0 h{h}: got {} want {want}",
out.data()[h]
);
}
for h in 0..4 {
let want = (x[12 + h] + x[16 + h] + x[20 + h]) / 3.0;
assert!(
(out.data()[4 + h] - want).abs() < 1e-6,
"row 1 h{h}: got {} want {want}",
out.data()[4 + h]
);
}
}
#[test]
fn masked_mean_pool_uses_a_per_row_denominator() {
let x = vec![5.0f32; 2 * 3 * 2];
let hidden = Tensor::new(&x, &[2, 3, 2]);
let mask = [1u8, 0, 0, 1, 1, 1];
let out = masked_mean_pool(&hidden, &mask).expect("pool must succeed");
for (i, &v) in out.data().iter().enumerate() {
assert!(
(v - 5.0).abs() < 1e-6,
"element {i}: a per-row mean of a constant row must be that constant, got {v}"
);
}
}
#[test]
fn masked_mean_pool_ignores_padded_values_entirely() {
let mut x = vec![1.0f32; 1 * 3 * 2];
x[4] = 1.0e9; x[5] = -1.0e9; let hidden = Tensor::new(&x, &[1, 3, 2]);
let out = masked_mean_pool(&hidden, &[1u8, 1, 0]).expect("pool must succeed");
assert!(
out.data().iter().all(|&v| (v - 1.0).abs() < 1e-6),
"padded values must not reach the mean, got {:?}",
out.data()
);
}
#[test]
fn masked_mean_pool_is_not_grad_connected_without_requires_grad() {
autograd::clear_graph();
let hidden = Tensor::new(&[1.0, 2.0, 3.0, 4.0], &[1, 2, 2]);
let out = masked_mean_pool(&hidden, &[1u8, 1]).expect("pool must succeed");
assert!(
!out.requires_grad_enabled(),
"pooling a frozen tensor must not fabricate a graph edge"
);
}
#[test]
fn masked_mean_pool_backward_matches_central_finite_differences() {
let x: Vec<f32> = (0..12)
.map(|i| 0.31 + 0.19 * (i as f32) - 0.011 * ((i * i) as f32))
.collect();
let mask = [1u8, 1, 0, 1, 1, 1];
gradcheck_input("masked_mean_pool", &x, &[2, 3, 2], move |t| {
masked_mean_pool(t, &mask).expect("pool must succeed")
});
}
#[test]
fn masked_mean_pool_backward_routes_zero_to_padded_positions() {
autograd::clear_graph();
let x: Vec<f32> = (0..6).map(|i| 0.5 + (i as f32)).collect();
let hidden = Tensor::new(&x, &[1, 3, 2]).requires_grad();
let hid = hidden.id();
masked_mean_pool(&hidden, &[1u8, 1, 0])
.expect("pool must succeed")
.sum()
.backward();
let grad = autograd::get_grad(hid).expect("input must receive gradient");
assert_eq!(grad.shape(), &[1, 3, 2]);
for i in 0..4 {
assert!(
(grad.data()[i] - 0.5).abs() < 1e-6,
"valid element {i} must receive 1/n_b = 0.5, got {}",
grad.data()[i]
);
}
assert_eq!(
grad.data()[4],
0.0,
"padded position must receive EXACTLY zero gradient"
);
assert_eq!(
grad.data()[5],
0.0,
"padded position must receive EXACTLY zero gradient"
);
}
#[test]
fn masked_mean_pool_backward_denominator_differs_per_row() {
autograd::clear_graph();
let x = vec![1.0f32; 2 * 3 * 1];
let hidden = Tensor::new(&x, &[2, 3, 1]).requires_grad();
let hid = hidden.id();
masked_mean_pool(&hidden, &[1u8, 1, 0, 1, 1, 1])
.expect("pool must succeed")
.sum()
.backward();
let g = autograd::get_grad(hid).expect("input must receive gradient");
assert!((g.data()[0] - 0.5).abs() < 1e-6, "row 0 must divide by 2");
assert!((g.data()[1] - 0.5).abs() < 1e-6, "row 0 must divide by 2");
assert_eq!(g.data()[2], 0.0, "row 0 padding gets nothing");
for i in 3..6 {
assert!(
(g.data()[i] - 1.0 / 3.0).abs() < 1e-6,
"row 1 must divide by 3, got {} at {i}",
g.data()[i]
);
}
}
#[test]
fn masked_mean_pool_rejects_all_padding_row() {
let hidden = Tensor::new(&vec![1.0f32; 12], &[2, 3, 2]);
assert_eq!(
masked_mean_pool(&hidden, &[1u8, 1, 1, 0, 0, 0])
.expect_err("a zero denominator must be rejected, never divided by"),
OpError::AllPaddingRow { row: 1 }
);
}
#[test]
fn masked_mean_pool_rejects_length_mismatch() {
let hidden = Tensor::new(&vec![1.0f32; 12], &[2, 3, 2]);
assert_eq!(
masked_mean_pool(&hidden, &[1u8, 1, 1]).expect_err("mask.len() must be batch * seq"),
OpError::LengthMismatch { ids: 6, mask: 3 }
);
}
#[test]
fn masked_mean_pool_rejects_non_binary_mask_value() {
let hidden = Tensor::new(&vec![1.0f32; 12], &[2, 3, 2]);
assert_eq!(
masked_mean_pool(&hidden, &[1u8, 3, 1, 1, 1, 1]).expect_err("only 0 and 1 are valid"),
OpError::NonBinaryMaskValue {
value: 3,
position: 1,
}
);
}
#[test]
fn masked_mean_pool_rejects_wrong_rank_and_zero_dimensions() {
let rank2 = Tensor::new(&[1.0, 2.0, 3.0, 4.0], &[2, 2]);
assert_eq!(
masked_mean_pool(&rank2, &[1u8, 1]).expect_err("hidden must be 3-D [B,S,H]"),
OpError::ShapeMismatch {
expected: vec![0, 0, 0],
got: vec![2, 2],
}
);
let zero_batch = Tensor::new(&[], &[0, 3, 2]);
assert_eq!(
masked_mean_pool(&zero_batch, &[]).expect_err("batch 0 must be rejected"),
OpError::ZeroDimension { which: "batch" }
);
let zero_seq = Tensor::new(&[], &[2, 0, 2]);
assert_eq!(
masked_mean_pool(&zero_seq, &[]).expect_err("seq 0 must be rejected"),
OpError::ZeroDimension { which: "seq" }
);
let zero_hidden = Tensor::new(&[], &[2, 3, 0]);
assert_eq!(
masked_mean_pool(&zero_hidden, &[1u8, 1, 1, 1, 1, 1])
.expect_err("hidden 0 must be rejected"),
OpError::ZeroDimension { which: "hidden" }
);
}
#[test]
fn masked_mean_pool_rejects_before_computing_anything() {
let hidden = Tensor::new(&[f32::MAX, f32::MAX, f32::MAX, f32::MAX], &[1, 2, 2]);
assert_eq!(
masked_mean_pool(&hidden, &[0u8, 0]).expect_err("all-padding row must be rejected"),
OpError::AllPaddingRow { row: 0 }
);
}