| // |
| // 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. |
| // |
| |
| #include "algorithms/partition-selection.h" |
| |
| #include <cmath> |
| #include <cstdlib> |
| |
| #include "base/testing/status_matchers.h" |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| #include "absl/memory/memory.h" |
| #include "absl/status/statusor.h" |
| #include "algorithms/numerical-mechanisms.h" |
| |
| namespace differential_privacy { |
| namespace { |
| |
| using ::testing::DoubleEq; |
| using ::testing::DoubleNear; |
| using ::testing::Eq; |
| using ::testing::HasSubstr; |
| using ::differential_privacy::base::testing::StatusIs; |
| |
| constexpr int kNumSamples = 10000000; |
| constexpr int kSmallNumSamples = 1000000; |
| |
| constexpr int64_t kInt64Min = std::numeric_limits<int64_t>::min(); |
| constexpr int64_t kInt64Max = std::numeric_limits<int64_t>::max(); |
| constexpr double kNaN = std::numeric_limits<double>::quiet_NaN(); |
| constexpr double kNegInf = -std::numeric_limits<double>::infinity(); |
| constexpr double kPosInf = std::numeric_limits<double>::infinity(); |
| constexpr double kDoubleMin = std::numeric_limits<double>::lowest(); |
| constexpr double kDoubleMinPos = std::numeric_limits<double>::min(); |
| constexpr double kDoubleMax = std::numeric_limits<double>::max(); |
| |
| constexpr double kCalcDeltaTestDefaultTolerance = 0.001; |
| constexpr double kCalcThresholdTestDefaultTolerance = 0.05; |
| |
| // NearTruncatedGeometricregationPartitionSelection Tests |
| |
| TEST(PartitionSelectionTest, |
| NearTruncatedGeometricPartitionSelectionUnsetEpsilon) { |
| NearTruncatedGeometricPartitionSelection::Builder test_builder; |
| EXPECT_THAT(test_builder.SetDelta(0.1).SetMaxPartitionsContributed(2).Build(), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Epsilon must be set"))); |
| } |
| |
| TEST(PartitionSelectionTest, |
| NearTruncatedGeometricPartitionSelectionNanEpsilon) { |
| NearTruncatedGeometricPartitionSelection::Builder test_builder; |
| EXPECT_THAT(test_builder.SetEpsilon(NAN) |
| .SetDelta(0.3) |
| .SetMaxPartitionsContributed(4) |
| .Build(), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Epsilon must be a valid numeric value"))); |
| } |
| |
| TEST(PartitionSelectionTest, |
| NearTruncatedGeometricPartitionSelectionNotFiniteEpsilon) { |
| NearTruncatedGeometricPartitionSelection::Builder test_builder; |
| EXPECT_THAT(test_builder.SetEpsilon(std::numeric_limits<double>::infinity()) |
| .SetDelta(0.3) |
| .SetMaxPartitionsContributed(4) |
| .Build(), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Epsilon must be finite"))); |
| } |
| |
| TEST(PartitionSelectionTest, |
| NearTruncatedGeometricPartitionSelectionNegativeEpsilon) { |
| NearTruncatedGeometricPartitionSelection::Builder test_builder; |
| EXPECT_THAT(test_builder.SetEpsilon(-5.0) |
| .SetDelta(0.6) |
| .SetMaxPartitionsContributed(7) |
| .Build(), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Epsilon must be finite and positive"))); |
| } |
| |
| TEST(PartitionSelectionTest, |
| NearTruncatedGeometricPartitionSelectionUnsetDelta) { |
| NearTruncatedGeometricPartitionSelection::Builder test_builder; |
| EXPECT_THAT( |
| test_builder.SetEpsilon(8.0).SetMaxPartitionsContributed(9).Build(), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Delta must be set."))); |
| } |
| |
| TEST(PartitionSelectionTest, NearTruncatedGeometricPartitionSelectionNanDelta) { |
| NearTruncatedGeometricPartitionSelection::Builder test_builder; |
| EXPECT_THAT(test_builder.SetEpsilon(1.2) |
| .SetDelta(NAN) |
| .SetMaxPartitionsContributed(3) |
| .Build(), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Delta must be a valid numeric value"))); |
| } |
| |
| TEST(PartitionSelectionTest, |
| NearTruncatedGeometricPartitionSelectionNotFiniteDelta) { |
| NearTruncatedGeometricPartitionSelection::Builder test_builder; |
| EXPECT_THAT(test_builder.SetEpsilon(1.2) |
| .SetDelta(std::numeric_limits<double>::infinity()) |
| .SetMaxPartitionsContributed(3) |
| .Build(), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Delta must be in the inclusive interval"))); |
| } |
| |
| TEST(PartitionSelectionTest, |
| NearTruncatedGeometricPartitionSelectionInvalidDelta) { |
| NearTruncatedGeometricPartitionSelection::Builder test_builder; |
| EXPECT_THAT(test_builder.SetEpsilon(4.5) |
| .SetDelta(6.0) |
| .SetMaxPartitionsContributed(7) |
| .Build(), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Delta must be in the inclusive interval"))); |
| } |
| |
| TEST(PartitionSelectionTest, |
| NearTruncatedGeometricPartitionSelectionUnsetMaxPartitionsContributed) { |
| NearTruncatedGeometricPartitionSelection::Builder test_builder; |
| EXPECT_THAT( |
| test_builder.SetEpsilon(0.8).SetDelta(0.9).Build(), |
| StatusIs( |
| absl::StatusCode::kInvalidArgument, |
| HasSubstr("Maximum number of partitions that can be contributed to " |
| "(i.e., L0 sensitivity) must be set"))); |
| } |
| |
| TEST(PartitionSelectionTest, |
| NearTruncatedGeometricPartitionSelectionNegativeMaxPartitionsContributed) { |
| NearTruncatedGeometricPartitionSelection::Builder test_builder; |
| EXPECT_THAT( |
| test_builder.SetEpsilon(0.1) |
| .SetDelta(0.2) |
| .SetMaxPartitionsContributed(-3) |
| .Build(), |
| StatusIs( |
| absl::StatusCode::kInvalidArgument, |
| HasSubstr("Maximum number of partitions that can be contributed to " |
| "(i.e., L0 sensitivity) must be positive, but is -3."))); |
| } |
| |
| TEST(PartitionSelectionTest, |
| NearTruncatedGeometricPartitionSelectionZeroMaxPartitionsContributed) { |
| NearTruncatedGeometricPartitionSelection::Builder test_builder; |
| EXPECT_THAT( |
| test_builder.SetEpsilon(0.1) |
| .SetDelta(0.2) |
| .SetMaxPartitionsContributed(0) |
| .Build(), |
| StatusIs( |
| absl::StatusCode::kInvalidArgument, |
| HasSubstr("Maximum number of partitions that can be contributed to " |
| "(i.e., L0 sensitivity) must be positive, but is 0."))); |
| } |
| |
| // We expect the probability of keeping a partition with one user |
| // will be approximately delta |
| TEST(PartitionSelectionTest, NearTruncatedGeometricPartitionSelectionOneUser) { |
| NearTruncatedGeometricPartitionSelection::Builder test_builder; |
| std::unique_ptr<PartitionSelectionStrategy> build = |
| test_builder.SetEpsilon(0.5) |
| .SetDelta(0.02) |
| .SetMaxPartitionsContributed(1) |
| .Build() |
| .value(); |
| double num_kept = 0.0; |
| for (int i = 0; i < kSmallNumSamples; i++) { |
| if (build->ShouldKeep(1)) num_kept++; |
| } |
| EXPECT_NEAR(build->ProbabilityOfKeep(1), build->GetDelta(), 1e-12); |
| EXPECT_THAT(num_kept / kSmallNumSamples, |
| DoubleNear(build->GetDelta(), 0.001)); |
| } |
| |
| // We expect the probability of keeping a partition with no users will be zero |
| TEST(PartitionSelectionTest, NearTruncatedGeometricPartitionSelectionNoUsers) { |
| NearTruncatedGeometricPartitionSelection::Builder test_builder; |
| std::unique_ptr<PartitionSelectionStrategy> build = |
| test_builder.SetEpsilon(0.5) |
| .SetDelta(0.02) |
| .SetMaxPartitionsContributed(1) |
| .Build() |
| .value(); |
| EXPECT_EQ(build->ProbabilityOfKeep(0), 0.0); |
| for (int i = 0; i < 1000; i++) { |
| EXPECT_FALSE(build->ShouldKeep(0)); |
| } |
| } |
| |
| TEST(PartitionSelectionTest, |
| NearTruncatedGeometricPartitionSelectionFirstCrossover) { |
| NearTruncatedGeometricPartitionSelection::Builder test_builder; |
| std::unique_ptr<PartitionSelectionStrategy> build = |
| test_builder.SetEpsilon(0.5) |
| .SetDelta(0.02) |
| .SetMaxPartitionsContributed(1) |
| .Build() |
| .value(); |
| NearTruncatedGeometricPartitionSelection* magic = |
| dynamic_cast<NearTruncatedGeometricPartitionSelection*>(build.get()); |
| EXPECT_THAT(magic->GetFirstCrossover(), DoubleEq(6)); |
| } |
| |
| TEST(PartitionSelectionTest, |
| NearTruncatedGeometricPartitionSelectionSecondCrossover) { |
| NearTruncatedGeometricPartitionSelection::Builder test_builder; |
| std::unique_ptr<PartitionSelectionStrategy> build = |
| test_builder.SetEpsilon(0.5) |
| .SetDelta(0.02) |
| .SetMaxPartitionsContributed(1) |
| .Build() |
| .value(); |
| NearTruncatedGeometricPartitionSelection* magic = |
| dynamic_cast<NearTruncatedGeometricPartitionSelection*>(build.get()); |
| EXPECT_THAT(magic->GetSecondCrossover(), DoubleEq(11)); |
| } |
| |
| // Values calculated with formula |
| TEST(PartitionSelectionTest, |
| NearTruncatedGeometricPartitionSelectionNumUsersEqFirstCrossover) { |
| NearTruncatedGeometricPartitionSelection::Builder test_builder; |
| std::unique_ptr<PartitionSelectionStrategy> build = |
| test_builder.SetEpsilon(0.5) |
| .SetDelta(0.02) |
| .SetMaxPartitionsContributed(1) |
| .Build() |
| .value(); |
| double num_kept = 0.0; |
| EXPECT_NEAR(build->ProbabilityOfKeep(6), 0.58840484458, 1e-10); |
| for (int i = 0; i < kNumSamples; i++) { |
| if (build->ShouldKeep(6)) num_kept++; |
| } |
| EXPECT_THAT(num_kept / kNumSamples, DoubleNear(0.58840484458, 0.001)); |
| } |
| |
| // Values calculated with formula |
| TEST(PartitionSelectionTest, |
| NearTruncatedGeometricPartitionSelectionNumUsersBtwnCrossovers) { |
| NearTruncatedGeometricPartitionSelection::Builder test_builder; |
| std::unique_ptr<PartitionSelectionStrategy> build = |
| test_builder.SetEpsilon(0.5) |
| .SetDelta(0.02) |
| .SetMaxPartitionsContributed(1) |
| .Build() |
| .value(); |
| double num_kept = 0.0; |
| for (int i = 0; i < kNumSamples; i++) { |
| if (build->ShouldKeep(8)) num_kept++; |
| } |
| EXPECT_NEAR(build->ProbabilityOfKeep(8), 0.86807080625, 1e-10); |
| EXPECT_THAT(num_kept / kNumSamples, DoubleNear(0.86807080625, 0.001)); |
| } |
| |
| // Values calculated with formula - 15 should be so large that this partition is |
| // always kept. |
| TEST(PartitionSelectionTest, |
| NearTruncatedGeometricPartitionSelectionNumUsersGreaterThanCrossovers) { |
| NearTruncatedGeometricPartitionSelection::Builder test_builder; |
| std::unique_ptr<PartitionSelectionStrategy> build = |
| test_builder.SetEpsilon(0.5) |
| .SetDelta(0.02) |
| .SetMaxPartitionsContributed(1) |
| .Build() |
| .value(); |
| EXPECT_EQ(build->ProbabilityOfKeep(15), 1); |
| for (int i = 0; i < 1000; i++) { |
| EXPECT_TRUE(build->ShouldKeep(15)); |
| } |
| } |
| |
| // For tiny epsilon probability of keeping is basically n * delta. |
| TEST(PartitionSelectionTest, |
| NearTruncatedGeometricPartitionSelectionTinyEpsilon) { |
| NearTruncatedGeometricPartitionSelection::Builder test_builder; |
| std::unique_ptr<PartitionSelectionStrategy> build = |
| test_builder.SetEpsilon(1e-20) |
| .SetDelta(0.02) |
| .SetMaxPartitionsContributed(1) |
| .Build() |
| .value(); |
| double num_kept = 0.0; |
| for (int i = 0; i < kNumSamples; i++) { |
| if (build->ShouldKeep(6)) num_kept++; |
| } |
| EXPECT_NEAR(build->ProbabilityOfKeep(6), 0.12, 1e-10); |
| EXPECT_THAT(num_kept / kNumSamples, DoubleNear(0.12, 0.001)); |
| } |
| |
| TEST(PartitionSelectionTest, |
| NearTruncatedGeometricPartitionSelectionTinyEpsilonLargeDelta) { |
| NearTruncatedGeometricPartitionSelection::Builder test_builder; |
| std::unique_ptr<PartitionSelectionStrategy> build = |
| test_builder.SetEpsilon(1e-20) |
| .SetDelta(0.15) |
| .SetMaxPartitionsContributed(1) |
| .Build() |
| .value(); |
| double num_kept = 0.0; |
| for (int i = 0; i < kNumSamples; i++) { |
| if (build->ShouldKeep(3)) num_kept++; |
| } |
| EXPECT_NEAR(build->ProbabilityOfKeep(3), 0.45, 1e-10); |
| EXPECT_THAT(num_kept / kNumSamples, DoubleNear(0.45, 0.001)); |
| } |
| |
| // For tiny epsilon probability of keeping is basically n * delta. |
| TEST(PartitionSelectionTest, |
| NearTruncatedGeometricPartitionSelectionTinyEpsilonBtwnCrossovers) { |
| NearTruncatedGeometricPartitionSelection::Builder test_builder; |
| std::unique_ptr<PartitionSelectionStrategy> build = |
| test_builder.SetEpsilon(1e-20) |
| .SetDelta(0.02) |
| .SetMaxPartitionsContributed(1) |
| .Build() |
| .value(); |
| double num_kept = 0.0; |
| for (int i = 0; i < kNumSamples; i++) { |
| if (build->ShouldKeep(40)) num_kept++; |
| } |
| EXPECT_NEAR(build->ProbabilityOfKeep(40), 0.8, 1e-10); |
| EXPECT_THAT(num_kept / kNumSamples, DoubleNear(0.8, 0.001)); |
| } |
| // LaplacePartitionSelection Tests |
| // Due to the inheritance, SetLaplaceMechanism must be |
| // called before SetDelta, SetEpsilon, etc. |
| |
| TEST(PartitionSelectionTest, |
| LaplacePartitionSelectionUnsetMaxPartitionsContributed) { |
| LaplacePartitionSelection::Builder test_builder; |
| EXPECT_THAT( |
| test_builder |
| .SetLaplaceMechanism(absl::make_unique<LaplaceMechanism::Builder>()) |
| .SetDelta(0.1) |
| .SetEpsilon(2) |
| .Build(), |
| StatusIs( |
| absl::StatusCode::kInvalidArgument, |
| HasSubstr("Maximum number of partitions that can be contributed to " |
| "(i.e., L0 sensitivity) must be set"))); |
| } |
| |
| TEST(PartitionSelectionTest, |
| LaplacePartitionSelectionNegativeMaxPartitionsContributed) { |
| LaplacePartitionSelection::Builder test_builder; |
| EXPECT_THAT( |
| test_builder |
| .SetLaplaceMechanism(absl::make_unique<LaplaceMechanism::Builder>()) |
| .SetDelta(0.1) |
| .SetEpsilon(2) |
| .SetMaxPartitionsContributed(-3) |
| .Build(), |
| StatusIs( |
| absl::StatusCode::kInvalidArgument, |
| HasSubstr("Maximum number of partitions that can be contributed to " |
| "(i.e., L0 sensitivity) must be positive, but is -3."))); |
| } |
| |
| TEST(PartitionSelectionTest, |
| LaplacePartitionSelectionZeroMaxPartitionsContributed) { |
| LaplacePartitionSelection::Builder test_builder; |
| EXPECT_THAT( |
| test_builder |
| .SetLaplaceMechanism(absl::make_unique<LaplaceMechanism::Builder>()) |
| .SetDelta(0.1) |
| .SetEpsilon(2) |
| .SetMaxPartitionsContributed(0) |
| .Build(), |
| StatusIs( |
| absl::StatusCode::kInvalidArgument, |
| HasSubstr("Maximum number of partitions that can be contributed to " |
| "(i.e., L0 sensitivity) must be positive, but is 0."))); |
| } |
| |
| TEST(PartitionSelectionTest, LaplacePartitionSelectionUnsetEpsilon) { |
| LaplacePartitionSelection::Builder test_builder; |
| EXPECT_THAT( |
| test_builder |
| .SetLaplaceMechanism(absl::make_unique<LaplaceMechanism::Builder>()) |
| .SetDelta(0.1) |
| .SetMaxPartitionsContributed(2) |
| .Build(), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Epsilon must be set."))); |
| } |
| |
| TEST(PartitionSelectionTest, LaplacePartitionSelectionUnsetDelta) { |
| LaplacePartitionSelection::Builder test_builder; |
| EXPECT_THAT( |
| test_builder |
| .SetLaplaceMechanism(absl::make_unique<LaplaceMechanism::Builder>()) |
| .SetEpsilon(0.1) |
| .SetMaxPartitionsContributed(2) |
| .Build(), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Delta must be set"))); |
| } |
| |
| TEST(PartitionSelectionTest, LaplacePartitionSelectionNanDelta) { |
| LaplacePartitionSelection::Builder test_builder; |
| EXPECT_THAT( |
| test_builder |
| .SetLaplaceMechanism(absl::make_unique<LaplaceMechanism::Builder>()) |
| .SetEpsilon(0.1) |
| .SetDelta(NAN) |
| .SetMaxPartitionsContributed(2) |
| .Build(), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Delta must be a valid numeric value"))); |
| } |
| |
| TEST(PartitionSelectionTest, LaplacePartitionSelectionNotFiniteDelta) { |
| LaplacePartitionSelection::Builder test_builder; |
| EXPECT_THAT( |
| test_builder |
| .SetLaplaceMechanism(absl::make_unique<LaplaceMechanism::Builder>()) |
| .SetEpsilon(0.1) |
| .SetDelta(std::numeric_limits<double>::infinity()) |
| .SetMaxPartitionsContributed(2) |
| .Build(), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Delta must be in the inclusive interval"))); |
| } |
| |
| TEST(PartitionSelectionTest, LaplacePartitionSelectionInvalidPositiveDelta) { |
| LaplacePartitionSelection::Builder test_builder; |
| EXPECT_THAT( |
| test_builder |
| .SetLaplaceMechanism(absl::make_unique<LaplaceMechanism::Builder>()) |
| .SetEpsilon(0.1) |
| .SetDelta(5.2) |
| .SetMaxPartitionsContributed(2) |
| .Build(), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Delta must be in the inclusive interval"))); |
| } |
| |
| TEST(PartitionSelectionTest, LaplacePartitionSelectionInvalidNegativeDelta) { |
| LaplacePartitionSelection::Builder test_builder; |
| EXPECT_THAT( |
| test_builder |
| .SetLaplaceMechanism(absl::make_unique<LaplaceMechanism::Builder>()) |
| .SetEpsilon(0.1) |
| .SetDelta(-0.1) |
| .SetMaxPartitionsContributed(2) |
| .Build(), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Delta must be in the inclusive interval"))); |
| } |
| |
| // We expect the probability of keeping a partition with one user |
| // will be approximately delta |
| TEST(PartitionSelectionTest, LaplacePartitionSelectionOneUser) { |
| LaplacePartitionSelection::Builder test_builder; |
| std::unique_ptr<PartitionSelectionStrategy> build = |
| test_builder |
| .SetLaplaceMechanism(absl::make_unique<LaplaceMechanism::Builder>()) |
| .SetEpsilon(0.5) |
| .SetDelta(0.02) |
| .SetMaxPartitionsContributed(1) |
| .Build() |
| .value(); |
| double num_kept = 0.0; |
| for (int i = 0; i < kSmallNumSamples; i++) { |
| if (build->ShouldKeep(1)) num_kept++; |
| } |
| EXPECT_NEAR(build->ProbabilityOfKeep(1), build->GetDelta(), 1e-12); |
| EXPECT_THAT(num_kept / kSmallNumSamples, |
| DoubleNear(build->GetDelta(), 0.0006)); |
| } |
| |
| // When the number of users is at the threshold, we expect drop/keep is 50/50. |
| // These numbers should make the threshold approximately 5. |
| TEST(PartitionSelectionTest, LaplacePartitionSelectionAtThreshold) { |
| LaplacePartitionSelection::Builder test_builder; |
| std::unique_ptr<PartitionSelectionStrategy> build = |
| test_builder |
| .SetLaplaceMechanism(absl::make_unique<LaplaceMechanism::Builder>()) |
| .SetEpsilon(0.5) |
| .SetDelta(0.06766764161) |
| .SetMaxPartitionsContributed(1) |
| .Build() |
| .value(); |
| double num_kept = 0.0; |
| for (int i = 0; i < kSmallNumSamples; i++) { |
| if (build->ShouldKeep(5)) num_kept++; |
| } |
| EXPECT_NEAR(build->ProbabilityOfKeep(5), 0.5, 1e-10); |
| EXPECT_THAT(num_kept / kSmallNumSamples, DoubleNear(0.5, 0.0025)); |
| } |
| |
| TEST(PartitionSelectionTest, LaplacePartitionSelectionThreshold) { |
| LaplacePartitionSelection::Builder test_builder; |
| std::unique_ptr<PartitionSelectionStrategy> build = |
| test_builder |
| .SetLaplaceMechanism(absl::make_unique<LaplaceMechanism::Builder>()) |
| .SetEpsilon(0.5) |
| .SetDelta(0.02) |
| .SetMaxPartitionsContributed(1) |
| .Build() |
| .value(); |
| LaplacePartitionSelection* laplace = |
| dynamic_cast<LaplacePartitionSelection*>(build.get()); |
| EXPECT_THAT(laplace->GetThreshold(), DoubleNear(7.43775164974, 0.001)); |
| } |
| |
| TEST(PartitionSelectionTest, LaplacePartitionSelectionUnsetBuilderThreshold) { |
| LaplacePartitionSelection::Builder test_builder; |
| std::unique_ptr<PartitionSelectionStrategy> build = |
| test_builder.SetEpsilon(0.5) |
| .SetDelta(0.02) |
| .SetMaxPartitionsContributed(1) |
| .Build() |
| .value(); |
| LaplacePartitionSelection* laplace = |
| dynamic_cast<LaplacePartitionSelection*>(build.get()); |
| EXPECT_THAT(laplace->GetThreshold(), DoubleNear(7.43775164974, 0.001)); |
| } |
| |
| TEST(PartitionSelectionTest, LaplacePartitionSelectionNoiseValueIfShouldKeep) { |
| LaplacePartitionSelection::Builder test_builder; |
| std::unique_ptr<PartitionSelectionStrategy> build = |
| test_builder |
| .SetLaplaceMechanism(absl::make_unique<LaplaceMechanism::Builder>()) |
| .SetEpsilon(0.5) |
| .SetDelta(0.06766764161) |
| .SetMaxPartitionsContributed(1) |
| .Build() |
| .value(); |
| double num_kept = 0.0; |
| auto* laplace_ps = dynamic_cast<LaplacePartitionSelection*>(build.get()); |
| const int kTinyNumSamples = 10000; |
| for (int i = 0; i < kTinyNumSamples; ++i) { |
| auto noised_value = laplace_ps->NoiseValueIfShouldKeep(5); |
| if (noised_value.has_value()) { |
| ++num_kept; |
| EXPECT_GE(noised_value.value(), laplace_ps->GetThreshold()); |
| } |
| } |
| |
| EXPECT_THAT(num_kept / kTinyNumSamples, DoubleNear(0.5, 0.02)); |
| } |
| |
| // CalculateDelta and CalculateThreshold structs and tests |
| |
| TEST(PartitionSelectionTest, |
| LaplacePartitionSelectionCalculateDeltaThresholdSymmetryAround1) { |
| double epsilon = std::log(3); |
| int64_t max_partitions_contributed = 1; |
| double delta_1_minus_i, delta_1_plus_i; |
| for (double i = 0.1; i < 5; i += 0.1) { |
| delta_1_minus_i = LaplacePartitionSelection::CalculateDelta( |
| epsilon, 1 - i, max_partitions_contributed) |
| .value(); |
| delta_1_plus_i = LaplacePartitionSelection::CalculateDelta( |
| epsilon, 1 + i, max_partitions_contributed) |
| .value(); |
| EXPECT_THAT(delta_1_minus_i, DoubleNear(1 - delta_1_plus_i, 0.0001)); |
| } |
| } |
| |
| struct CalculateDeltaTest { |
| CalculateDeltaTest(double epsilon_in, double threshold_in, |
| int64_t max_partitions_contributed_in, |
| absl::optional<double> expected_delta_in, |
| double tolerance_in = kCalcDeltaTestDefaultTolerance) |
| : epsilon(epsilon_in), |
| threshold(threshold_in), |
| max_partitions_contributed(max_partitions_contributed_in), |
| expected_delta(expected_delta_in), |
| tolerance(tolerance_in) {} |
| |
| const double epsilon; |
| const double threshold; |
| const int64_t max_partitions_contributed; |
| // Missing implies an error is expected to be returned. |
| const absl::optional<double> expected_delta; |
| const double tolerance; |
| }; |
| |
| static CalculateDeltaTest DeltaTest( |
| double epsilon, double threshold, int64_t max_partitions_contributed, |
| double expected_delta, |
| double tolerance = kCalcThresholdTestDefaultTolerance) { |
| return CalculateDeltaTest(epsilon, threshold, max_partitions_contributed, |
| expected_delta, tolerance); |
| } |
| |
| static CalculateDeltaTest DeltaErrorTest( |
| double epsilon, double threshold, int64_t max_partitions_contributed, |
| double tolerance = kCalcThresholdTestDefaultTolerance) { |
| return CalculateDeltaTest(epsilon, threshold, max_partitions_contributed, |
| std::optional<double>(), tolerance); |
| } |
| |
| TEST(PartitionSelectionTest, CalculateDeltaTests) { |
| std::vector<CalculateDeltaTest> delta_test_cases = { |
| // In all tests, "max_pc" is shorthand for "max_partitions_contributed". |
| // |
| // Fix epsilon = ln(3) & max_pc = 1, and vary threshold. |
| // |
| // expected test |
| // epsilon threshold max_pc delta tolerance |
| // -------- ---------- ------ ------------- --------- |
| DeltaTest(std::log(3.0), 1, 1, 0.5), |
| DeltaTest(std::log(3.0), 2, 1, 0.16666667), |
| DeltaTest(std::log(3.0), 3, 1, 0.05555555556, 1e-05), |
| DeltaTest(std::log(3.0), 4, 1, 0.01851851852, 1e-05), |
| DeltaTest(std::log(3.0), 5, 1, 0.00617283960, 1e-06), |
| DeltaTest(std::log(3.0), 10, 1, 2.5402631e-05, 1e-08), |
| DeltaTest(std::log(3.0), 20, 1, 4.3019580e-10, 1e-13), |
| DeltaTest(std::log(3.0), 50, 1, 2.0894334e-24, 1e-27), |
| DeltaTest(std::log(3.0), 75, 1, 2.4660232e-36, 1e-39), |
| DeltaTest(std::log(3.0), 87, 1, 4.6402600e-42, 6e-46), |
| DeltaTest(std::log(3.0), 93, 1, 6.3652400e-45, 1e-48), |
| DeltaTest(std::log(3.0), 94, 1, 2.1217500e-45, 1e-48), |
| DeltaTest(std::log(3.0), 95, 1, 7.0724900e-46, 1e-49), |
| DeltaTest(std::log(3.0), 96, 1, 2.3575000e-46, 1e-49), |
| DeltaTest(std::log(3.0), 100, 1, 2.9104900e-48, 1e-51), |
| DeltaTest(std::log(3.0), 1000, 1, 0.0, 1e-100), |
| DeltaTest(std::log(3.0), 10000, 1, 0.0, 1e-100), |
| DeltaTest(std::log(3.0), 100000, 1, 0.0, 1e-100), |
| DeltaTest(std::log(3.0), 1000000, 1, 0.0, 1e-100), |
| DeltaTest(std::log(3.0), kDoubleMax, 1, 0.0, 1e-100), |
| DeltaTest(std::log(3.0), kPosInf, 1, 0.0, 1e-100), |
| |
| // Fix threshold = 50 & max_pc = 1, and vary epsilon. |
| // |
| // expected test |
| // epsilon threshold max_pc delta tolerance |
| // ------------- --------- ------ ------------- --------- |
| DeltaTest(kDoubleMinPos, 50, 1, 0.5), |
| DeltaTest( 1e-308, 50, 1, 0.5), |
| DeltaTest( 1e-100, 50, 1, 0.5), |
| DeltaTest( 1e-50, 50, 1, 0.5), |
| DeltaTest( 1e-20, 50, 1, 0.5), |
| DeltaTest( 1e-10, 50, 1, 0.5), |
| DeltaTest( 1e-5, 50, 1, 0.49975505), |
| DeltaTest( 1e-2, 50, 1, 0.30631319), |
| DeltaTest( 1e-1, 50, 1, 0.0037232914, 1e-06), |
| DeltaTest( 0.5, 50, 1, 1.1448674e-11, 1e-14), |
| DeltaTest( 1.0, 50, 1, 2.6214428e-22, 1e-25), |
| DeltaTest(std::log(3.0), 50, 1, 2.0894334e-24, 1e-27), |
| DeltaTest( 1.5, 50, 1, 6.0024092e-33, 1e-36), |
| DeltaTest( 2.0, 50, 1, 1.3732725e-43, 1.2e-46), |
| DeltaTest( 5.0, 50, 1, 0.0, 1e-100), |
| DeltaTest( 1e1, 50, 1, 0.0, 1e-100), |
| DeltaTest( 1e2, 50, 1, 0.0, 1e-100), |
| DeltaTest( 1e5, 50, 1, 0.0, 1e-100), |
| DeltaTest( 1e10, 50, 1, 0.0, 1e-100), |
| DeltaTest( 1e20, 50, 1, 0.0, 1e-100), |
| DeltaTest( 1e50, 50, 1, 0.0, 1e-100), |
| DeltaTest( 1e100, 50, 1, 0.0, 1e-100), |
| DeltaTest( 1e308, 50, 1, 0.0, 1e-100), |
| DeltaTest( kDoubleMax, 50, 1, 0.0, 1e-100), |
| |
| // Fix epsilon & threshold and vary max_pc. |
| // expected test |
| // epsilon threshold max_pc delta tolerance |
| // -------- --------- --------- ------------- --------- |
| DeltaTest(std::log(3.0), 50, 1, 2.0894334e-24, 1e-27), |
| DeltaTest(std::log(3.0), 50, 2, 2.0442300e-12, 1e-15), |
| DeltaTest(std::log(3.0), 50, 3, 2.4160800e-08, 1e-11), |
| DeltaTest(std::log(3.0), 50, 4, 2.8595300e-06, 1e-09), |
| DeltaTest(std::log(3.0), 50, 5, 5.2740300e-05, 1e-08), |
| DeltaTest(std::log(3.0), 50, 10, 0.0227296, 1e-05), |
| DeltaTest(std::log(3.0), 50, 100, 1.0), |
| DeltaTest(std::log(3.0), 50, 1000, 1.0), |
| DeltaTest(std::log(3.0), 50, 10000, 1.0), |
| DeltaTest(std::log(3.0), 50, 100000, 1.0), |
| DeltaTest(std::log(3.0), 50, 1000000, 1.0), |
| DeltaTest(std::log(3.0), 50, kInt64Max, 1.0), |
| |
| // Error cases. |
| // |
| // Epsilon must be finite and greater than 0. |
| // |
| // epsilon threshold max_pc |
| // ---------- --------- ------ |
| DeltaErrorTest(kDoubleMin, 50, 1), |
| DeltaErrorTest( -1.0, 50, 1), |
| DeltaErrorTest( 0.0, 50, 1), |
| DeltaErrorTest( kPosInf, 50, 1), |
| DeltaErrorTest( kNegInf, 50, 1), |
| DeltaErrorTest( kNaN, 50, 1), |
| |
| // Threshold must be finite. |
| // |
| // epsilon threshold max_pc |
| // -------- --------- ------ |
| DeltaErrorTest(std::log(3.0), kNaN, 1), |
| |
| // Max_partitions_contributed (max_pc) must be greater than 0. |
| // |
| // epsilon threshold max_pc |
| // -------- --------- --------- |
| DeltaErrorTest(std::log(3.0), 50, kInt64Min), |
| DeltaErrorTest(std::log(3.0), 50, -1), |
| DeltaErrorTest(std::log(3.0), 50, 0), |
| }; |
| |
| for (const CalculateDeltaTest& test_case : delta_test_cases) { |
| const double epsilon = test_case.epsilon; |
| const double threshold = test_case.threshold; |
| const int64_t max_partitions_contributed = |
| test_case.max_partitions_contributed; |
| |
| const std::string test_case_string = absl::StrCat( |
| "epsilon: ", epsilon, ", threshold: ", threshold, |
| ", max_partitions_contributed: ", max_partitions_contributed); |
| |
| absl::StatusOr<double> status_or_delta = |
| LaplacePartitionSelection::CalculateDelta(epsilon, threshold, |
| max_partitions_contributed); |
| |
| if (test_case.expected_delta.has_value()) { |
| // We expect the test to succeed, and a delta to be computed. |
| ASSERT_TRUE(status_or_delta.ok()) << status_or_delta.status() << "\n" |
| << test_case_string; |
| const double delta = status_or_delta.value(); |
| EXPECT_THAT(delta, DoubleNear(test_case.expected_delta.value(), |
| test_case.tolerance)) |
| << test_case_string; |
| } else { |
| EXPECT_FALSE(status_or_delta.ok()) |
| << test_case_string |
| << "\nunexpected successfully computed delta value: " |
| << status_or_delta.value(); |
| } |
| } |
| } |
| |
| struct CalculateThresholdTest { |
| CalculateThresholdTest( |
| double epsilon_in, double delta_in, int64_t max_partitions_contributed_in, |
| absl::optional<double> expected_threshold_in, |
| double tolerance_in = kCalcThresholdTestDefaultTolerance) |
| : epsilon(epsilon_in), |
| delta(delta_in), |
| max_partitions_contributed(max_partitions_contributed_in), |
| expected_threshold(expected_threshold_in), |
| tolerance(tolerance_in) {} |
| |
| const double epsilon; |
| const double delta; |
| const int64_t max_partitions_contributed; |
| // Missing implies an error is expected to be returned. |
| const absl::optional<double> expected_threshold; |
| const double tolerance; |
| }; |
| |
| static CalculateThresholdTest ThresholdTest( |
| double epsilon, double delta, int64_t max_partitions_contributed, |
| double expected_threshold, |
| double tolerance = kCalcThresholdTestDefaultTolerance) { |
| return CalculateThresholdTest(epsilon, delta, max_partitions_contributed, |
| expected_threshold, tolerance); |
| } |
| |
| static CalculateThresholdTest ThresholdErrorTest( |
| double epsilon, double delta, int64_t max_partitions_contributed, |
| double tolerance = kCalcThresholdTestDefaultTolerance) { |
| return CalculateThresholdTest(epsilon, delta, max_partitions_contributed, |
| std::optional<double>(), tolerance); |
| } |
| |
| TEST(PartitionSelectionTest, CalculateThresholdTests) { |
| std::vector<CalculateThresholdTest> threshold_test_cases = { |
| // In all tests, "max_pc" is shorthand for "max_partitions_contributed". |
| // |
| // Fix epsilon = ln(3) & max_pc = 1, and vary delta. |
| // |
| // expected test |
| // epsilon delta max_pc threshold tolerance |
| // -------- ------------- ------ --------- --------- |
| ThresholdTest(std::log(3.0), 0.0, 1, kPosInf), |
| ThresholdTest(std::log(3.0), kDoubleMinPos, 1, 645.17900), |
| ThresholdTest(std::log(3.0), 1e-308, 1, 645.90700), |
| ThresholdTest(std::log(3.0), 1e-256, 1, 536.92000), |
| ThresholdTest(std::log(3.0), 1e-128, 1, 268.64500), |
| ThresholdTest(std::log(3.0), 1e-64, 1, 134.50700), |
| ThresholdTest(std::log(3.0), 1e-32, 1, 67.43800), |
| ThresholdTest(std::log(3.0), 2.0894334e-24, 1, 50.00000), |
| ThresholdTest(std::log(3.0), 1e-16, 1, 33.90350), |
| ThresholdTest(std::log(3.0), 1e-8, 1, 17.13630), |
| ThresholdTest(std::log(3.0), 1e-4, 1, 8.75268), |
| ThresholdTest(std::log(3.0), 1e-2, 1, 4.56088), |
| ThresholdTest(std::log(3.0), 0.1, 1, 2.46497), |
| ThresholdTest(std::log(3.0), 0.3, 1, 1.46497), |
| ThresholdTest(std::log(3.0), 0.5, 1, 1.00000), |
| ThresholdTest(std::log(3.0), 0.7, 1, 0.53503), |
| ThresholdTest(std::log(3.0), 0.9, 1, -0.46497), |
| ThresholdTest(std::log(3.0), 1.0, 1, kNegInf, 1e-46), |
| |
| // Fix epsilon = 10^9 & max_pc = 1, and vary delta |
| // |
| // expected test |
| // epsilon delta max_pc threshold tolerance |
| // ---------- ------------- ------ --------- --------- |
| ThresholdTest(1000000000, 0.0, 1, kPosInf), |
| ThresholdTest(1000000000, kDoubleMinPos, 1, 1), |
| ThresholdTest(1000000000, 1e-308, 1, 1), |
| ThresholdTest(1000000000, 1e-200, 1, 1), |
| ThresholdTest(1000000000, 1e-100, 1, 1), |
| ThresholdTest(1000000000, 1e-50, 1, 1), |
| ThresholdTest(1000000000, 1e-8, 1, 1), |
| ThresholdTest(1000000000, 0.1, 1, 1), |
| ThresholdTest(1000000000, 0.5, 1, 1), |
| ThresholdTest(1000000000, 0.8, 1, 1), |
| ThresholdTest(1000000000, 1.0, 1, kNegInf, 1e-46), |
| |
| // Fix delta = 2.0894334e-24 & max_pc = 1, and vary epsilon. |
| // |
| // expected test |
| // epsilon delta max_pc threshold tolerance |
| // ------------- ------------- ------ ---------- ----------- |
| ThresholdTest(kDoubleMinPos, 2.0894334e-24, 1, kPosInf), |
| ThresholdTest( 1e-308, 2.0894334e-24, 1, kPosInf), |
| ThresholdTest( 1e-100, 2.0894334e-24, 1, 5.3832e101, 2.12256e94), |
| ThresholdTest( 1e-50, 2.0894334e-24, 1, 5.3832e051, 2.12256e44), |
| ThresholdTest( 1e-20, 2.0894334e-24, 1, 5.3832e021, 2.12256e14), |
| ThresholdTest( 1e-10, 2.0894334e-24, 1, 5.3832e011, 21227), |
| ThresholdTest( 1e-5, 2.0894334e-24, 1, 5.3832e006, 1.3), |
| ThresholdTest( 1e-2, 2.0894334e-24, 1, 5384.2000, 0.15), |
| ThresholdTest( 1e-1, 2.0894334e-24, 1, 539.3200), |
| ThresholdTest( 0.5, 2.0894334e-24, 1, 108.6640), |
| ThresholdTest( 1.0, 2.0894334e-24, 1, 54.8320), |
| ThresholdTest(std::log(3.0), 2.0894334e-24, 1, 50.0000), |
| ThresholdTest( 1.5, 2.0894334e-24, 1, 36.8880), |
| ThresholdTest( 2.0, 2.0894334e-24, 1, 27.9160), |
| ThresholdTest( 5.0, 2.0894334e-24, 1, 11.7664), |
| ThresholdTest( 1e1, 2.0894334e-24, 1, 6.3832), |
| ThresholdTest( 1e2, 2.0894334e-24, 1, 1.53832), |
| ThresholdTest( 1e5, 2.0894334e-24, 1, 1.00054), |
| ThresholdTest( 1e10, 2.0894334e-24, 1, 1.0000), |
| ThresholdTest( 1e20, 2.0894334e-24, 1, 1.0000), |
| ThresholdTest( 1e50, 2.0894334e-24, 1, 1.0000), |
| ThresholdTest( 1e100, 2.0894334e-24, 1, 1.0000), |
| ThresholdTest( 1e308, 2.0894334e-24, 1, 1.0000), |
| ThresholdTest( kDoubleMax, 2.0894334e-24, 1, 1.0000), |
| |
| // Fix epsilon & delta and vary max_pc. |
| // expected test |
| // epsilon delta max_pc threshold tolerance |
| // -------- ------------- --------- ---------- --------- |
| ThresholdTest(std::log(3.0), 2.0894334e-24, 1, 50.000), |
| ThresholdTest(std::log(3.0), 2.0894334e-24, 2, 100.262), |
| ThresholdTest(std::log(3.0), 2.0894334e-24, 3, 151.000), |
| ThresholdTest(std::log(3.0), 2.0894334e-24, 4, 202.047), |
| ThresholdTest(std::log(3.0), 2.0894334e-24, 5, 253.325), |
| ThresholdTest(std::log(3.0), 2.0894334e-24, 10, 511.959), |
| ThresholdTest(std::log(3.0), 2.0894334e-24, 100, 5320.180), |
| ThresholdTest(std::log(3.0), 2.0894334e-24, 1000, 55288.700), |
| ThresholdTest(std::log(3.0), 2.0894334e-24, 10000, 573837.000, 0.131), |
| ThresholdTest(std::log(3.0), 2.0894334e-24, 100000, 5.94795e6, 2.640), |
| ThresholdTest(std::log(3.0), 2.0894334e-24, 1000000, 6.15754e7, 20.63), |
| ThresholdTest(std::log(3.0), 2.0894334e-24, kInt64Max, 8.18561e20, 1.19938e14), |
| |
| // Test that we can legitimately compute a negative threshold. |
| ThresholdTest(0.001, 0.99999999999, 1, -24634.3), |
| |
| // Error cases. |
| // |
| // Epsilon must be finite and greater than 0. |
| // |
| // epsilon delta max_pc |
| // ---------- ------------- ------ |
| ThresholdErrorTest(kDoubleMin, 2.0894334e-24, 1), |
| ThresholdErrorTest( -1.0, 2.0894334e-24, 1), |
| ThresholdErrorTest( 0.0, 2.0894334e-24, 1), |
| ThresholdErrorTest( kPosInf, 2.0894334e-24, 1), |
| ThresholdErrorTest( kNegInf, 2.0894334e-24, 1), |
| ThresholdErrorTest( kNaN, 2.0894334e-24, 1), |
| |
| // Max_pc must be greater than 0. |
| // |
| // epsilon delta max_pc |
| // --------- ------------- ------ |
| ThresholdErrorTest(std::log(3.0), 2.0894334e-24, kInt64Min), |
| ThresholdErrorTest(std::log(3.0), 2.0894334e-24, -1), |
| ThresholdErrorTest(std::log(3.0), 2.0894334e-24, 0), |
| |
| // Delta must be in range [0, 1]. |
| // |
| // epsilon delta max_pc |
| // -------- ----------------- ------ |
| ThresholdErrorTest(std::log(3.0), kNegInf, 1), |
| ThresholdErrorTest(std::log(3.0), kNegInf, 1), |
| ThresholdErrorTest(std::log(3.0), -1, 1), |
| ThresholdErrorTest(std::log(3.0), -1, 1), |
| ThresholdErrorTest(std::log(3.0), 0 - kDoubleMinPos, 1), |
| ThresholdErrorTest(std::log(3.0), 0 - kDoubleMinPos, 1), |
| ThresholdErrorTest(std::log(3.0), 1.0000000000001, 1), |
| ThresholdErrorTest(std::log(3.0), 1.0000000000001, 1), |
| ThresholdErrorTest(std::log(3.0), 2, 1), |
| ThresholdErrorTest(std::log(3.0), 2, 1), |
| ThresholdErrorTest(std::log(3.0), kPosInf, 1), |
| ThresholdErrorTest(std::log(3.0), kPosInf, 1), |
| }; |
| |
| for (const CalculateThresholdTest& test_case : threshold_test_cases) { |
| const double epsilon = test_case.epsilon; |
| const double delta = test_case.delta; |
| const int64_t max_partitions_contributed = |
| test_case.max_partitions_contributed; |
| |
| const std::string test_case_string = absl::StrCat( |
| "epsilon: ", epsilon, ", delta: ", delta, |
| ", max_partitions_contributed: ", max_partitions_contributed); |
| |
| absl::StatusOr<double> status_or_threshold = |
| LaplacePartitionSelection::CalculateThreshold( |
| epsilon, delta, max_partitions_contributed); |
| |
| if (test_case.expected_threshold.has_value()) { |
| // We expect the test to succeed, and a threshold to be computed. |
| ASSERT_TRUE(status_or_threshold.ok()) |
| << status_or_threshold.status() << "\n" |
| << test_case_string; |
| const double threshold = status_or_threshold.value(); |
| EXPECT_THAT(threshold, DoubleNear(test_case.expected_threshold.value(), |
| test_case.tolerance)) |
| << test_case_string; |
| } else { |
| EXPECT_FALSE(status_or_threshold.ok()) |
| << test_case_string |
| << "\nunexpected successfully computed threshold value: " |
| << status_or_threshold.value(); |
| } |
| } |
| } |
| |
| TEST(PartitionSelectionTest, RoundTripThresholdTests) { |
| // Vary threshold from 1 to 100, and max_partitions_contributed from 1 to 5. |
| // Calculate delta for each each tuple (epsilon, threshold, |
| // max_partitions_contributed), and then re-compute threshold to ensure it is |
| // the same. Note that once the threshold gets high enough so that the |
| // computed delta is 0, round-tripping back to the original threshold longer |
| // works so we stop early in that case. |
| const double epsilon = std::log(3); |
| for (int64_t max_partitions_contributed = 1; max_partitions_contributed < 5; |
| ++max_partitions_contributed) { |
| bool computed_delta_of_zero = false; |
| for (int64_t threshold = -20; threshold < 1000; threshold += 10) { |
| std::string test_case_string = absl::StrCat( |
| "threshold: ", threshold, |
| ", max_partitions_contributed: ", max_partitions_contributed); |
| absl::StatusOr<double> status_or_delta = |
| LaplacePartitionSelection::CalculateDelta(epsilon, threshold, |
| max_partitions_contributed); |
| ASSERT_TRUE(status_or_delta.ok()) << status_or_delta.status() << "\n" |
| << test_case_string; |
| const double delta = status_or_delta.value(); |
| absl::StrAppend(&test_case_string, ", computed delta: ", delta); |
| |
| // If the computed delta is 0, then computing threshold from this will |
| // result in the maximum value. Therefore round tripping will not work. |
| // Note that all remaining input threshold values are higher that this one |
| // within this loop, which implies that all remaining computed deltas will |
| // be 0 as well. |
| if (computed_delta_of_zero) { |
| EXPECT_EQ(delta, 0); |
| continue; |
| } |
| if (delta == 0) { |
| computed_delta_of_zero = true; |
| continue; |
| } |
| |
| absl::StatusOr<double> status_or_threshold = |
| LaplacePartitionSelection::CalculateThreshold( |
| epsilon, delta, max_partitions_contributed); |
| ASSERT_TRUE(status_or_threshold.ok()) |
| << status_or_threshold.status() << "\n" |
| << test_case_string; |
| |
| // We normally expect that the original threshold and the round tripped |
| // threshold are the same. However, because of the loss of precision |
| // due to floating point calculations, there may be some variance. |
| const double computed_threshold = status_or_threshold.value(); |
| |
| EXPECT_THAT(computed_threshold, DoubleNear(threshold, 0.001)) |
| << test_case_string; |
| } |
| } |
| } |
| |
| TEST(PartitionSelectionTest, RoundTripDeltaTests) { |
| // Ideally, vary max_partitions_contributed from 1 to 5, and delta from 0 to 1 |
| // (exclusively). Calculate threshold for each triple (epsilon, delta, |
| // max_partitions_contributed), and then re-compute the delta. |
| // |
| // We can't check the recomputed delta for equality against the original |
| // delta because there are many deltas that map to the same threshold. |
| // We also cannot check that the recomputed delta is closer to the |
| // original delta than to computed deltas for threshold +/-1, and we |
| // cannot check that the original delta is closer to the recomputed delta |
| // than to the computed deltas for threshold +/1. This is because the |
| // curve is not linear, and, for example, the original delta can be closer |
| // to the computed delta for threshold-1 than to the recomputed delta. |
| // |
| // So we just test here that the original delta is between the recomputed |
| // delta and either the computed delta for threshold+1 or threshold-1. |
| const double epsilon = std::log(3); |
| for (int64_t max_partitions_contributed = 1; max_partitions_contributed < 5; |
| ++max_partitions_contributed) { |
| for (double delta = 1e-308; delta < 1.0; delta *= 10) { |
| std::string test_case_string = absl::StrCat( |
| "delta: ", delta, |
| ", max_partitions_contributed: ", max_partitions_contributed); |
| |
| absl::StatusOr<double> status_or_threshold = |
| LaplacePartitionSelection::CalculateThreshold( |
| epsilon, delta, max_partitions_contributed); |
| ASSERT_TRUE(status_or_threshold.ok()) |
| << status_or_threshold.status() << "\n" |
| << test_case_string; |
| const double threshold = status_or_threshold.value(); |
| |
| absl::StrAppend(&test_case_string, ", computed threshold: ", threshold); |
| |
| const absl::StatusOr<double> computed_delta = |
| LaplacePartitionSelection::CalculateDelta(epsilon, threshold, |
| max_partitions_contributed); |
| |
| const absl::StatusOr<double> computed_delta_plus_one = |
| LaplacePartitionSelection::CalculateDelta(epsilon, threshold + 1, |
| max_partitions_contributed); |
| |
| const absl::StatusOr<double> computed_delta_minus_one = |
| LaplacePartitionSelection::CalculateDelta(epsilon, threshold - 1, |
| max_partitions_contributed); |
| |
| // Expect that the original delta is closer to the computed delta than |
| // to the computed delta for threshold+1 or threshold-1. |
| ASSERT_TRUE(computed_delta.ok()); |
| ASSERT_TRUE(computed_delta_plus_one.ok()); |
| ASSERT_TRUE(computed_delta_minus_one.ok()); |
| |
| absl::StrAppend(&test_case_string, |
| ", computed delta: ", computed_delta.value()); |
| absl::StrAppend(&test_case_string, ", computed delta (k plus_one): ", |
| computed_delta_plus_one.value()); |
| absl::StrAppend(&test_case_string, ", computed delta (k minus one): ", |
| computed_delta_minus_one.value()); |
| |
| // The original delta and recomputed delta for threshold is greater |
| // than the computed delta for threshold+1. |
| // The original delta and recomputed delta for threshold is less than |
| // the computed delta for threshold-1. |
| EXPECT_GT(delta, computed_delta_plus_one.value()); |
| EXPECT_LT(delta, computed_delta_minus_one.value()); |
| |
| EXPECT_GT(computed_delta.value(), computed_delta_plus_one.value()); |
| EXPECT_LT(computed_delta.value(), computed_delta_minus_one.value()); |
| |
| EXPECT_TRUE((delta >= computed_delta.value() && |
| delta <= computed_delta_minus_one.value()) || |
| (delta <= computed_delta.value() && |
| delta >= computed_delta_plus_one.value())); |
| } |
| } |
| } |
| |
| // GaussianPartitionSelection Tests |
| // Due to the inheritance, SetGaussianMechanism must be |
| // called before SetDelta, SetEpsilon, etc. |
| |
| TEST(PartitionSelectionTest, |
| GaussianPartitionSelectionUnsetMaxPartitionsContributed) { |
| GaussianPartitionSelection::Builder test_builder; |
| EXPECT_THAT( |
| test_builder |
| .SetGaussianMechanism(absl::make_unique<GaussianMechanism::Builder>()) |
| .SetDelta(0.1) |
| .SetEpsilon(2) |
| .Build(), |
| StatusIs( |
| absl::StatusCode::kInvalidArgument, |
| HasSubstr("Maximum number of partitions that can be contributed to " |
| "(i.e., L0 sensitivity) must be set"))); |
| } |
| |
| TEST(PartitionSelectionTest, |
| GaussianPartitionSelectionNegativeMaxPartitionsContributed) { |
| GaussianPartitionSelection::Builder test_builder; |
| EXPECT_THAT( |
| test_builder |
| .SetGaussianMechanism(absl::make_unique<GaussianMechanism::Builder>()) |
| .SetDelta(0.1) |
| .SetEpsilon(2) |
| .SetMaxPartitionsContributed(-3) |
| .Build(), |
| StatusIs( |
| absl::StatusCode::kInvalidArgument, |
| HasSubstr("Maximum number of partitions that can be contributed to " |
| "(i.e., L0 sensitivity) must be positive"))); |
| } |
| |
| TEST(PartitionSelectionTest, |
| GaussianPartitionSelectionZeroMaxPartitionsContributed) { |
| GaussianPartitionSelection::Builder test_builder; |
| EXPECT_THAT( |
| test_builder |
| .SetGaussianMechanism(absl::make_unique<GaussianMechanism::Builder>()) |
| .SetDelta(0.1) |
| .SetEpsilon(2) |
| .SetMaxPartitionsContributed(0) |
| .Build(), |
| StatusIs( |
| absl::StatusCode::kInvalidArgument, |
| HasSubstr("Maximum number of partitions that can be contributed to " |
| "(i.e., L0 sensitivity) must be positive, but is 0."))); |
| } |
| |
| TEST(PartitionSelectionTest, GaussianPartitionSelectionUnsetEpsilon) { |
| GaussianPartitionSelection::Builder test_builder; |
| EXPECT_THAT( |
| test_builder |
| .SetGaussianMechanism(absl::make_unique<GaussianMechanism::Builder>()) |
| .SetDelta(0.1) |
| .SetMaxPartitionsContributed(2) |
| .Build(), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Epsilon must be set."))); |
| } |
| |
| TEST(PartitionSelectionTest, GaussianPartitionSelectionUnsetDelta) { |
| GaussianPartitionSelection::Builder test_builder; |
| EXPECT_THAT( |
| test_builder |
| .SetGaussianMechanism(absl::make_unique<GaussianMechanism::Builder>()) |
| .SetEpsilon(0.1) |
| .SetMaxPartitionsContributed(2) |
| .Build(), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Delta must be set."))); |
| } |
| |
| TEST(PartitionSelectionTest, GaussianPartitionSelectionNanDelta) { |
| GaussianPartitionSelection::Builder test_builder; |
| EXPECT_THAT( |
| test_builder |
| .SetGaussianMechanism(absl::make_unique<GaussianMechanism::Builder>()) |
| .SetEpsilon(0.1) |
| .SetDelta(NAN) |
| .SetMaxPartitionsContributed(2) |
| .Build(), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Delta must be a valid numeric value"))); |
| } |
| |
| TEST(PartitionSelectionTest, GaussianPartitionSelectionNotFiniteDelta) { |
| GaussianPartitionSelection::Builder test_builder; |
| EXPECT_THAT( |
| test_builder |
| .SetGaussianMechanism(absl::make_unique<GaussianMechanism::Builder>()) |
| .SetEpsilon(0.1) |
| .SetDelta(std::numeric_limits<double>::infinity()) |
| .SetMaxPartitionsContributed(2) |
| .Build(), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Delta must be in the inclusive interval"))); |
| } |
| |
| TEST(PartitionSelectionTest, GaussianPartitionSelectionInvalidPositiveDelta) { |
| GaussianPartitionSelection::Builder test_builder; |
| EXPECT_THAT( |
| test_builder |
| .SetGaussianMechanism(absl::make_unique<GaussianMechanism::Builder>()) |
| .SetEpsilon(0.1) |
| .SetDelta(5.2) |
| .SetMaxPartitionsContributed(2) |
| .Build(), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Delta must be in the inclusive interval"))); |
| } |
| |
| TEST(PartitionSelectionTest, GaussianPartitionSelectionInvalidNegativeDelta) { |
| GaussianPartitionSelection::Builder test_builder; |
| EXPECT_THAT( |
| test_builder |
| .SetGaussianMechanism(absl::make_unique<GaussianMechanism::Builder>()) |
| .SetEpsilon(0.1) |
| .SetDelta(-0.1) |
| .SetMaxPartitionsContributed(2) |
| .Build(), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Delta must be in the inclusive interval"))); |
| } |
| |
| TEST(PartitionSelectionTest, CalculateGaussianThresholdTests) { |
| std::vector<CalculateThresholdTest> threshold_test_cases = { |
| // In all tests, "max_pc" is shorthand for "max_partitions_contributed". |
| // |
| // Fix epsilon = ln(3) & max_pc = 1, and vary delta. |
| // |
| // expected test |
| // epsilon delta max_pc threshold tolerance |
| // -------- ------------- ------ --------- --------- |
| ThresholdTest(std::log(3.0), 0.0, 1, kPosInf), |
| ThresholdTest(std::log(3.0), kDoubleMinPos, 1, kPosInf), |
| ThresholdTest(std::log(3.0), 1e-308, 1, kPosInf), |
| ThresholdTest(std::log(3.0), 1e-256, 1, kPosInf), |
| ThresholdTest(std::log(3.0), 1e-128, 1, kPosInf), |
| ThresholdTest(std::log(3.0), 1e-64, 1, kPosInf), |
| ThresholdTest(std::log(3.0), 1e-32, 1, kPosInf), |
| ThresholdTest(std::log(3.0), 1e-16, 1, kPosInf), |
| ThresholdTest(std::log(3.0), 1e-8, 1, 28.3774), |
| ThresholdTest(std::log(3.0), 1e-4, 1, 13.0061), |
| ThresholdTest(std::log(3.0), 1e-2, 1, 6.02085), |
| ThresholdTest(std::log(3.0), 0.1, 1, 3.06731), |
| ThresholdTest(std::log(3.0), 0.3, 1, 1.93016), |
| ThresholdTest(std::log(3.0), 0.5, 1, 1.49072), |
| ThresholdTest(std::log(3.0), 0.7, 1, 1.23706), |
| ThresholdTest(std::log(3.0), 0.9, 1, 1.06663), |
| ThresholdTest(std::log(3.0), 1.0, 1, 1), |
| ThresholdTest(std::log(3.0), 2.0, 1, kNegInf), |
| |
| // Fix epsilon = 10^9 & max_pc = 1, and vary delta |
| // |
| // expected test |
| // epsilon delta max_pc threshold tolerance |
| // ---------- ------------- ------ --------- --------- |
| ThresholdTest(1000000000, 0.0, 1, kPosInf), |
| ThresholdTest(1000000000, kDoubleMinPos, 1, kPosInf), |
| ThresholdTest(1000000000, 1e-308, 1, kPosInf), |
| ThresholdTest(1000000000, 1e-200, 1, kPosInf), |
| ThresholdTest(1000000000, 1e-100, 1, kPosInf), |
| ThresholdTest(1000000000, 1e-50, 1, kPosInf), |
| ThresholdTest(1000000000, 1e-8, 1, 1), |
| ThresholdTest(1000000000, 0.1, 1, 1), |
| ThresholdTest(1000000000, 0.5, 1, 1), |
| ThresholdTest(1000000000, 0.8, 1, 1), |
| ThresholdTest(1000000000, 1.0, 1, 1), |
| ThresholdTest(1000000000, 2.0, 1, kNegInf), |
| |
| // Fix delta = 2.0894334e-24 & max_pc = 1, and vary epsilon. |
| // |
| // expected test |
| // epsilon delta max_pc threshold tolerance |
| // ------------- ------------- ------ ---------- ----------- |
| ThresholdTest(kDoubleMinPos, 2.0894334e-14, 1, 2.82558e+14, 1e+09), |
| ThresholdTest( 1e-308, 2.0894334e-14, 1, 2.82558e+14, 1e+09), |
| ThresholdTest( 1e-100, 2.0894334e-14, 1, 2.82558e+14, 1e+09), |
| ThresholdTest( 1e-50, 2.0894334e-14, 1, 2.82558e+14, 1e+09), |
| ThresholdTest( 1e-20, 2.0894334e-14, 1, 2.83579e+14, 1e+09), |
| ThresholdTest( 1e-10, 2.0894334e-14, 1, 2.26706e+11, 1e+05), |
| ThresholdTest( 1e-5, 2.0894334e-14, 1, 4.01647e+06, 10), |
| ThresholdTest( 1e-2, 2.0894334e-14, 1, 4818.48), |
| ThresholdTest( 1e-1, 2.0894334e-14, 1, 507.077), |
| ThresholdTest( 0.5, 2.0894334e-14, 1, 105.872), |
| ThresholdTest( 1.0, 2.0894334e-14, 1, 54.3645), |
| ThresholdTest(std::log(3.0), 2.0894334e-14, 1, 49.7216), |
| ThresholdTest( 1.5, 2.0894334e-14, 1, 37.0116), |
| ThresholdTest( 2.0, 2.0894334e-14, 1, 28.2626), |
| ThresholdTest( 5.0, 2.0894334e-14, 1, 12.3534), |
| ThresholdTest( 1e1, 2.0894334e-14, 1, 6.95236), |
| ThresholdTest( 1e2, 2.0894334e-14, 1, 1.87689), |
| ThresholdTest( 1e5, 2.0894334e-14, 1, 1.00054), |
| ThresholdTest( 1e10, 2.0894334e-14, 1, 1.0000), |
| ThresholdTest( 1e20, 2.0894334e-14, 1, 1.0000), |
| ThresholdTest( 1e50, 2.0894334e-14, 1, 1.0000), |
| ThresholdTest( 1e100, 2.0894334e-14, 1, 1.0000), |
| ThresholdTest( 1e308, 2.0894334e-14, 1, 1.0000), |
| ThresholdTest( kDoubleMax, 2.0894334e-14, 1, 1.0000), |
| |
| // Fix epsilon & delta and vary max_pc. |
| // expected test |
| // epsilon delta max_pc threshold tolerance |
| // -------- ------------- --------- ---------- --------- |
| ThresholdTest(std::log(3.0), 2.0894334e-14, 1, 49.7216), |
| ThresholdTest(std::log(3.0), 2.0894334e-14, 2, 98.8924), |
| ThresholdTest(std::log(3.0), 2.0894334e-14, 3, 148.135), |
| ThresholdTest(std::log(3.0), 2.0894334e-14, 4, 197.364), |
| ThresholdTest(std::log(3.0), 2.0894334e-14, 5, 246.615), |
| ThresholdTest(std::log(3.0), 2.0894334e-14, 10, 492.543), |
| ThresholdTest(std::log(3.0), 2.0894334e-14, 100, 4851), |
| ThresholdTest(std::log(3.0), 2.0894334e-14, 1000, kPosInf), |
| ThresholdTest(std::log(3.0), 2.0894334e-14, 10000, kPosInf), |
| ThresholdTest(std::log(3.0), 2.0894334e-14, 100000, kPosInf), |
| ThresholdTest(std::log(3.0), 2.0894334e-14, 1000000, kPosInf), |
| ThresholdTest(std::log(3.0), 2.0894334e-14, kInt64Max, kPosInf), |
| |
| // Error cases. |
| // |
| // Epsilon must be finite and greater than 0. |
| // |
| // epsilon delta max_pc |
| // ---------- ------------- ------ |
| ThresholdErrorTest(kDoubleMin, 2.0894334e-24, 1), |
| ThresholdErrorTest( -1.0, 2.0894334e-24, 1), |
| ThresholdErrorTest( 0.0, 2.0894334e-24, 1), |
| ThresholdErrorTest( kPosInf, 2.0894334e-24, 1), |
| ThresholdErrorTest( kNegInf, 2.0894334e-24, 1), |
| ThresholdErrorTest( kNaN, 2.0894334e-24, 1), |
| |
| // Max_pc must be greater than 0. |
| // |
| // epsilon delta max_pc |
| // --------- ------------- ------ |
| ThresholdErrorTest(std::log(3.0), 2.0894334e-24, kInt64Min), |
| ThresholdErrorTest(std::log(3.0), 2.0894334e-24, -1), |
| ThresholdErrorTest(std::log(3.0), 2.0894334e-24, 0), |
| |
| // Delta must be in range [0, 2]. |
| // |
| // epsilon delta max_pc |
| // -------- ----------------- ------ |
| ThresholdErrorTest(std::log(3.0), kNegInf, 1), |
| ThresholdErrorTest(std::log(3.0), kNegInf, 1), |
| ThresholdErrorTest(std::log(3.0), -1, 1), |
| ThresholdErrorTest(std::log(3.0), -1, 1), |
| ThresholdErrorTest(std::log(3.0), 0 - kDoubleMinPos, 1), |
| ThresholdErrorTest(std::log(3.0), 0 - kDoubleMinPos, 1), |
| ThresholdErrorTest(std::log(3.0), 2.0000000000001, 1), |
| ThresholdErrorTest(std::log(3.0), 2.0000000000001, 1), |
| ThresholdErrorTest(std::log(3.0), 3, 1), |
| ThresholdErrorTest(std::log(3.0), 3, 1), |
| ThresholdErrorTest(std::log(3.0), kPosInf, 1), |
| ThresholdErrorTest(std::log(3.0), kPosInf, 1), |
| }; |
| |
| for (const CalculateThresholdTest& test_case : threshold_test_cases) { |
| const double epsilon = test_case.epsilon; |
| const double delta = test_case.delta; |
| const int64_t max_partitions_contributed = |
| test_case.max_partitions_contributed; |
| |
| const std::string test_case_string = absl::StrCat( |
| "epsilon: ", epsilon, ", delta: ", delta, |
| ", max_partitions_contributed: ", max_partitions_contributed); |
| |
| const double noise_delta = delta / 2; |
| const double threshold_delta = delta - noise_delta; |
| absl::StatusOr<double> status_or_threshold = |
| GaussianPartitionSelection::CalculateThreshold( |
| epsilon, noise_delta, threshold_delta, max_partitions_contributed); |
| |
| if (test_case.expected_threshold.has_value()) { |
| // We expect the test to succeed, and a threshold to be computed. |
| ASSERT_TRUE(status_or_threshold.ok()) |
| << status_or_threshold.status() << "\n" |
| << test_case_string; |
| const double threshold = status_or_threshold.value(); |
| EXPECT_THAT(threshold, DoubleNear(test_case.expected_threshold.value(), |
| test_case.tolerance)) |
| << test_case_string; |
| } else { |
| EXPECT_FALSE(status_or_threshold.ok()) |
| << test_case_string |
| << "\nunexpected successfully computed threshold value: " |
| << status_or_threshold.value(); |
| } |
| } |
| } |
| |
| TEST(PartitionSelectionTest, CalculateGaussianDeltaTests) { |
| std::vector<CalculateDeltaTest> delta_test_cases = { |
| // In all tests, "max_pc" is shorthand for "max_partitions_contributed". |
| // |
| // Fix epsilon = ln(3) & max_pc = 1, and vary threshold. |
| // |
| // expected test |
| // epsilon threshold max_pc delta tolerance |
| // -------- ---------- ------ ------------- --------- |
| DeltaTest(std::log(3.0), 1, 1, 0.5), |
| DeltaTest(std::log(3.0), 2, 1, 0.0216028), |
| DeltaTest(std::log(3.0), 3, 1, 2.63368e-05, 1e-05), |
| DeltaTest(std::log(3.0), 4, 1, 6.5914e-10, 1e-05), |
| DeltaTest(std::log(3.0), 5, 1, 3.33067e-16, 1e-06), |
| DeltaTest(std::log(3.0), 10, 1, 0.0, 1e-100), |
| DeltaTest(std::log(3.0), 20, 1, 0.0, 1e-100), |
| DeltaTest(std::log(3.0), 50, 1, 0.0, 1e-100), |
| DeltaTest(std::log(3.0), 75, 1, 0.0, 1e-100), |
| DeltaTest(std::log(3.0), 87, 1, 0.0, 6e-100), |
| DeltaTest(std::log(3.0), 93, 1, 0.0, 1e-100), |
| DeltaTest(std::log(3.0), 94, 1, 0.0, 1e-100), |
| DeltaTest(std::log(3.0), 95, 1, 0.0, 1e-100), |
| DeltaTest(std::log(3.0), 96, 1, 0.0, 1e-100), |
| DeltaTest(std::log(3.0), 100, 1, 0.0, 1e-100), |
| DeltaTest(std::log(3.0), 1000, 1, 0.0, 1e-100), |
| DeltaTest(std::log(3.0), 10000, 1, 0.0, 1e-100), |
| DeltaTest(std::log(3.0), 100000, 1, 0.0, 1e-100), |
| DeltaTest(std::log(3.0), 1000000, 1, 0.0, 1e-100), |
| DeltaTest(std::log(3.0), kDoubleMax, 1, 0.0, 1e-100), |
| DeltaTest(std::log(3.0), kPosInf, 1, 0.0, 1e-100), |
| |
| // Fix threshold = 2 & max_pc = 1, and vary epsilon. |
| // |
| // expected test |
| // epsilon threshold max_pc delta tolerance |
| // ------------- --------- ------ ------------- --------- |
| DeltaTest(kDoubleMinPos, 2, 1, 0.0887881), |
| DeltaTest( 1e-308, 2, 1, 0.0887881), |
| DeltaTest( 1e-100, 2, 1, 0.0887881), |
| DeltaTest( 1e-50, 2, 1, 0.0887881), |
| DeltaTest( 1e-20, 2, 1, 0.0887881), |
| DeltaTest( 1e-10, 2, 1, 0.0887881), |
| DeltaTest( 1e-5, 2, 1, 0.0887881), |
| DeltaTest( 1e-2, 2, 1, 0.0875032), |
| DeltaTest( 1e-1, 2, 1, 0.0771943), |
| DeltaTest( 0.5, 2, 1, 0.0454027), |
| DeltaTest( 1.0, 2, 1, 0.0243547), |
| DeltaTest(std::log(3.0), 2, 1, 0.0216028), |
| DeltaTest( 1.5, 2, 1, 0.0134125), |
| DeltaTest( 2.0, 2, 1, 0.00750155), |
| DeltaTest( 5.0, 2, 1, 0.000273579), |
| DeltaTest( 1e1, 2, 1, 1.38959e-06), |
| DeltaTest( 1e2, 2, 1, 0.0, 1e-100), |
| DeltaTest( 1e5, 2, 1, 0.0, 1e-100), |
| DeltaTest( 1e10, 2, 1, 0.0, 1e-100), |
| DeltaTest( 1e20, 2, 1, 0.0, 1e-100), |
| DeltaTest( 1e50, 2, 1, 0.0, 1e-100), |
| DeltaTest( 1e100, 2, 1, 0.0, 1e-100), |
| DeltaTest( 1e308, 2, 1, 0.0, 1e-100), |
| DeltaTest( kDoubleMax, 2, 1, 0.0, 1e-100), |
| |
| // Fix epsilon & threshold and vary max_pc. |
| // expected test |
| // epsilon threshold max_pc delta tolerance |
| // -------- --------- --------- ------------- --------- |
| DeltaTest(std::log(3.0), 2, 1, 0.0216028), |
| DeltaTest(std::log(3.0), 2, 2, 0.287735), |
| DeltaTest(std::log(3.0), 2, 3, 0.578438), |
| DeltaTest(std::log(3.0), 2, 4, 0.768866), |
| DeltaTest(std::log(3.0), 2, 5, 0.877569), |
| DeltaTest(std::log(3.0), 2, 10, 0.995684), |
| DeltaTest(std::log(3.0), 2, 100, 1.0), |
| DeltaTest(std::log(3.0), 2, 1000, 1.0), |
| DeltaTest(std::log(3.0), 2, 10000, 1.0), |
| DeltaTest(std::log(3.0), 2, 100000, 1.0), |
| DeltaTest(std::log(3.0), 2, 1000000, 1.0), |
| DeltaTest(std::log(3.0), 2, kInt64Max, 1.0), |
| |
| // Error cases. |
| // |
| // Epsilon must be finite and greater than 0. |
| // |
| // epsilon threshold max_pc |
| // ---------- --------- ------ |
| DeltaErrorTest(kDoubleMin, 2, 1), |
| DeltaErrorTest( -1.0, 2, 1), |
| DeltaErrorTest( 0.0, 2, 1), |
| DeltaErrorTest( kPosInf, 2, 1), |
| DeltaErrorTest( kNegInf, 2, 1), |
| DeltaErrorTest( kNaN, 2, 1), |
| |
| // Threshold must be finite. |
| // |
| // epsilon threshold max_pc |
| // -------- --------- ------ |
| DeltaErrorTest(std::log(3.0), kNaN, 1), |
| |
| // Max_partitions_contributed (max_pc) must be greater than 0. |
| // |
| // epsilon threshold max_pc |
| // -------- --------- --------- |
| DeltaErrorTest(std::log(3.0), 2, kInt64Min), |
| DeltaErrorTest(std::log(3.0), 2, -1), |
| DeltaErrorTest(std::log(3.0), 2, 0), |
| }; |
| |
| for (const CalculateDeltaTest& test_case : delta_test_cases) { |
| const double epsilon = test_case.epsilon; |
| const double threshold = test_case.threshold; |
| const double noise_delta = 0.5; |
| const int64_t max_partitions_contributed = |
| test_case.max_partitions_contributed; |
| |
| const std::string test_case_string = absl::StrCat( |
| "epsilon: ", epsilon, ", threshold: ", threshold, |
| ", max_partitions_contributed: ", max_partitions_contributed); |
| |
| absl::StatusOr<double> status_or_delta = |
| GaussianPartitionSelection::CalculateThresholdDelta( |
| epsilon, noise_delta, threshold, max_partitions_contributed); |
| |
| if (test_case.expected_delta.has_value()) { |
| // We expect the test to succeed, and a delta to be computed. |
| ASSERT_TRUE(status_or_delta.ok()) << status_or_delta.status() << "\n" |
| << test_case_string; |
| const double delta = status_or_delta.value(); |
| EXPECT_THAT(delta, DoubleNear(test_case.expected_delta.value(), |
| test_case.tolerance)) |
| << test_case_string; |
| } else { |
| EXPECT_FALSE(status_or_delta.ok()) |
| << test_case_string |
| << "\nunexpected successfully computed delta value: " |
| << status_or_delta.value(); |
| } |
| } |
| } |
| |
| TEST(PartitionSelectionTest, GaussianRoundTripThresholdTests) { |
| // Vary threshold from 1 to 1000, and max_partitions_contributed from 1 to 5. |
| // Calculate delta for each each tuple (epsilon, threshold, |
| // max_partitions_contributed), and then re-compute threshold to ensure it is |
| // the same. Note that once the threshold gets high enough so that the |
| // computed delta is 0, round-tripping back to the original threshold longer |
| // works so we stop early in that case. |
| const double epsilon = std::log(3); |
| const double noise_delta = 0.5; |
| for (int64_t max_partitions_contributed = 1; max_partitions_contributed < 5; |
| ++max_partitions_contributed) { |
| bool computed_delta_of_zero = false; |
| for (int64_t threshold = 0; threshold < 1000; threshold += 10) { |
| std::string test_case_string = absl::StrCat( |
| "threshold: ", threshold, |
| ", max_partitions_contributed: ", max_partitions_contributed); |
| absl::StatusOr<double> status_or_delta = |
| GaussianPartitionSelection::CalculateThresholdDelta( |
| epsilon, noise_delta, threshold, max_partitions_contributed); |
| ASSERT_TRUE(status_or_delta.ok()) << status_or_delta.status() << "\n" |
| << test_case_string; |
| const double delta = status_or_delta.value(); |
| absl::StrAppend(&test_case_string, ", computed delta: ", delta); |
| |
| // If the computed delta is 0, then computing threshold from this will |
| // result in the maximum value. Therefore round tripping will not work. |
| // Note that all remaining input threshold values are higher that this one |
| // within this loop, which implies that all remaining computed deltas will |
| // be 0 as well. |
| if (computed_delta_of_zero) { |
| EXPECT_EQ(delta, 0); |
| continue; |
| } |
| if (delta == 0) { |
| computed_delta_of_zero = true; |
| continue; |
| } |
| |
| absl::StatusOr<double> status_or_threshold = |
| GaussianPartitionSelection::CalculateThreshold( |
| epsilon, noise_delta, delta, max_partitions_contributed); |
| ASSERT_TRUE(status_or_threshold.ok()) |
| << status_or_threshold.status() << "\n" |
| << test_case_string; |
| |
| // We normally expect that the original threshold and the round tripped |
| // threshold are the same. However, because of the loss of precision |
| // due to floating point calculations, there may be some variance. |
| const double computed_threshold = status_or_threshold.value(); |
| |
| EXPECT_THAT(computed_threshold, DoubleNear(threshold, 0.001)) |
| << test_case_string; |
| } |
| } |
| } |
| |
| TEST(PartitionSelectionTest, GaussianRoundTripDeltaTests) { |
| // Ideally, vary max_partitions_contributed from 1 to 5, and threshold delta |
| // from 0 to 0.5 (exclusively). Calculate threshold for each triple (epsilon, |
| // delta, max_partitions_contributed), and then re-compute the delta. |
| // |
| // We can't check the recomputed delta for equality against the original |
| // delta because there are many deltas that map to the same threshold. |
| // We also cannot check that the recomputed delta is closer to the |
| // original delta than to computed deltas for threshold +/-1, and we |
| // cannot check that the original delta is closer to the recomputed delta |
| // than to the computed deltas for threshold +/1. This is because the |
| // curve is not linear, and, for example, the original delta can be closer |
| // to the computed delta for threshold-1 than to the recomputed delta. |
| // |
| // So we just test here that the original delta is between the recomputed |
| // delta and either the computed delta for threshold+1 or threshold-1. |
| const double epsilon = std::log(3); |
| const double noise_delta = 0.5; |
| for (int64_t max_partitions_contributed = 1; max_partitions_contributed < 5; |
| ++max_partitions_contributed) { |
| for (double delta = 0.00001; delta < 0.5; delta *= 10) { |
| std::string test_case_string = absl::StrCat( |
| "delta: ", delta, |
| ", max_partitions_contributed: ", max_partitions_contributed); |
| |
| absl::StatusOr<double> status_or_threshold = |
| GaussianPartitionSelection::CalculateThreshold( |
| epsilon, noise_delta, delta, max_partitions_contributed); |
| ASSERT_TRUE(status_or_threshold.ok()) |
| << status_or_threshold.status() << "\n" |
| << test_case_string; |
| const double threshold = status_or_threshold.value(); |
| |
| absl::StrAppend(&test_case_string, ", computed threshold: ", threshold); |
| |
| const absl::StatusOr<double> computed_delta = |
| GaussianPartitionSelection::CalculateThresholdDelta( |
| epsilon, noise_delta, threshold, max_partitions_contributed); |
| |
| const absl::StatusOr<double> computed_delta_plus_one = |
| GaussianPartitionSelection::CalculateThresholdDelta( |
| epsilon, noise_delta, threshold + 1, max_partitions_contributed); |
| |
| const absl::StatusOr<double> computed_delta_minus_one = |
| GaussianPartitionSelection::CalculateThresholdDelta( |
| epsilon, noise_delta, threshold - 1, max_partitions_contributed); |
| |
| // Expect that the original delta is closer to the computed delta than |
| // to the computed delta for threshold+1 or threshold-1. |
| ASSERT_TRUE(computed_delta.ok()); |
| ASSERT_TRUE(computed_delta_plus_one.ok()); |
| ASSERT_TRUE(computed_delta_minus_one.ok()); |
| |
| absl::StrAppend(&test_case_string, |
| ", computed delta: ", computed_delta.value()); |
| absl::StrAppend(&test_case_string, ", computed delta (k plus_one): ", |
| computed_delta_plus_one.value()); |
| absl::StrAppend(&test_case_string, ", computed delta (k minus one): ", |
| computed_delta_minus_one.value()); |
| |
| // The original delta and recomputed delta for threshold is greater |
| // than the computed delta for threshold+1. |
| // The original delta and recomputed delta for threshold is less than |
| // the computed delta for threshold-1. |
| EXPECT_GT(delta, computed_delta_plus_one.value()); |
| EXPECT_LT(delta, computed_delta_minus_one.value()); |
| |
| EXPECT_GT(computed_delta.value(), computed_delta_plus_one.value()); |
| EXPECT_LT(computed_delta.value(), computed_delta_minus_one.value()); |
| |
| EXPECT_TRUE((delta >= computed_delta.value() && |
| delta <= computed_delta_minus_one.value()) || |
| (delta <= computed_delta.value() && |
| delta >= computed_delta_plus_one.value())); |
| } |
| } |
| } |
| |
| TEST(PartitionSelectionTest, GaussianPartitionSelectionNoiseValueIfShouldKeep) { |
| GaussianPartitionSelection::Builder test_builder; |
| std::unique_ptr<PartitionSelectionStrategy> build = |
| test_builder |
| .SetGaussianMechanism(absl::make_unique<GaussianMechanism::Builder>()) |
| .SetEpsilon(0.5) |
| .SetDelta(0.01) |
| .SetMaxPartitionsContributed(1) |
| .Build() |
| .value(); |
| double num_kept = 0.0; |
| auto* gaussian_ps = dynamic_cast<GaussianPartitionSelection*>(build.get()); |
| const int kTinyNumSamples = 10000; |
| for (int i = 0; i < kTinyNumSamples; ++i) { |
| auto noised_value = gaussian_ps->NoiseValueIfShouldKeep(5); |
| if (noised_value.has_value()) { |
| ++num_kept; |
| EXPECT_GE(noised_value.value(), gaussian_ps->GetThreshold()); |
| } |
| } |
| |
| EXPECT_THAT(num_kept / kTinyNumSamples, DoubleNear(0.07, 0.02)); |
| } |
| |
| } // namespace |
| } // namespace differential_privacy |