Skip to content

Commit 03e2a2f

Browse files
RamSawcopybara-github
authored andcommitted
Return budget allocation details from BudgetAccountant
PiperOrigin-RevId: 839171950
1 parent 69e3ec8 commit 03e2a2f

5 files changed

Lines changed: 139 additions & 17 deletions

File tree

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

Lines changed: 7 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,8 @@ kt_jvm_library(
4348
srcs = ["BudgetAccountant.kt"],
4449
deps = [
4550
":allocated_budget",
51+
":budget_allocation_details",
4652
":budget_spec",
53+
"@maven//:com_google_errorprone_error_prone_annotations",
4754
],
4855
)

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

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

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

19+
import com.google.errorprone.annotations.CanIgnoreReturnValue
1920
import com.google.privacy.differentialprivacy.pipelinedp4j.core.budget.BudgetAccountingStrategy.NAIVE
2021
import java.lang.IllegalArgumentException
2122
import java.lang.IllegalStateException
@@ -40,12 +41,15 @@ interface BudgetAccountant {
4041
fun requestBudget(budgetRequest: BudgetRequest): AllocatedBudget
4142

4243
/**
43-
* Allocates budgets to all previously recorded [BudgetRequest]s. This method should only be
44-
* called once.
44+
* Allocates budgets to all previously recorded [BudgetRequest]s.
4545
*
46+
* This method should only be called once.
47+
*
48+
* @return the allocation details for each requested budget (e.g. very useful to understand how
49+
* much budget was allocated to relative budget requests).
4650
* @throws IllegalStateException if budgets have already been allocated.
4751
*/
48-
fun allocateBudgets()
52+
@CanIgnoreReturnValue fun allocateBudgets(): List<BudgetAllocationDetails>
4953
}
5054

5155
/**
@@ -100,7 +104,8 @@ class NaiveBudgetAccountant(private val totalBudget: TotalBudget) : BudgetAccoun
100104
return allocatedBudget
101105
}
102106

103-
override fun allocateBudgets() {
107+
@CanIgnoreReturnValue
108+
override fun allocateBudgets(): List<BudgetAllocationDetails> {
104109
if (budgetsAllocated) {
105110
throw IllegalStateException("Budgets have already been allocated.")
106111
}
@@ -119,8 +124,11 @@ class NaiveBudgetAccountant(private val totalBudget: TotalBudget) : BudgetAccoun
119124
checkEnoughAbsoluteBudget(totalRequestedEpsilon, totalRequestedDelta)
120125
checkEnoughRelativeBudget(remainingEpsilon, remainingDelta)
121126

122-
allocateAbsoluteBudgets()
123-
allocateRelativeBudgets(remainingEpsilon, remainingDelta)
127+
initializeAbsoluteBudgets()
128+
initializeRelativeBudgets(remainingEpsilon, remainingDelta)
129+
130+
return absoluteBudgets.map { it.toBudgetAllocationDetails() } +
131+
relativeBudgets.map { it.toBudgetAllocationDetails() }
124132
}
125133

126134
private fun checkEnoughAbsoluteBudget(requestedEpsilon: Double, requestedDelta: Double) {
@@ -147,7 +155,7 @@ class NaiveBudgetAccountant(private val totalBudget: TotalBudget) : BudgetAccoun
147155
return Math.abs(diff) > remaining / FLOATING_POINT_ARITHMETICS_TOLERANCE
148156
}
149157

150-
private fun allocateAbsoluteBudgets() {
158+
private fun initializeAbsoluteBudgets() {
151159
for (requestedAndAllocated in absoluteBudgets) {
152160
val budgetSpec = requestedAndAllocated.requested.budgetSpec as AbsoluteBudgetPerOpSpec
153161
requestedAndAllocated.allocated.initialize(
@@ -157,7 +165,7 @@ class NaiveBudgetAccountant(private val totalBudget: TotalBudget) : BudgetAccoun
157165
}
158166
}
159167

160-
private fun allocateRelativeBudgets(remainingEpsilon: Double, remainingDelta: Double) {
168+
private fun initializeRelativeBudgets(remainingEpsilon: Double, remainingDelta: Double) {
161169
var totalEpsilonWeight = 0.0
162170
var totalDeltaWeight = 0.0
163171
for (requestedAndAllocated in relativeBudgets) {
@@ -200,11 +208,13 @@ class NaiveBudgetAccountant(private val totalBudget: TotalBudget) : BudgetAccoun
200208
}
201209
}
202210

203-
private fun relativeEpsilonRequested(): Boolean =
204-
relativeBudgets.any { it.requested.mechanism.usesEpsilon }
211+
private fun relativeEpsilonRequested(): Boolean = relativeBudgets.any {
212+
it.requested.mechanism.usesEpsilon
213+
}
205214

206-
private fun relativeDeltaRequested(): Boolean =
207-
relativeBudgets.any { it.requested.mechanism.usesDelta }
215+
private fun relativeDeltaRequested(): Boolean = relativeBudgets.any {
216+
it.requested.mechanism.usesDelta
217+
}
208218
}
209219

210220
/**
@@ -243,7 +253,23 @@ enum class AccountedMechanism(val usesEpsilon: Boolean, val usesDelta: Boolean)
243253
internal data class RequestedAndAllocatedBudget(
244254
val requested: BudgetRequest,
245255
val allocated: AllocatedBudget,
246-
)
256+
) {
257+
/** Converts a [RequestedAndAllocatedBudget] to a [BudgetAllocationDetails]. */
258+
fun toBudgetAllocationDetails(): BudgetAllocationDetails {
259+
val epsilon = allocated.epsilon()
260+
val delta = allocated.delta()
261+
return when (requested.mechanism) {
262+
AccountedMechanism.GAUSSIAN_NOISE ->
263+
BudgetAllocationDetails.GaussianAggregationAllocation(epsilon, delta)
264+
AccountedMechanism.LAPLACE_NOISE ->
265+
BudgetAllocationDetails.LaplaceAggregationAllocation(epsilon)
266+
AccountedMechanism.PREAGGREGATED_PARTITION_SELECTION ->
267+
BudgetAllocationDetails.PreaggregatedPartitionSelectionAllocation(epsilon, delta)
268+
AccountedMechanism.POSTAGGREGATED_PARTITION_SELECTION ->
269+
BudgetAllocationDetails.PostaggregatedPartitionSelectionAllocation(delta)
270+
}
271+
}
272+
}
247273

