Skip to content

Commit d3e7c74

Browse files
committed
auto merge of #6162 : graydon/rust/random-retry, r=graydon
Sample from the normal and exponential distributions using the Ziggurat algorithm.
2 parents 9411547 + 1eb5efc commit d3e7c74

File tree

4 files changed

+687
-0
lines changed

4 files changed

+687
-0
lines changed

src/etc/ziggurat_tables.py

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#!/usr/bin/env python
2+
# xfail-license
3+
4+
# This creates the tables used for distributions implemented using the
5+
# ziggurat algorithm in `core::rand::distributions;`. They are
6+
# (basically) the tables as used in the ZIGNOR variant (Doornik 2005).
7+
# They are changed rarely, so the generated file should be checked in
8+
# to git.
9+
#
10+
# It creates 3 tables: X as in the paper, F which is f(x_i), and
11+
# F_DIFF which is f(x_i) - f(x_{i-1}). The latter two are just cached
12+
# values which is not done in that paper (but is done in other
13+
# variants). Note that the adZigR table is unnecessary because of
14+
# algebra.
15+
#
16+
# It is designed to be compatible with Python 2 and 3.
17+
18+
from math import exp, sqrt, log, floor
19+
import random
20+
21+
# The order should match the return value of `tables`
22+
TABLE_NAMES = ['X', 'F', 'F_DIFF']
23+
24+
# The actual length of the table is 1 more, to stop
25+
# index-out-of-bounds errors. This should match the bitwise operation
26+
# to find `i` in `zigurrat` in `libstd/rand/mod.rs`. Also the *_R and
27+
# *_V constants below depend on this value.
28+
TABLE_LEN = 256
29+
30+
# equivalent to `zigNorInit` in Doornik2005, but generalised to any
31+
# distribution. r = dR, v = dV, f = probability density function,
32+
# f_inv = inverse of f
33+
def tables(r, v, f, f_inv):
34+
# compute the x_i
35+
xvec = [0]*(TABLE_LEN+1)
36+
37+
xvec[0] = v / f(r)
38+
xvec[1] = r
39+
40+
for i in range(2, TABLE_LEN):
41+
last = xvec[i-1]
42+
xvec[i] = f_inv(v / last + f(last))
43+
44+
# cache the f's
45+
fvec = [0]*(TABLE_LEN+1)
46+
fdiff = [0]*(TABLE_LEN+1)
47+
for i in range(TABLE_LEN+1):
48+
fvec[i] = f(xvec[i])
49+
if i > 0:
50+
fdiff[i] = fvec[i] - fvec[i-1]
51+
52+
return xvec, fvec, fdiff
53+
54+
# Distributions
55+
# N(0, 1)
56+
def norm_f(x):
57+
return exp(-x*x/2.0)
58+
def norm_f_inv(y):
59+
return sqrt(-2.0*log(y))
60+
61+
NORM_R = 3.6541528853610088
62+
NORM_V = 0.00492867323399
63+
64+
NORM = tables(NORM_R, NORM_V,
65+
norm_f, norm_f_inv)
66+
67+
# Exp(1)
68+
def exp_f(x):
69+
return exp(-x)
70+
def exp_f_inv(y):
71+
return -log(y)
72+
73+
EXP_R = 7.69711747013104972
74+
EXP_V = 0.0039496598225815571993
75+
76+
EXP = tables(EXP_R, EXP_V,
77+
exp_f, exp_f_inv)
78+
79+
80+
# Output the tables/constants/types
81+
82+
def render_static(name, type, value):
83+
# no space or
84+
return 'pub static %s: %s =%s;\n' % (name, type, value)
85+
86+
# static `name`: [`type`, .. `len(values)`] =
87+
# [values[0], ..., values[3],
88+
# values[4], ..., values[7],
89+
# ... ];
90+
def render_table(name, values):
91+
rows = []
92+
# 4 values on each row
93+
for i in range(0, len(values), 4):
94+
row = values[i:i+4]
95+
rows.append(', '.join('%.18f' % f for f in row))
96+
97+
rendered = '\n [%s]' % ',\n '.join(rows)
98+
return render_static(name, '[f64, .. %d]' % len(values), rendered)
99+
100+
101+
with open('ziggurat_tables.rs', 'w') as f:
102+
f.write('''// Copyright 2013 The Rust Project Developers. See the COPYRIGHT
103+
// file at the top-level directory of this distribution and at
104+
// http://rust-lang.org/COPYRIGHT.
105+
//
106+
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
107+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
108+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
109+
// option. This file may not be copied, modified, or distributed
110+
// except according to those terms.
111+
112+
// Tables for distributions which are sampled using the ziggurat
113+
// algorithm. Autogenerated by `ziggurat_tables.py`.
114+
115+
pub type ZigTable = &\'static [f64, .. %d];
116+
''' % (TABLE_LEN + 1))
117+
for name, tables, r in [('NORM', NORM, NORM_R),
118+
('EXP', EXP, EXP_R)]:
119+
f.write(render_static('ZIG_%s_R' % name, 'f64', ' %.18f' % r))
120+
for (tabname, table) in zip(TABLE_NAMES, tables):
121+
f.write(render_table('ZIG_%s_%s' % (name, tabname), table))

