#[cfg(feature = "ffi")]
mod ffi;
use std::collections::HashSet;
use opendp_derive::bootstrap;
use crate::core::{Function, Measurement, PrivacyMap};
use crate::domains::AtomDomain;
use crate::error::Fallible;
use crate::measures::MaxDivergence;
use crate::metrics::DiscreteDistance;
use crate::traits::samplers::sample_bernoulli_float;
use crate::traits::{ExactIntCast, Hashable, InfDiv, InfLn, InfMul, InfSub};
#[bootstrap(features("contrib"), arguments(constant_time(default = false)))]
pub fn make_randomized_response_bool(
prob: f64,
constant_time: bool,
) -> Fallible<Measurement<AtomDomain<bool>, DiscreteDistance, MaxDivergence, bool>> {
if !(0.5f64..=1.0).contains(&prob) {
return fallible!(MakeMeasurement, "probability must be within [0.5, 1]");
}
let privacy_constant = if prob == 1.0 {
f64::INFINITY
} else {
prob.inf_div(&(1.0).neg_inf_sub(&prob)?)?.inf_ln()?
};
Measurement::new(
AtomDomain::default(),
DiscreteDistance,
MaxDivergence,
Function::new_fallible(move |arg: &bool| {
Ok(arg ^ !sample_bernoulli_float(prob, constant_time)?)
}),
PrivacyMap::new(move |d_in| if *d_in == 0 { 0.0 } else { privacy_constant }),
)
}
#[bootstrap(
features("contrib"),
arguments(categories(rust_type = "Vec<T>"), constant_time(default = false)),
generics(T(example = "$get_first(categories)"))
)]
pub fn make_randomized_response<T: Hashable>(
categories: HashSet<T>,
prob: f64,
) -> Fallible<Measurement<AtomDomain<T>, DiscreteDistance, MaxDivergence, T>> {
use crate::traits::samplers::sample_uniform_uint_below;
let categories = categories.into_iter().collect::<Vec<_>>();
if categories.len() < 2 {
return fallible!(MakeMeasurement, "length of categories must be at least two");
}
let num_categories = f64::exact_int_cast(categories.len())?;
if !(num_categories.recip()..=1f64).contains(&prob) {
return fallible!(
MakeMeasurement,
"probability must be within [1/num_categories, 1]"
);
}
let privacy_constant = if prob == 1.0 {
f64::INFINITY
} else {
prob.inf_div(&(1.0).neg_inf_sub(&prob)?)?
.inf_mul(&num_categories.inf_sub(&1.0)?)?
.inf_ln()?
};
Measurement::new(
AtomDomain::default(),
DiscreteDistance,
MaxDivergence,
Function::new_fallible(move |truth: &T| {
let index = categories.iter().position(|cat| cat == truth);
let mut sample =
sample_uniform_uint_below(categories.len() - if index.is_some() { 1 } else { 0 })?;
if let Some(i) = index {
if sample >= i {
sample += 1
}
}
let lie = &categories[sample];
let be_honest = sample_bernoulli_float(prob, false)?;
let is_member = index.is_some();
Ok(if be_honest && is_member { truth } else { lie }.clone())
}),
PrivacyMap::new(move |d_in| if *d_in == 0 { 0.0 } else { privacy_constant }),
)
}
#[cfg(test)]
mod test;