use crate::Rng;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Ensemble {
seed: u64,
count: u64,
threads: usize,
}
impl Ensemble {
pub fn new(seed: u64, count: u64) -> Ensemble {
Ensemble {
seed,
count,
threads: 1,
}
}
pub fn with_threads(mut self, threads: usize) -> Ensemble {
self.threads = threads.max(1);
self
}
pub fn seed(&self) -> u64 {
self.seed
}
pub fn count(&self) -> u64 {
self.count
}
pub fn run<T, F>(&self, sample: F) -> Vec<T>
where
F: Fn(u64, Rng) -> T + Sync,
T: Send + Default + Clone,
{
let n = self.count as usize;
let mut out = vec![T::default(); n];
let threads = self.threads.min(n.max(1));
let seed = self.seed;
if threads <= 1 {
for (i, slot) in out.iter_mut().enumerate() {
*slot = sample(i as u64, Rng::for_index(seed, i as u64));
}
return out;
}
#[cfg(not(target_family = "wasm"))]
{
let chunk = n.div_ceil(threads);
let sample = &sample;
std::thread::scope(|scope| {
for (c, slice) in out.chunks_mut(chunk).enumerate() {
let base = (c * chunk) as u64;
scope.spawn(move || {
for (k, slot) in slice.iter_mut().enumerate() {
let i = base + k as u64;
*slot = sample(i, Rng::for_index(seed, i));
}
});
}
});
out
}
#[cfg(target_family = "wasm")]
{
for (i, slot) in out.iter_mut().enumerate() {
*slot = sample(i as u64, Rng::for_index(seed, i as u64));
}
out
}
}
pub fn estimate<F>(&self, sample: F) -> Option<Estimate>
where
F: Fn(u64, Rng) -> f64 + Sync,
{
if self.count < 2 {
return None;
}
let blocks = self.blocks(
|from, to, sample| Partial::of(from, to, self.seed, sample),
&sample,
);
let total: Partial = blocks
.iter()
.copied()
.reduce(Partial::merge)
.expect("count >= 2 means at least one block");
Some(total.finish())
}
pub fn blocks<B, M, F>(&self, of_block: M, sample: &F) -> Vec<B>
where
M: Fn(u64, u64, &F) -> B + Sync,
B: Send + Default + Clone,
F: Sync,
{
let n_blocks = self.count.div_ceil(BLOCK) as usize;
let mut out = vec![B::default(); n_blocks];
let threads = self.threads.min(n_blocks.max(1));
let (count, of_block) = (self.count, &of_block);
let one = |slice: &mut [B], base: usize| {
for (k, slot) in slice.iter_mut().enumerate() {
let from = (base + k) as u64 * BLOCK;
*slot = of_block(from, (from + BLOCK).min(count), sample);
}
};
if threads <= 1 {
one(&mut out, 0);
return out;
}
#[cfg(not(target_family = "wasm"))]
{
let chunk = n_blocks.div_ceil(threads);
std::thread::scope(|scope| {
for (c, slice) in out.chunks_mut(chunk).enumerate() {
let base = c * chunk;
scope.spawn(move || one(slice, base));
}
});
out
}
#[cfg(target_family = "wasm")]
{
one(&mut out, 0);
out
}
}
}
const BLOCK: u64 = 4096;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
struct Partial {
n: f64,
mean: f64,
m2: f64,
}
impl Partial {
fn of<F: Fn(u64, Rng) -> f64>(from: u64, to: u64, seed: u64, sample: &F) -> Partial {
let mut p = Partial::default();
for i in from..to {
let x = sample(i, Rng::for_index(seed, i));
p.n += 1.0;
let delta = x - p.mean;
p.mean += delta / p.n;
p.m2 += delta * (x - p.mean);
}
p
}
fn merge(a: Partial, b: Partial) -> Partial {
if a.n == 0.0 {
return b;
}
if b.n == 0.0 {
return a;
}
let n = a.n + b.n;
let delta = b.mean - a.mean;
Partial {
n,
mean: a.mean + delta * (b.n / n),
m2: a.m2 + b.m2 + delta * delta * (a.n * b.n / n),
}
}
fn finish(self) -> Estimate {
let variance = self.m2 / (self.n - 1.0);
Estimate {
mean: self.mean,
standard_error: (variance / self.n).sqrt(),
samples: self.n as u64,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Estimate {
pub mean: f64,
pub standard_error: f64,
pub samples: u64,
}
impl Estimate {
pub fn standard_deviation(&self) -> f64 {
self.standard_error * (self.samples as f64).sqrt()
}
pub fn within(&self, k: f64, value: f64) -> bool {
(value - self.mean).abs() <= k * self.standard_error
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_answer_does_not_depend_on_how_many_threads_produced_it() {
let draw = |i: u64, mut rng: Rng| rng.gaussian() * (1.0 + i as f64 % 3.0);
let one = Ensemble::new(4242, 5_000).run(draw);
for threads in [2usize, 3, 7, 16, 64] {
let many = Ensemble::new(4242, 5_000).with_threads(threads).run(draw);
assert_eq!(one.len(), many.len());
for (i, (a, b)) in one.iter().zip(&many).enumerate() {
assert_eq!(
a.to_bits(),
b.to_bits(),
"sample {i} differed at {threads} threads: {a} against {b}"
);
}
}
let spread = one.iter().cloned().fold(f64::MIN, f64::max)
- one.iter().cloned().fold(f64::MAX, f64::min);
assert!(spread > 1.0, "the draws should vary; spread {spread}");
}
#[test]
fn the_error_falls_as_one_over_root_n() {
let err = |n: u64| {
let e = Ensemble::new(7, n)
.with_threads(4)
.estimate(|_, mut rng| rng.unit())
.expect("more than one sample");
(e.mean - 0.5).abs()
};
let (coarse, fine) = (err(4_000), err(64_000));
let ratio = coarse / fine;
assert!(
(1.5..12.0).contains(&ratio),
"16x the samples gave {ratio:.2}x the accuracy (expected about 4)"
);
let e = Ensemble::new(11, 40_000)
.with_threads(4)
.estimate(|_, mut rng| rng.unit())
.unwrap();
assert!(
e.within(3.0, 0.5),
"0.5 is {:.2} standard errors from {:.6}",
(e.mean - 0.5).abs() / e.standard_error,
e.mean
);
assert_eq!(e.samples, 40_000);
assert!(
(e.standard_deviation() - (1.0f64 / 12.0).sqrt()).abs() < 0.01,
"standard deviation {:.6}",
e.standard_deviation()
);
}
#[test]
fn a_run_too_large_to_hold_still_agrees_across_threads() {
let draw = |_: u64, mut rng: Rng| rng.unit();
let n = 10_000_000;
let one = Ensemble::new(5, n).estimate(draw).expect("plenty");
for threads in [4usize, 16] {
let many = Ensemble::new(5, n)
.with_threads(threads)
.estimate(draw)
.expect("plenty");
assert_eq!(
one.mean.to_bits(),
many.mean.to_bits(),
"mean moved at {threads} threads: {} against {}",
one.mean,
many.mean
);
assert_eq!(one.standard_error.to_bits(), many.standard_error.to_bits());
assert_eq!(one.samples, n);
}
assert!(one.within(4.0, 0.5), "mean {:.8}", one.mean);
assert!(
(one.standard_deviation() - (1.0f64 / 12.0).sqrt()).abs() < 1e-3,
"spread {:.6}",
one.standard_deviation()
);
}
#[test]
fn the_estimator_survives_a_large_mean_and_a_small_spread() {
let n = 1_000_000u64;
let e = Ensemble::new(0, n)
.with_threads(8)
.estimate(|i, _| 1e9 + (i % 2) as f64)
.expect("plenty");
assert!(
(e.mean - (1e9 + 0.5)).abs() < 1e-6,
"mean {:.6} against 1000000000.5",
e.mean
);
let want = (0.25 * n as f64 / (n as f64 - 1.0)).sqrt();
assert!(
(e.standard_deviation() / want - 1.0).abs() < 1e-9,
"spread {:.9} against {want:.9}",
e.standard_deviation()
);
}
#[test]
fn one_sample_is_not_an_estimate() {
assert!(Ensemble::new(1, 1).estimate(|_, mut r| r.unit()).is_none());
assert!(Ensemble::new(1, 0).estimate(|_, mut r| r.unit()).is_none());
assert!(Ensemble::new(1, 2).estimate(|_, mut r| r.unit()).is_some());
}
}