blob: 345cc3a7905d7b49f7c1ffe9b0e35482b1871172 [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;
import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.Math.exp;
import com.google.privacy.differentialprivacy.proto.SummaryOuterClass.MechanismType;
import java.security.SecureRandom;
import java.util.Random;
import javax.annotation.Nullable;
/**
* Generates and adds Laplace noise to a raw piece of numerical data such that the result is
* securely differentially private.
*
* <p>The Laplace noise is generated according to the geometric sampling mechanism described <a
* href="https://github.com/google/differential-privacy/blob/main/common_docs/Secure_Noise_Generation.pdf">here</a>.
* This approach is robust against unintentional privacy leaks due to artifacts of floating point
* arithmetic.
*/
public class LaplaceNoise implements Noise {
/**
* This parameter determines the resolution of the numerical noise that is being generated
* relative to the L_inf sensitivity and privacy parameter epsilon. More precisely, the
* granularity parameter corresponds to the value 2^k as described <a
* href="https://github.com/google/differential-privacy/blob/main/common_docs/Secure_Noise_Generation.pdf">here</a>.
* Larger values result in more fine grained noise, but increase the chance of sampling
* inaccuracies due to overflows. The probability of an overflow is less than 2^-1000, if the
* granularity parameter is set to a value of 2^40 or less and the epsilon passed to addNoise is
* at least 2^-50.
*
* <p>This parameter should be a power of 2.
*/
private static final double GRANULARITY_PARAM = (double) (1L << 40);
private final Random random;
/** Returns a Noise instance initialized with a secure randomness source. */
public LaplaceNoise() {
this(new SecureRandom());
}
private LaplaceNoise(Random random) {
this.random = random;
}
/**
* Returns a Noise instance initialized with a specified randomness source. This should only be
* used for testing and may only be called via the static methods in {@link TestNoiseFactory}.
*
* <p>This method is package-private for use by the factory.
*/
static LaplaceNoise createForTesting(Random random) {
return new LaplaceNoise(random);
}
/**
* Adds Laplace noise to {@code x} such that the output is {@code epsilon}-differentially private,
* with respect to the specified L_0 and L_inf sensitivities. Note that {@code delta} must be set
* to {@code null} because it does not parameterize Laplace noise. Moreover, {@code epsilon} must
* be at least 2^-50.
*/
@Override
public double addNoise(
double x, int l0Sensitivity, double lInfSensitivity, double epsilon, @Nullable Double delta) {
DpPreconditions.checkSensitivities(l0Sensitivity, lInfSensitivity);
return addNoise(x, Noise.getL1Sensitivity(l0Sensitivity, lInfSensitivity), epsilon, delta);
}
/**
* See {@link #addNoise(double, int, double, double, Double)}.
*
* <p>As opposed to the latter method, this accepts the L_1 sensitivity of {@code x} directly
* instead of the L_0 and L_Inf proxies. This should be used in settings where it is feasible or
* more convenient to calculate the L_1 sensitivity directly.
*/
public double addNoise(double x, double l1Sensitivity, double epsilon, @Nullable Double delta) {
checkParameters(l1Sensitivity, epsilon, delta);
double granularity = getGranularity(l1Sensitivity, epsilon);
long twoSidedGeomericSample =
SamplingUtil.sampleTwoSidedGeometric(
random, granularity * epsilon / (l1Sensitivity + granularity));
return SecureNoiseMath.roundToMultipleOfPowerOfTwo(x, granularity)
+ twoSidedGeomericSample * granularity;
}
/**
* Adds Laplace noise to the integer {@code x} such that the output is {@code
* epsilon}-differentially private, with respect to the specified L_0 and L_inf sensitivities.
* Note that {@code delta} must be set to {@code null} because it does not parameterize Laplace
* noise. Moreover, {@code epsilon} must be at least 2^-50.
*/
@Override
public long addNoise(
long x, int l0Sensitivity, long lInfSensitivity, double epsilon, @Nullable Double delta) {
DpPreconditions.checkSensitivities(l0Sensitivity, lInfSensitivity);
return addNoise(
x, (long) Noise.getL1Sensitivity(l0Sensitivity, lInfSensitivity), epsilon, delta);
}
/**
* See {@link #addNoise(long, int, long, double, Double)}.
*
* <p>As opposed to the latter method, this accepts the L_1 sensitivity of {@code x} directly
* instead of the L_0 and L_Inf proxies. This should be used in settings where it is feasible or
* more convenient to calculate the L_1 sensitivity directly.
*/
public long addNoise(long x, long l1Sensitivity, double epsilon, @Nullable Double delta) {
checkParameters(l1Sensitivity, epsilon, delta);
double granularity = getGranularity(l1Sensitivity, epsilon);
long twoSidedGeomericSample =
SamplingUtil.sampleTwoSidedGeometric(
random, granularity * epsilon / (l1Sensitivity + granularity));
if (granularity <= 1.0) {
return x + Math.round(twoSidedGeomericSample * granularity);
} else {
return SecureNoiseMath.roundToMultiple(x, (long) granularity)
+ twoSidedGeomericSample * (long) granularity;
}
}
@Override
public MechanismType getMechanismType() {
return MechanismType.LAPLACE;
}
/**
* Computes a confidence interval that contains the raw value {@code x} passed to {@link
* #addNoise(double, int, double, double, Double)} with a probability equal to {@code 1 - alpha}
* based on the specified {@code noisedX} and noise parameters. Note that {@code delta} must be
* set to {@code null} because it does not parameterize Laplace noise. Moreover, {@code epsilon}
* must be at least 2^-50.
*
* <p>Refer to <a
* href="https://github.com/google/differential-privacy/tree/main/common_docs/confidence_intervals.md">this</a> doc for
* more information.
*/
@Override
public ConfidenceInterval computeConfidenceInterval(
double noisedX,
int l0Sensitivity,
double lInfSensitivity,
double epsilon,
@Nullable Double delta,
double alpha) {
checkConfidenceIntervalParameters(l0Sensitivity, lInfSensitivity, epsilon, delta, alpha);
double z =
computeQuantile(alpha / 2.0, noisedX, l0Sensitivity, lInfSensitivity, epsilon, delta);
// Because of the symmetry of the Laplace distribution, 2 * noisedX - z corresponds to the
// (1 - alpha/2)-quantile of the distribution, meaning that the interval [z, 2 * noisedX - z]
// contains 1-alpha of the probability mass. Deriving the (1 - alpha/2)-quantile from the
// (alpha/2)-quantile and not vice versa is a deliberate choice. The reason is that alpha tends
// to be very small. Consequently, alpha/2 is more accurately representable as a double than
// 1 - alpha/2, facilitating numerical computations.
return ConfidenceInterval.create(z, 2.0 * noisedX - z);
}
/**
* Computes a confidence interval that contains the raw integer value {@code x} passed to {@link
* #addNoise(long, int, long, double, Double)} with a probability greater or equal to {@code 1 -
* alpha} based on the specified {@code noisedX} and noise parameters. Note that {@code delta}
* must be set to {@code null} because it does not parameterize Laplace noise. Moreover, {@code
* epsilon} must be at least 2^-50.
*
* <p>Refer to <a
* href="https://github.com/google/differential-privacy/tree/main/common_docs/confidence_intervals.md">this</a> doc for
* more information.
*/
@Override
public ConfidenceInterval computeConfidenceInterval(
long noisedX,
int l0Sensitivity,
long lInfSensitivity,
double epsilon,
@Nullable Double delta,
double alpha) {
// Computing the confidence interval around zero rather than nosiedX helps represent the
// interval bounds more accurately. The reason is that the resolution of double values is most
// fine grained around zero.
ConfidenceInterval confIntAroundZero =
computeConfidenceInterval(0.0, l0Sensitivity, lInfSensitivity, epsilon, delta, alpha);
// Adding noisedX after converting the interval bounds to long ensures that no precision is lost
// due to the coarse resolution of double values for large instances of noisedX.
return ConfidenceInterval.create(
SecureNoiseMath.nextSmallerDouble(Math.round(confIntAroundZero.lowerBound()) + noisedX),
SecureNoiseMath.nextLargerDouble(Math.round(confIntAroundZero.upperBound()) + noisedX));
}
/**
* Computes the cumulative density function, i.e. Pr[Y <= z] for a Laplace random variable Y whose
* distribution is given by applying the Laplace mechanism to the raw value {@code x} using the
* specified privacy parameters {@code epsilon}, {@code delta}, and {@code l1Sensitivity}.
*
* <p>This is inverse to {@link #computeQuantile(double, double, double, double)} with the same
* parameters.
*/
public static double cumulativeDensity(double z, double x, double l1Sensitivity, double epsilon) {
DpPreconditions.checkL1Sensitivity(l1Sensitivity);
DpPreconditions.checkEpsilon(epsilon);
double scale = l1Sensitivity / epsilon;
double y = z - x;
if (y > 0) {
return 1 - 0.5 * exp(-y / scale);
}
return 0.5 * exp(y / scale);
}
/**
* Computes the quantile z satisfying Pr[Y <= z] = {@code rank} for a Laplace random variable Y
* whose distribution is given by applying the Laplace mechanism to the raw value {@code x} using
* the specified privacy parameters {@code epsilon}, {@code delta}, {@code l0Sensitivity}, and
* {@code lInfSensitivity}.
*/
@Override
public double computeQuantile(
double rank,
double x,
int l0Sensitivity,
double lInfSensitivity,
double epsilon,
@Nullable Double delta) {
DpPreconditions.checkNoiseComputeQuantileArguments(
this, rank, l0Sensitivity, lInfSensitivity, epsilon, delta);
double l1Sensitivity = Noise.getL1Sensitivity(l0Sensitivity, lInfSensitivity);
return computeQuantile(rank, x, l1Sensitivity, epsilon);
}
/**
* Computes the quantile z satisfying Pr[Y <= z] = {@code rank} for a Laplace random variable Y
* whose distribution is given by applying the Laplace mechanism to the raw value {@code x} using
* the specified privacy parameters {@code epsilon}, {@code delta}, and {@code l1Sensitivity}.
*
* <p>This is inverse to {@link #cumulativeDensity} with the same parameters.
*/
public static double computeQuantile(
double rank, double x, double l1Sensitivity, double epsilon) {
DpPreconditions.checkL1Sensitivity(l1Sensitivity);
DpPreconditions.checkEpsilon(epsilon);
checkArgument(rank > 0 && rank < 1, "rank must be > 0 and < 1. Provided value: %s", rank);
double lambda = l1Sensitivity / epsilon;
if (rank < 0.5) {
return x + lambda * Math.log(2 * rank);
}
return x - lambda * Math.log(2 * (1 - rank));
}
private void checkParameters(double l1Sensitivity, double epsilon, @Nullable Double delta) {
DpPreconditions.checkEpsilon(epsilon);
DpPreconditions.checkNoiseDelta(delta, this);
DpPreconditions.checkL1Sensitivity(l1Sensitivity);
}
private void checkConfidenceIntervalParameters(
int l0Sensitivity,
double lInfSensitivity,
double epsilon,
@Nullable Double delta,
double alpha) {
DpPreconditions.checkAlpha(alpha);
DpPreconditions.checkSensitivities(l0Sensitivity, lInfSensitivity);
checkParameters(Noise.getL1Sensitivity(l0Sensitivity, lInfSensitivity), epsilon, delta);
}
/**
* Determines the granularity of the output of {@link addNoise} based on the epsilon and
* l1Sensitivity of the Laplace mechanism.
*/
private static double getGranularity(double l1Sensitivity, double epsilon) {
return SecureNoiseMath.ceilPowerOfTwo((l1Sensitivity / epsilon) / GRANULARITY_PARAM);
}
}