blob: ab858a360fd5da21ea665ae7950fc1790e36b2e8 [file] [log] [blame]
//
// Copyright 2020 Google LLC
//
// 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.
//
package noise
import (
"math"
"testing"
"github.com/grd/stat"
)
func TestLaplaceStatistics(t *testing.T) {
const numberOfSamples = 125000
for _, tc := range []struct {
l0Sensitivity int64
lInfSensitivity, epsilon, mean, variance float64
}{
{
l0Sensitivity: 1,
lInfSensitivity: 1.0,
epsilon: 1.0,
mean: 0.0,
variance: 2.0,
},
{
l0Sensitivity: 1,
lInfSensitivity: 1.0,
epsilon: ln3,
mean: 0.0,
variance: 2.0 / (ln3 * ln3),
},
{
l0Sensitivity: 1,
lInfSensitivity: 1.0,
epsilon: ln3,
mean: 45941223.02107,
variance: 2.0 / (ln3 * ln3),
},
{
l0Sensitivity: 1,
lInfSensitivity: 1.0,
epsilon: 2.0 * ln3,
mean: 0.0,
variance: 2.0 / (2.0 * ln3 * 2.0 * ln3),
},
{
l0Sensitivity: 1,
lInfSensitivity: 2.0,
epsilon: 2.0 * ln3,
mean: 0.0,
variance: 2.0 / (ln3 * ln3),
},
{
l0Sensitivity: 2,
lInfSensitivity: 1.0,
epsilon: 2.0 * ln3,
mean: 0.0,
variance: 2.0 / (ln3 * ln3),
},
} {
noisedSamples := make(stat.Float64Slice, numberOfSamples)
for i := 0; i < numberOfSamples; i++ {
noisedSamples[i] = lap.AddNoiseFloat64(tc.mean, tc.l0Sensitivity, tc.lInfSensitivity, tc.epsilon, 0)
}
sampleMean, sampleVariance := stat.Mean(noisedSamples), stat.Variance(noisedSamples)
// Assuming that the Laplace samples have a mean of 0 and the specified variance of tc.variance,
// sampleMeanFloat64 and sampleMeanInt64 are approximately Gaussian distributed with a mean of 0
// and standard deviation of sqrt(tc.variance⁻ / numberOfSamples).
//
// The meanErrorTolerance is set to the 99.9995% quantile of the anticipated distribution. Thus,
// the test falsely rejects with a probability of 10⁻⁵.
meanErrorTolerance := 4.41717 * math.Sqrt(tc.variance/float64(numberOfSamples))
// Assuming that the Laplace samples have the specified variance of tc.variance, sampleVarianceFloat64
// and sampleVarianceInt64 are approximately Gaussian distributed with a mean of tc.variance and a
// standard deviation of sqrt(5) * tc.variance / sqrt(numberOfSamples).
//
// The varianceErrorTolerance is set to the 99.9995% quantile of the anticipated distribution. Thus,
// the test falsely rejects with a probability of 10⁻⁵.
varianceErrorTolerance := 4.41717 * math.Sqrt(5.0) * tc.variance / math.Sqrt(float64(numberOfSamples))
if !nearEqual(sampleMean, tc.mean, meanErrorTolerance) {
t.Errorf("float64 got mean = %f, want %f (parameters %+v)", sampleMean, tc.mean, tc)
}
if !nearEqual(sampleVariance, tc.variance, varianceErrorTolerance) {
t.Errorf("float64 got variance = %f, want %f (parameters %+v)", sampleVariance, tc.variance, tc)
}
}
}
func TestThresholdLaplace(t *testing.T) {
// For the l0Sensitivity=1 cases, we make certain that we have implemented
// both tails of the Laplace distribution. To do so, we write tests in pairs by
// reflecting the value of delta around the axis 0.5 and the threshold "want"
// value around the axis lInfSensitivity.
//
// This symmetry in the CDF is exploited by implicitly reflecting
// partitionDelta (the per-partition delta) around the 0.5 axis. When
// l0Sensitivity is 1, this can be easily expressed in tests since delta ==
// partitionDelta. When l0Sensitivity != 1, it is no longer easy to express.
for _, tc := range []struct {
l0Sensitivity int64
lInfSensitivity, epsilon, delta, want float64
}{
// Base test case
{1, 1, ln3, 1e-10, 21.33},
{1, 1, ln3, 1 - 1e-10, -19.33},
// Scale lambda
{1, 1, 2 * ln3, 1e-10, 11.16},
{1, 1, 2 * ln3, 1 - 1e-10, -9.16},
// Scale lInfSensitivity and lambda
{1, 10, ln3, 1e-10, 213.28},
{1, 10, ln3, 1 - 1e-10, -193.28},
// Scale l0Sensitivity
{10, 10, 10 * ln3, 1e-9, 213.28},
{10, 10, 10 * ln3, 1 - 1e-9, -2.55}, // l0Sensitivity != 1, not expecting symmetry in "want" threhsold around lInfSensitivity.
// High precision delta case
{1, 1, ln3, 1e-200, 419.55},
} {
got := lap.Threshold(tc.l0Sensitivity, tc.lInfSensitivity, tc.epsilon, 0, tc.delta)
if !nearEqual(got, tc.want, 0.01) {
t.Errorf("ThresholdForLaplace(%d,%f,%f,%e)=%f, want %f", tc.l0Sensitivity, tc.lInfSensitivity, tc.epsilon, tc.delta, got, tc.want)
}
}
}
func TestDeltaForThresholdLaplace(t *testing.T) {
// For the l0Sensitivity=1 cases, we make certain that we have implemented
// both tails of the Laplace distribution. To do so, we write tests in pairs by
// reflecting the "want" value of delta around the axis 0.5 and the threshold
// value k around the axis lInfSensitivity.
//
// This symmetry in the CDF is exploited by implicitly reflecting
// partitionDelta (the per-partition delta) around the 0.5 axis. When
// l0Sensitivity is 1, this can be easily expressed in tests since delta ==
// partitionDelta. When l0Sensitivity != 1, it is no longer easy to express.
for _, tc := range []struct {
l0Sensitivity int64
lInfSensitivity, epsilon, k, want float64
}{
// Base test case
{1, 1, ln3, 20, 4.3e-10},
{1, 1, ln3, -18, 1 - 4.3e-10},
// Scale lInfSensitivity, lambda, and k
{1, 10, ln3, 200, 4.3e-10},
{1, 10, ln3, -180, 1 - 4.3e-10},
// Scale lambda and k
{1, 1, 2 * ln3, 10, 1.29e-9},
{1, 1, 2 * ln3, -8, 1 - 1.29e-9},
// Scale l0Sensitivity
{10, 1, 10 * ln3, 20, 4.3e-9},
{10, 1, 10 * ln3, -18, 1}, // l0Sensitivity != 1, not expecting symmetry in "want" delta around 0.5.
// High precision delta case
{1, 1, ln3, 419.55, 1e-200},
} {
got := lap.(laplace).DeltaForThreshold(tc.l0Sensitivity, tc.lInfSensitivity, tc.epsilon, 0, tc.k)
if !nearEqual(got, tc.want, 1e-2*tc.want) {
t.Errorf("ThresholdForLaplace(%d,%f,%f,%f)=%e, want %e", tc.l0Sensitivity, tc.lInfSensitivity, tc.epsilon, tc.k, got, tc.want)
}
}
}
func TestInverseCDFLaplace(t *testing.T) {
for _, tc := range []struct {
desc string
lambda, prob, want float64
}{
{
desc: "Arbitrary test",
lambda: 4,
prob: 0.7875404240919761168041,
want: 3.4234254367,
},
{
desc: "Arbitrary test",
lambda: 2,
prob: 0.1479685611330654517049,
want: -2.435216544,
},
// For a probability of 0.5, the result should be the zero regardless of lambda.
{
desc: "0.5 Probability, output is zero",
lambda: 5,
prob: 0.5,
want: 0,
},
{
desc: "0.5 Probability, output is zero",
lambda: 10,
prob: 0.5,
want: 0,
},
// Tests for convergence to infinities with low and high probabilities.
{
desc: "Low probability",
lambda: 3,
prob: 1.23757362e-15,
want: -100.897429529251364,
},
{
desc: "High probability",
lambda: 3,
prob: 0.999999999876242638,
want: 66.3586531343406788,
},
} {
got := inverseCDFLaplace(tc.lambda, tc.prob)
if !approxEqual(got, tc.want) {
t.Errorf("TestInverseCDFLaplace(%f,%f)=%0.12f, want %0.12f, desc: %s", tc.lambda,
tc.prob, got, tc.want, tc.desc)
}
}
}
func TestComputeConfidenceIntervalLaplace(t *testing.T) {
for _, tc := range []struct {
desc string
noisedX float64
lambda, alpha float64
want ConfidenceInterval
}{
{
desc: "Arbitrary test",
noisedX: 13,
lambda: 27.33333333333,
alpha: 0.05,
want: ConfidenceInterval{-68.88334881, 94.88334881},
},
{
desc: "Arbitrary test",
noisedX: 83.1235,
lambda: 60,
alpha: 0.24,
want: ConfidenceInterval{-2.503481338, 168.7504813},
},
{
desc: "Arbitrary test",
noisedX: 5,
lambda: 6.6666666666667,
alpha: 0.6,
want: ConfidenceInterval{1.594495842, 8.405504158},
},
{
desc: "Arbitrary test",
noisedX: 65.4621,
lambda: 700,
alpha: 0.8,
want: ConfidenceInterval{-90.73838592, 221.6625859},
},
{
desc: "Extremely low confidence level",
noisedX: 0,
lambda: 10,
alpha: 1 - 3.548957438e-10,
want: ConfidenceInterval{-3.548957437370245055312e-9, 3.548957437370245055312e-9},
},
{
desc: "Extremely high confidence level",
noisedX: 50,
lambda: 10,
alpha: 7.856382354e-10,
want: ConfidenceInterval{-159.6452468975697118041, 259.6452468975697118041},
},
} {
got := computeConfidenceIntervalLaplace(tc.noisedX, tc.lambda, tc.alpha)
if !approxEqual(got.LowerBound, tc.want.LowerBound) {
t.Errorf("TestComputeConfidenceIntervalLaplace(%f, %f, %f)=%0.10f, want %0.10f, desc %s, LowerBounds are not equal",
tc.noisedX, tc.lambda, tc.alpha, got.LowerBound, tc.want.LowerBound, tc.desc)
}
if !approxEqual(got.UpperBound, tc.want.UpperBound) {
t.Errorf("TestComputeConfidenceIntervalLaplace(%f, %f, %f)=%0.10f, want %0.10f, desc %s, UpperBounds are not equal",
tc.noisedX, tc.lambda, tc.alpha, got.UpperBound, tc.want.UpperBound, tc.desc)
}
}
}
func TestComputeConfidenceIntervalFloat64Laplace(t *testing.T) {
for _, tc := range []struct {
desc string
noisedX float64
l0Sensitivity int64
lInfSensitivity, epsilon, alpha, delta float64
want ConfidenceInterval
wantErr bool
}{
{
desc: "Arbitrary test",
noisedX: 38.4234,
l0Sensitivity: 2,
lInfSensitivity: 4.3,
epsilon: 0.6,
alpha: 0.2,
want: ConfidenceInterval{15.35478992, 61.49201008},
},
// Testing that bounds are accurate for abs(bound) < 2^53
{
desc: "Large positive noisedX",
noisedX: 958655.4745,
l0Sensitivity: 1,
lInfSensitivity: 1.0,
epsilon: 0.1,
alpha: 0.1,
want: ConfidenceInterval{958632.45, 958678.50},
},
{
desc: "Large negative noisedX",
noisedX: -958655.4745,
l0Sensitivity: 1,
lInfSensitivity: 1.0,
epsilon: 0.1,
alpha: 0.1,
want: ConfidenceInterval{-958678.50, -958632.45},
},
// Argument checking
{
desc: "Zero l0Sensitivity",
noisedX: 0,
l0Sensitivity: 0,
lInfSensitivity: 1,
epsilon: 0.1,
alpha: 0.5,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "Negative l0Sensitivity",
noisedX: 0,
l0Sensitivity: -1,
lInfSensitivity: 1,
epsilon: 0.1,
alpha: 0.5,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "Zero lInfSensitivity",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: 0,
epsilon: 0.1,
alpha: 0.5,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "Negative lInfSensitivity",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: -1,
epsilon: 0.1,
alpha: 0.5,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "Infinite lInfSensitivity",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: math.Inf(1),
epsilon: 0.1,
alpha: 0.5,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "NaN lInfSensitivity",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: math.NaN(),
epsilon: 0.1,
alpha: 0.5,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "Epsilon less than 2^-50",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: 1,
epsilon: 1.0 / (1 << 51),
alpha: 0.5,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "Negative epsilon",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: 1,
epsilon: -1,
alpha: 0.5,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "Infinite epsilon",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: 1,
epsilon: math.Inf(1),
alpha: 0.5,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "NaN epsilon",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: 1,
epsilon: math.NaN(),
alpha: 0.5,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "Non-zero delta",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: 1,
epsilon: 0.1,
delta: 1,
alpha: 0.5,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "Zero alpha",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: 1,
epsilon: 0.1,
alpha: 0,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "Negative alpha",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: 1,
epsilon: 0.1,
alpha: -1,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "1 alpha",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: 1,
epsilon: 0.1,
alpha: 1,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "Greater than 1 alpha",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: 1,
epsilon: 0.1,
alpha: 2,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "NaN alpha",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: 1,
epsilon: 0.1,
alpha: math.NaN(),
want: ConfidenceInterval{},
wantErr: true,
},
} {
got, err := lap.ComputeConfidenceIntervalFloat64(tc.noisedX, tc.l0Sensitivity, tc.lInfSensitivity,
tc.epsilon, tc.delta, tc.alpha)
if (err != nil) != tc.wantErr {
t.Errorf("ComputeConfidenceIntervalFloat64Laplace: when %v for err got %v, want %t", tc.desc, err, tc.wantErr)
}
if !approxEqual(got.LowerBound, tc.want.LowerBound) {
t.Errorf("TestComputeConfidenceIntervalFloat64Laplace(%f, %d, %f, %f, %f)=%0.10f, want %0.10f, desc %s, LowerBounds are not equal",
tc.noisedX, tc.l0Sensitivity, tc.lInfSensitivity, tc.epsilon, tc.alpha, got.LowerBound, tc.want.LowerBound, tc.desc)
}
if !approxEqual(got.UpperBound, tc.want.UpperBound) {
t.Errorf("TestComputeConfidenceIntervalFloat64Laplace(%f, %d, %f, %f, %f)=%0.10f, want %0.10f, desc %s, UpperBounds are not equal",
tc.noisedX, tc.l0Sensitivity, tc.lInfSensitivity, tc.epsilon, tc.alpha, got.UpperBound, tc.want.UpperBound, tc.desc)
}
}
}
func TestComputeConfidenceIntervalInt64Laplace(t *testing.T) {
for _, tc := range []struct {
desc string
noisedX, l0Sensitivity, lInfSensitivity int64
epsilon, delta, alpha float64
want ConfidenceInterval
wantErr bool
}{
{
desc: "Arbitrary test",
noisedX: -12,
l0Sensitivity: 5,
lInfSensitivity: 6,
epsilon: 0.1,
alpha: 0.8,
want: ConfidenceInterval{-79, 55},
},
// Tests for nextSmallerFloat64 and nextLargerFloat64.
{
desc: "Large positive noisedX",
// Distance to neighbouring float64 values is greater than half the size of the confidence interval.
noisedX: (1 << 58),
l0Sensitivity: 1,
lInfSensitivity: 1,
epsilon: 0.1,
alpha: 0.1,
want: ConfidenceInterval{math.Nextafter(1<<58, math.Inf(-1)), math.Nextafter(1<<58, math.Inf(1))},
},
{
desc: "Large negative noisedX",
// Distance to neighbouring float64 values is greater than half the size of the confidence interval.
noisedX: -(1 << 58),
l0Sensitivity: 1,
lInfSensitivity: 1,
epsilon: 0.1,
alpha: 0.1,
want: ConfidenceInterval{math.Nextafter(-1<<58, math.Inf(-1)), math.Nextafter(-1<<58, math.Inf(1))},
},
// Argument checking
{
desc: "Zero l0Sensitivity",
noisedX: 0,
l0Sensitivity: 0,
lInfSensitivity: 1,
epsilon: 0.1,
alpha: 0.5,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "Negative l0Sensitivity",
noisedX: 0,
l0Sensitivity: -1,
lInfSensitivity: 1,
epsilon: 0.1,
alpha: 0.5,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "Zero lInfSensitivity",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: 0,
epsilon: 0.1,
alpha: 0.5,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "Negative lInfSensitivity",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: -1,
epsilon: 0.1,
alpha: 0.5,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "Epsilon less than 2^-50",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: 1,
epsilon: 1.0 / (1 << 51),
alpha: 0.5,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "Negative epsilon",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: 1,
epsilon: -1,
alpha: 0.5,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "Infinite epsilon",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: 1,
epsilon: math.Inf(1),
alpha: 0.5,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "NaN epsilon",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: 1,
epsilon: math.NaN(),
alpha: 0.5,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "Non-zero delta",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: 1,
epsilon: 0.1,
delta: 1,
alpha: 0.5,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "Zero alpha",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: 1,
epsilon: 0.1,
alpha: 0,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "Negative alpha",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: 1,
epsilon: 0.1,
alpha: -1,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "1 alpha",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: 1,
epsilon: 0.1,
alpha: 1,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "Greater than 1 alpha",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: 1,
epsilon: 0.1,
alpha: 2,
want: ConfidenceInterval{},
wantErr: true,
},
{
desc: "NaN alpha",
noisedX: 0,
l0Sensitivity: 1,
lInfSensitivity: 1,
epsilon: 0.1,
alpha: math.NaN(),
want: ConfidenceInterval{},
wantErr: true,
},
} {
got, err := lap.ComputeConfidenceIntervalInt64(tc.noisedX, tc.l0Sensitivity, tc.lInfSensitivity,
tc.epsilon, tc.delta, tc.alpha)
if (err != nil) != tc.wantErr {
t.Errorf("ComputeConfidenceIntervalInt64Laplace: when %v for err got %v, want %t", tc.desc, err, tc.wantErr)
}
if got.LowerBound != tc.want.LowerBound {
t.Errorf("TestComputeConfidenceIntervalInt64Laplace(%d, %d, %d, %f, %f)=%0.10f, want %0.10f, desc %s, LowerBounds are not equal",
tc.noisedX, tc.l0Sensitivity, tc.lInfSensitivity, tc.epsilon, tc.alpha, got.LowerBound, tc.want.LowerBound, tc.desc)
}
if got.UpperBound != tc.want.UpperBound {
t.Errorf("TestComputeConfidenceIntervalInt64Laplace(%d, %d, %d, %f, %f)=%0.10f, want %0.10f, desc %s, UpperBounds are not equal",
tc.noisedX, tc.l0Sensitivity, tc.lInfSensitivity, tc.epsilon, tc.alpha, got.UpperBound, tc.want.UpperBound, tc.desc)
}
}
}
func TestGeometricStatistics(t *testing.T) {
const numberOfSamples = 125000
for _, tc := range []struct {
lambda float64
mean float64
stdDev float64
}{
{
lambda: 0.1,
mean: 10.50833,
stdDev: 9.99583,
},
{
lambda: 0.0001,
mean: 10000.50001,
stdDev: 9999.99999,
},
{
lambda: 0.0000001,
mean: 10000000.5,
stdDev: 9999999.99999,
},
} {
geometricSamples := make(stat.IntSlice, numberOfSamples)
for i := 0; i < numberOfSamples; i++ {
geometricSamples[i] = geometric(tc.lambda)
}
sampleMean := stat.Mean(geometricSamples)
// Assuming that the geometric samples are distributed according to the specified lambda, the
// sampleMean is approximately Gaussian distributed with a mean of tc.mean and standard deviation
// of tc.stdDev / sqrt(numberOfSamples).
//
// The meanErrorTolerance is set to the 99.9995% quantile of the anticipated distribution
// of sampleMean. Thus, the test falsely rejects with a probability of 10⁻⁵.
meanErrorTolerance := 4.41717 * tc.stdDev / math.Sqrt(float64(numberOfSamples))
if !nearEqual(sampleMean, tc.mean, meanErrorTolerance) {
t.Errorf("got mean = %f, want %f (parameters %+v)", sampleMean, tc.mean, tc)
}
}
}