use rand::rngs::StdRng;
use rand::Rng;
use std::f32::consts::TAU;
use std::fmt;
use super::texture::GRAIN_SIGMA;
const MAX_DRAWS: usize = 64;
#[derive(Debug)]
pub struct RejectionExhausted;
impl fmt::Display for RejectionExhausted {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"conditioned sampling did not converge in {MAX_DRAWS} draws; a base level of the \
texture sits against the end of the range"
)
}
}
impl std::error::Error for RejectionExhausted {}
pub(crate) fn draw_free(rng: &mut StdRng, base: f32) -> u8 {
(base + gaussian(rng, GRAIN_SIGMA)).clamp(0.0, 255.0) as u8
}
pub(crate) fn draw_with_lsb(
rng: &mut StdRng,
base: f32,
bit: u8,
) -> Result<u8, RejectionExhausted> {
for _ in 0..MAX_DRAWS {
let value = draw_free(rng, base);
if value & 1 == bit & 1 {
return Ok(value);
}
}
Err(RejectionExhausted)
}
fn gaussian(rng: &mut StdRng, sigma: f32) -> f32 {
let uniform = unit(rng).max(f32::EPSILON);
let angle = unit(rng) * TAU;
sigma * (-2.0 * uniform.ln()).sqrt() * angle.cos()
}
fn unit(rng: &mut StdRng) -> f32 {
(rng.next_u32() >> 8) as f32 / 16_777_216.0
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
#![allow(clippy::panic)]
use super::*;
use rand::SeedableRng;
const BASE: f32 = 128.0;
const SAMPLES: usize = 20_000;
fn rng(seed: u64) -> StdRng {
StdRng::seed_from_u64(seed)
}
#[test]
fn a_conditioned_sample_carries_the_bit_it_was_given() {
let mut rng = rng(11);
for index in 0..SAMPLES {
let bit = (index % 2) as u8;
match draw_with_lsb(&mut rng, BASE, bit) {
Ok(sample) => assert_eq!(sample & 1, bit),
Err(error) => panic!("a mid-range level must converge: {error}"),
}
}
}
#[test]
fn conditioning_reproduces_the_unconditioned_distribution() {
let mut free_rng = rng(101);
let mut conditioned_rng = rng(202);
let free: Vec<u8> = (0..SAMPLES)
.map(|_| draw_free(&mut free_rng, BASE))
.collect();
let conditioned: Vec<u8> = (0..SAMPLES)
.map(|index| {
draw_with_lsb(&mut conditioned_rng, BASE, (index % 2) as u8)
.expect("a mid-range level must converge")
})
.collect();
let mean = |samples: &[u8]| {
samples.iter().map(|&sample| sample as f64).sum::<f64>() / samples.len() as f64
};
assert!(
(mean(&free) - mean(&conditioned)).abs() < 0.1,
"free {} against conditioned {}",
mean(&free),
mean(&conditioned)
);
let count = |samples: &[u8], value: u8| {
samples.iter().filter(|&&sample| sample == value).count()
};
for value in 120..=136u8 {
let free_count = count(&free, value) as f64;
let conditioned_count = count(&conditioned, value) as f64;
let spread = (free_count + conditioned_count).sqrt().max(1.0);
assert!(
(free_count - conditioned_count).abs() < 6.0 * spread,
"value {value}: free {free_count} against conditioned {conditioned_count}"
);
}
}
#[test]
fn a_free_sample_is_clamped_to_the_range() {
let mut rng = rng(7);
for base in [0.0f32, 4.0, 128.0, 251.0, 255.0] {
for _ in 0..1_000 {
let _sample = draw_free(&mut rng, base);
}
}
}
#[test]
fn an_unreachable_parity_is_an_error_rather_than_a_hang() {
let mut rng = rng(3);
let error = draw_with_lsb(&mut rng, -1_000.0, 1)
.map(|_| ())
.expect_err("an unreachable parity must be reported");
assert!(error.to_string().contains("did not converge"));
assert!(draw_with_lsb(&mut rng, -1_000.0, 0).is_ok());
}
}