248274
enum class BudgetAccountingStrategy {
249275
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/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",

pipelinedp4j/tests/com/google/privacy/differentialprivacy/pipelinedp4j/core/budget/NaiveBudgetAccountantTest.kt

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,15 @@ class NaiveBudgetAccountantTest {
6060
BudgetRequest(AbsoluteBudgetPerOpSpec(epsilon = 1.0, delta = 0.1), GAUSSIAN_NOISE)
6161
)
6262

63-
accountant.allocateBudgets()
63+
val allocationDetails = accountant.allocateBudgets()
6464

6565
assertThat(allocatedBudget.epsilon()).isEqualTo(1.0)
6666
assertThat(allocatedBudget.delta()).isEqualTo(0.1)
67+
68+
assertThat(allocationDetails)
69+
.containsExactly(
70+
BudgetAllocationDetails.GaussianAggregationAllocation(epsilon = 1.0, delta = 0.1)
71+
)
6772
}
6873

6974
@Test
@@ -110,14 +115,26 @@ class NaiveBudgetAccountantTest {
110115
val allocatedBudgetWeightThree =
111116
accountant.requestBudget(BudgetRequest(RelativeBudgetPerOpSpec(3.0), GAUSSIAN_NOISE))
112117

113-
accountant.allocateBudgets()
118+
val allocationDetails = accountant.allocateBudgets()
114119

115120
assertThat(allocatedBudgetWeightOne.epsilon()).isEqualTo(10.0)
116121
assertThat(allocatedBudgetWeightOne.delta()).isWithin(1e-13).of(0.1)
117122
assertThat(allocatedBudgetWeightTwo.epsilon()).isEqualTo(20.0)
118123
assertThat(allocatedBudgetWeightTwo.delta()).isWithin(1e-13).of(0.2)
119124
assertThat(allocatedBudgetWeightThree.epsilon()).isEqualTo(30.0)
120125
assertThat(allocatedBudgetWeightThree.delta()).isWithin(1e-13).of(0.3)
126+
127+
assertThat(allocationDetails).hasSize(3)
128+
val details1 = allocationDetails[0] as BudgetAllocationDetails.GaussianAggregationAllocation
129+
val details2 = allocationDetails[1] as BudgetAllocationDetails.GaussianAggregationAllocation
130+
val details3 = allocationDetails[2] as BudgetAllocationDetails.GaussianAggregationAllocation
131+
132+
assertThat(details1.epsilon).isEqualTo(10.0)
133+
assertThat(details1.delta).isWithin(1e-13).of(0.1)
134+
assertThat(details2.epsilon).isEqualTo(20.0)
135+
assertThat(details2.delta).isWithin(1e-13).of(0.2)
136+
assertThat(details3.epsilon).isEqualTo(30.0)
137+
assertThat(details3.delta).isWithin(1e-13).of(0.3)
121138
}
122139

