Skip to content

Commit 33ada89

Browse files
RamSawcopybara-github
authored andcommitted
Return allocated budget details from DPEngine.done()
PiperOrigin-RevId: 839172211
1 parent 69e3ec8 commit 33ada89

9 files changed

Lines changed: 186 additions & 23 deletions

File tree

pipelinedp4j/main/com/google/privacy/differentialprivacy/pipelinedp4j/core/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ kt_jvm_library(
115115
":framework_collections",
116116
"//main/com/google/privacy/differentialprivacy/pipelinedp4j/core/budget:allocated_budget",
117117
"//main/com/google/privacy/differentialprivacy/pipelinedp4j/core/budget:budget_accountant",
118+
"//main/com/google/privacy/differentialprivacy/pipelinedp4j/core/budget:budget_allocation_details",
118119
"//main/com/google/privacy/differentialprivacy/pipelinedp4j/core/budget:budget_spec",
119120
"//main/com/google/privacy/differentialprivacy/pipelinedp4j/dplibrary:noise_factories",
120121
"//main/com/google/privacy/differentialprivacy/pipelinedp4j/dplibrary:pre_aggregation_partition_selection_factory",

pipelinedp4j/main/com/google/privacy/differentialprivacy/pipelinedp4j/core/DpEngine.kt

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.google.privacy.differentialprivacy.pipelinedp4j.core
1818

19+
import com.google.errorprone.annotations.CanIgnoreReturnValue
1920
import com.google.privacy.differentialprivacy.Noise
2021
import com.google.privacy.differentialprivacy.pipelinedp4j.core.MetricType.COUNT
2122
import com.google.privacy.differentialprivacy.pipelinedp4j.core.MetricType.MEAN
@@ -35,6 +36,7 @@ import com.google.privacy.differentialprivacy.pipelinedp4j.core.budget.BudgetAcc
3536
import com.google.privacy.differentialprivacy.pipelinedp4j.core.budget.BudgetAccountantFactory
3637
import com.google.privacy.differentialprivacy.pipelinedp4j.core.budget.BudgetAccountingStrategy
3738
import com.google.privacy.differentialprivacy.pipelinedp4j.core.budget.BudgetAccountingStrategy.NAIVE
39+
import com.google.privacy.differentialprivacy.pipelinedp4j.core.budget.BudgetAllocationDetails
3840
import com.google.privacy.differentialprivacy.pipelinedp4j.core.budget.BudgetPerOpSpec
3941
import com.google.privacy.differentialprivacy.pipelinedp4j.core.budget.BudgetRequest
4042
import com.google.privacy.differentialprivacy.pipelinedp4j.core.budget.RelativeBudgetPerOpSpec
@@ -204,13 +206,26 @@ internal constructor(
204206
}
205207

206208
/**
207-
* Allocates privacy budgets to the metrics whose computation has been requested by calling
208-
* [aggregate]. This method must be called once per [DpEngine] instance.
209+
* Allocates privacy budgets to privacy-preserving operations in [aggregate] and
210+
* [selectPartitions] calls.
211+
*
212+
* Privacy-preserving operations are various aggregation metrics, like COUNT or SUM, and partition
213+
* selection. There might be multiple privacy-preserving operations in a single [DpEngine]
214+
* instance.
215+
*
216+
* This method must be called once per [DpEngine] instance.
217+
*
218+
* @return a list of [BudgetAllocationDetails] for each privacy-preserving operation. This reports
219+
* the actual budgets used during computation, which may include budgets for operations that
220+
* were not directly requested (e.g., for a MEAN aggregation, budget details for both SUM and
221+
* COUNT will be returned).
222+
* @throws IllegalStateException if [done] has already been called on this instance.
209223
*/
210-
fun done() {
224+
@CanIgnoreReturnValue
225+
fun done(): List<BudgetAllocationDetails> {
211226
throwIfDoneWasCalled()
212227
doneCalled = true
213-
budgetAccountant.allocateBudgets()
228+
return budgetAccountant.allocateBudgets()
214229
}
215230

216231
private fun throwIfDoneWasCalled() {

pipelinedp4j/main/com/google/privacy/differentialprivacy/pipelinedp4j/core/budget/BUILD.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ package(
2525
],
2626
)
2727

28+
kt_jvm_library(
29+
name = "budget_allocation_details",
30+
srcs = ["BudgetAllocationDetails.kt"],
31+
)
32+
2833
kt_jvm_library(
2934
name = "budget_spec",
3035
srcs = ["BudgetSpec.kt"],
@@ -43,6 +48,7 @@ kt_jvm_library(
4348
srcs = ["BudgetAccountant.kt"],
4449
deps = [
4550
":allocated_budget",
51+
":budget_allocation_details",
4652
":budget_spec",
4753
],
4854
)

pipelinedp4j/main/com/google/privacy/differentialprivacy/pipelinedp4j/core/budget/BudgetAccountant.kt

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ interface BudgetAccountant {
4545
*
4646
* @throws IllegalStateException if budgets have already been allocated.
4747
*/
48-
fun allocateBudgets()
48+
fun allocateBudgets(): List<BudgetAllocationDetails>
4949
}
5050

5151
/**
@@ -100,7 +100,7 @@ class NaiveBudgetAccountant(private val totalBudget: TotalBudget) : BudgetAccoun
100100
return allocatedBudget
101101
}
102102

103-
override fun allocateBudgets() {
103+
override fun allocateBudgets(): List<BudgetAllocationDetails> {
104104
if (budgetsAllocated) {
105105
throw IllegalStateException("Budgets have already been allocated.")
106106
}
@@ -119,8 +119,11 @@ class NaiveBudgetAccountant(private val totalBudget: TotalBudget) : BudgetAccoun
119119
checkEnoughAbsoluteBudget(totalRequestedEpsilon, totalRequestedDelta)
120120
checkEnoughRelativeBudget(remainingEpsilon, remainingDelta)
121121

122-
allocateAbsoluteBudgets()
123-
allocateRelativeBudgets(remainingEpsilon, remainingDelta)
122+
initializeAbsoluteBudgets()
123+
initializeRelativeBudgets(remainingEpsilon, remainingDelta)
124+
125+
return absoluteBudgets.map { it.toBudgetAllocationDetails() } +
126+
relativeBudgets.map { it.toBudgetAllocationDetails() }
124127
}
125128

126129
private fun checkEnoughAbsoluteBudget(requestedEpsilon: Double, requestedDelta: Double) {
@@ -147,7 +150,7 @@ class NaiveBudgetAccountant(private val totalBudget: TotalBudget) : BudgetAccoun
147150
return Math.abs(diff) > remaining / FLOATING_POINT_ARITHMETICS_TOLERANCE
148151
}
149152

150-
private fun allocateAbsoluteBudgets() {
153+
private fun initializeAbsoluteBudgets() {
151154
for (requestedAndAllocated in absoluteBudgets) {
152155
val budgetSpec = requestedAndAllocated.requested.budgetSpec as AbsoluteBudgetPerOpSpec
153156
requestedAndAllocated.allocated.initialize(
@@ -157,7 +160,7 @@ class NaiveBudgetAccountant(private val totalBudget: TotalBudget) : BudgetAccoun
157160
}
158161
}
159162

160-
private fun allocateRelativeBudgets(remainingEpsilon: Double, remainingDelta: Double) {
163+
private fun initializeRelativeBudgets(remainingEpsilon: Double, remainingDelta: Double) {
161164
var totalEpsilonWeight = 0.0
162165
var totalDeltaWeight = 0.0
163166
for (requestedAndAllocated in relativeBudgets) {
@@ -200,11 +203,13 @@ class NaiveBudgetAccountant(private val totalBudget: TotalBudget) : BudgetAccoun
200203
}
201204
}
202205

203-
private fun relativeEpsilonRequested(): Boolean =
204-
relativeBudgets.any { it.requested.mechanism.usesEpsilon }
206+
private fun relativeEpsilonRequested(): Boolean = relativeBudgets.any {
207+
it.requested.mechanism.usesEpsilon
208+
}
205209

206-
private fun relativeDeltaRequested(): Boolean =
207-
relativeBudgets.any { it.requested.mechanism.usesDelta }
210+
private fun relativeDeltaRequested(): Boolean = relativeBudgets.any {
211+
it.requested.mechanism.usesDelta
212+
}
208213
}
209214

210215
/**
@@ -243,7 +248,23 @@ enum class AccountedMechanism(val usesEpsilon: Boolean, val usesDelta: Boolean)
243248
internal data class RequestedAndAllocatedBudget(
244249
val requested: BudgetRequest,
245250
val allocated: AllocatedBudget,
246-
)
251+
) {
252+
/** Converts a [RequestedAndAllocatedBudget] to a [BudgetAllocationDetails]. */
253+
fun toBudgetAllocationDetails(): BudgetAllocationDetails {
254+
val epsilon = allocated.epsilon()
255+
val delta = allocated.delta()
256+
return when (requested.mechanism) {
257+
AccountedMechanism.GAUSSIAN_NOISE ->
258+
BudgetAllocationDetails.GaussianAggregationAllocation(epsilon, delta)
259+
AccountedMechanism.LAPLACE_NOISE ->
260+
BudgetAllocationDetails.LaplaceAggregationAllocation(epsilon)
261+
AccountedMechanism.PREAGGREGATED_PARTITION_SELECTION ->
262+
BudgetAllocationDetails.PreaggregatedPartitionSelectionAllocation(epsilon, delta)
263+
AccountedMechanism.POSTAGGREGATED_PARTITION_SELECTION ->
264+
BudgetAllocationDetails.PostaggregatedPartitionSelectionAllocation(delta)
265+
}
266+
}
267+
}
247268

248269
enum class BudgetAccountingStrategy {
249270
NAIVE
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.google.privacy.differentialprivacy.pipelinedp4j.core.budget
2+
3+
/**
4+
* Sealed class representing the details of a budget allocation for different differential privacy
5+
* mechanisms.
6+
*
7+
* Each subclass contains only the parameters relevant to that mechanism.
8+
*
9+
* This class is used to pass budget allocation details from [DpEngine] to API implementations
10+
* (e.g., BeamApi), but it is not intended for direct use by end-users of the library.
11+
*
12+
* Extend this class if you need to propagate more details about the budget allocation from DPEngine
13+
* to the backend-specific API implementations in the API package (e.g. BeamApi, etc.).
14+
*/
15+
sealed class BudgetAllocationDetails {
16+
/**
17+
* Budget allocation details for Gaussian mechanism used for aggregation.
18+
*
19+
* Uses both epsilon and delta.
20+
*/
21+
data class GaussianAggregationAllocation(val epsilon: Double, val delta: Double) :
22+
BudgetAllocationDetails()
23+
24+
/**
25+
* Budget allocation details for Laplace mechanism used for aggregation.
26+
*
27+
* Uses only epsilon.
28+
*/
29+
data class LaplaceAggregationAllocation(val epsilon: Double) : BudgetAllocationDetails()
30+
31+
/**
32+
* Budget allocation details for pre-aggregated partition selection.
33+
*
34+
* Uses both epsilon and delta.
35+
*/
36+
data class PreaggregatedPartitionSelectionAllocation(val epsilon: Double, val delta: Double) :
37+
BudgetAllocationDetails()
38+
39+
/**
40+
* Budget allocation details for post-aggregated partition selection.
41+
*
42+
* Only uses delta.
43+
*/
44+
data class PostaggregatedPartitionSelectionAllocation(val delta: Double) :
45+
BudgetAllocationDetails()
46+
}

pipelinedp4j/tests/com/google/privacy/differentialprivacy/pipelinedp4j/core/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ kt_jvm_test(
5858
"//main/com/google/privacy/differentialprivacy/pipelinedp4j/core:framework_collections",
5959
"//main/com/google/privacy/differentialprivacy/pipelinedp4j/core/budget:allocated_budget",
6060
"//main/com/google/privacy/differentialprivacy/pipelinedp4j/core/budget:budget_accountant",
61+
"//main/com/google/privacy/differentialprivacy/pipelinedp4j/core/budget:budget_allocation_details",
6162
"//main/com/google/privacy/differentialprivacy/pipelinedp4j/core/budget:budget_spec",
6263
"//main/com/google/privacy/differentialprivacy/pipelinedp4j/dplibrary:noise_factories",
6364
"//main/com/google/privacy/differentialprivacy/pipelinedp4j/dplibrary:pre_aggregation_partition_selection_factory",

pipelinedp4j/tests/com/google/privacy/differentialprivacy/pipelinedp4j/core/DpEngineTest.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import com.google.privacy.differentialprivacy.pipelinedp4j.core.NoiseKind.GAUSSI
3434
import com.google.privacy.differentialprivacy.pipelinedp4j.core.NoiseKind.LAPLACE
3535
import com.google.privacy.differentialprivacy.pipelinedp4j.core.budget.AbsoluteBudgetPerOpSpec
3636
import com.google.privacy.differentialprivacy.pipelinedp4j.core.budget.BudgetAccountingStrategy.NAIVE
37+
import com.google.privacy.differentialprivacy.pipelinedp4j.core.budget.BudgetAllocationDetails
3738
import com.google.privacy.differentialprivacy.pipelinedp4j.core.budget.BudgetPerOpSpec
3839
import com.google.privacy.differentialprivacy.pipelinedp4j.core.budget.RelativeBudgetPerOpSpec
3940
import com.google.privacy.differentialprivacy.pipelinedp4j.core.budget.TotalBudget
@@ -96,6 +97,36 @@ class DpEngineTest {
9697
assertThat(e).hasMessageThat().contains("done() has already been called")
9798
}
9899

100+
@Test
101+
fun done_returnsBudgetAllocationDetails() {
102+
val dpEngine = DpEngine.createForTesting(LOCAL_EF, LARGE_BUDGET_SPEC, ZeroNoiseFactory())
103+
val params =
104+
AggregationParams(
105+
metrics = ImmutableList.of(MetricDefinition(COUNT, AbsoluteBudgetPerOpSpec(1.0, 1e-5))),
106+
noiseKind = GAUSSIAN,
107+
maxPartitionsContributed = 1,
108+
maxContributionsPerPartition = 1,
109+
partitionSelectionBudget = AbsoluteBudgetPerOpSpec(2.0, 2e-5),
110+
)
111+
112+
val unused =
113+
dpEngine.aggregate(
114+
LocalCollection(sequenceOf(TestDataRow("Alice", "US", 1.0))),
115+
params,
116+
testDataExtractors,
117+
)
118+
val details = dpEngine.done()
119+
120+
assertThat(details)
121+
.containsExactly(
122+
BudgetAllocationDetails.GaussianAggregationAllocation(epsilon = 1.0, delta = 1e-5),
123+
BudgetAllocationDetails.PreaggregatedPartitionSelectionAllocation(
124+
epsilon = 2.0,
125+
delta = 2e-5,
126+
),
127+
)
128+
}
129+
99130
@Test
100131
fun aggregate_incorrectAggregateParams_throws() {
101132
val e =

pipelinedp4j/tests/com/google/privacy/differentialprivacy/pipelinedp4j/core/budget/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ kt_jvm_test(
2727
test_class = "com.google.privacy.differentialprivacy.pipelinedp4j.core.budget.BudgetTests",
2828
deps = [
2929
"//main/com/google/privacy/differentialprivacy/pipelinedp4j/core/budget:budget_accountant",
30+
"//main/com/google/privacy/differentialprivacy/pipelinedp4j/core/budget:budget_allocation_details",
3031
"//main/com/google/privacy/differentialprivacy/pipelinedp4j/core/budget:budget_spec",
3132
"@maven//:com_google_testparameterinjector_test_parameter_injector",
3233
"@maven//:com_google_truth_truth",

0 commit comments

Comments
 (0)