| // |
| // 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 dpagg contains differentially private aggregation primitives. |
| package dpagg |
| |
| import ( |
| "fmt" |
| "math" |
| |
| log "github.com/golang/glog" |
| "github.com/google/differential-privacy/go/v2/checks" |
| "github.com/google/differential-privacy/go/v2/noise" |
| ) |
| |
| // BoundedSumInt64 calculates a differentially private sum of a collection of |
| // int64 values. It supports privacy units that contribute to multiple partitions |
| // (via the MaxPartitionsContributed parameter) by scaling the added noise |
| // appropriately. However, it assumes that for each BoundedSumInt64 instance |
| // (partition), each privacy unit contributes at most one value. If a privacy unit |
| // contributes more, the contributions should be pre-aggregated before passing them |
| // to BoundedSumInt64. |
| // |
| // The provided differentially private sum is an unbiased estimate of the raw |
| // bounded sum in the sense that its expected value is equal to the raw bounded sum. |
| // |
| // For general details and key definitions, see |
| // https://github.com/google/differential-privacy/blob/main/differential_privacy.md#key-definitions. |
| // |
| // Note: Do not use when your results may cause overflows for int64 |
| // values. This aggregation is not hardened for such applications yet. |
| // |
| // Not thread-safe. |
| type BoundedSumInt64 struct { |
| // Parameters |
| epsilon float64 |
| delta float64 |
| l0Sensitivity int64 |
| lInfSensitivity int64 |
| lower int64 |
| upper int64 |
| Noise noise.Noise |
| noiseKind noise.Kind // necessary for serializing noise.Noise information |
| |
| // State variables |
| sum int64 |
| state aggregationState |
| noisedSum int64 |
| } |
| |
| func bsEquallyInitializedint64(s1, s2 *BoundedSumInt64) bool { |
| return s1.epsilon == s2.epsilon && |
| s1.delta == s2.delta && |
| s1.l0Sensitivity == s2.l0Sensitivity && |
| s1.lInfSensitivity == s2.lInfSensitivity && |
| s1.lower == s2.lower && |
| s1.upper == s2.upper && |
| s1.noiseKind == s2.noiseKind && |
| s1.state == s2.state |
| } |
| |
| // BoundedSumInt64Options contains the options necessary to initialize a BoundedSumInt64. |
| type BoundedSumInt64Options struct { |
| Epsilon float64 // Privacy parameter ε. Required. |
| Delta float64 // Privacy parameter δ. Required with Gaussian noise, must be 0 with Laplace noise. |
| MaxPartitionsContributed int64 // How many distinct partitions may a single privacy unit contribute to? Defaults to 1. |
| // Lower and Upper bounds for clamping. Default to 0; must be such that Lower <= Upper. |
| Lower, Upper int64 |
| Noise noise.Noise // Type of noise used in BoundedSum. Defaults to Laplace noise. |
| // How many times may a single privacy unit contribute to a single partition? |
| // Defaults to 1. This is only needed for other aggregation functions using BoundedSum; |
| // which is why the option is not exported. |
| maxContributionsPerPartition int64 |
| } |
| |
| // NewBoundedSumInt64 returns a new BoundedSumInt64, whose sum is initialized at 0. |
| func NewBoundedSumInt64(opt *BoundedSumInt64Options) (*BoundedSumInt64, error) { |
| if opt == nil { |
| opt = &BoundedSumInt64Options{} |
| } |
| // Set defaults. |
| l0 := opt.MaxPartitionsContributed |
| if l0 == 0 { |
| l0 = 1 |
| } |
| |
| maxContributionsPerPartition := opt.maxContributionsPerPartition |
| if maxContributionsPerPartition == 0 { |
| maxContributionsPerPartition = 1 |
| } |
| |
| n := opt.Noise |
| if n == nil { |
| n = noise.Laplace() |
| } |
| // Check bounds & use them to compute L_∞ sensitivity |
| lower, upper := opt.Lower, opt.Upper |
| if lower == 0 && upper == 0 { |
| return nil, fmt.Errorf("NewBoundedSumInt64 requires a non-default value for Lower and Upper (automatic bounds determination is not implemented yet). Lower and Upper cannot be both 0") |
| } |
| var err error |
| switch noise.ToKind(opt.Noise) { |
| case noise.Unrecognised: |
| err = checks.CheckBoundsInt64IgnoreOverflows(lower, upper) |
| default: |
| err = checks.CheckBoundsInt64(lower, upper) |
| } |
| if err != nil { |
| return nil, fmt.Errorf("NewBoundedSumInt64: %w", err) |
| } |
| lInf, err := getLInfInt(lower, upper, maxContributionsPerPartition) |
| if err != nil { |
| if noise.ToKind(opt.Noise) == noise.Unrecognised { |
| // Ignore sensitivity overflows if noise is not recognised. |
| log.Warningf("NewBoundedSumInt64: getLInfInt failed with %q, using largest representable integer as lInf_sensitivity", err.Error()) |
| } else { |
| return nil, fmt.Errorf("NewBoundedSumInt64: %w", err) |
| } |
| } |
| // Check that the parameters are compatible with the noise chosen by calling |
| // the noise on some placeholder value. |
| eps, del := opt.Epsilon, opt.Delta |
| _, err = n.AddNoiseInt64(0, l0, lInf, eps, del) |
| if err != nil { |
| return nil, fmt.Errorf("NewBoundedSumInt64: %w", err) |
| } |
| |
| return &BoundedSumInt64{ |
| epsilon: eps, |
| delta: del, |
| l0Sensitivity: l0, |
| lInfSensitivity: lInf, |
| lower: lower, |
| upper: upper, |
| Noise: n, |
| noiseKind: noise.ToKind(n), |
| sum: 0, |
| state: defaultState, |
| }, nil |
| } |
| |
| // lInfIntOverflows checks if multiplication of the given number overflows int64. |
| // If x != x*y/y then x*y overflowed and the multiplication result is incorrect. |
| // Thus, the equation evaluates to false. |
| func lInfIntOverflows(bound, maxContributionsPerPartition int64) bool { |
| mult := bound * maxContributionsPerPartition |
| return mult/maxContributionsPerPartition != bound |
| } |
| |
| // getLInfInt checks that the sensitivity parameters will not create overflow errors, |
| // and returns the L_inf sensitivity of the BoundedSum object, which is calculated by the |
| // formula = max(|lower|, |upper|) * maxContributionsPerPartition. |
| func getLInfInt(lower, upper, maxContributionsPerPartition int64) (int64, error) { |
| // If lower or upper is math.MinInt64, the sensitivity will overflow. |
| if lower == math.MinInt64 || upper == math.MinInt64 { |
| return math.MaxInt64, fmt.Errorf("lower = %d and upper = %d must be strictly larger than math.MinInt64 to avoid sensitivity overflow", lower, upper) |
| } |
| if lower < 0 { |
| lower = -lower |
| } |
| if upper < 0 { |
| upper = -upper |
| } |
| if lInfIntOverflows(lower, maxContributionsPerPartition) { |
| return math.MaxInt64, fmt.Errorf( |
| "lower = %d and maxContributionsPerPartition = %d are too high - the lInf sensitivity may overflow", |
| lower, maxContributionsPerPartition) |
| } |
| if lInfIntOverflows(upper, maxContributionsPerPartition) { |
| return math.MaxInt64, fmt.Errorf( |
| "upper = %dr and maxContributionsPerPartition = %d are too high - the lInf sensitivity may overflow", |
| upper, maxContributionsPerPartition) |
| } |
| if lower > upper { |
| return lower * maxContributionsPerPartition, nil |
| } |
| return upper * maxContributionsPerPartition, nil |
| } |
| |
| // Add adds a new summand to the BoundedSumInt64. |
| func (bs *BoundedSumInt64) Add(e int64) error { |
| if bs.state != defaultState { |
| return fmt.Errorf("BoundedSumInt64 cannot be amended: %v", bs.state.errorMessage()) |
| } |
| clamped, err := ClampInt64(e, bs.lower, bs.upper) |
| if err != nil { |
| return fmt.Errorf("couldn't clamp input value %v, err %v", e, err) |
| } |
| bs.sum += clamped |
| return nil |
| } |
| |
| // Merge merges bs2 into bs (i.e., adds to bs all entries that were added to |
| // bs2). bs2 is consumed by this operation: bs2 may not be used after it is |
| // merged into bs. |
| func (bs *BoundedSumInt64) Merge(bs2 *BoundedSumInt64) error { |
| if err := checkMergeBoundedSumInt64(bs, bs2); err != nil { |
| return err |
| } |
| bs.sum += bs2.sum |
| bs2.state = merged |
| return nil |
| } |
| |
| func checkMergeBoundedSumInt64(bs1, bs2 *BoundedSumInt64) error { |
| if bs1.state != defaultState { |
| return fmt.Errorf("checkMergeBoundedSumInt64: bs1 cannot be merged with another BoundedSum instance: %v", bs1.state.errorMessage()) |
| } |
| if bs2.state != defaultState { |
| return fmt.Errorf("checkMergeBoundedSumInt64: bs2 cannot be merged with another BoundedSum instance: %v", bs2.state.errorMessage()) |
| } |
| |
| if !bsEquallyInitializedint64(bs1, bs2) { |
| return fmt.Errorf("checkMergeBoundedSumInt64: bs1 and bs2 are not compatible") |
| } |
| return nil |
| } |
| |
| // Result returns a differentially private estimate of the sum of bounded |
| // elements added so far. The method can be called only once. |
| // |
| // The returned value is an unbiased estimate of the raw bounded sum. |
| // |
| // The returned value may sometimes be outside the set of possible raw bounded |
| // sums, e.g., the differentially private bounded sum may be positive although |
| // neither the lower nor the upper bound are positive. This can be corrected |
| // by the caller of this method, e.g., by snapping the result to the closest |
| // value representing a bounded sum that is possible. Note that such post |
| // processing introduces bias to the result. |
| func (bs *BoundedSumInt64) Result() (int64, error) { |
| if bs.state != defaultState { |
| return 0, fmt.Errorf("BoundedSumInt64's noised result cannot be computed: " + bs.state.errorMessage()) |
| } |
| bs.state = resultReturned |
| var err error |
| bs.noisedSum, err = bs.Noise.AddNoiseInt64(bs.sum, bs.l0Sensitivity, bs.lInfSensitivity, bs.epsilon, bs.delta) |
| return bs.noisedSum, err |
| } |
| |
| // ThresholdedResult is similar to Result() but applies thresholding to the result. |
| // So, if the result is less than the threshold specified by the parameters of |
| // BoundedSumInt64 as well as thresholdDelta, it returns nil. Otherwise, it returns |
| // the result. |
| // |
| // Note that the nil results should not be published when the existence of a |
| // partition in the output depends on private data. |
| func (bs *BoundedSumInt64) ThresholdedResult(thresholdDelta float64) (*int64, error) { |
| threshold, err := bs.Noise.Threshold(bs.l0Sensitivity, float64(bs.lInfSensitivity), bs.epsilon, bs.delta, thresholdDelta) |
| if err != nil { |
| return nil, err |
| } |
| result, err := bs.Result() |
| if err != nil { |
| return nil, err |
| } |
| // Rounding up the threshold when converting it to int64 to ensure that no DP guarantees |
| // are violated due to a result being returned that is less than the fractional threshold. |
| if result < int64(math.Ceil(threshold)) { |
| return nil, nil |
| } |
| return &result, nil |
| } |
| |
| // ComputeConfidenceInterval computes a confidence interval with integer bounds that |
| // contains the true sum with a probability greater than or equal to 1 - alpha using |
| // the noised sum computed by Result(). The computation is based exclusively on the |
| // noised sum returned by Result(). Thus no privacy budget is consumed by this operation. |
| // |
| // Result() needs to be called before ComputeConfidenceInterval, otherwise this will return |
| // an error. |
| // |
| // See https://github.com/google/differential-privacy/tree/main/common_docs/confidence_intervals.md. |
| func (bs *BoundedSumInt64) ComputeConfidenceInterval(alpha float64) (noise.ConfidenceInterval, error) { |
| if bs.state != resultReturned { |
| return noise.ConfidenceInterval{}, fmt.Errorf("Result() must be called before calling ComputeConfidenceInterval()") |
| } |
| confInt, err := bs.Noise.ComputeConfidenceIntervalInt64(bs.noisedSum, bs.l0Sensitivity, bs.lInfSensitivity, bs.epsilon, bs.delta, alpha) |
| if err != nil { |
| return noise.ConfidenceInterval{}, err |
| } |
| // If lower and upper bounds are non-negative, trim the negative part of the interval. |
| if bs.lower >= 0 { |
| confInt.LowerBound, confInt.UpperBound = math.Max(0, confInt.LowerBound), math.Max(0, confInt.UpperBound) |
| } |
| // Similarly, if lower and upper bounds are non-positive, trim the positive part of the interval. |
| if bs.upper <= 0 { |
| confInt.LowerBound, confInt.UpperBound = math.Min(0, confInt.LowerBound), math.Min(0, confInt.UpperBound) |
| } |
| return confInt, nil |
| } |
| |
| // encodableBoundedSumFloat64 can be encoded by the gob package. |
| type encodableBoundedSumInt64 struct { |
| Epsilon float64 |
| Delta float64 |
| L0Sensitivity int64 |
| LInfSensitivity int64 |
| Lower int64 |
| Upper int64 |
| NoiseKind noise.Kind |
| Sum int64 |
| } |
| |
| // GobEncode encodes BoundedSumInt64. |
| func (bs *BoundedSumInt64) GobEncode() ([]byte, error) { |
| if bs.state != defaultState && bs.state != serialized { |
| return nil, fmt.Errorf("BoundedSumInt64 object cannot be serialized: " + bs.state.errorMessage()) |
| } |
| enc := encodableBoundedSumInt64{ |
| Epsilon: bs.epsilon, |
| Delta: bs.delta, |
| L0Sensitivity: bs.l0Sensitivity, |
| LInfSensitivity: bs.lInfSensitivity, |
| Lower: bs.lower, |
| Upper: bs.upper, |
| NoiseKind: noise.ToKind(bs.Noise), |
| Sum: bs.sum, |
| } |
| bs.state = serialized |
| return encode(enc) |
| } |
| |
| // GobDecode decodes BoundedSumInt64. |
| func (bs *BoundedSumInt64) GobDecode(data []byte) error { |
| var enc encodableBoundedSumInt64 |
| err := decode(&enc, data) |
| if err != nil { |
| return fmt.Errorf("couldn't decode BoundedSumInt64 from bytes") |
| } |
| *bs = BoundedSumInt64{ |
| epsilon: enc.Epsilon, |
| delta: enc.Delta, |
| l0Sensitivity: enc.L0Sensitivity, |
| lInfSensitivity: enc.LInfSensitivity, |
| lower: enc.Lower, |
| upper: enc.Upper, |
| noiseKind: enc.NoiseKind, |
| Noise: noise.ToNoise(enc.NoiseKind), |
| sum: enc.Sum, |
| state: defaultState, |
| } |
| return nil |
| } |
| |
| // BoundedSumFloat64 calculates a differentially private sum of a collection of |
| // float64 values. It supports privacy units that contribute to multiple partitions |
| // (via the MaxPartitionsContributed parameter) by scaling the added noise |
| // appropriately. However, it assumes that for each BoundedSumFloat64 instance |
| // (partition), each privacy unit contributes at most one value. If a privacy unit |
| // contributes more, the contributions should be pre-aggregated before passing them |
| // to BoundedSumFloat64. |
| // |
| // The provided differentially private sum is an unbiased estimate of the raw |
| // bounded sum meaning that its expected value is equal to the raw bounded sum. |
| // |
| // For general details and key definitions, see |
| // https://github.com/google/differential-privacy/blob/main/differential_privacy.md#key-definitions, |
| // |
| // Not thread-safe. |
| type BoundedSumFloat64 struct { |
| // Parameters |
| epsilon float64 |
| delta float64 |
| l0Sensitivity int64 |
| lInfSensitivity float64 |
| lower float64 |
| upper float64 |
| Noise noise.Noise |
| noiseKind noise.Kind // necessary for serializing noise.Noise information |
| |
| // State variables |
| sum float64 |
| state aggregationState |
| noisedSum float64 |
| } |
| |
| func bsEquallyInitializedFloat64(s1, s2 *BoundedSumFloat64) bool { |
| return s1.epsilon == s2.epsilon && |
| s1.delta == s2.delta && |
| s1.l0Sensitivity == s2.l0Sensitivity && |
| s1.lInfSensitivity == s2.lInfSensitivity && |
| s1.lower == s2.lower && |
| s1.upper == s2.upper && |
| s1.noiseKind == s2.noiseKind && |
| s1.state == s2.state |
| } |
| |
| // BoundedSumFloat64Options contains the options necessary to initialize a BoundedSumFloat64. |
| type BoundedSumFloat64Options struct { |
| Epsilon float64 // Privacy parameter ε. Required. |
| Delta float64 // Privacy parameter δ. Required with Gaussian noise, must be 0 with Laplace noise. |
| MaxPartitionsContributed int64 // How many distinct partitions may a single privacy unit contribute to? Defaults to 1. |
| // Lower and Upper bounds for clamping. Default to 0; must be such that Lower <= Upper. |
| Lower, Upper float64 |
| Noise noise.Noise // Type of noise used in BoundedSum. Defaults to Laplace noise. |
| // How many times may a single privacy unit contribute to a single partition? |
| // Defaults to 1. This is only needed for other aggregation functions using BoundedSum; |
| // which is why the option is not exported. |
| maxContributionsPerPartition int64 |
| } |
| |
| // NewBoundedSumFloat64 returns a new BoundedSumFloat64, whose sum is initialized at 0. |
| func NewBoundedSumFloat64(opt *BoundedSumFloat64Options) (*BoundedSumFloat64, error) { |
| if opt == nil { |
| opt = &BoundedSumFloat64Options{} |
| } |
| // Set defaults. |
| l0 := opt.MaxPartitionsContributed |
| if l0 == 0 { |
| l0 = 1 |
| } |
| |
| maxContributionsPerPartition := opt.maxContributionsPerPartition |
| if maxContributionsPerPartition == 0 { |
| maxContributionsPerPartition = 1 |
| } |
| |
| n := opt.Noise |
| if n == nil { |
| n = noise.Laplace() |
| } |
| // Check bounds & use them to compute L_∞ sensitivity |
| lower, upper := opt.Lower, opt.Upper |
| if lower == 0 && upper == 0 { |
| return nil, fmt.Errorf("NewBoundedSumFloat64 requires a non-default value for Lower and Upper (automatic bounds determination is not implemented yet). Lower and Upper cannot be both 0") |
| } |
| var err error |
| switch noise.ToKind(opt.Noise) { |
| case noise.Unrecognised: |
| err = checks.CheckBoundsFloat64IgnoreOverflows(lower, upper) |
| default: |
| err = checks.CheckBoundsFloat64(lower, upper) |
| } |
| if err != nil { |
| return nil, fmt.Errorf("NewBoundedSumFloat64: %w", err) |
| } |
| lInf, err := getLInfFloat(lower, upper, maxContributionsPerPartition) |
| if err != nil { |
| if noise.ToKind(opt.Noise) == noise.Unrecognised { |
| // Ignore sensitivity overflows if noise is not recognised. |
| log.Warningf("NewBoundedSumFloat64: getLInfFloat failed with %q, using largest representable integer as lInf_sensitivity", err.Error()) |
| } else { |
| return nil, fmt.Errorf("NewBoundedSumFloat64: %w", err) |
| } |
| } |
| // Check that the parameters are compatible with the noise chosen by calling |
| // the noise on some placeholder value. |
| eps, del := opt.Epsilon, opt.Delta |
| _, err = n.AddNoiseFloat64(0, l0, lInf, eps, del) |
| if err != nil { |
| return nil, fmt.Errorf("NewBoundedSumFloat64: %w", err) |
| } |
| |
| return &BoundedSumFloat64{ |
| epsilon: eps, |
| delta: del, |
| l0Sensitivity: l0, |
| lInfSensitivity: lInf, |
| lower: lower, |
| upper: upper, |
| Noise: n, |
| noiseKind: noise.ToKind(n), |
| sum: 0, |
| state: defaultState, |
| }, nil |
| } |
| |
| func lInfFloatOverflows(bound float64, maxContributionsPerPartition int64) bool { |
| return math.IsInf(bound*float64(maxContributionsPerPartition), 0) |
| } |
| |
| // getLInfFloat checks that the sensitivity parameters will not create overflow errors, |
| // and returns the L_inf sensitivity of the BoundedSum object, which is calculated by the |
| // formula = max(|lower|, |upper|) * maxContributionsPerPartition. |
| func getLInfFloat(lower, upper float64, maxContributionsPerPartition int64) (float64, error) { |
| if lower < 0 { |
| lower = -lower |
| } |
| if upper < 0 { |
| upper = -upper |
| } |
| if lInfFloatOverflows(lower, maxContributionsPerPartition) { |
| return math.Inf(1), fmt.Errorf( |
| "lower = %f and maxContributionsPerPartition =%d are too high - the lInf sensitivity may overflow", |
| lower, maxContributionsPerPartition) |
| } |
| if lInfFloatOverflows(upper, maxContributionsPerPartition) { |
| return math.Inf(1), fmt.Errorf( |
| "upper = %f and maxContributionsPerPartition = %d are too high - the lInf sensitivity may overflow", |
| upper, maxContributionsPerPartition) |
| } |
| if lower > upper { |
| return lower * float64(maxContributionsPerPartition), nil |
| } |
| return upper * float64(maxContributionsPerPartition), nil |
| } |
| |
| // Add adds a new summand to the BoundedSumFloat64. It ignores NaN summands |
| // because introducing even a single NaN summand will result in a NaN sum |
| // regardless of other summands, which would break the indistinguishability |
| // property required for differential privacy. |
| func (bs *BoundedSumFloat64) Add(e float64) error { |
| if bs.state != defaultState { |
| return fmt.Errorf("BoundedSumFloat64 cannot be amended: %v", bs.state.errorMessage()) |
| } |
| if !math.IsNaN(e) { |
| clamped, err := ClampFloat64(e, bs.lower, bs.upper) |
| if err != nil { |
| return fmt.Errorf("couldn't clamp input value %v, err %w", e, err) |
| } |
| bs.sum += clamped |
| } |
| return nil |
| } |
| |
| // Merge merges bs2 into bs (i.e., adds to bs all entries that were added to |
| // bs2). bs2 is consumed by this operation: bs2 may not be used after it is |
| // merged into bs. |
| func (bs *BoundedSumFloat64) Merge(bs2 *BoundedSumFloat64) error { |
| if err := checkMergeBoundedSumFloat64(bs, bs2); err != nil { |
| return err |
| } |
| bs.sum += bs2.sum |
| bs2.state = merged |
| return nil |
| } |
| |
| func checkMergeBoundedSumFloat64(bs1, bs2 *BoundedSumFloat64) error { |
| if bs1.state != defaultState { |
| return fmt.Errorf("checkMergeBoundedSumFloat64: bs1 cannot be merged with another BoundedSum instance: %v", bs1.state.errorMessage()) |
| } |
| if bs2.state != defaultState { |
| return fmt.Errorf("checkMergeBoundedSumFloat64: bs2 cannot be merged with another BoundedSum instance: %v", bs2.state.errorMessage()) |
| } |
| |
| if !bsEquallyInitializedFloat64(bs1, bs2) { |
| return fmt.Errorf("checkMergeBoundedSumFloat64: bs1 and bs2 are not compatible") |
| } |
| return nil |
| } |
| |
| // Result returns a differentially private estimate of the sum of bounded |
| // elements added so far. The method can be called only once. |
| // |
| // The returned value is an unbiased estimate of the raw bounded sum. |
| // |
| // The returned value may sometimes be outside the set of possible raw bounded |
| // sums, e.g., the differentially private bounded sum may be positive although |
| // neither the lower nor the upper bound are positive. This can be corrected |
| // by the caller of this method, e.g., by snapping the result to the closest |
| // value representing a bounded sum that is possible. Note that such post |
| // processing introduces bias to the result. |
| func (bs *BoundedSumFloat64) Result() (float64, error) { |
| if bs.state != defaultState { |
| return 0, fmt.Errorf("BoundedSumFloat64's noised result cannot be computed: " + bs.state.errorMessage()) |
| } |
| bs.state = resultReturned |
| var err error |
| bs.noisedSum, err = bs.Noise.AddNoiseFloat64(bs.sum, bs.l0Sensitivity, bs.lInfSensitivity, bs.epsilon, bs.delta) |
| return bs.noisedSum, err |
| } |
| |
| // ThresholdedResult is similar to Result() but applies thresholding to the |
| // result. So, if the result is less than the threshold specified by the noise, |
| // mechanism, it returns nil. Otherwise, it returns the result. |
| func (bs *BoundedSumFloat64) ThresholdedResult(thresholdDelta float64) (*float64, error) { |
| threshold, err := bs.Noise.Threshold(bs.l0Sensitivity, bs.lInfSensitivity, bs.epsilon, bs.delta, thresholdDelta) |
| if err != nil { |
| return nil, err |
| } |
| result, err := bs.Result() |
| if err != nil { |
| return nil, err |
| } |
| if result < threshold { |
| return nil, nil |
| } |
| return &result, nil |
| } |
| |
| // ComputeConfidenceInterval computes a confidence interval that contains the true sum |
| // with a probability greater than or equal to 1 - alpha using the noised sum computed by |
| // Result(). The computation is based exclusively on the noised sum returned by Result(). |
| // Thus no privacy budget is consumed by this operation. |
| // |
| // Result() needs to be called before ComputeConfidenceInterval, otherwise this will return |
| // an error. |
| // |
| // See https://github.com/google/differential-privacy/tree/main/common_docs/confidence_intervals.md. |
| func (bs *BoundedSumFloat64) ComputeConfidenceInterval(alpha float64) (noise.ConfidenceInterval, error) { |
| if bs.state != resultReturned { |
| return noise.ConfidenceInterval{}, fmt.Errorf("Result() must be called before calling ComputeConfidenceInterval()") |
| } |
| confInt, err := bs.Noise.ComputeConfidenceIntervalFloat64(bs.noisedSum, bs.l0Sensitivity, bs.lInfSensitivity, bs.epsilon, bs.delta, alpha) |
| if err != nil { |
| return noise.ConfidenceInterval{}, err |
| } |
| // If lower and upper bounds are non-negative, trim the negative part of the interval. |
| if bs.lower >= 0 { |
| confInt.LowerBound, confInt.UpperBound = math.Max(0, confInt.LowerBound), math.Max(0, confInt.UpperBound) |
| } |
| // Similarly if lower and upper bounds are non-positive, trim the positive part of the interval. |
| if bs.upper <= 0 { |
| confInt.LowerBound, confInt.UpperBound = math.Min(0, confInt.LowerBound), math.Min(0, confInt.UpperBound) |
| } |
| return confInt, nil |
| } |
| |
| // encodableBoundedSumFloat64 can be encoded by the gob package. |
| type encodableBoundedSumFloat64 struct { |
| Epsilon float64 |
| Delta float64 |
| L0Sensitivity int64 |
| LInfSensitivity float64 |
| Lower float64 |
| Upper float64 |
| NoiseKind noise.Kind |
| Sum float64 |
| } |
| |
| // GobEncode encodes BoundedSumInt64. |
| func (bs *BoundedSumFloat64) GobEncode() ([]byte, error) { |
| if bs.state != defaultState && bs.state != serialized { |
| return nil, fmt.Errorf("BoundedSumFloat64 object cannot be serialized: " + bs.state.errorMessage()) |
| } |
| enc := encodableBoundedSumFloat64{ |
| Epsilon: bs.epsilon, |
| Delta: bs.delta, |
| L0Sensitivity: bs.l0Sensitivity, |
| LInfSensitivity: bs.lInfSensitivity, |
| Lower: bs.lower, |
| Upper: bs.upper, |
| NoiseKind: noise.ToKind(bs.Noise), |
| Sum: bs.sum, |
| } |
| bs.state = serialized |
| return encode(enc) |
| } |
| |
| // GobDecode decodes BoundedSumInt64. |
| func (bs *BoundedSumFloat64) GobDecode(data []byte) error { |
| var enc encodableBoundedSumFloat64 |
| err := decode(&enc, data) |
| if err != nil { |
| return fmt.Errorf("couldn't decode BoundedSumFloat64 from bytes") |
| } |
| *bs = BoundedSumFloat64{ |
| epsilon: enc.Epsilon, |
| delta: enc.Delta, |
| l0Sensitivity: enc.L0Sensitivity, |
| lInfSensitivity: enc.LInfSensitivity, |
| lower: enc.Lower, |
| upper: enc.Upper, |
| noiseKind: enc.NoiseKind, |
| Noise: noise.ToNoise(enc.NoiseKind), |
| sum: enc.Sum, |
| state: defaultState, |
| } |
| return nil |
| } |