use crate::error::InferenceError;
const SPLITMIX64_GAMMA: u64 = 0x9E37_79B9_7F4A_7C15;
pub fn walsh_hadamard_in_place(data: &mut [f32]) -> Result<(), InferenceError> {
let n = data.len();
if n == 0 || !n.is_power_of_two() {
return Err(InferenceError::Inference(format!(
"walsh_hadamard requires a power-of-two length, got {n}"
)));
}
let mut h = 1;
while h < n {
let mut i = 0;
while i < n {
for j in i..i + h {
let x = data[j];
let y = data[j + h];
data[j] = x + y;
data[j + h] = x - y;
}
i += h * 2;
}
h *= 2;
}
Ok(())
}
pub fn walsh_hadamard_orthonormal_in_place(data: &mut [f32]) -> Result<(), InferenceError> {
walsh_hadamard_in_place(data)?;
let scale = 1.0_f32 / (data.len() as f32).sqrt();
for v in data.iter_mut() {
*v *= scale;
}
Ok(())
}
pub fn walsh_hadamard_f64_in_place(data: &mut [f64]) -> Result<(), InferenceError> {
let n = data.len();
if n == 0 || !n.is_power_of_two() {
return Err(InferenceError::Inference(format!(
"walsh_hadamard_f64 requires a power-of-two length, got {n}"
)));
}
let mut h = 1;
while h < n {
let mut i = 0;
while i < n {
for j in i..i + h {
let x = data[j];
let y = data[j + h];
data[j] = x + y;
data[j + h] = x - y;
}
i += h * 2;
}
h *= 2;
}
Ok(())
}
pub fn walsh_hadamard_orthonormal_f64_in_place(data: &mut [f64]) -> Result<(), InferenceError> {
walsh_hadamard_f64_in_place(data)?;
let scale = 1.0_f64 / (data.len() as f64).sqrt();
for v in data.iter_mut() {
*v *= scale;
}
Ok(())
}
fn signs_from_seed(seed: u64, n: usize) -> Vec<f32> {
let mut signs = Vec::with_capacity(n);
push_signs_from_seed(seed, n, &mut signs);
signs
}
fn push_signs_from_seed(seed: u64, n: usize, out: &mut Vec<f32>) {
let mut state = seed.wrapping_add(SPLITMIX64_GAMMA);
for _ in 0..n {
state = state.wrapping_add(SPLITMIX64_GAMMA);
out.push(if splitmix64_mix(state) & 1 == 0 {
1.0
} else {
-1.0
});
}
}
fn splitmix64_mix(state: u64) -> u64 {
let mut z = state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
pub(crate) fn derive_block_seed(seed: u64, block_index: usize) -> u64 {
splitmix64_mix(seed.wrapping_add((block_index as u64).wrapping_mul(SPLITMIX64_GAMMA)))
}
#[derive(Debug, Clone)]
pub struct RandomizedHadamard {
signs: Vec<f32>,
}
impl RandomizedHadamard {
pub fn new(seed: u64, n: usize) -> Result<Self, InferenceError> {
if n == 0 || !n.is_power_of_two() {
return Err(InferenceError::Inference(format!(
"RandomizedHadamard requires a power-of-two length, got {n}"
)));
}
Ok(Self {
signs: signs_from_seed(seed, n),
})
}
pub fn dim(&self) -> usize {
self.signs.len()
}
pub fn apply(&self, data: &mut [f32]) -> Result<(), InferenceError> {
if data.len() != self.signs.len() {
return Err(InferenceError::Inference(format!(
"RandomizedHadamard::apply: length mismatch (have {}, want {})",
data.len(),
self.signs.len()
)));
}
for (v, s) in data.iter_mut().zip(self.signs.iter()) {
*v *= s;
}
walsh_hadamard_orthonormal_in_place(data)
}
pub fn apply_inverse(&self, data: &mut [f32]) -> Result<(), InferenceError> {
if data.len() != self.signs.len() {
return Err(InferenceError::Inference(format!(
"RandomizedHadamard::apply_inverse: length mismatch (have {}, want {})",
data.len(),
self.signs.len()
)));
}
walsh_hadamard_orthonormal_in_place(data)?;
for (v, s) in data.iter_mut().zip(self.signs.iter()) {
*v *= s;
}
Ok(())
}
pub fn apply_f64(&self, data: &mut [f64]) -> Result<(), InferenceError> {
if data.len() != self.signs.len() {
return Err(InferenceError::Inference(format!(
"RandomizedHadamard::apply_f64: length mismatch (have {}, want {})",
data.len(),
self.signs.len()
)));
}
for (v, s) in data.iter_mut().zip(self.signs.iter()) {
*v *= f64::from(*s);
}
walsh_hadamard_orthonormal_f64_in_place(data)
}
pub fn apply_inverse_f64(&self, data: &mut [f64]) -> Result<(), InferenceError> {
if data.len() != self.signs.len() {
return Err(InferenceError::Inference(format!(
"RandomizedHadamard::apply_inverse_f64: length mismatch (have {}, want {})",
data.len(),
self.signs.len()
)));
}
walsh_hadamard_orthonormal_f64_in_place(data)?;
for (v, s) in data.iter_mut().zip(self.signs.iter()) {
*v *= f64::from(*s);
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct BlockHadamard {
block_size: usize,
num_blocks: usize,
signs: Vec<f32>,
}
pub(crate) const MAX_BLOCK_HADAMARD_LEN: usize = 1 << 24;
pub(crate) const MAX_BLOCK_HADAMARD_BLOCKS: usize = 4096;
impl BlockHadamard {
pub fn new(seed: u64, n: usize, block_size: usize) -> Result<Self, InferenceError> {
if n == 0 {
return Err(InferenceError::Inference(
"BlockHadamard requires a non-zero length".to_string(),
));
}
if n > MAX_BLOCK_HADAMARD_LEN {
return Err(InferenceError::Inference(format!(
"BlockHadamard requires n <= {MAX_BLOCK_HADAMARD_LEN} \
(generous headroom over any real model dimension), got {n} \
— refusing rather than attempting an unbounded allocation"
)));
}
if block_size == 0 || !block_size.is_power_of_two() {
return Err(InferenceError::Inference(format!(
"BlockHadamard requires a power-of-two block_size, got {block_size}"
)));
}
if !n.is_multiple_of(block_size) {
return Err(InferenceError::Inference(format!(
"BlockHadamard block_size {block_size} does not evenly divide length {n}"
)));
}
let num_blocks = n / block_size;
if num_blocks > MAX_BLOCK_HADAMARD_BLOCKS {
return Err(InferenceError::Inference(format!(
"BlockHadamard requires n / block_size <= {MAX_BLOCK_HADAMARD_BLOCKS} \
blocks (generous headroom over any real model geometry), got \
{num_blocks} ({n} / {block_size}) — refusing rather than \
deriving that many blocks' worth of per-block signs"
)));
}
let mut signs: Vec<f32> = Vec::new();
signs.try_reserve_exact(n).map_err(|e| {
InferenceError::Inference(format!(
"BlockHadamard: failed to reserve {n} f32 signs \
({} bytes) — refusing rather than aborting the process: {e}",
n * std::mem::size_of::<f32>()
))
})?;
for i in 0..num_blocks {
push_signs_from_seed(derive_block_seed(seed, i), block_size, &mut signs);
}
Ok(Self {
block_size,
num_blocks,
signs,
})
}
pub fn dim(&self) -> usize {
self.block_size * self.num_blocks
}
pub fn block_size(&self) -> usize {
self.block_size
}
pub fn num_blocks(&self) -> usize {
self.num_blocks
}
pub fn apply(&self, data: &mut [f32]) -> Result<(), InferenceError> {
if data.len() != self.dim() {
return Err(InferenceError::Inference(format!(
"BlockHadamard::apply: length mismatch (have {}, want {})",
data.len(),
self.dim()
)));
}
for (chunk, sign_chunk) in data
.chunks_mut(self.block_size)
.zip(self.signs.chunks(self.block_size))
{
for (v, s) in chunk.iter_mut().zip(sign_chunk.iter()) {
*v *= s;
}
walsh_hadamard_orthonormal_in_place(chunk)?;
}
Ok(())
}
pub fn apply_inverse(&self, data: &mut [f32]) -> Result<(), InferenceError> {
if data.len() != self.dim() {
return Err(InferenceError::Inference(format!(
"BlockHadamard::apply_inverse: length mismatch (have {}, want {})",
data.len(),
self.dim()
)));
}
for (chunk, sign_chunk) in data
.chunks_mut(self.block_size)
.zip(self.signs.chunks(self.block_size))
{
walsh_hadamard_orthonormal_in_place(chunk)?;
for (v, s) in chunk.iter_mut().zip(sign_chunk.iter()) {
*v *= s;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: &[f32], b: &[f32], tol: f32) {
assert_eq!(a.len(), b.len(), "length mismatch");
for (i, (x, y)) in a.iter().zip(b.iter()).enumerate() {
assert!(
(x - y).abs() < tol,
"index {i}: {x} vs {y} (delta {})",
(x - y).abs()
);
}
}
#[test]
fn adjacent_block_sign_streams_are_not_shifted_copies() {
for seed in [0u64, 1, 42, 0xDEAD_BEEF] {
for b in [64usize, 128, 256] {
let s0 = signs_from_seed(derive_block_seed(seed, 0), b);
let s1 = signs_from_seed(derive_block_seed(seed, 1), b);
let shifted_copy = s1[..b - 1]
.iter()
.zip(&s0[1..])
.all(|(a, c)| a.to_bits() == c.to_bits());
assert!(
!shifted_copy,
"seed {seed}, block_size {b}: adjacent blocks' sign streams \
are one-position-shifted copies (shared PRNG states)"
);
}
}
}
#[test]
fn block_hadamard_rejects_usize_max_length_without_allocating() {
let err = BlockHadamard::new(0, usize::MAX, 1).unwrap_err();
assert!(
format!("{err}").contains("MAX_BLOCK_HADAMARD_LEN")
|| format!("{err}").to_lowercase().contains("n <="),
"expected an n-bound rejection, got: {err}"
);
}
#[test]
#[ignore = "stress test: constructs a single block at the 16M-element \
cap, deriving a 64 MiB sign buffer — run explicitly with \
`cargo test -- --ignored` rather than on every default run"]
fn block_hadamard_accepts_n_at_the_cap_boundary() {
let bh = BlockHadamard::new(0, MAX_BLOCK_HADAMARD_LEN, MAX_BLOCK_HADAMARD_LEN).unwrap();
assert_eq!(bh.dim(), MAX_BLOCK_HADAMARD_LEN);
assert_eq!(bh.num_blocks(), 1);
}
#[test]
fn block_hadamard_rejects_n_one_past_the_cap() {
let err = BlockHadamard::new(0, MAX_BLOCK_HADAMARD_LEN + 1, 1).unwrap_err();
assert!(
format!("{err}").contains(&(MAX_BLOCK_HADAMARD_LEN).to_string()),
"expected the cap value in the error, got: {err}"
);
}
#[test]
fn block_hadamard_rejects_num_blocks_above_cap_without_allocating() {
let err = BlockHadamard::new(0, 1 << 24, 1).unwrap_err();
assert!(
format!("{err}").contains(&MAX_BLOCK_HADAMARD_BLOCKS.to_string())
|| format!("{err}").to_lowercase().contains("blocks"),
"expected a num_blocks-bound rejection, got: {err}"
);
}
#[test]
fn block_hadamard_accepts_num_blocks_at_the_cap_boundary() {
let bh = BlockHadamard::new(0, MAX_BLOCK_HADAMARD_BLOCKS, 1).unwrap();
assert_eq!(bh.num_blocks(), MAX_BLOCK_HADAMARD_BLOCKS);
assert_eq!(bh.dim(), MAX_BLOCK_HADAMARD_BLOCKS);
}
#[test]
fn block_hadamard_accepts_large_valid_n_via_single_fallible_reservation() {
let n = 1 << 16;
let bh = BlockHadamard::new(0x5EED, n, n).unwrap();
assert_eq!(bh.num_blocks(), 1);
assert_eq!(bh.dim(), n);
let expected = RandomizedHadamard::new(derive_block_seed(0x5EED, 0), n).unwrap();
let mut lhs = vec![1.0_f32; n];
let mut rhs = vec![1.0_f32; n];
bh.apply(&mut lhs).unwrap();
expected.apply(&mut rhs).unwrap();
assert_eq!(
lhs.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
rhs.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
"large-but-valid single-block construction must match the \
per-block reference bit-for-bit"
);
}
#[test]
fn block_hadamard_signs_match_per_block_randomized_hadamard() {
for (seed, block_size, num_blocks) in [
(0x51ED_u64, 4usize, 5usize),
(42, 8, 3),
(0xDEAD_BEEF, 2, 7),
] {
let n = block_size * num_blocks;
let input: Vec<f32> = (0..n).map(|i| (i as f32) * 0.5 - 3.0).collect();
let bh = BlockHadamard::new(seed, n, block_size).unwrap();
let mut new_out = input.clone();
bh.apply(&mut new_out).unwrap();
let mut old_out = input.clone();
for (i, chunk) in old_out.chunks_mut(block_size).enumerate() {
let rh = RandomizedHadamard::new(derive_block_seed(seed, i), block_size).unwrap();
rh.apply(chunk).unwrap();
}
assert_eq!(
new_out.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
old_out.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
"seed {seed}, block_size {block_size}, num_blocks {num_blocks}: \
flat-signs BlockHadamard must match the per-block \
RandomizedHadamard layout bit-for-bit"
);
let mut round = new_out.clone();
bh.apply_inverse(&mut round).unwrap();
approx_eq(&round, &input, 1e-5);
}
}
#[test]
fn walsh_hadamard_size_1_is_identity() {
let mut data = [3.5_f32];
walsh_hadamard_in_place(&mut data).unwrap();
approx_eq(&data, &[3.5], 1e-6);
}
#[test]
fn walsh_hadamard_size_2_known_result() {
let mut data = [1.0_f32, 2.0];
walsh_hadamard_in_place(&mut data).unwrap();
approx_eq(&data, &[3.0, -1.0], 1e-6);
}
#[test]
fn walsh_hadamard_size_4_known_result() {
let mut data = [1.0_f32, 0.0, 0.0, 0.0];
walsh_hadamard_in_place(&mut data).unwrap();
approx_eq(&data, &[1.0, 1.0, 1.0, 1.0], 1e-6);
}
#[test]
fn walsh_hadamard_double_application_scales_by_n() {
let n = 16;
let original: Vec<f32> = (0..n).map(|i| i as f32 - 8.0).collect();
let mut data = original.clone();
walsh_hadamard_in_place(&mut data).unwrap();
walsh_hadamard_in_place(&mut data).unwrap();
let scaled: Vec<f32> = original.iter().map(|&x| x * n as f32).collect();
approx_eq(&data, &scaled, 1e-4);
}
#[test]
fn walsh_hadamard_orthonormal_is_isometry() {
let original: Vec<f32> = (0..32).map(|i| (i as f32 * 0.137).sin()).collect();
let original_norm: f32 = original.iter().map(|x| x * x).sum::<f32>().sqrt();
let mut data = original.clone();
walsh_hadamard_orthonormal_in_place(&mut data).unwrap();
let transformed_norm: f32 = data.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!(
(original_norm - transformed_norm).abs() < 1e-4,
"||x||={original_norm} vs ||Hx||={transformed_norm}"
);
}
#[test]
fn walsh_hadamard_orthonormal_is_involution() {
let original: Vec<f32> = (0..64).map(|i| (i as f32 * 0.71).cos()).collect();
let mut data = original.clone();
walsh_hadamard_orthonormal_in_place(&mut data).unwrap();
walsh_hadamard_orthonormal_in_place(&mut data).unwrap();
approx_eq(&data, &original, 1e-4);
}
#[test]
fn walsh_hadamard_rejects_non_power_of_two() {
let mut data = [1.0_f32, 2.0, 3.0];
assert!(walsh_hadamard_in_place(&mut data).is_err());
}
#[test]
fn walsh_hadamard_rejects_empty() {
let mut data: [f32; 0] = [];
assert!(walsh_hadamard_in_place(&mut data).is_err());
}
#[test]
fn randomized_hadamard_is_orthogonal() {
let r = RandomizedHadamard::new(42, 128).unwrap();
let original: Vec<f32> = (0..128).map(|i| (i as f32 * 0.13).sin()).collect();
let original_norm: f32 = original.iter().map(|x| x * x).sum::<f32>().sqrt();
let mut data = original.clone();
r.apply(&mut data).unwrap();
let transformed_norm: f32 = data.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!(
(original_norm - transformed_norm).abs() < 1e-3,
"||x||={original_norm} vs ||Rx||={transformed_norm}"
);
}
#[test]
fn randomized_hadamard_inverse_round_trips() {
let r = RandomizedHadamard::new(0xDEAD_BEEF, 256).unwrap();
let original: Vec<f32> = (0..256).map(|i| (i as f32 * 0.41).cos() + 0.3).collect();
let mut data = original.clone();
r.apply(&mut data).unwrap();
r.apply_inverse(&mut data).unwrap();
approx_eq(&data, &original, 1e-3);
}
#[test]
fn randomized_hadamard_seed_determinism() {
let r1 = RandomizedHadamard::new(7, 64).unwrap();
let r2 = RandomizedHadamard::new(7, 64).unwrap();
let mut a: Vec<f32> = (0..64).map(|i| i as f32 + 0.5).collect();
let mut b = a.clone();
r1.apply(&mut a).unwrap();
r2.apply(&mut b).unwrap();
assert_eq!(a, b, "same seed must produce bit-identical output");
}
#[test]
fn randomized_hadamard_seed_differs() {
let r1 = RandomizedHadamard::new(7, 64).unwrap();
let r2 = RandomizedHadamard::new(8, 64).unwrap();
let mut a: Vec<f32> = (0..64).map(|i| i as f32 + 0.5).collect();
let mut b = a.clone();
r1.apply(&mut a).unwrap();
r2.apply(&mut b).unwrap();
let diff: f32 = a.iter().zip(b.iter()).map(|(x, y)| (x - y).abs()).sum();
assert!(
diff > 1.0,
"different seeds should produce different output"
);
}
#[test]
fn randomized_hadamard_rejects_length_mismatch() {
let r = RandomizedHadamard::new(1, 16).unwrap();
let mut data = vec![0.0_f32; 8];
assert!(r.apply(&mut data).is_err());
}
fn approx_eq_f64(a: &[f64], b: &[f64], tol: f64) {
assert_eq!(a.len(), b.len(), "length mismatch");
for (i, (x, y)) in a.iter().zip(b.iter()).enumerate() {
assert!(
(x - y).abs() < tol,
"index {i}: {x} vs {y} (delta {})",
(x - y).abs()
);
}
}
#[test]
fn walsh_hadamard_f64_size_2_known_result() {
let mut data = [1.0_f64, 2.0];
walsh_hadamard_f64_in_place(&mut data).unwrap();
approx_eq_f64(&data, &[3.0, -1.0], 1e-12);
}
#[test]
fn walsh_hadamard_f64_orthonormal_is_isometry() {
let original: Vec<f64> = (0..32).map(|i| (i as f64 * 0.137).sin()).collect();
let original_norm: f64 = original.iter().map(|x| x * x).sum::<f64>().sqrt();
let mut data = original.clone();
walsh_hadamard_orthonormal_f64_in_place(&mut data).unwrap();
let transformed_norm: f64 = data.iter().map(|x| x * x).sum::<f64>().sqrt();
assert!(
(original_norm - transformed_norm).abs() < 1e-12,
"||x||={original_norm} vs ||Hx||={transformed_norm}"
);
}
#[test]
fn walsh_hadamard_f64_orthonormal_is_involution() {
let original: Vec<f64> = (0..64).map(|i| (i as f64 * 0.71).cos()).collect();
let mut data = original.clone();
walsh_hadamard_orthonormal_f64_in_place(&mut data).unwrap();
walsh_hadamard_orthonormal_f64_in_place(&mut data).unwrap();
approx_eq_f64(&data, &original, 1e-12);
}
#[test]
fn randomized_hadamard_f64_inverse_round_trips() {
let r = RandomizedHadamard::new(0xDEAD_BEEF, 256).unwrap();
let original: Vec<f64> = (0..256).map(|i| (i as f64 * 0.41).cos() + 0.3).collect();
let mut data = original.clone();
r.apply_f64(&mut data).unwrap();
r.apply_inverse_f64(&mut data).unwrap();
approx_eq_f64(&data, &original, 1e-12);
}
#[test]
fn randomized_hadamard_f32_f64_agree_in_precision() {
let r = RandomizedHadamard::new(7, 256).unwrap();
let mut f32_data: Vec<f32> = (0..256).map(|i| (i as f32 * 0.13).sin()).collect();
let mut f64_data: Vec<f64> = f32_data.iter().map(|&x| x as f64).collect();
r.apply(&mut f32_data).unwrap();
r.apply_f64(&mut f64_data).unwrap();
for (i, (a, b)) in f32_data.iter().zip(f64_data.iter()).enumerate() {
let delta = (*a as f64 - b).abs();
assert!(delta < 1e-5, "index {i}: f32={a} vs f64={b}, delta={delta}");
}
}
#[test]
fn randomized_hadamard_outlier_redistribution() {
let r = RandomizedHadamard::new(123, 1024).unwrap();
let mut data = vec![0.0_f32; 1024];
data[42] = 100.0;
let pre_max = data.iter().fold(0.0_f32, |m, x| m.max(x.abs()));
r.apply(&mut data).unwrap();
let post_max = data.iter().fold(0.0_f32, |m, x| m.max(x.abs()));
assert!(
post_max < pre_max * 0.5,
"outlier should be redistributed: pre_max={pre_max}, post_max={post_max}"
);
}
fn synthetic_vec(n: usize, seed: u64) -> Vec<f32> {
let mut state = seed;
(0..n)
.map(|_| {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let bits = (state >> 11) as u32;
(bits as f32 / u32::MAX as f32) - 0.5
})
.collect()
}
#[test]
fn block_hadamard_round_trips() {
for &(n, b) in &[(3584usize, 64usize), (3584, 128), (3584, 256), (17408, 256)] {
let bh = BlockHadamard::new(0xB10C_5EED, n, b).unwrap();
let original = synthetic_vec(n, 7);
let mut data = original.clone();
bh.apply(&mut data).unwrap();
bh.apply_inverse(&mut data).unwrap();
approx_eq(&data, &original, 1e-3);
}
}
#[test]
fn block_hadamard_preserves_norm() {
let bh = BlockHadamard::new(42, 3584, 128).unwrap();
let original = synthetic_vec(3584, 11);
let original_norm: f32 = original.iter().map(|x| x * x).sum::<f32>().sqrt();
let mut data = original.clone();
bh.apply(&mut data).unwrap();
let transformed_norm: f32 = data.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!(
(original_norm - transformed_norm).abs() < 1e-2,
"||x||={original_norm} vs ||Rx||={transformed_norm}"
);
}
#[test]
fn block_hadamard_exact_partition_coverage() {
for &(n, b) in &[
(3584usize, 64usize),
(3584, 128),
(3584, 256),
(17408, 64),
(17408, 128),
(17408, 256),
] {
assert_eq!(n % b, 0, "test precondition: b must divide n");
let bh = BlockHadamard::new(0xC0FF_EE00, n, b).unwrap();
assert_eq!(bh.num_blocks(), n / b);
assert_eq!(bh.dim(), n);
let base = synthetic_vec(n, 3);
let mut perturbed = base.clone();
let target = n / 2 + 3; perturbed[target] += 5.0;
let mut out_base = base.clone();
bh.apply(&mut out_base).unwrap();
let mut out_perturbed = perturbed.clone();
bh.apply(&mut out_perturbed).unwrap();
let block_start = (target / b) * b;
let block_end = block_start + b;
for i in 0..n {
let changed = (out_base[i] - out_perturbed[i]).abs() > 1e-6;
if i >= block_start && i < block_end {
} else {
assert!(
!changed,
"element {i} outside block [{block_start},{block_end}) \
changed when only element {target} was perturbed \
(n={n}, b={b}) — partition coverage violated"
);
}
}
}
}
#[test]
fn block_hadamard_rejects_block_size_not_dividing_n() {
assert!(BlockHadamard::new(1, 3584, 1024).is_err());
}
#[test]
fn block_hadamard_rejects_non_power_of_two_block_size() {
assert!(BlockHadamard::new(1, 3584, 7).is_err());
}
#[test]
fn block_hadamard_rejects_zero_length() {
assert!(BlockHadamard::new(1, 0, 64).is_err());
}
#[test]
fn block_hadamard_rejects_zero_block_size() {
assert!(BlockHadamard::new(1, 3584, 0).is_err());
}
#[test]
fn block_hadamard_rejects_length_mismatch_on_apply() {
let bh = BlockHadamard::new(1, 3584, 128).unwrap();
let mut data = vec![0.0_f32; 100];
assert!(bh.apply(&mut data).is_err());
}
#[test]
fn block_hadamard_mutation_sensitive_round_trip() {
let n = 3584;
let b = 128;
let good_seed = 0xB10C_5EED;
let corrupted_seed = 0xB10C_5EED ^ 0x1;
let original = synthetic_vec(n, 7);
let encoder = BlockHadamard::new(good_seed, n, b).unwrap();
let corrupted_decoder = BlockHadamard::new(corrupted_seed, n, b).unwrap();
let mut data = original.clone();
encoder.apply(&mut data).unwrap();
corrupted_decoder.apply_inverse(&mut data).unwrap();
let corrupted_delta: f32 = data
.iter()
.zip(original.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0, f32::max);
assert!(
corrupted_delta > 1e-2,
"mismatched-seed round trip should diverge from the original, got delta {corrupted_delta}"
);
let restored_decoder = BlockHadamard::new(good_seed, n, b).unwrap();
let mut data_restored = original.clone();
encoder.apply(&mut data_restored).unwrap();
restored_decoder.apply_inverse(&mut data_restored).unwrap();
approx_eq(&data_restored, &original, 1e-3);
}
}