Skip to content

Commit 6d236fd

Browse files
authored
Merge pull request #1121 from vks/distr-test
Compare sampled normal distribution to PDF
2 parents eeeb9ea + 66da6e6 commit 6d236fd

File tree

22 files changed

+287
-49
lines changed

22 files changed

+287
-49
lines changed

benches/seq.rs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ fn seq_shuffle_100(b: &mut Bencher) {
3737
fn seq_slice_choose_1_of_1000(b: &mut Bencher) {
3838
let mut rng = SmallRng::from_rng(thread_rng()).unwrap();
3939
let x: &mut [usize] = &mut [1; 1000];
40-
for i in 0..1000 {
41-
x[i] = i;
40+
for (i, r) in x.iter_mut().enumerate() {
41+
*r = i;
4242
}
4343
b.iter(|| {
4444
let mut s = 0;
@@ -78,8 +78,8 @@ seq_slice_choose_multiple!(seq_slice_choose_multiple_90_of_100, 90, 100);
7878
fn seq_iter_choose_from_1000(b: &mut Bencher) {
7979
let mut rng = SmallRng::from_rng(thread_rng()).unwrap();
8080
let x: &mut [usize] = &mut [1; 1000];
81-
for i in 0..1000 {
82-
x[i] = i;
81+
for (i, r) in x.iter_mut().enumerate() {
82+
*r = i;
8383
}
8484
b.iter(|| {
8585
let mut s = 0;
@@ -172,11 +172,11 @@ macro_rules! sample_indices {
172172
sample_indices!(misc_sample_indices_1_of_1k, sample, 1, 1000);
173173
sample_indices!(misc_sample_indices_10_of_1k, sample, 10, 1000);
174174
sample_indices!(misc_sample_indices_100_of_1k, sample, 100, 1000);
175-
sample_indices!(misc_sample_indices_100_of_1M, sample, 100, 1000_000);
176-
sample_indices!(misc_sample_indices_100_of_1G, sample, 100, 1000_000_000);
177-
sample_indices!(misc_sample_indices_200_of_1G, sample, 200, 1000_000_000);
178-
sample_indices!(misc_sample_indices_400_of_1G, sample, 400, 1000_000_000);
179-
sample_indices!(misc_sample_indices_600_of_1G, sample, 600, 1000_000_000);
175+
sample_indices!(misc_sample_indices_100_of_1M, sample, 100, 1_000_000);
176+
sample_indices!(misc_sample_indices_100_of_1G, sample, 100, 1_000_000_000);
177+
sample_indices!(misc_sample_indices_200_of_1G, sample, 200, 1_000_000_000);
178+
sample_indices!(misc_sample_indices_400_of_1G, sample, 400, 1_000_000_000);
179+
sample_indices!(misc_sample_indices_600_of_1G, sample, 600, 1_000_000_000);
180180

181181
macro_rules! sample_indices_rand_weights {
182182
($name:ident, $amount:expr, $length:expr) => {
@@ -193,8 +193,8 @@ macro_rules! sample_indices_rand_weights {
193193
sample_indices_rand_weights!(misc_sample_weighted_indices_1_of_1k, 1, 1000);
194194
sample_indices_rand_weights!(misc_sample_weighted_indices_10_of_1k, 10, 1000);
195195
sample_indices_rand_weights!(misc_sample_weighted_indices_100_of_1k, 100, 1000);
196-
sample_indices_rand_weights!(misc_sample_weighted_indices_100_of_1M, 100, 1000_000);
197-
sample_indices_rand_weights!(misc_sample_weighted_indices_200_of_1M, 200, 1000_000);
198-
sample_indices_rand_weights!(misc_sample_weighted_indices_400_of_1M, 400, 1000_000);
199-
sample_indices_rand_weights!(misc_sample_weighted_indices_600_of_1M, 600, 1000_000);
200-
sample_indices_rand_weights!(misc_sample_weighted_indices_1k_of_1M, 1000, 1000_000);
196+
sample_indices_rand_weights!(misc_sample_weighted_indices_100_of_1M, 100, 1_000_000);
197+
sample_indices_rand_weights!(misc_sample_weighted_indices_200_of_1M, 200, 1_000_000);
198+
sample_indices_rand_weights!(misc_sample_weighted_indices_400_of_1M, 400, 1_000_000);
199+
sample_indices_rand_weights!(misc_sample_weighted_indices_600_of_1M, 600, 1_000_000);
200+
sample_indices_rand_weights!(misc_sample_weighted_indices_1k_of_1M, 1000, 1_000_000);

rand_core/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,7 @@ mod test {
485485
// This is the binomial distribution B(64, 0.5), so chance of
486486
// weight < 20 is binocdf(19, 64, 0.5) = 7.8e-4, and same for
487487
// weight > 44.
488-
assert!(weight >= 20 && weight <= 44);
488+
assert!((20..=44).contains(&weight));
489489

490490
for (i2, r2) in results.iter().enumerate() {
491491
if i1 == i2 {

rand_distr/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ std_math = ["num-traits/std"]
2828
[dev-dependencies]
2929
rand_pcg = { version = "0.3.0", path = "../rand_pcg" }
3030
# For inline examples
31-
rand = { path = "..", version = "0.8.0", default-features = false, features = ["std_rng", "std"] }
31+
rand = { path = "..", version = "0.8.0", default-features = false, features = ["std_rng", "std", "small_rng"] }
3232
# Histogram implementation for testing uniformity
33-
average = { version = "0.12", features = [ "std" ] }
33+
average = { version = "0.13", features = [ "std" ] }

rand_distr/benches/distributions.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ distr_int!(distr_geometric, u64, Geometric::new(0.5).unwrap());
144144
distr_int!(distr_standard_geometric, u64, StandardGeometric);
145145

146146
#[bench]
147+
#[allow(clippy::approx_constant)]
147148
fn dist_iter(b: &mut Bencher) {
148149
let mut rng = Pcg64Mcg::from_entropy();
149150
let distr = Normal::new(-2.71828, 3.14159).unwrap();
@@ -177,4 +178,4 @@ sample_binomial!(misc_binomial_1, 1, 0.9);
177178
sample_binomial!(misc_binomial_10, 10, 0.9);
178179
sample_binomial!(misc_binomial_100, 100, 0.99);
179180
sample_binomial!(misc_binomial_1000, 1000, 0.01);
180-
sample_binomial!(misc_binomial_1e12, 1000_000_000_000, 0.2);
181+
sample_binomial!(misc_binomial_1e12, 1_000_000_000_000, 0.2);

rand_distr/src/cauchy.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,9 @@ mod test {
107107
let mut rng = crate::test::rng(123);
108108
let mut numbers: [f64; 1000] = [0.0; 1000];
109109
let mut sum = 0.0;
110-
for i in 0..1000 {
111-
numbers[i] = cauchy.sample(&mut rng);
112-
sum += numbers[i];
110+
for number in &mut numbers[..] {
111+
*number = cauchy.sample(&mut rng);
112+
sum += *number;
113113
}
114114
let median = median(&mut numbers);
115115
#[cfg(feature = "std")]

rand_distr/src/gamma.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99

1010
//! The Gamma and derived distributions.
1111
12+
// We use the variable names from the published reference, therefore this
13+
// warning is not helpful.
14+
#![allow(clippy::many_single_char_names)]
15+
1216
use self::ChiSquaredRepr::*;
1317
use self::GammaRepr::*;
1418

rand_distr/src/hypergeometric.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ impl Hypergeometric {
221221
}
222222
};
223223

224-
Ok(Hypergeometric { n1, n2, k, sign_x, offset_x, sampling_method })
224+
Ok(Hypergeometric { n1, n2, k, offset_x, sign_x, sampling_method })
225225
}
226226
}
227227

rand_distr/src/weighted_alias.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ mod test {
466466
weights[ZERO_WEIGHT_INDEX as usize] = W::ZERO;
467467
weights
468468
};
469-
let weight_sum = weights.iter().map(|w| *w).sum::<W>();
469+
let weight_sum = weights.iter().copied().sum::<W>();
470470
let expected_counts = weights
471471
.iter()
472472
.map(|&w| w_to_f64(w) / w_to_f64(weight_sum) * NUM_SAMPLES as f64)

rand_distr/tests/pdf.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright 2021 Developers of the Rand project.
2+
//
3+
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4+
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5+
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6+
// option. This file may not be copied, modified, or distributed
7+
// except according to those terms.
8+
9+
#![allow(clippy::float_cmp)]
10+
11+
use average::Histogram;
12+
use rand::{Rng, SeedableRng};
13+
use rand_distr::Normal;
14+
15+
const HIST_LEN: usize = 100;
16+
average::define_histogram!(hist, crate::HIST_LEN);
17+
use hist::Histogram as Histogram100;
18+
19+
mod sparkline;
20+
21+
#[test]
22+
fn normal() {
23+
const N_SAMPLES: u64 = 1_000_000;
24+
const MEAN: f64 = 2.;
25+
const STD_DEV: f64 = 0.5;
26+
const MIN_X: f64 = -1.;
27+
const MAX_X: f64 = 5.;
28+
29+
let dist = Normal::new(MEAN, STD_DEV).unwrap();
30+
let mut hist = Histogram100::with_const_width(MIN_X,MAX_X);
31+
let mut rng = rand::rngs::SmallRng::seed_from_u64(1);
32+
33+
for _ in 0..N_SAMPLES {
34+
let _ = hist.add(rng.sample(dist)); // Ignore out-of-range values
35+
}
36+
37+
println!("Sampled normal distribution:\n{}",
38+
sparkline::render_u64_as_string(hist.bins()));
39+
40+
fn pdf(x: f64) -> f64 {
41+
(-0.5 * ((x - MEAN) / STD_DEV).powi(2)).exp() /
42+
(STD_DEV * (2. * core::f64::consts::PI).sqrt())
43+
}
44+
45+
let mut bin_centers = hist.centers();
46+
let mut expected = [0.; HIST_LEN];
47+
for e in &mut expected[..] {
48+
*e = pdf(bin_centers.next().unwrap());
49+
}
50+
51+
println!("Expected normal distribution:\n{}",
52+
sparkline::render_u64_as_string(hist.bins()));
53+
54+
let mut diff = [0.; HIST_LEN];
55+
for (i, n) in hist.normalized_bins().enumerate() {
56+
let bin = (n as f64) / (N_SAMPLES as f64) ;
57+
diff[i] = (bin - expected[i]).abs();
58+
}
59+
60+
println!("Difference:\n{}",
61+
sparkline::render_f64_as_string(&diff[..]));
62+
println!("max diff: {:?}", diff.iter().fold(
63+
core::f64::NEG_INFINITY, |a, &b| a.max(b)));
64+
65+
// Check that the differences are significantly smaller than the expected error.
66+
let mut expected_error = [0.; HIST_LEN];
67+
// Calculate error from histogram
68+
for (err, var) in expected_error.iter_mut().zip(hist.variances()) {
69+
*err = var.sqrt() / (N_SAMPLES as f64);
70+
}
71+
// Normalize error by bin width
72+
for (err, width) in expected_error.iter_mut().zip(hist.widths()) {
73+
*err /= width;
74+
}
75+
// TODO: Calculate error from distribution cutoff / normalization
76+
77+
println!("max expected_error: {:?}", expected_error.iter().fold(
78+
core::f64::NEG_INFINITY, |a, &b| a.max(b)));
79+
for (&d, &e) in diff.iter().zip(expected_error.iter()) {
80+
// Difference larger than 3 standard deviations or cutoff
81+
let tol = (3. * e).max(1e-4);
82+
if d > tol {
83+
panic!("Difference = {} * tol", d / tol);
84+
}
85+
}
86+
}

rand_distr/tests/sparkline.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright 2021 Developers of the Rand project.
2+
//
3+
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4+
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5+
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6+
// option. This file may not be copied, modified, or distributed
7+
// except according to those terms.
8+
9+
/// Number of ticks.
10+
const N: usize = 8;
11+
/// Ticks used for the sparkline.
12+
static TICKS: [char; N] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
13+
14+
/// Render a sparkline of `data` into `buffer`.
15+
pub fn render_u64(data: &[u64], buffer: &mut String) {
16+
match data.len() {
17+
0 => {
18+
return;
19+
},
20+
1 => {
21+
if data[0] == 0 {
22+
buffer.push(TICKS[0]);
23+
} else {
24+
buffer.push(TICKS[N - 1]);
25+
}
26+
return;
27+
},
28+
_ => {},
29+
}
30+
let max = data.iter().max().unwrap();
31+
let min = data.iter().min().unwrap();
32+
let scale = ((N - 1) as f64) / ((max - min) as f64);
33+
for i in data {
34+
let tick = (((i - min) as f64) * scale) as usize;
35+
buffer.push(TICKS[tick]);
36+
}
37+
}
38+
39+
/// Calculate the required capacity for the sparkline, given the length of the
40+
/// input data.
41+
pub fn required_capacity(len: usize) -> usize {
42+
len * TICKS[0].len_utf8()
43+
}
44+
45+
/// Render a sparkline of `data` into a newly allocated string.
46+
pub fn render_u64_as_string(data: &[u64]) -> String {
47+
let cap = required_capacity(data.len());
48+
let mut s = String::with_capacity(cap);
49+
render_u64(data, &mut s);
50+
debug_assert_eq!(s.capacity(), cap);
51+
s
52+
}
53+
54+
/// Render a sparkline of `data` into `buffer`.
55+
pub fn render_f64(data: &[f64], buffer: &mut String) {
56+
match data.len() {
57+
0 => {
58+
return;
59+
},
60+
1 => {
61+
if data[0] == 0. {
62+
buffer.push(TICKS[0]);
63+
} else {
64+
buffer.push(TICKS[N - 1]);
65+
}
66+
return;
67+
},
68+
_ => {},
69+
}
70+
for x in data {
71+
assert!(x.is_finite(), "can only render finite values");
72+
}
73+
let max = data.iter().fold(
74+
core::f64::NEG_INFINITY, |a, &b| a.max(b));
75+
let min = data.iter().fold(
76+
core::f64::INFINITY, |a, &b| a.min(b));
77+
let scale = ((N - 1) as f64) / (max - min);
78+
for x in data {
79+
let tick = ((x - min) * scale) as usize;
80+
buffer.push(TICKS[tick]);
81+
}
82+
}
83+
84+
/// Render a sparkline of `data` into a newly allocated string.
85+
pub fn render_f64_as_string(data: &[f64]) -> String {
86+
let cap = required_capacity(data.len());
87+
let mut s = String::with_capacity(cap);
88+
render_f64(data, &mut s);
89+
debug_assert_eq!(s.capacity(), cap);
90+
s
91+
}
92+
93+
#[cfg(test)]
94+
mod tests {
95+
#[test]
96+
fn render_u64() {
97+
let data = [2, 250, 670, 890, 2, 430, 11, 908, 123, 57];
98+
let mut s = String::with_capacity(super::required_capacity(data.len()));
99+
super::render_u64(&data, &mut s);
100+
println!("{}", s);
101+
assert_eq!("▁▂▆▇▁▄▁█▁▁", &s);
102+
}
103+
104+
#[test]
105+
fn render_u64_as_string() {
106+
let data = [2, 250, 670, 890, 2, 430, 11, 908, 123, 57];
107+
let s = super::render_u64_as_string(&data);
108+
println!("{}", s);
109+
assert_eq!("▁▂▆▇▁▄▁█▁▁", &s);
110+
}
111+
112+
#[test]
113+
fn render_f64() {
114+
let data = [2., 250., 670., 890., 2., 430., 11., 908., 123., 57.];
115+
let mut s = String::with_capacity(super::required_capacity(data.len()));
116+
super::render_f64(&data, &mut s);
117+
println!("{}", s);
118+
assert_eq!("▁▂▆▇▁▄▁█▁▁", &s);
119+
}
120+
121+
#[test]
122+
fn render_f64_as_string() {
123+
let data = [2., 250., 670., 890., 2., 430., 11., 908., 123., 57.];
124+
let s = super::render_f64_as_string(&data);
125+
println!("{}", s);
126+
assert_eq!("▁▂▆▇▁▄▁█▁▁", &s);
127+
}
128+
}

rand_distr/tests/uniformity.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
// option. This file may not be copied, modified, or distributed
77
// except according to those terms.
88

9+
#![allow(clippy::float_cmp)]
10+
911
use average::Histogram;
1012
use rand::prelude::*;
1113

rand_hc/src/hc128.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
// option. This file may not be copied, modified, or distributed
77
// except according to those terms.
88

9+
// Disable some noisy clippy lints.
10+
#![allow(clippy::many_single_char_names)]
11+
#![allow(clippy::identity_op)]
12+
// Disable a lint that cannot be fixed without increasing the MSRV
13+
#![allow(clippy::op_ref)]
14+
915
//! The HC-128 random number generator.
1016
1117
use core::fmt;

src/distributions/bernoulli.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ impl Bernoulli {
9696
/// 2<sup>-64</sup> in `[0, 1]` can be represented as a `f64`.)
9797
#[inline]
9898
pub fn new(p: f64) -> Result<Bernoulli, BernoulliError> {
99-
if !(p >= 0.0 && p < 1.0) {
99+
if !(0.0..1.0).contains(&p) {
100100
if p == 1.0 {
101101
return Ok(Bernoulli { p_int: ALWAYS_TRUE });
102102
}
@@ -157,6 +157,9 @@ mod test {
157157

158158
#[test]
159159
fn test_trivial() {
160+
// We prefer to be explicit here.
161+
#![allow(clippy::bool_assert_comparison)]
162+
160163
let mut r = crate::test::rng(1);
161164
let always_false = Bernoulli::new(0.0).unwrap();
162165
let always_true = Bernoulli::new(1.0).unwrap();

0 commit comments

Comments
 (0)