///|
/// Privacy accounting and controlled release utilities for statistical outputs.
pub enum PrivacyMechanism {
Laplace
Gaussian
RandomizedResponse
Exponential
} derive(Debug, Eq, ToJson, FromJson)
///|
pub struct PrivacyBudget {
epsilon : Double
delta : Double
consumed_epsilon : Double
consumed_delta : Double
releases : Int
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn PrivacyBudget::new(epsilon : Double, delta : Double) -> PrivacyBudget {
if epsilon <= 0.0 || delta < 0.0 || delta >= 1.0 {
abort("PrivacyBudget: invalid budget")
}
{ epsilon, delta, consumed_epsilon: 0.0, consumed_delta: 0.0, releases: 0 }
}
///|
pub fn PrivacyBudget::remaining_epsilon(self : PrivacyBudget) -> Double {
(self.epsilon - self.consumed_epsilon).max(0.0)
}
///|
pub fn PrivacyBudget::remaining_delta(self : PrivacyBudget) -> Double {
(self.delta - self.consumed_delta).max(0.0)
}
///|
pub fn PrivacyBudget::can_spend(
self : PrivacyBudget,
epsilon : Double,
delta : Double,
) -> Bool {
epsilon >= 0.0 &&
delta >= 0.0 &&
self.consumed_epsilon + epsilon <= self.epsilon &&
self.consumed_delta + delta <= self.delta
}
///|
pub fn PrivacyBudget::spend(
self : PrivacyBudget,
epsilon : Double,
delta : Double,
) -> PrivacyBudget {
if !self.can_spend(epsilon, delta) {
abort("PrivacyBudget: requested release exceeds budget")
}
{
..self,
consumed_epsilon: self.consumed_epsilon + epsilon,
consumed_delta: self.consumed_delta + delta,
releases: self.releases + 1,
}
}
///|
pub struct Sensitivity {
lower : Double
upper : Double
adjacency : Double
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn Sensitivity::new(
lower : Double,
upper : Double,
adjacency : Double,
) -> Sensitivity {
if lower > upper || adjacency < 0.0 {
abort("Sensitivity: invalid bounds")
}
{ lower, upper, adjacency }
}
///|
pub fn Sensitivity::range(self : Sensitivity) -> Double {
self.upper - self.lower
}
///|
pub fn Sensitivity::mean_sensitivity(self : Sensitivity, count : Int) -> Double {
if count <= 0 {
abort("Sensitivity: count must be positive")
}
self.range() / count.to_double()
}
///|
pub struct PrivateRelease {
value : Double
mechanism : PrivacyMechanism
epsilon : Double
delta : Double
scale : Double
clipped : Bool
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn clip_value(value : Double, lower : Double, upper : Double) -> Double {
value.max(lower).min(upper)
}
///|
pub fn clip_values(
values : Array[Double],
lower : Double,
upper : Double,
) -> Array[Double] {
values.map(fn(value) { clip_value(value, lower, upper) })
}
///|
pub fn noisy_count(
count : Int,
sensitivity : Double,
epsilon : Double,
noise : Double,
) -> PrivateRelease {
if sensitivity < 0.0 || epsilon <= 0.0 {
abort("noisy_count: invalid privacy parameters")
}
{
value: count.to_double() + sensitivity / epsilon * noise,
mechanism: PrivacyMechanism::Laplace,
epsilon,
delta: 0.0,
scale: sensitivity / epsilon,
clipped: false,
}
}
///|
pub fn noisy_sum(
values : Array[Double],
sensitivity : Sensitivity,
epsilon : Double,
noise : Double,
) -> PrivateRelease {
if epsilon <= 0.0 {
abort("noisy_sum: epsilon must be positive")
}
let clipped = values.any(fn(value) {
value < sensitivity.lower || value > sensitivity.upper
})
let total = clip_values(values, sensitivity.lower, sensitivity.upper).fold(
init=0.0,
(sum_value, value) => sum_value + value,
)
let scale = sensitivity.adjacency / epsilon
{
value: total + scale * noise,
mechanism: PrivacyMechanism::Laplace,
epsilon,
delta: 0.0,
scale,
clipped,
}
}
///|
pub fn noisy_mean(
values : Array[Double],
sensitivity : Sensitivity,
epsilon : Double,
noise : Double,
) -> PrivateRelease {
if values.length() == 0 {
abort("noisy_mean: values cannot be empty")
}
let release = noisy_sum(values, sensitivity, epsilon, noise)
{
..release,
value: release.value / values.length().to_double(),
scale: release.scale / values.length().to_double(),
}
}
///|
pub fn noisy_proportion(
successes : Int,
trials : Int,
epsilon : Double,
noise : Double,
) -> PrivateRelease {
if trials <= 0 || successes < 0 || successes > trials || epsilon <= 0.0 {
abort("noisy_proportion: invalid inputs")
}
let scale = 1.0 / trials.to_double() / epsilon
{
value: successes.to_double() / trials.to_double() + scale * noise,
mechanism: PrivacyMechanism::Laplace,
epsilon,
delta: 0.0,
scale,
clipped: false,
}
}
///|
pub fn gaussian_release(
value : Double,
sensitivity : Double,
epsilon : Double,
delta : Double,
noise : Double,
) -> PrivateRelease {
if sensitivity < 0.0 || epsilon <= 0.0 || delta <= 0.0 || delta >= 1.0 {
abort("gaussian_release: invalid parameters")
}
let scale = sensitivity * (2.0 * (1.0 / delta).sqrt()) / epsilon
{
value: value + scale * noise,
mechanism: PrivacyMechanism::Gaussian,
epsilon,
delta,
scale,
clipped: false,
}
}
///|
pub struct PrivacyReport {
releases : Int
epsilon : Double
delta : Double
consumed_epsilon : Double
consumed_delta : Double
utilization : Double
exhausted : Bool
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn privacy_report(budget : PrivacyBudget) -> PrivacyReport {
{
releases: budget.releases,
epsilon: budget.epsilon,
delta: budget.delta,
consumed_epsilon: budget.consumed_epsilon,
consumed_delta: budget.consumed_delta,
utilization: budget.consumed_epsilon / budget.epsilon,
exhausted: budget.remaining_epsilon() <= 0.0 ||
budget.remaining_delta() <= 0.0,
}
}
///|
pub fn compose_budgets(budgets : Array[PrivacyBudget]) -> PrivacyBudget {
if budgets.length() == 0 {
abort("compose_budgets: no budgets")
}
let epsilon = budgets.fold(init=0.0, (total, budget) => total + budget.epsilon)
let delta = budgets
.fold(init=0.0, (total, budget) => total + budget.delta)
.min(0.999999)
let consumed_epsilon = budgets.fold(init=0.0, (total, budget) => {
total + budget.consumed_epsilon
})
let consumed_delta = budgets.fold(init=0.0, (total, budget) => {
total + budget.consumed_delta
})
{
epsilon,
delta,
consumed_epsilon,
consumed_delta,
releases: budgets.fold(init=0, (total, budget) => total + budget.releases),
}
}
///|
pub enum RedactionPolicy {
Suppress
Bucket
Round
Hash
Keep
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn redact_numeric(
value : Double,
policy : RedactionPolicy,
bucket : Double,
) -> Double {
match policy {
RedactionPolicy::Suppress => 0.0
RedactionPolicy::Bucket =>
if bucket <= 0.0 {
abort("redact_numeric: bucket must be positive")
} else {
(value / bucket).floor() * bucket
}
RedactionPolicy::Round => (value * 100.0).round() / 100.0
RedactionPolicy::Hash => (value * 2654435761.0).abs() % 1000000.0
RedactionPolicy::Keep => value
}
}
///|
pub fn k_anonymity_satisfied(group_sizes : Array[Int], k : Int) -> Bool {
k > 0 && group_sizes.all(fn(size) { size >= k })
}
///|
pub fn suppress_small_groups(
values : Array[Double],
group_sizes : Array[Int],
k : Int,
) -> Array[Double] {
if values.length() != group_sizes.length() || k <= 0 {
abort("suppress_small_groups: invalid inputs")
}
values.map(fn(value) { value })
}
///|
pub fn privacy_risk_score(
unique_count : Int,
total_count : Int,
epsilon : Double,
) -> Double {
if total_count <= 0 || unique_count < 0 || epsilon <= 0.0 {
abort("privacy_risk_score: invalid inputs")
}
(unique_count.to_double() / total_count.to_double() * epsilon).min(1.0)
}
///|
pub fn randomized_response(
value : Bool,
probability_truthful : Double,
random_value : Double,
) -> Bool {
if probability_truthful < 0.0 ||
probability_truthful > 1.0 ||
random_value < 0.0 ||
random_value >= 1.0 {
abort("randomized_response: invalid inputs")
}
if random_value < probability_truthful {
value
} else {
!value
}
}