blob: a40ef41a522a2821c0394f72b8627591717d7dc6 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package privacy
import (
"math"
"testing"
)
// equalWithError compares whether two values are at most error apart
func equalWithError(value1, value2, error float64) bool {
return math.Abs(value1-value2) <= error
}
// arraysEqual compares if two arrays have the same values.
func arraysEqual(array1 []float64, array2 []float64) bool {
// Check if the arrays have the same length.
if len(array1) != len(array2) {
return false
}
// Iterate over the arrays and compare each element.
for i := range array1 {
if array1[i] != array2[i] {
return false
}
}
return true
}
// deepEqualWithError compares two maps that define distributions such that for each value
// corresponding probabilities are compared up to specified additive error.
func deepEqualWithError(distr1, distr2 ProbabilityMassFunction, error float64) bool {
for key, value := range distr1 {
if !equalWithError(value, distr2[key], error) {
return false
}
}
for key, value := range distr2 {
if !equalWithError(value, distr1[key], error) {
return false
}
}
return true
}
func TestComputeTruncationValueBounds(t *testing.T) {
distrArray := []float64{0.1, 0.5, 0.41, 0.07, 0.02}
truncatedMass := 0.2
minBound, maxBound := computeTruncationValueBounds(distrArray, truncatedMass)
expectedMinBound, expectedMaxBound := 1, 2
if minBound != expectedMinBound || maxBound != expectedMaxBound {
t.Errorf(
"Incorrect result returned by computeTruncationValueBounds: expected (%d, %d), got (%d, %d)",
expectedMinBound, expectedMaxBound, minBound, maxBound)
}
}
func TestComputeTruncationValueBoundsTruncateAll(t *testing.T) {
distrArray := []float64{0.1, 0.1, 0.1, 0.1, 0.1}
truncatedMass := 0.600001
minBound, maxBound := computeTruncationValueBounds(distrArray, truncatedMass)
expectedMinBound, expectedMaxBound := 3, 1
if minBound != expectedMinBound || maxBound != expectedMaxBound {
t.Errorf(
"Incorrect result returned by computeTruncationValueBounds: expected (%d, %d), got (%d, %d)",
expectedMinBound, expectedMaxBound, minBound, maxBound)
}
}
func TestMinMaxKeyValues(t *testing.T) {
pmf := ProbabilityMassFunction{
-1: 0.1,
5: 0.2,
2: 0.4,
}
minKey, maxKey := minMaxKeyValues(pmf)
expectedMinKey, expectedMaxKey := -1, 5
if minKey != expectedMinKey || maxKey != expectedMaxKey {
t.Errorf(
"Incorrect result returned by minMaxKeyValue: expected (%d, %d), got (%d, %d)", expectedMinKey, expectedMaxKey, minKey, maxKey)
}
}
func TestConvertMapToShiftedArrayError(t *testing.T) {
pmf := ProbabilityMassFunction{
-1: 0.3,
1: 0.3,
3: 0.4,
}
size := 3
_, err := convertMapToShiftedArray(pmf, size)
if err == nil {
t.Errorf("Expected error, but no error was found.")
}
}
func TestConvertMapToShiftedArray(t *testing.T) {
pmf := ProbabilityMassFunction{
-1: 0.3,
1: 0.3,
3: 0.4,
}
size := 6
shiftedDistr, _ := convertMapToShiftedArray(pmf, size)
array := []float64{0.3, 0.0, 0.3, 0.0, 0.4, 0.0}
expectedShiftedDistr := shiftedDistribution{array, -1}
if !arraysEqual(shiftedDistr.array, expectedShiftedDistr.array) ||
shiftedDistr.shift != expectedShiftedDistr.shift {
t.Errorf(
"Incorrect result of convertMapToShiftedArray. Expected %v, got %v",
expectedShiftedDistr, shiftedDistr)
}
}
func TestConvolveHeterogeneousDistributionsNoTruncation(t *testing.T) {
pmf1 := ProbabilityMassFunction{0: 0.5, 1: 0.5, 2: 0.0}
pmf2 := ProbabilityMassFunction{0: 0.0, 1: 0.5, 2: 0.5}
result, err := convolveHeterogeneousDistributions(pmf1, pmf2, 0)
if err != nil {
t.Errorf("ConvolveHeterogeneousDistributions returned error: %s", err.Error())
}
expectedPmf := ProbabilityMassFunction{
0: 0.0,
1: 0.25,
2: 0.5,
3: 0.25,
4: 0.0,
}
if !deepEqualWithError(expectedPmf, result, 0.00001) {
t.Errorf("Incorrect result of convolution of a pair of distributions: expected %v, got %v", expectedPmf, result)
}
}
func TestConvolveHeterogeneousDistributionsWithTruncation(t *testing.T) {
pmf1 := ProbabilityMassFunction{0: 0.1, 1: 0.9, 2: 0.0}
pmf2 := ProbabilityMassFunction{0: 0.0, 1: 0.9, 2: 0.1}
result, err := convolveHeterogeneousDistributions(pmf1, pmf2, 0.2)
if err != nil {
t.Errorf("ConvolveHeterogeneousDistributions returned error: %s", err.Error())
}
// Mass of 0.09 at 1 and 3 respectively get truncated.
expectedPmf := ProbabilityMassFunction{2: 0.82}
if !deepEqualWithError(expectedPmf, result, 0.00001) {
t.Errorf("Incorrect result of convolution of a pair of distributions: expected %v, got %v", expectedPmf, result)
}
}
func TestConvolve1Time(t *testing.T) {
pmf := ProbabilityMassFunction{0: 0.5, 1: 0.5}
result, err := convolveHomogeneousDistributions(pmf, 1, 0)
if err != nil {
t.Errorf("ConvolveHomogeneousDistributions returned error: %s", err.Error())
}
if !deepEqualWithError(pmf, result, 0.00001) {
t.Errorf("Incorrect result of 1 convolution computation: expected %v, got %v", pmf, result)
}
}
func TestConvolve3Times(t *testing.T) {
pmf := ProbabilityMassFunction{0: 0.5, 1: 0.5}
result, err := convolveHomogeneousDistributions(pmf, 3, 1e-5)
if err != nil {
t.Errorf("ConvolveHomogeneousDistributions returned error: %s", err.Error())
}
expectedPmf := ProbabilityMassFunction{
0: 0.125,
1: 0.375,
2: 0.375,
3: 0.125,
}
if !deepEqualWithError(expectedPmf, result, 1e-10) {
t.Errorf("Incorrect result of 3-convolution computation: expected %v, got %v", expectedPmf, result)
}
}
func TestConvolve1000Times(t *testing.T) {
pmf := ProbabilityMassFunction{1: 1.0}
result, err := convolveHomogeneousDistributions(pmf, 1000, 1e-5)
if err != nil {
t.Errorf("ConvolveHomogeneousDistributions returned error: %s", err.Error())
}
expectedPmf := ProbabilityMassFunction{1000: 1.0}
if !deepEqualWithError(expectedPmf, result, 1e-10) {
t.Errorf("Incorrect result of 1000-convolution computation: expected %v, got %v", expectedPmf, result)
}
}
func TestComputeKConvolutionTruncationParams(t *testing.T) {
distrArray := []float64{0.1, 0.4, 0.5}
minBound, maxBound := computeTruncationValueBoundsForHomogeneousConvolution(distrArray, 3, 0.5)
if minBound != 2 || maxBound != 6 {
t.Errorf("Incorrect result of computation truncation parameters for KConvolution: expected 2 and 6, got %d and %d", minBound, maxBound)
}
}
func TestRescaleFFTArray(t *testing.T) {
distr := []float64{3, 2}
result := rescaleFFTArray(distr)
if result[0] != 1.5 || result[1] != 1 {
t.Errorf("Incorrect result of rescaling of a distribution: expected [0:1.5 1:1], got %v", result)
}
}
func TestGeneratePLDPessimisticZeroInfiniteMass(t *testing.T) {
pmfUpper := ProbabilityMassFunction{1: 0.2, 2: 0.8}
pmfLower := ProbabilityMassFunction{1: 0.5, 2: 0.5}
result := generatePLD(pmfUpper, pmfLower, -50, 1e-4, true)
expectedPmf := ProbabilityMassFunction{-9162: 0.2, 4701: 0.8}
if !deepEqualWithError(expectedPmf, result.pmf, 0.00001) {
t.Errorf("Incorrect result of PLD generation: expected %v got %v", expectedPmf, result.pmf)
}
if result.infiniteMass != 0.0 {
t.Errorf("Incorrect result of PLD generation: expected infinite mass 0.0 got %f", result.infiniteMass)
}
}
func TestGeneratePLDOptimistic(t *testing.T) {
pmfUpper := ProbabilityMassFunction{1: 0.6, 2: 0.4}
pmfLower := ProbabilityMassFunction{1: 0.5, 2: 0.5}
result := generatePLD(pmfUpper, pmfLower, -0.55, 1e-4, false)
expectedPmf := ProbabilityMassFunction{1823: 0.6}
if !deepEqualWithError(expectedPmf, result.pmf, 0.00001) {
t.Errorf("Incorrect result of PLD generation: expected %v got %v", expectedPmf, result.pmf)
}
if result.infiniteMass != 0.0 {
t.Errorf("Incorrect result of PLD generation: expected infinite mass 0.0 got %f", result.infiniteMass)
}
}
func TestGeneratePLDSmallandLargeDiscretization(t *testing.T) {
pmfUpper := ProbabilityMassFunction{1: 0.7, 2: 0.3}
pmfLower := ProbabilityMassFunction{1: 0.5, 2: 0.5}
// Small discretization : 1e-6
result := generatePLD(pmfUpper, pmfLower, -0.55, 1e-6, false)
expectedPmf := ProbabilityMassFunction{336472: 0.7}
if !deepEqualWithError(expectedPmf, result.pmf, 0.00001) {
t.Errorf("Incorrect result of PLD generation: expected %v got %v", expectedPmf, result.pmf)
}
if result.infiniteMass != 0.0 {
t.Errorf("Incorrect result of PLD generation: expected infinite mass 0.0 got %f", result.infiniteMass)
}
// Large discretization : 1e-2
result = generatePLD(pmfUpper, pmfLower, -0.55, 1e-2, false)
expectedPmf = ProbabilityMassFunction{33: 0.7}
if !deepEqualWithError(expectedPmf, result.pmf, 0.00001) {
t.Errorf("Incorrect result of PLD generation: expected %v got %v", expectedPmf, result.pmf)
}
if result.infiniteMass != 0.0 {
t.Errorf("Incorrect result of PLD generation: expected infinite mass 0.0 got %f", result.infiniteMass)
}
}
func TestGenerateEmptyPLDPessimistic(t *testing.T) {
pmfUpper := ProbabilityMassFunction{1: 1.0}
pmfLower := ProbabilityMassFunction{2: 0.5, 4: 0.5}
result := generatePLD(pmfUpper, pmfLower, -50.0, 1e-4, true)
if len(result.pmf) != 0 {
t.Errorf("Incorrect result of PLD generation: expected empty distribution got %v", result.pmf)
}
if result.infiniteMass != 1.0 {
t.Errorf("Incorrect result of PLD generation: expected infinite mass 1.0 got %f", result.infiniteMass)
}
}
func TestGenerateEmptyPLDOptimistic(t *testing.T) {
pmfUpper := ProbabilityMassFunction{1: 1.0}
pmfLower := ProbabilityMassFunction{2: 0.5, 4: 0.5}
result := generatePLD(pmfUpper, pmfLower, -50.0, 1e-4, false)
if len(result.pmf) != 0 {
t.Errorf("Incorrect result of PLD generation: expected empty distribution got %v", result.pmf)
}
if result.infiniteMass != 1.0 {
t.Errorf("Incorrect result of PLD generation: expected infinite mass 1.0 got %f", result.infiniteMass)
}
}
func TestGeneratePLDNonzeroInfiniteMass(t *testing.T) {
pmfUpper := ProbabilityMassFunction{1: 0.6, 2: 0.4}
pmfLower := ProbabilityMassFunction{1: 0.5, 3: 0.5}
result := generatePLD(pmfUpper, pmfLower, -50, 1e-4, true)
expectedPmf := ProbabilityMassFunction{1824: 0.6}
if !deepEqualWithError(expectedPmf, result.pmf, 0.00001) {
t.Errorf("Incorrect result of PLD generation: expected %v got %v", expectedPmf, result.pmf)
}
if result.infiniteMass != 0.4 {
t.Errorf("Incorrect result of PLD generation: expected infinite mass 0.0 got %f", result.infiniteMass)
}
}
func TestComposeHeterogeneousPLD(t *testing.T) {
discretization := 0.01
pld1 := PrivacyLossDistribution{
discretization: discretization,
infiniteMass: 0.1,
pmf: ProbabilityMassFunction{0: 0.05, 1: 0.7, 2: 0.15},
pessimisticEstimation: true}
pld2 := PrivacyLossDistribution{
discretization: discretization,
infiniteMass: 0.05,
pmf: ProbabilityMassFunction{0: 0.2, 1: 0.65, 2: 0.1},
pessimisticEstimation: true}
result, err := composeHeterogeneousPLD(&pld1, &pld2, 0.005)
if err != nil {
t.Errorf("ComposeHeterogeneousPLD returned error: %s", err.Error())
}
expectedPmf := ProbabilityMassFunction{
0: 0.01,
1: 0.1725,
2: 0.49,
3: 0.1675,
4: 0.015,
}
if !deepEqualWithError(expectedPmf, result.pmf, 0.000001) {
t.Errorf("Incorrect result of PLD product: expected %v got %v", expectedPmf, result.pmf)
}
if !equalWithError(result.infiniteMass, 0.15, 0.000001) {
t.Errorf("Incorrect result of PLD product: expected infinite mass 0.15 got %f", result.infiniteMass)
}
}
func TestComposeHeterogeneousPLDWithTruncation(t *testing.T) {
discretization := 0.01
pld1 := PrivacyLossDistribution{
discretization: discretization,
infiniteMass: 0.0,
pmf: ProbabilityMassFunction{0: 0.05, 1: 0.9, 2: 0.05},
pessimisticEstimation: true}
pld2 := PrivacyLossDistribution{
discretization: discretization,
infiniteMass: 0.0,
pmf: ProbabilityMassFunction{0: 0.1, 1: 0.8, 2: 0.1},
pessimisticEstimation: true}
result, err := composeHeterogeneousPLD(&pld1, &pld2, 0.011)
if err != nil {
t.Errorf("ComposeHeterogeneousPLD returned error: %s", err.Error())
}
expectedPmf := ProbabilityMassFunction{
1: 0.13,
2: 0.73,
3: 0.13,
}
if !deepEqualWithError(expectedPmf, result.pmf, 0.000001) {
t.Errorf("Incorrect result of PLD product: expected %v got %v", expectedPmf, result.pmf)
}
if !equalWithError(result.infiniteMass, 0.011, 0.000001) {
t.Errorf("Incorrect result of PLD product: expected infinite mass 0.011 got %f", result.infiniteMass)
}
}
func TestComposeHeterogeneousPLDDifferentDiscretizationError(t *testing.T) {
pld1 := PrivacyLossDistribution{
discretization: 0.01,
infiniteMass: 0.0,
pmf: ProbabilityMassFunction{0: 0.3, 1: 0.7},
pessimisticEstimation: true}
pld2 := PrivacyLossDistribution{
discretization: 0.0001,
infiniteMass: 0.0,
pmf: ProbabilityMassFunction{0: 0.3, 1: 0.7},
pessimisticEstimation: true}
_, err := composeHeterogeneousPLD(&pld1, &pld2, 0.005)
if err == nil {
t.Errorf("ComposeHeterogeneousPLD didn't return an error while composing PLDs with distinct discretization")
}
}
func TestSelfCompositionWithTailTruncation(t *testing.T) {
pld := PrivacyLossDistribution{
discretization: 0.1,
infiniteMass: 0.1,
pmf: ProbabilityMassFunction{0: 0.05, 1: 0.7, 2: 0.15},
pessimisticEstimation: true}
truncatedMass := 0.001
result, err := composeHomogeneousPLD(&pld, 4, truncatedMass)
if err != nil {
t.Errorf("ComposeHomogeneousPLD returned error: %s", err.Error())
}
expectedPmf := ProbabilityMassFunction{
1: 0.00035,
2: 0.007425,
3: 0.07175,
4: 0.284538,
5: 0.21525,
6: 0.066825,
7: 0.00945,
8: 0.000506,
}
if !deepEqualWithError(expectedPmf, result.pmf, 1e-5) {
t.Errorf("Incorrect result of PLD self-composition with tail-mass truncation: expected %v, got %v", expectedPmf, result.pmf)
}
if !equalWithError(0.3439+truncatedMass, result.infiniteMass, 0.00001) {
t.Errorf("Incorrect result of PLD self-composition with tail-mass truncation: expected infinite mass %f got %f", 0.1+truncatedMass, result.infiniteMass)
}
}
func TestPLDSelfComposition(t *testing.T) {
pld := PrivacyLossDistribution{
discretization: 0.1,
infiniteMass: 0.1,
pmf: ProbabilityMassFunction{0: 0.05, 1: 0.7, 2: 0.15},
pessimisticEstimation: true}
result, err := composeHomogeneousPLD(&pld, 4, 0)
if err != nil {
t.Errorf("ComposeHomogeneousPLD returned error: %s", err.Error())
}
expectedPmf := ProbabilityMassFunction{
0: 0.000006,
1: 0.00035,
2: 0.007425,
3: 0.07175,
4: 0.284538,
5: 0.21525,
6: 0.066825,
7: 0.00945,
8: 0.000506,
}
if !deepEqualWithError(expectedPmf, result.pmf, 1e-5) {
t.Errorf("Incorrect result of PLD self-composition: expected %v, got %v", expectedPmf, result.pmf)
}
if !equalWithError(0.3439, result.infiniteMass, 0.0001) {
t.Errorf("Incorrect result of PLD self-composition: expected infinite mass %f got %f", 0.3439, result.infiniteMass)
}
}
func TestGetDeltaForPrivacyLossDistribution(t *testing.T) {
pmf := ProbabilityMassFunction{
1824: 0.6,
-2231: 0.4,
}
infMass := 0.0
pld := PrivacyLossDistribution{
discretization: 0.0001,
infiniteMass: infMass,
pmf: pmf,
pessimisticEstimation: false}
divergence := pld.getDeltaForPrivacyLossDistribution(0.0)
if !equalWithError(0.1, divergence, 0.0001) {
t.Errorf(
"Incorrect result of getDeltaForPrivacyLossDistribution: expected divergence value to be %f got %f",
0.1, divergence)
}
}