diff --git a/go/dpagg/count.go b/go/dpagg/count.go index 283de654..5e31dc0a 100644 --- a/go/dpagg/count.go +++ b/go/dpagg/count.go @@ -173,13 +173,18 @@ func (c *Count) Result() int64 { return c.noisedCount } -// 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. +// 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 Count +// 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 (c *Count) ThresholdedResult(thresholdDelta float64) *int64 { threshold := c.Noise.Threshold(c.l0Sensitivity, float64(c.lInfSensitivity), c.epsilon, c.delta, thresholdDelta) result := c.Result() - if result < int64(threshold) { + // 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 } return &result diff --git a/go/dpagg/count_test.go b/go/dpagg/count_test.go index ca081d6b..0303663a 100644 --- a/go/dpagg/count_test.go +++ b/go/dpagg/count_test.go @@ -339,14 +339,14 @@ func TestCountResultSetsStateCorrectly(t *testing.T) { } func TestCountThresholdedResult(t *testing.T) { - // ThresholdedResult outputs the result when it is greater than the threshold (5 using noNoise) + // ThresholdedResult outputs the result when it is greater than the threshold (5.00001 using noNoise) c1 := getNoiselessCount() for i := 0; i < 10; i++ { c1.Increment() } got := c1.ThresholdedResult(tenten) if got == nil || *got != 10 { - t.Errorf("ThresholdedResult(%f): when 10 addings got %v, want 10", tenten, got) + t.Errorf("ThresholdedResult(%f): after 10 entries got %v, want 10", tenten, got) } // ThresholdedResult outputs nil when it is less than the threshold @@ -355,7 +355,17 @@ func TestCountThresholdedResult(t *testing.T) { c2.Increment() got = c2.ThresholdedResult(tenten) if got != nil { - t.Errorf("ThresholdedResult(%f): when 2 addings got %v, want nil", tenten, got) + t.Errorf("ThresholdedResult(%f): after 2 entries got %v, want nil", tenten, got) + } + + // Edge case when noisy result is 5 and threshold is 5.00001, ThresholdedResult outputs nil. + c3 := getNoiselessCount() + for i := 0; i < 5; i++ { + c3.Increment() + } + got = c3.ThresholdedResult(tenten) + if got != nil { + t.Errorf("ThresholdedResult(%f): after 5 entries got %v, want nil", tenten, got) } } diff --git a/go/dpagg/dpagg_test.go b/go/dpagg/dpagg_test.go index 9d76b117..f058e448 100644 --- a/go/dpagg/dpagg_test.go +++ b/go/dpagg/dpagg_test.go @@ -61,7 +61,7 @@ func (noNoise) AddNoiseFloat64(x float64, _ int64, _, _, _ float64) float64 { } func (noNoise) Threshold(_ int64, _, _, _, _ float64) float64 { - return 5 + return 5.00001 } // If noNoise is not initialized with a noise distribution, confidence interval functions will return a default confidence interval, i.e [0,0]. diff --git a/go/dpagg/sum.go b/go/dpagg/sum.go index bec62b28..e8aee514 100644 --- a/go/dpagg/sum.go +++ b/go/dpagg/sum.go @@ -254,15 +254,19 @@ func (bs *BoundedSumInt64) Result() int64 { return bs.noisedSum } -// 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. +// 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 { threshold := bs.Noise.Threshold(bs.l0Sensitivity, float64(bs.lInfSensitivity), bs.epsilon, bs.delta, thresholdDelta) result := bs.Result() - // To make sure floating-point rounding doesn't break DP guarantees, we err on - // the side of dropping the result if it is exactly equal to the threshold. - if float64(result) <= threshold { + // 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 } return &result diff --git a/go/dpagg/sum_test.go b/go/dpagg/sum_test.go index df76f650..8c742af5 100644 --- a/go/dpagg/sum_test.go +++ b/go/dpagg/sum_test.go @@ -868,29 +868,38 @@ func TestBoundedSumFloat64ResultSetsStateCorrectly(t *testing.T) { } func TestThresholdedResultInt64(t *testing.T) { - // ThresholdedResult outputs the result when it is more than the threshold (5 using noNoise) + // ThresholdedResult outputs the result when it is more than the threshold (5.00001 using noNoise) bs1 := getNoiselessBSI() bs1.Add(1) bs1.Add(2) bs1.Add(3) bs1.Add(4) - got := bs1.ThresholdedResult(5) + got := bs1.ThresholdedResult(0.1) if got == nil || *got != 10 { - t.Errorf("ThresholdedResult(5): when 1, 2, 3, 4 were added got %v, want 10", got) + t.Errorf("ThresholdedResult(0.1): when 1, 2, 3, 4 were added got %v, want 10", got) } // ThresholdedResult outputs nil when it is less than the threshold bs2 := getNoiselessBSI() bs2.Add(1) bs2.Add(2) - got = bs2.ThresholdedResult(5) // the parameter here is for the reader's eyes, the actual threshold value (5) is specified in noNoise.Threshold() + got = bs2.ThresholdedResult(0.1) + if got != nil { + t.Errorf("ThresholdedResult(0.1): when 1,2 were added got %v, want nil", got) + } + + // Edge case when noisy result is 5 and threshold is 5.00001, ThresholdedResult outputs nil. + bs3 := getNoiselessBSI() + bs3.Add(2) + bs3.Add(3) + got = bs3.ThresholdedResult(0.1) if got != nil { - t.Errorf("ThresholdedResult(5): when 1,2 were added got %v, want nil", got) + t.Errorf("ThresholdedResult(0.1): when 2,3 were added got %v, want nil", got) } } func TestThresholdedResultFloat64(t *testing.T) { - // ThresholdedResult outputs the result when it is more than the threshold (5 using noNoise) + // ThresholdedResult outputs the result when it is more than the threshold (5.00001 using noNoise) bs1 := getNoiselessBSF() bs1.Add(1.5) bs1.Add(2.5) @@ -905,9 +914,9 @@ func TestThresholdedResultFloat64(t *testing.T) { bs2 := getNoiselessBSF() bs2.Add(1) bs2.Add(2.5) - got = bs2.ThresholdedResult(5) // the parameter here is for the reader's eyes, the actual threshold value (5) is specified in noNoise.Threshold() + got = bs2.ThresholdedResult(0.1) if got != nil { - t.Errorf("ThresholdedResult(5): when 1, 2.5 were added got %v, want nil", got) + t.Errorf("ThresholdedResult(0.1): when 1, 2.5 were added got %v, want nil", got) } }