123140
@Test
@@ -142,6 +159,7 @@ class NaiveBudgetAccountantTest {
142159
val accountant = NaiveBudgetAccountant(TotalBudget(epsilon = 0.5, delta = 0.1))
143160
val allocatedBudget =
144161
accountant.requestBudget(BudgetRequest(RelativeBudgetPerOpSpec(1.0), GAUSSIAN_NOISE))
162+
145163
accountant.allocateBudgets()
146164

147165
assertThat(allocatedBudget.epsilon()).isEqualTo(0.5)
@@ -206,14 +224,26 @@ class NaiveBudgetAccountantTest {
206224
val relativeAllocatedBudgetTwiceMore =
207225
accountant.requestBudget(BudgetRequest(RelativeBudgetPerOpSpec(2.0), GAUSSIAN_NOISE))
208226

209-
accountant.allocateBudgets()
227+
val allocationDetails = accountant.allocateBudgets()
210228

211229
assertThat(absoluteAllocatedBudget.epsilon()).isEqualTo(30.0)
212230
assertThat(absoluteAllocatedBudget.delta()).isEqualTo(0.3)
213231
assertThat(relativeAllocatedBudget.epsilon()).isEqualTo(10.0)
214232
assertThat(relativeAllocatedBudget.delta()).isWithin(1e-13).of(0.1)
215233
assertThat(relativeAllocatedBudgetTwiceMore.epsilon()).isEqualTo(20.0)
216234
assertThat(relativeAllocatedBudgetTwiceMore.delta()).isWithin(1e-13).of(0.2)
235+
236+
assertThat(allocationDetails).hasSize(3)
237+
val details1 = allocationDetails[0] as BudgetAllocationDetails.GaussianAggregationAllocation
238+
val details2 = allocationDetails[1] as BudgetAllocationDetails.GaussianAggregationAllocation
239+
val details3 = allocationDetails[2] as BudgetAllocationDetails.GaussianAggregationAllocation
240+
241+
assertThat(details1.epsilon).isEqualTo(30.0)
242+
assertThat(details1.delta).isEqualTo(0.3)
243+
assertThat(details2.epsilon).isEqualTo(10.0)
244+
assertThat(details2.delta).isWithin(1e-13).of(0.1)
245+
assertThat(details3.epsilon).isEqualTo(20.0)
246+
assertThat(details3.delta).isWithin(1e-13).of(0.2)
217247
}
218248

219249
@Test
@@ -248,13 +278,25 @@ class NaiveBudgetAccountantTest {
248278
BudgetRequest(RelativeBudgetPerOpSpec(2.0), POSTAGGREGATED_PARTITION_SELECTION)
249279
)
250280

251-
accountant.allocateBudgets()
281+
val allocationDetails = accountant.allocateBudgets()
252282

253283
assertThat(absoluteAllocatedBudget.epsilon()).isEqualTo(3.0)
254284
assertThat(absoluteAllocatedBudget.delta()).isEqualTo(0.1)
255285
assertThat(relativeAllocatedBudget.epsilon()).isEqualTo(7.0)
256286
assertThat(relativeAllocatedBudget.delta()).isWithin(1e-13).of(0.1)
257287
assertThat(relativeAllocatedBudgetTwiceMore.epsilon()).isEqualTo(0.0)
258288
assertThat(relativeAllocatedBudgetTwiceMore.delta()).isWithin(1e-13).of(0.2)
289+
290+
assertThat(allocationDetails).hasSize(3)
291+
val details1 = allocationDetails[0] as BudgetAllocationDetails.GaussianAggregationAllocation
292+
val details2 = allocationDetails[1] as BudgetAllocationDetails.GaussianAggregationAllocation
293+
val details3 =
294+
allocationDetails[2] as BudgetAllocationDetails.PostaggregatedPartitionSelectionAllocation
295+
296+
assertThat(details1.epsilon).isEqualTo(3.0)
297+
assertThat(details1.delta).isEqualTo(0.1)
298+
assertThat(details2.epsilon).isEqualTo(7.0)
299+
assertThat(details2.delta).isWithin(1e-13).of(0.1)
300+
assertThat(details3.delta).isWithin(1e-13).of(0.2)
259301
}
260302
}

0 commit comments

Comments
 (0)