@@ -39,6 +39,7 @@ pub enum ObfuscateBelow10Mode {
3939/// * obfuscate_zero - A flag indicating whether zero counts should be obfuscated.
4040/// * below_10_obfuscation_mode: 0 - return 0, 1 - return 10, 2 - obfuscate using Laplace distribution and rounding
4141/// * rounding_step - The granularity of the rounding.
42+ /// * domain_limit - optional parameter limiting the distribution to [-domain_limit, domain_limit]
4243/// * rng - A secure random generator for seeded randomness.
4344///
4445/// # Returns
@@ -54,10 +55,11 @@ pub fn get_from_cache_or_privatize(
5455 obfuscate_zero : bool ,
5556 obfuscate_below_10_mode : ObfuscateBelow10Mode ,
5657 rounding_step : usize ,
58+ domain_limit : Option < f64 > ,
5759 rng : & mut rand:: rngs:: ThreadRng ,
5860) -> Result < u64 , LaplaceError > {
5961 let obfuscated: u64 = match obf_cache_option {
60- None => privatize ( value, delta, epsilon, rounding_step, rng) . unwrap ( ) ,
62+ None => privatize ( value, delta, epsilon, rounding_step, domain_limit , rng) ? ,
6163 Some ( obf_cache) => {
6264 if !obfuscate_zero && value == 0 {
6365 return Ok ( 0 ) ;
@@ -78,7 +80,7 @@ pub fn get_from_cache_or_privatize(
7880 Some ( obfuscated_reference) => * obfuscated_reference,
7981 None => {
8082 let obfuscated_value =
81- privatize ( value, delta, epsilon, rounding_step, rng) . unwrap ( ) ;
83+ privatize ( value, delta, epsilon, rounding_step, domain_limit , rng) ? ;
8284
8385 obf_cache
8486 . cache
@@ -101,6 +103,7 @@ pub fn get_from_cache_or_privatize(
101103/// * `sensitivity` - Sensitivity of query.
102104/// * `epsilon` - Privacy budget parameter.
103105/// * `rounding_step` - Rounding to the given number is performed.
106+ /// * `domain_limit` - optional parameter limiting the distribution to [-domain_limit, domain_limit]
104107/// * rng - A secure random generator for seeded randomness.
105108///
106109/// # Returns
@@ -111,9 +114,25 @@ pub fn privatize(
111114 sensitivity : f64 ,
112115 epsilon : f64 ,
113116 rounding_step : usize ,
117+ domain_limit : Option < f64 > ,
114118 rng : & mut rand:: rngs:: ThreadRng ,
115119) -> Result < u64 , LaplaceError > {
116- let obfuscated_value = value as f64 + laplace ( 0.0 , sensitivity / epsilon, rng) . unwrap ( ) ;
120+ let permutation = match domain_limit {
121+ Some ( f64:: INFINITY ) | None => laplace ( 0.0 , sensitivity / epsilon, rng) ?,
122+ Some ( boundary) if boundary <= 0. => Err ( LaplaceError :: InvalidDomain ) ?,
123+ // Resample, if clamped to a specific domain
124+ Some ( boundary) => {
125+ let mut sample: f64 ;
126+ loop {
127+ sample = laplace ( 0.0 , sensitivity / epsilon, rng) ?;
128+ if sample >= -boundary && sample <= boundary {
129+ break ;
130+ }
131+ }
132+ sample
133+ }
134+ } ;
135+ let obfuscated_value = value as f64 + permutation;
117136 round_parametric ( obfuscated_value, rounding_step)
118137}
119138
@@ -140,14 +159,13 @@ fn round_parametric(value: f64, step_parameter: usize) -> Result<u64, LaplaceErr
140159///
141160/// * `mu` - the mean of the distribution.
142161/// * `b` - the scale parameter of the distribution, often equal to `sensitivity`/`epsilon`.
143- /// /// * `rng` - random generator.
162+ /// * `rng` - random generator.
144163///
145164/// # Returns
146165///
147166/// Returns a random sample from the Laplace distribution with the given `mu` and `b`, or an error if the distribution creation failed.
148167fn laplace ( mu : f64 , b : f64 , rng : & mut rand:: rngs:: ThreadRng ) -> Result < f64 , LaplaceError > {
149- let dist =
150- Laplace :: new ( mu, b) . map_err ( |e| LaplaceError :: DistributionCreationError ( e) ) ?;
168+ let dist = Laplace :: new ( mu, b) . map_err ( LaplaceError :: DistributionCreationError ) ?;
151169 Ok ( dist. sample ( rng) )
152170}
153171
@@ -219,28 +237,74 @@ mod test {
219237 let sensitivity = 10.0 ;
220238 let epsilon = 0.5 ;
221239 let rounding_step = 10 ;
240+ let domain_limit = None ;
222241 let result = privatize (
223242 value,
224243 sensitivity,
225244 epsilon,
226245 rounding_step,
246+ domain_limit,
227247 & mut rng,
228248 ) ;
229249 assert ! ( result. is_ok( ) ) ;
230250 }
231251
252+ #[ test]
253+ fn test_privatize_within_domain ( ) {
254+ let mut rng = rand:: thread_rng ( ) ;
255+ let value = 27 ;
256+ let sensitivity = 10.0 ;
257+ let epsilon = 0.5 ;
258+ let rounding_step = 1 ;
259+ let domain_limit = 10 ;
260+ for _ in 0 ..10000 {
261+ let result = privatize (
262+ value,
263+ sensitivity,
264+ epsilon,
265+ rounding_step,
266+ Some ( domain_limit as f64 ) ,
267+ & mut rng,
268+ )
269+ . unwrap ( ) ;
270+ assert ! ( result <= ( value + domain_limit) && result >= ( value - domain_limit) ) ;
271+ }
272+ }
273+
232274 #[ test]
233275 fn test_obfuscate_value_zero ( ) {
234276 let mut rng = rand:: thread_rng ( ) ;
235- let result = get_from_cache_or_privatize ( 0 , 1.0 , 1.0 , 1 , None , true , ObfuscateBelow10Mode :: Obfuscate , 1 , & mut rng) ;
277+ let result = get_from_cache_or_privatize (
278+ 0 ,
279+ 1.0 ,
280+ 1.0 ,
281+ 1 ,
282+ None ,
283+ true ,
284+ ObfuscateBelow10Mode :: Obfuscate ,
285+ 1 ,
286+ None ,
287+ & mut rng,
288+ ) ;
236289
237290 assert ! ( result. is_ok( ) ) ;
238291 }
239292
240293 #[ test]
241294 fn test_obfuscate_value_non_zero ( ) {
242295 let mut rng = rand:: thread_rng ( ) ;
243- let result = get_from_cache_or_privatize ( 10 , 1.0 , 1.0 , 1 , None , true , ObfuscateBelow10Mode :: Obfuscate , 1 , & mut rng) ;
296+ let result = get_from_cache_or_privatize (
297+ 10 ,
298+ 1.0 ,
299+ 1.0 ,
300+ 1 ,
301+ None ,
302+ true ,
303+ ObfuscateBelow10Mode :: Obfuscate ,
304+ 1 ,
305+ None ,
306+ & mut rng,
307+ ) ;
244308
245309 assert ! ( result. is_ok( ) ) ;
246310 }
@@ -252,17 +316,37 @@ mod test {
252316 cache : HashMap :: new ( ) ,
253317 } ;
254318
255- let result =
256- get_from_cache_or_privatize ( 10 , 1.0 , 1.0 , 1 , Some ( & mut obf_cache) , true , ObfuscateBelow10Mode :: Obfuscate , 1 , & mut rng) ;
319+ let result = get_from_cache_or_privatize (
320+ 10 ,
321+ 1.0 ,
322+ 1.0 ,
323+ 1 ,
324+ Some ( & mut obf_cache) ,
325+ true ,
326+ ObfuscateBelow10Mode :: Obfuscate ,
327+ 1 ,
328+ None ,
329+ & mut rng,
330+ ) ;
257331 assert ! ( result. is_ok( ) ) ;
258332
259333 let obfuscated_value = obf_cache. cache . get ( & ( 1 , 10 , 1 ) ) ;
260334 assert ! ( obfuscated_value. is_some( ) ) ;
261335 let result_ok = result. unwrap ( ) ;
262336 assert_eq ! ( result_ok. clone( ) , * obfuscated_value. unwrap( ) ) ;
263337
264- let result2 =
265- get_from_cache_or_privatize ( 10 , 1.0 , 1.0 , 1 , Some ( & mut obf_cache) , true , ObfuscateBelow10Mode :: Obfuscate , 1 , & mut rng) ;
338+ let result2 = get_from_cache_or_privatize (
339+ 10 ,
340+ 1.0 ,
341+ 1.0 ,
342+ 1 ,
343+ Some ( & mut obf_cache) ,
344+ true ,
345+ ObfuscateBelow10Mode :: Obfuscate ,
346+ 1 ,
347+ None ,
348+ & mut rng,
349+ ) ;
266350 assert ! ( result2. is_ok( ) ) ;
267351 assert_eq ! ( result_ok, result2. unwrap( ) ) ;
268352 }
0 commit comments