src/libcore/rand.rs

+6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ and so can be used to generate any type that implements `Rand`. Type inference
1616
means that often a simple call to `rand::random()` or `rng.gen()` will
1717
suffice, but sometimes an annotation is required, e.g. `rand::random::<float>()`.
1818
19+
See the `distributions` submodule for sampling random numbers from
20+
distributions like normal and exponential.
21+
1922
# Examples
2023
~~~
2124
use core::rand::RngUtil;
@@ -47,6 +50,9 @@ use util;
4750
use vec;
4851
use libc::size_t;
4952

53+
#[path="rand/distributions.rs"]
54+
pub mod distributions;
55+
5056
/// A type that can be randomly generated using an Rng
5157
pub trait Rand {
5258
fn rand<R: Rng>(rng: &R) -> Self;

src/libcore/rand/distributions.rs

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// Copyright 2013 The Rust Project Developers. See the COPYRIGHT
2+
// file at the top-level directory of this distribution and at
3+
// http://rust-lang.org/COPYRIGHT.
4+
//
5+
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8+
// option. This file may not be copied, modified, or distributed
9+
// except according to those terms.
10+
11+
//! Sampling from random distributions
12+
13+
// Some implementations use the Ziggurat method
14+
// https://en.wikipedia.org/wiki/Ziggurat_algorithm
15+
//
16+
// The version used here is ZIGNOR [Doornik 2005, "An Improved
17+
// Ziggurat Method to Generate Normal Random Samples"] which is slower
18+
// (about double, it generates an extra random number) than the
19+
// canonical version [Marsaglia & Tsang 2000, "The Ziggurat Method for
20+
// Generating Random Variables"], but more robust. If one wanted, one
21+
// could implement VIZIGNOR the ZIGNOR paper for more speed.
22+
23+
use prelude::*;
24+
use rand::{Rng,Rand};
25+
26+
mod ziggurat_tables;
27+
28+
// inlining should mean there is no performance penalty for this
29+
#[inline(always)]
30+
fn ziggurat<R:Rng>(rng: &R,
31+
center_u: bool,
32+
X: ziggurat_tables::ZigTable,
33+
F: ziggurat_tables::ZigTable,
34+
F_DIFF: ziggurat_tables::ZigTable,
35+
pdf: &'static fn(f64) -> f64, // probability density function
36+
zero_case: &'static fn(&R, f64) -> f64) -> f64 {
37+
loop {
38+
let u = if center_u {2.0 * rng.gen() - 1.0} else {rng.gen()};
39+
let i: uint = rng.gen::<uint>() & 0xff;
40+
let x = u * X[i];
41+
42+
let test_x = if center_u {f64::abs(x)} else {x};
43+
44+
// algebraically equivalent to |u| < X[i+1]/X[i] (or u < X[i+1]/X[i])
45+
if test_x < X[i + 1] {
46+
return x;
47+
}
48+
if i == 0 {
49+
return zero_case(rng, u);
50+
}
51+
// algebraically equivalent to f1 + DRanU()*(f0 - f1) < 1
52+
if F[i+1] + F_DIFF[i+1] * rng.gen() < pdf(x) {
53+
return x;
54+
}
55+
}
56+
}
57+
58+
/// A wrapper around an `f64` to generate N(0, 1) random numbers (a.k.a. a
59+
/// standard normal, or Gaussian). Multiplying the generated values by the
60+
/// desired standard deviation `sigma` then adding the desired mean `mu` will
61+
/// give N(mu, sigma^2) distributed random numbers.
62+
///
63+
/// Note that this has to be unwrapped before use as an `f64` (using either
64+
/// `*` or `cast::transmute` is safe).
65+
///
66+
/// # Example
67+
///
68+
/// ~~~
69+
/// use core::rand::distributions::StandardNormal;
70+
///
71+
/// fn main() {
72+
/// let normal = 2.0 + (*rand::random::<StandardNormal>()) * 3.0;
73+
/// println(fmt!("%f is from a N(2, 9) distribution", normal))
74+
/// }
75+
/// ~~~
76+
pub struct StandardNormal(f64);
77+
78+
impl Rand for StandardNormal {
79+
fn rand<R:Rng>(rng: &R) -> StandardNormal {
80+
#[inline(always)]
81+
fn pdf(x: f64) -> f64 {
82+
f64::exp((-x*x/2.0) as f64) as f64
83+
}
84+
#[inline(always)]
85+
fn zero_case<R:Rng>(rng: &R, u: f64) -> f64 {
86+
// compute a random number in the tail by hand
87+
88+
// strange initial conditions, because the loop is not
89+
// do-while, so the condition should be true on the first
90+
// run, they get overwritten anyway (0 < 1, so these are
91+
// good).
92+
let mut x = 1.0, y = 0.0;
93+
94+
// XXX infinities?
95+
while -2.0*y < x * x {
96+
x = f64::ln(rng.gen()) / ziggurat_tables::ZIG_NORM_R;
97+
y = f64::ln(rng.gen());
98+
}
99+
if u < 0.0 {x-ziggurat_tables::ZIG_NORM_R} else {ziggurat_tables::ZIG_NORM_R-x}
100+
}
101+
102+
StandardNormal(ziggurat(
103+
rng,
104+
true, // this is symmetric
105+
&ziggurat_tables::ZIG_NORM_X,
106+
&ziggurat_tables::ZIG_NORM_F, &ziggurat_tables::ZIG_NORM_F_DIFF,
107+
pdf, zero_case))
108+
}
109+
}
110+
111+
/// A wrapper around an `f64` to generate Exp(1) random numbers. Dividing by
112+
/// the desired rate `lambda` will give Exp(lambda) distributed random
113+
/// numbers.
114+
///
115+
/// Note that this has to be unwrapped before use as an `f64` (using either
116+
/// `*` or `cast::transmute` is safe).
117+
///
118+
/// # Example
119+
///
120+
/// ~~~
121+
/// use core::rand::distributions::Exp1;
122+
///
123+
/// fn main() {
124+
/// let exp2 = (*rand::random::<Exp1>()) * 0.5;
125+
/// println(fmt!("%f is from a Exp(2) distribution", exp2));
126+
/// }
127+
/// ~~~
128+
pub struct Exp1(f64);
129+
130+
// This could be done via `-f64::ln(rng.gen::<f64>())` but that is slower.
131+
impl Rand for Exp1 {
132+
#[inline]
133+
fn rand<R:Rng>(rng: &R) -> Exp1 {
134+
#[inline(always)]
135+
fn pdf(x: f64) -> f64 {
136+
f64::exp(-x)
137+
}
138+
#[inline(always)]
139+
fn zero_case<R:Rng>(rng: &R, _u: f64) -> f64 {
140+
ziggurat_tables::ZIG_EXP_R - f64::ln(rng.gen())
141+
}
142+
143+
Exp1(ziggurat(rng, false,
144+
&ziggurat_tables::ZIG_EXP_X,
145+
&ziggurat_tables::ZIG_EXP_F, &ziggurat_tables::ZIG_EXP_F_DIFF,
146+
pdf, zero_case))
147+
}
148+
}

0 commit comments

Comments
 (0)