blob: c04251514ad64f1a0bd12adf9c822d354c7a905e [file] [log] [blame]
// Copyright 2023 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 (
"fmt"
"math"
"gonum.org/v1/gonum/mathext"
)
type poissonDist struct {
lambda float64
}
func newPoissonDist(lambda float64) (*poissonDist, error) {
if lambda < 0 {
return nil, fmt.Errorf("Poisson parameter must be greater than 0.")
}
return &poissonDist{lambda: lambda}, nil
}
// Computes the probability of an output less than or equal to |x|.
func (d poissonDist) cdf(x float64) float64 {
if x < 0 {
return 0
}
return mathext.GammaIncRegComp(math.Floor(x+1), d.lambda)
}
// Computes the probability of an output greater than or equal to |x|.
//
// This is equivalent to 1 - d.cdf(x), but may be more numerically stable.
func (d poissonDist) sf(x float64) float64 {
if x < 0 {
return 1
}
return mathext.GammaIncReg(math.Floor(x+1), d.lambda)
}
// Computes the probability of an output of |x| for a poisson distribution with parameter |lambda|.
func (d poissonDist) pmf(x float64) float64 {
if x < 0 || math.Floor(x) != x {
return 0
}
// Since the pmf for the Poisson distribution includes division by a factorial
// it is hard to compute directly. This is why we compute the logarithm of the
// pmf instead.
lg, _ := math.Lgamma(math.Floor(x) + 1.0) // Computes log(x!)
lpmf := x*math.Log(d.lambda) - d.lambda - lg
return math.Exp(lpmf)
}
// Assuming fn is true for integers in the range [0, n] and false for all
// integers greater than n. This function will return n.
func findBoundary(fn func(x float64) bool) float64 {
c1, c2 := 0.0, 1.0
for {
if !fn(c2) {
break
}
c1 = c2
c2 = c2 * 2
}
for c1 != c2 {
mid := math.Ceil((c1 + c2) / 2)
if fn(mid) {
c1 = mid
} else {
c2 = mid - 1
}
}
return c1
}
// Assuming fn(x) is true for lowGuess <= x < n (for some n) and false for
// x >= n, this function will return some value y such that fn(y) is false and
// n - precision < y < n + precision.
func findBoundaryFloat(fn func(x float64) bool, lowGuess float64, highGuess float64, precision float64) float64 {
l := lowGuess
h := highGuess
for {
if !fn(h) {
break
}
l = h
h = h * 2
}
for (h - l) > precision {
mid := l + (h-l)/2
if fn(mid) {
l = mid
} else {
h = mid
}
}
return h
}
// Creates a privacy loss distribution relative to one observation being added.
//
// The Poisson mechanism for computing a scalar-valued function f outputs
// the sum of the true value of the function and a noise drawn from the Poisson
// distribution. Recall that the Poisson distribution with parameter p has
// probability density function exp(-p) * p^x / x! at x for any non-negative
// integer x. We only consider f with sensitivity 1 for classes defined here.
//
// The privacy loss distribution of the Poisson mechanism w.r.t. adding one is
// equivalent to the privacy loss distribution between the Poisson distribution
// and the same distribution shifted by +1. Specifically, the privacy loss
// distribution is generated as follows: first pick x according to the Poisson
// distribution and, let the privacy loss be ln(pmf(x) / pmf(x - 1)), which
// is equal to ln(p / x).
func newPoissonAddPld(lambda, discretization, truncation float64) *PrivacyLossDistribution {
dist, _ := newPoissonDist(lambda)
lower := findBoundary(func(x float64) bool { return dist.cdf(x-1) <= truncation/2 })
if lower < 1.0 {
lower = 1.0
}
upper := 1 + findBoundary(func(x float64) bool { return (dist.sf(x)) > truncation/2 })
pmf := make(ProbabilityMassFunction)
// Upper truncated tail
privacyLoss := math.Log(lambda / upper)
roundedPrivacyLoss := int(math.Ceil(privacyLoss / discretization))
pmf[roundedPrivacyLoss] = dist.sf(upper)
for x := lower; x <= upper; x++ {
privacyLoss := math.Log(lambda / x)
roundedPrivacyLoss := int(math.Ceil(privacyLoss / discretization))
val, ok := pmf[roundedPrivacyLoss]
if !ok {
val = 0.0
}
pmf[roundedPrivacyLoss] = val + dist.pmf(x)
}
// Lower truncated tail
infiniteMass := dist.cdf(lower - 1)
return &PrivacyLossDistribution{
discretization: discretization,
infiniteMass: infiniteMass,
pmf: pmf,
pessimisticEstimation: true,
}
}
// Creates a privacy loss distribution relative to one observation being removed.
//
// The Poisson mechanism for computing a scalar-valued function f simply outputs
// the sum of the true value of the function and a noise drawn from the Poisson
// distribution. Recall that the Poisson distribution with parameter p has
// probability density function exp(-p) * p^x / x! at x for any non-negative
// integer x. We only consider f with sensitivity 1 for classes defined here.
//
// The privacy loss distribution of the Poisson mechanism w.r.t. removing one is
// equivalent to the privacy loss distribution between the Poisson distribution
// and the same distribution shifted by -1. Specifically, the privacy loss
// distribution is generated as follows: first pick x according to the Poisson
// distribution. Then, let the privacy loss be ln(pmf(x) / pmf(x + 1)), which
// is equal to ln((x + 1) / p).
func newPoissonRemovePld(lambda, discretization, truncation float64) *PrivacyLossDistribution {
dist, _ := newPoissonDist(lambda)
lower := findBoundary(func(x float64) bool { return dist.cdf(x-2) <= truncation/2 })
upper := 1 + findBoundary(func(x float64) bool { return (dist.sf(x - 1)) > truncation/2 })
pmf := make(ProbabilityMassFunction)
// Lower truncated tail
privacyLoss := math.Log(lower / lambda)
roundedPrivacyLoss := int(math.Ceil(privacyLoss / discretization))
pmf[roundedPrivacyLoss] = dist.cdf(lower - 2)
for x := lower; x <= upper; x++ {
privacyLoss := math.Log(x / lambda)
roundedPrivacyLoss := int(math.Ceil(privacyLoss / discretization))
val, ok := pmf[roundedPrivacyLoss]
if !ok {
val = 0.0
}
pmf[roundedPrivacyLoss] = val + dist.pmf(x-1)
}
// Upper truncated tail
infiniteMass := dist.sf(upper - 1)
return &PrivacyLossDistribution{
discretization: discretization,
infiniteMass: infiniteMass,
pmf: pmf,
pessimisticEstimation: true,
}
}
// Creates a privacy loss distribution using the specified parameter for the Poisson mechanism.
func newPoissonPld(lambda float64, sparsity uint64, discretization float64, truncation float64) (*PrivacyLossDistribution, error) {
add := newPoissonAddPld(lambda, discretization, truncation)
rm := newPoissonRemovePld(lambda, discretization, truncation)
pld, err := composeHeterogeneousPLD(add, rm, truncation)
if err != nil {
return nil, err
}
return composeHomogeneousPLD(pld, sparsity, truncation)
}
type PoissonParameterCalculator struct {
discretization float64
truncation float64
precision float64
}
// Builds a new PoissonParameterCalculator with default discretization and truncation parameters.
func NewPoissonParameterCalculator() PoissonParameterCalculator {
return PoissonParameterCalculator{
discretization: 1e-5,
truncation: 1e-15,
precision: 1e-5,
}
}
// Checks if the provided lambda for the specified sparsity provides privacy at the specified epsilon and delta..
func (c PoissonParameterCalculator) IsPrivate(lambda float64, sparsity uint64, epsilon float64, delta float64) bool {
if lambda == 0 {
return false
}
// Delta >= 1 implies no privacy is added.
if delta >= 1.0 {
return true
}
// Computes a lower bound on the infinite mass.
// This check allows Cobalt to handle very small lambda values.
infiniteMassLowerBound := -math.Expm1(float64(sparsity) * math.Log(-math.Expm1(-lambda)))
if infiniteMassLowerBound > delta {
return false
}
pld, err := newPoissonPld(lambda, sparsity, c.discretization, c.truncation)
if err != nil {
return false
}
return pld.getDeltaForPrivacyLossDistribution(epsilon) < delta
}
func (c PoissonParameterCalculator) GetBestPoissonMean(sparsity uint64, epsilon float64, delta float64) float64 {
fn := func(x float64) bool {
return !c.IsPrivate(x, sparsity, epsilon, delta)
}
return findBoundaryFloat(fn, 0.0, 1.0, c.precision)
}