use crate::{
core::{Function, Measure, Measurement, PrivacyMap},
domains::{AtomDomain, VectorDomain},
error::Fallible,
measures::{MaxDivergence, ZeroConcentratedDivergence},
metrics::LInfDistance,
traits::{
CastInternalRational, FiniteBounds, InfCast, InfDiv, InfMul, InfPowI, Number, ProductOrd,
samplers::{sample_bernoulli_exp, sample_uniform_uint_below},
},
};
use dashu::{base::Sign, float::FBig, ibig, rational::RBig};
use num::Zero;
use opendp_derive::{bootstrap, proven};
use std::{collections::BTreeSet, ops::Range};
#[cfg(feature = "ffi")]
mod ffi;
#[cfg(test)]
mod test;
#[bootstrap(
features("contrib"),
arguments(
output_measure(c_type = "AnyMeasure *", rust_type = b"null"),
negate(default = false),
),
generics(MO(suppress), TIA(suppress))
)]
pub fn make_noisy_top_k<MO: TopKMeasure, TIA>(
input_domain: VectorDomain<AtomDomain<TIA>>,
input_metric: LInfDistance<TIA>,
output_measure: MO,
k: usize,
scale: f64,
negate: bool,
) -> Fallible<Measurement<VectorDomain<AtomDomain<TIA>>, LInfDistance<TIA>, MO, Vec<usize>>>
where
TIA: Number + CastInternalRational,
f64: InfCast<TIA> + InfCast<usize>,
FBig: TryFrom<TIA>,
{
if input_domain.element_domain.nan() {
return fallible!(MakeMeasurement, "input_domain elements must be non-nan");
}
if let Some(size) = input_domain.size {
if k > size {
return fallible!(
MakeMeasurement,
"k ({k}) must not exceed the number of candidates ({size})"
);
}
}
if !scale.is_finite() || scale.is_sign_negative() {
return fallible!(
MakeMeasurement,
"scale ({scale}) must not be finite and non-negative"
);
}
let monotonic = input_metric.monotonic;
Measurement::new(
input_domain,
input_metric,
output_measure,
Function::new_fallible(move |x: &Vec<TIA>| {
noisy_top_k(x, scale, k, negate, MO::REPLACEMENT)
}),
PrivacyMap::new_fallible(move |d_in: &TIA| {
let d_in = if monotonic {
d_in.clone()
} else {
d_in.inf_add(&d_in)?
};
let d_in = f64::inf_cast(d_in)?;
if d_in.is_sign_negative() {
return fallible!(InvalidDistance, "sensitivity ({d_in}) must be non-negative");
}
if d_in.is_zero() {
return Ok(0.0);
}
if scale.is_zero() {
return Ok(f64::INFINITY);
}
MO::privacy_map(d_in, scale)?.inf_mul(&f64::inf_cast(k)?)
}),
)
}
pub trait TopKMeasure: Measure<Distance = f64> + 'static {
const REPLACEMENT: bool;
fn privacy_map(d_in: f64, scale: f64) -> Fallible<f64>;
}
#[proven(proof_path = "measurements/noisy_top_k/TopKMeasure_MaxDivergence.tex")]
impl TopKMeasure for MaxDivergence {
const REPLACEMENT: bool = false;
fn privacy_map(d_in: f64, scale: f64) -> Fallible<f64> {
d_in.inf_div(&scale)
}
}
#[proven(proof_path = "measurements/noisy_top_k/TopKMeasure_ZeroConcentratedDivergence.tex")]
impl TopKMeasure for ZeroConcentratedDivergence {
const REPLACEMENT: bool = true;
fn privacy_map(d_in: f64, scale: f64) -> Fallible<f64> {
d_in.inf_div(&scale)?.inf_powi(ibig!(2))?.inf_div(&8.0)
}
}
#[proven]
pub(crate) fn noisy_top_k<TIA: Clone + CastInternalRational + ProductOrd + FiniteBounds>(
x: &[TIA],
scale: f64,
k: usize,
negate: bool,
replacement: bool,
) -> Fallible<Vec<usize>> {
let sign = Sign::from(negate);
let scale = scale.into_rational()?;
let y = (x.into_iter().cloned())
.map(|x_i| {
x_i.total_clamp(TIA::MIN_FINITE, TIA::MAX_FINITE)?
.into_rational()
.map(|x_i| x_i * sign)
})
.collect::<Fallible<_>>()?;
peel_permute_and_flip(y, scale, k, replacement)
}
#[proven]
fn peel_permute_and_flip(
mut x: Vec<RBig>,
scale: RBig,
k: usize,
replacement: bool,
) -> Fallible<Vec<usize>> {
let mut natural_order = Vec::new();
let mut sorted_order = BTreeSet::new();
for _ in 0..k.min(x.len()) {
let mut index = permute_and_flip(&x, &scale, replacement)?;
x.remove(index);
for &del in &sorted_order {
if del <= index { index += 1 } else { break }
}
sorted_order.insert(index);
natural_order.push(index);
}
Ok(natural_order)
}
#[proven]
fn permute_and_flip(x: &[RBig], scale: &RBig, replacement: bool) -> Fallible<usize> {
let x_is_empty = || err!(FailedFunction, "x is empty");
if scale.is_zero() {
return (0..x.len()).max_by_key(|&i| &x[i]).ok_or_else(x_is_empty);
}
let x_max = x.iter().max().ok_or_else(x_is_empty)?;
let mut candidates: Vec<usize> = (0..x.len()).collect();
let sequence = match replacement {
false => Sequence::Range(0..x.len()),
true => Sequence::Zero,
};
for left in sequence {
let right = left + sample_uniform_uint_below(x.len() - left)?;
candidates.swap(left, right);
let candidate = candidates[left];
if sample_bernoulli_exp((x_max - &x[candidate]) / scale)? {
return Ok(candidate);
}
}
unreachable!("at least one x[candidate] is equal to x_max")
}
enum Sequence {
Range(Range<usize>),
Zero,
}
impl Iterator for Sequence {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
match self {
Sequence::Range(range) => range.next(),
Sequence::Zero => Some(0),
}
}
}