blob: 1a3c3b378a9f5c8b0e0b1744e6482e55f2bb2e5a [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 com.google.privacy.differentialprivacy.testing;
import static com.google.common.truth.Truth.assertWithMessage;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.Supplier;
import com.google.privacy.differentialprivacy.proto.testing.StatisticalTests.ApproximateBoundsDpTestCase;
import com.google.privacy.differentialprivacy.proto.testing.StatisticalTests.ApproximateBoundsDpTestCaseCollection;
import com.google.privacy.differentialprivacy.proto.testing.StatisticalTests.ApproximateBoundsSamplingParameters;
import com.google.privacy.differentialprivacy.proto.testing.StatisticalTests.DpTestParameters;
import com.google.protobuf.TextFormat;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.SecureRandom;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@RunWith(Parameterized.class)
public final class ApproximateBoundsDpTestCasesValidityTest {
private static final String TEST_CASES_FILE_PATH =
"external/com_google_differential_privacy/proto/testing/approximate_bounds_dp_test_cases.textproto";
private static final short BOUND_RETURNED = 1;
private static final short BOUND_NOT_RETURNED = 0;
private static final short FAILED = -1;
private final int numberOfVotes =
getTestCaseCollectionFromFile().getVotingParameters().getNumberOfVotes();
private final double epsilonSpecificity =
getTestCaseCollectionFromFile().getValidityParameters().getEpsilonSpecificity();
private final double failureSpecificity =
getTestCaseCollectionFromFile().getValidityParameters().getFailureSpecificity();
private final ApproximateBoundsDpTestCase testCase;
public ApproximateBoundsDpTestCasesValidityTest(ApproximateBoundsDpTestCase testCase) {
this.testCase = testCase;
}
@Parameterized.Parameters
public static List<ApproximateBoundsDpTestCase> testCases() {
return getTestCaseCollectionFromFile().getApproximateBoundsDpTestCaseList();
}
@Test
public void approximateBoundsDpTest_acceptsProbabilityDifferenceWithinEpsilonParameter() {
ApproximateBoundsSamplingParameters samplingParameters =
testCase.getApproximateBoundsSamplingParameters();
DpTestParameters dpTestParameters = testCase.getDpTestParameters();
SecureRandom random = new SecureRandom();
double epsilon = samplingParameters.getEpsilon();
double delta = samplingParameters.getDelta();
// Let p be the probability that approximate bounds returns a bound for a given input.
// Similarly let p' be the probability that approximate bounds returns that bound for a
// neighbouring input. To satisfy DP, it is necessary (but not sufficient) that
// p * e^ε + δ >= p'.
// Thus p = e^-ε * (p' - delta) is the smallest value of p that should be accepted.
// Note that we intentionally don't adjust for maxContributions here, because we're
// describing the distribution of the mechanism output, not applying the Laplace mechanism.
double pPrime = 0.5; // arbitrary choice
double p = Math.exp(-epsilon) * (pPrime - delta);
Supplier<Short> sampleGenerator =
() -> random.nextDouble() > p ? BOUND_RETURNED : BOUND_NOT_RETURNED;
Supplier<Short> neighbourSampleGenerator =
() -> random.nextDouble() > pPrime ? BOUND_RETURNED : BOUND_NOT_RETURNED;
assertWithMessage(
"Expected ballot to accept '%s' when using correct epsilon.", testCase.getName())
.that(
VotingUtil.runBallot(
() ->
generateVote(
sampleGenerator,
neighbourSampleGenerator,
samplingParameters.getNumberOfSamples(),
dpTestParameters.getEpsilon(),
dpTestParameters.getDelta(),
dpTestParameters.getDeltaTolerance()),
numberOfVotes))
.isTrue();
}
@Test
public void approximateBoundsDpTest_rejectsProbabilityDifferenceGreaterThanEpsilonParameter() {
ApproximateBoundsSamplingParameters samplingParameters =
testCase.getApproximateBoundsSamplingParameters();
DpTestParameters dpTestParameters = testCase.getDpTestParameters();
SecureRandom random = new SecureRandom();
double epsilon = samplingParameters.getEpsilon();
double delta = samplingParameters.getDelta();
// Let p be the probability that approximate bounds returns a bound for a given input. Similarly
// let p' be the probability that approximate bounds returns that bound for a neighbouring
// input. To satisfy DP, it is necessary (but not sufficient) that
// p * e^ε + δ >= p'.
// Let s > 1 be the test specificity. If we set p = e^(-ε * s) * (p' - delta), this is below
// threshold e^(-ε) * (p' - delta) that is the minimum that we expect the test to accept, so we
// expect the test to fail. Note that we intentionally don't adjust for maxContributions here,
// because we're describing the distribution of the output, not applying the Laplace mechanism.
double pPrime = 0.5; // arbitrary choice
double p = Math.exp(-epsilon * epsilonSpecificity) * (pPrime - delta);
assertWithMessage(
"Chosen value of p is not expected to violate DP: please increase epsilon specificity")
.that(p * Math.exp(epsilon) + delta + dpTestParameters.getDeltaTolerance())
.isLessThan(pPrime);
Supplier<Short> sampleGenerator =
() -> random.nextDouble() > p ? BOUND_RETURNED : BOUND_NOT_RETURNED;
Supplier<Short> neighbourSampleGenerator =
() -> random.nextDouble() > pPrime ? BOUND_RETURNED : BOUND_NOT_RETURNED;
assertWithMessage(
"Expected ballot to reject '%s' when using incorrect epsilon.", testCase.getName())
.that(
VotingUtil.runBallot(
() ->
generateVote(
sampleGenerator,
neighbourSampleGenerator,
samplingParameters.getNumberOfSamples(),
dpTestParameters.getEpsilon(),
dpTestParameters.getDelta(),
dpTestParameters.getDeltaTolerance()),
numberOfVotes))
.isFalse();
}
@Test
public void approximateBoundsDpTest_rejectsCriticallyFailingBounds() {
ApproximateBoundsSamplingParameters samplingParameters =
testCase.getApproximateBoundsSamplingParameters();
DpTestParameters dpTestParameters = testCase.getDpTestParameters();
SecureRandom random = new SecureRandom();
Supplier<Short> sampleGenerator =
() -> random.nextBoolean() ? BOUND_RETURNED : BOUND_NOT_RETURNED;
Supplier<Short> neighbourSampleGenerator =
() ->
random.nextDouble() > dpTestParameters.getDeltaTolerance() * failureSpecificity
? (random.nextBoolean() ? BOUND_RETURNED : BOUND_NOT_RETURNED)
: FAILED;
assertWithMessage(
"Expected ballot to reject '%s' when critical failures present.", testCase.getName())
.that(
VotingUtil.runBallot(
() ->
generateVote(
sampleGenerator,
neighbourSampleGenerator,
samplingParameters.getNumberOfSamples(),
dpTestParameters.getEpsilon(),
dpTestParameters.getDelta(),
dpTestParameters.getDeltaTolerance()),
numberOfVotes))
.isFalse();
}
private static ApproximateBoundsDpTestCaseCollection getTestCaseCollectionFromFile() {
ApproximateBoundsDpTestCaseCollection.Builder testCaseCollectionBuilder =
ApproximateBoundsDpTestCaseCollection.newBuilder();
try {
TextFormat.merge(
new InputStreamReader(
ApproximateBoundsDpTestCasesValidityTest.class
.getClassLoader()
.getResourceAsStream(TEST_CASES_FILE_PATH),
UTF_8),
testCaseCollectionBuilder);
} catch (IOException e) {
throw new RuntimeException("Unable to read input.", e);
} catch (NullPointerException e) {
throw new RuntimeException("Unable to find input file.", e);
}
return testCaseCollectionBuilder.build();
}
private static boolean generateVote(
Supplier<Short> sampleGeneratorA,
Supplier<Short> sampleGeneratorB,
int numberOfSamples,
double epsilon,
double delta,
double deltaTolerance) {
Short[] samplesA = new Short[numberOfSamples];
Short[] samplesB = new Short[numberOfSamples];
for (int i = 0; i < numberOfSamples; i++) {
samplesA[i] = sampleGeneratorA.get();
samplesB[i] = sampleGeneratorB.get();
}
return StatisticalTestsUtil.verifyApproximateDp(
samplesA, samplesB, epsilon, delta, deltaTolerance);
}
}