blob: e20e5281cda36255efa61e39c055e4bd9bc2a715 [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2016 The Fuchsia Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import math
import random
class Laplacian(object):
""" A generator of pseudorandom numbers from a Laplacian distribution.
"""
def __init__(self, b):
"""
Args:
b {float} A positive scaling factor. The distribution will have
mu = 0 and sigma = sqrt(2) * b.
"""
if b <= 0:
raise Exception("b must be positive: %f" % b)
self.b = b
self.rand = random.SystemRandom()
def sample(self):
""" Returns the next sample from the distribution.
"""
# The PDF for the distribution we are sampling is:
#
# { (1/2b) exp(x/b) ; if x < 0
# f(x) = |
# { (1/2b) exp(-x/b) ; if x >= 0
#
# The CDF is therefore:
#
# { (1/2)exp(x/b) ; if x < 0
# F(x) = |
# { 1 - (1/2)exp(-x/b) ; if x >= 0
#
# Now let u be a uniform random variable on (-1, 1)
# and define Y by
# { b ln(1 + u) ; if u < 0
# Y = |
# { -b ln(1 - u) ; if u > 0
#
# We claim that Y has the distribution we want to sample.
# To see this we show that the CDF of Y is equal to F(X).
#
# Y < x
# iff
# u < 0 and b ln(1 + u) < x or u >=0 and -b ln(1 - u) > x
# iff
# u < 0 and u < exp(x/b) - 1 or u >= 0 and u < 1 - exp(-x/b)
#
# First consider the case x >= 0. Then Y < x iff
# u < 0 or u >= 0 and u < 1 - exp(-x/b)
# So P(Y < x) = 1/2 + 1/2 [1 - exp(-x/b)] = 1 - (1/2)exp(-x/b) = F(x)
#
# Now consider the case x < 0. Then Y < x iff
# u > -1 and u < exp(x/b) - 1
# So P(Y < x) = 1/2 (exp(x/b) - 1 - -1) = 1/2 exp(x/b) = F(x)
u = self.rand.uniform(-1.0, 1.0)
if u >= 0:
return -1 * self.b * math.log(1.0 - u)
else:
return self.b * math.log(u + 1.0)
def main():
# This main function is a manual test of the code in this file.
# P(|Laplacian(1)| > 1) = 0.368
# P(|Laplacian(1)| > 2) = 0.135
# TODO(rudominer, mironov) Add more in-depth testing. In particular consider
# the chi-squared test written by mironov@ in the RAPPOR Github repo.
laplacian_1 = Laplacian(1)
count1 = 0
count2 = 0
for i in xrange(10000):
x = laplacian_1.sample()
if abs(x) > 1:
count1 = count1 + 1
if abs(x) > 2:
count2 = count2 + 1
print float(count1) / 10000
print float(count2) / 10000
# P(|Laplacian(2)| > 1) = 0.607
# P(|Laplacian(2)| > 2) = 0.368
laplacian_2 = Laplacian(2)
count1 = 0
count2 = 0
for i in xrange(10000):
x = laplacian_2.sample()
if abs(x) > 1:
count1 = count1 + 1
if abs(x) > 2:
count2 = count2 + 1
print float(count1) / 10000
print float(count2) / 10000
if __name__ == '__main__':
main()