1#![allow(deprecated)]
27
28#[cfg(not(any(feature = "std", test)))]
31use crate::math::FloatExt;
32use crate::math::{f64_i64, hash_index, i64_f64, u64_f64};
33use alice_det_math::ln64;
34#[cfg(feature = "std")]
36use alice_det_math::exp64;
37
38#[derive(Clone, Debug)]
46#[deprecated(
47 since = "0.4.0",
48 note = "not differentially private (predictable xorshift64 state, time-derived seed, floating-point inverse transform); use alice_crypto::dp instead. Removed in 0.5.0"
49)]
50pub struct XorShift64 {
51 state: u64,
52}
53
54impl XorShift64 {
55 #[inline]
57 #[must_use]
58 pub const fn new(seed: u64) -> Self {
59 Self {
61 state: if seed == 0 {
62 0x853c_49e6_748f_ea9b
63 } else {
64 seed
65 },
66 }
67 }
68
69 #[cfg(feature = "std")]
76 #[must_use]
77 pub fn from_entropy() -> Self {
78 use std::time::{SystemTime, UNIX_EPOCH};
79 let seed = SystemTime::now()
80 .duration_since(UNIX_EPOCH)
81 .map_or(0x853c_49e6_748f_ea9b, |d| {
83 #[allow(clippy::cast_possible_truncation)]
84 let low = d.as_nanos() as u64;
85 low
86 });
87 Self::new(seed)
88 }
89
90 #[inline]
92 pub const fn next_u64(&mut self) -> u64 {
93 let mut x = self.state;
94 x ^= x << 13;
95 x ^= x >> 7;
96 x ^= x << 17;
97 self.state = x;
98 x
99 }
100
101 #[inline(always)]
103 pub fn next_f64(&mut self) -> f64 {
104 const RCP_POW2_53: f64 = 1.0 / u64_f64(1u64 << 53);
106 u64_f64(self.next_u64() >> 11) * RCP_POW2_53
107 }
108
109 #[inline]
111 pub fn next_f64_range(&mut self, low: f64, high: f64) -> f64 {
112 (high - low).mul_add(self.next_f64(), low)
113 }
114
115 #[inline]
117 pub fn next_bool(&mut self, p: f64) -> bool {
118 self.next_f64() < p
119 }
120}
121
122#[cfg(feature = "std")]
123impl Default for XorShift64 {
124 fn default() -> Self {
125 Self::from_entropy()
126 }
127}
128
129#[derive(Clone, Debug)]
140#[deprecated(
141 since = "0.4.0",
142 note = "not differentially private (predictable xorshift64 state, time-derived seed, floating-point inverse transform); use alice_crypto::dp instead. Removed in 0.5.0"
143)]
144pub struct LaplaceNoise {
145 scale: f64,
147 rng: XorShift64,
149}
150
151impl LaplaceNoise {
152 #[cfg(feature = "std")]
160 #[must_use]
161 pub fn new(sensitivity: f64, epsilon: f64) -> Self {
162 let scale = sensitivity / epsilon;
163 Self {
164 scale,
165 rng: XorShift64::from_entropy(),
166 }
167 }
168
169 #[must_use]
171 pub fn with_seed(sensitivity: f64, epsilon: f64, seed: u64) -> Self {
172 let scale = sensitivity / epsilon;
173 Self {
174 scale,
175 rng: XorShift64::new(seed),
176 }
177 }
178
179 #[inline]
181 pub fn sample(&mut self) -> f64 {
182 let u = self.rng.next_f64() - 0.5;
184 let sign = if u < 0.0 { -1.0 } else { 1.0 };
185 -sign * self.scale * ln64(2.0f64.mul_add(-u.abs(), 1.0))
186 }
187
188 #[inline]
190 pub fn privatize(&mut self, value: f64) -> f64 {
191 value + self.sample()
192 }
193
194 #[inline]
196 pub fn privatize_int(&mut self, value: i64) -> i64 {
197 f64_i64((i64_f64(value) + self.sample()).round())
198 }
199
200 #[inline]
202 #[must_use]
203 pub const fn scale(&self) -> f64 {
204 self.scale
205 }
206}
207
208#[derive(Clone, Debug)]
220#[deprecated(
221 since = "0.4.0",
222 note = "not differentially private (predictable xorshift64 state, time-derived seed, floating-point inverse transform); use alice_crypto::dp instead. Removed in 0.5.0"
223)]
224pub struct RandomizedResponse {
225 p_true: f64,
227 rng: XorShift64,
229}
230
231impl RandomizedResponse {
232 #[cfg(feature = "std")]
257 #[must_use]
258 pub fn new(epsilon: f64) -> Self {
259 assert!(
260 epsilon.is_finite() && epsilon >= 0.0,
261 "epsilon must be finite and non-negative"
262 );
263 let p_true = 1.0 / (1.0 + exp64(-epsilon));
266 Self {
267 p_true,
268 rng: XorShift64::from_entropy(),
269 }
270 }
271
272 #[must_use]
291 #[allow(clippy::manual_range_contains)]
295 pub const fn with_probability(p_true: f64, seed: u64) -> Self {
296 assert!(
297 p_true >= 0.5 && p_true <= 1.0,
298 "p_true must be in [0.5, 1.0]"
299 );
300 Self {
301 p_true,
302 rng: XorShift64::new(seed),
303 }
304 }
305
306 #[inline]
308 pub fn privatize(&mut self, value: bool) -> bool {
309 if self.rng.next_bool(self.p_true) {
310 value
312 } else {
313 self.rng.next_bool(0.5)
315 }
316 }
317
318 #[inline]
320 pub fn privatize_bit(&mut self, bit: u8) -> u8 {
321 u8::from(self.privatize(bit != 0))
322 }
323
324 #[inline]
326 #[must_use]
327 pub const fn p_true(&self) -> f64 {
328 self.p_true
329 }
330
331 #[must_use]
336 pub fn estimate_proportion(p_true: f64, n: u64, k: u64) -> f64 {
337 if n == 0 {
338 return 0.0;
339 }
340 let inv_n = 1.0 / u64_f64(n);
341 let observed_rate = u64_f64(k) * inv_n;
342 let inv_p_true = 1.0 / p_true;
351 let true_rate = 0.5f64.mul_add(p_true, observed_rate - 0.5) * inv_p_true;
352 true_rate.clamp(0.0, 1.0)
353 }
354}
355
356#[deprecated(
362 since = "0.4.0",
363 note = "not differentially private (predictable xorshift64 state, time-derived seed, floating-point inverse transform); use alice_crypto::dp instead. Removed in 0.5.0"
364)]
365pub const RAPPOR_BITS: usize = 64;
366
367#[derive(Clone, Debug)]
372#[deprecated(
373 since = "0.4.0",
374 note = "not differentially private (predictable xorshift64 state, time-derived seed, floating-point inverse transform); use alice_crypto::dp instead. Removed in 0.5.0"
375)]
376pub struct Rappor {
377 f: f64,
379 p: f64,
381 q: f64,
382 rng: XorShift64,
384}
385
386impl Rappor {
387 pub const BITS: usize = RAPPOR_BITS;
389
390 #[cfg(feature = "std")]
399 #[must_use]
400 pub fn new(f: f64, p: f64, q: f64) -> Self {
401 Self::with_seed_params(f, p, q, XorShift64::from_entropy().next_u64())
402 }
403
404 #[must_use]
406 pub fn with_seed_params(f: f64, p: f64, q: f64, seed: u64) -> Self {
407 Self {
408 f: f.clamp(0.0, 0.5),
409 p: p.clamp(0.0, 1.0),
410 q: q.clamp(0.0, 1.0),
411 rng: XorShift64::new(seed),
412 }
413 }
414
415 #[cfg(feature = "std")]
419 #[must_use]
420 pub fn default_params() -> Self {
421 Self::new(0.5, 0.75, 0.25)
422 }
423
424 #[allow(clippy::unused_self)]
426 fn encode_bloom(&self, value: u64) -> [u8; RAPPOR_BITS] {
427 use crate::sketch::FnvHasher;
428
429 let mut bloom = [0u8; RAPPOR_BITS];
430 for i in 0..3u128 {
432 let h = FnvHasher::hash_u128(u128::from(value) | (i << 64));
433 let idx = hash_index(h) % RAPPOR_BITS;
434 bloom[idx] = 1;
435 }
436 bloom
437 }
438
439 fn permanent_response(&mut self, bloom: &[u8; RAPPOR_BITS]) -> [u8; RAPPOR_BITS] {
441 let mut result = [0u8; RAPPOR_BITS];
442 for i in 0..RAPPOR_BITS {
443 if self.rng.next_bool(self.f) {
444 result[i] = u8::from(self.rng.next_bool(0.5));
446 } else {
447 result[i] = bloom[i];
449 }
450 }
451 result
452 }
453
454 fn instantaneous_response(&mut self, permanent: &[u8; RAPPOR_BITS]) -> [u8; RAPPOR_BITS] {
456 let mut result = [0u8; RAPPOR_BITS];
457 for i in 0..RAPPOR_BITS {
458 if permanent[i] == 1 {
459 result[i] = u8::from(self.rng.next_bool(self.p));
460 } else {
461 result[i] = u8::from(self.rng.next_bool(self.q));
462 }
463 }
464 result
465 }
466
467 pub fn privatize(&mut self, value: u64) -> [u8; RAPPOR_BITS] {
471 let bloom = self.encode_bloom(value);
472 let permanent = self.permanent_response(&bloom);
473 self.instantaneous_response(&permanent)
474 }
475
476 #[must_use]
478 pub const fn params(&self) -> (f64, f64, f64) {
479 (self.f, self.p, self.q)
480 }
481}
482
483#[derive(Clone, Debug)]
492#[deprecated(
493 since = "0.4.0",
494 note = "not differentially private (predictable xorshift64 state, time-derived seed, floating-point inverse transform); use alice_crypto::dp instead. Removed in 0.5.0"
495)]
496pub struct PrivacyBudget {
497 total_epsilon: f64,
499 max_epsilon: f64,
501 query_count: u64,
503}
504
505impl PrivacyBudget {
506 #[must_use]
508 pub const fn new(max_epsilon: f64) -> Self {
509 Self {
510 total_epsilon: 0.0,
511 max_epsilon,
512 query_count: 0,
513 }
514 }
515
516 pub fn try_spend(&mut self, epsilon: f64) -> bool {
536 if !epsilon.is_finite() || epsilon < 0.0 {
540 return false;
541 }
542 if self.total_epsilon + epsilon <= self.max_epsilon {
543 self.total_epsilon += epsilon;
544 self.query_count += 1;
545 true
546 } else {
547 false
548 }
549 }
550
551 #[inline]
553 #[must_use]
554 pub fn remaining(&self) -> f64 {
555 (self.max_epsilon - self.total_epsilon).max(0.0)
556 }
557
558 #[inline]
560 #[must_use]
561 pub const fn spent(&self) -> f64 {
562 self.total_epsilon
563 }
564
565 #[inline]
567 #[must_use]
568 pub const fn query_count(&self) -> u64 {
569 self.query_count
570 }
571
572 #[inline]
574 #[must_use]
575 pub fn is_exhausted(&self) -> bool {
576 self.total_epsilon >= self.max_epsilon
577 }
578
579 pub const fn reset(&mut self) {
581 self.total_epsilon = 0.0;
582 self.query_count = 0;
583 }
584}
585
586#[derive(Clone, Debug)]
592#[deprecated(
593 since = "0.4.0",
594 note = "not differentially private (predictable xorshift64 state, time-derived seed, floating-point inverse transform); use alice_crypto::dp instead. Removed in 0.5.0"
595)]
596pub struct PrivateAggregator {
597 noisy_sum: f64,
599 count: u64,
601 noise_scale: f64,
603}
604
605impl PrivateAggregator {
606 #[must_use]
608 pub const fn new(noise_scale: f64) -> Self {
609 Self {
610 noisy_sum: 0.0,
611 count: 0,
612 noise_scale,
613 }
614 }
615
616 #[inline]
618 pub fn add(&mut self, noisy_value: f64) {
619 self.noisy_sum += noisy_value;
620 self.count += 1;
621 }
622
623 #[must_use]
627 pub fn estimate_mean(&self) -> f64 {
628 if self.count == 0 {
629 0.0
630 } else {
631 let inv_count = 1.0 / u64_f64(self.count);
632 self.noisy_sum * inv_count
633 }
634 }
635
636 #[must_use]
638 pub const fn estimate_sum(&self) -> f64 {
639 self.noisy_sum
640 }
641
642 #[must_use]
646 pub fn standard_error(&self) -> f64 {
647 if self.count == 0 {
648 f64::INFINITY
649 } else {
650 let inv_sqrt_n = 1.0 / (u64_f64(self.count)).sqrt();
651 self.noise_scale * core::f64::consts::SQRT_2 * inv_sqrt_n
652 }
653 }
654
655 #[inline]
657 #[must_use]
658 pub const fn count(&self) -> u64 {
659 self.count
660 }
661
662 pub const fn reset(&mut self) {
664 self.noisy_sum = 0.0;
665 self.count = 0;
666 }
667}
668
669#[cfg(test)]
674#[allow(clippy::float_cmp)]
675mod tests {
676 use super::*;
677
678 #[test]
679 fn test_xorshift() {
680 let mut rng = XorShift64::new(12345);
681 let v1 = rng.next_u64();
682 let v2 = rng.next_u64();
683 assert_ne!(v1, v2);
684
685 let mut rng2 = XorShift64::new(12345);
687 assert_eq!(v1, rng2.next_u64());
688 }
689
690 #[test]
691 fn test_laplace_noise() {
692 let mut noise = LaplaceNoise::with_seed(1.0, 1.0, 42);
693
694 let mut sum = 0.0;
696 let n = 10000;
697 for _ in 0..n {
698 sum += noise.sample();
699 }
700 let mean = sum / f64::from(n);
701 assert!(mean.abs() < 0.1, "mean = {mean}");
702 }
703
704 #[test]
705 fn test_randomized_response() {
706 let mut rr = RandomizedResponse::with_probability(0.75, 42);
707
708 let mut correct = 0;
710 let n = 1000;
711 for i in 0..n {
712 let truth = i % 2 == 0;
713 let response = rr.privatize(truth);
714 if response == truth {
715 correct += 1;
716 }
717 }
718
719 assert!(correct > n / 2, "correct = {correct}");
721 }
722
723 #[test]
724 fn test_rr_proportion_estimation() {
725 let p_true = 0.8;
727 let true_rate = 0.3;
728 let n = 10000u64;
729
730 let mut rr = RandomizedResponse::with_probability(p_true, 42);
731
732 let mut positive_reports = 0u64;
733 for i in 0..n {
734 let truth = (u64_f64(i) / u64_f64(n)) < true_rate;
736 if rr.privatize(truth) {
737 positive_reports += 1;
738 }
739 }
740
741 let estimated = RandomizedResponse::estimate_proportion(p_true, n, positive_reports);
742 assert!(
744 (estimated - true_rate).abs() < 0.1,
745 "estimated = {estimated}, true = {true_rate}"
746 );
747 }
748
749 #[test]
750 fn test_privacy_budget() {
751 let mut budget = PrivacyBudget::new(1.0);
752
753 assert!(budget.try_spend(0.3));
754 assert!(budget.try_spend(0.3));
755 assert!(budget.try_spend(0.3));
756 assert!(!budget.try_spend(0.3)); assert_eq!(budget.query_count(), 3);
759 assert!((budget.spent() - 0.9).abs() < 0.001);
760 }
761
762 #[test]
763 fn test_private_aggregator() {
764 let noise_scale = 1.0;
765 let mut aggregator = PrivateAggregator::new(noise_scale);
766 let mut noise = LaplaceNoise::with_seed(1.0, 1.0, 42);
767
768 let true_value = 100.0;
770 let n = 10000;
771
772 for _ in 0..n {
773 let privatized = noise.privatize(true_value);
774 aggregator.add(privatized);
775 }
776
777 let estimated_mean = aggregator.estimate_mean();
778 assert!(
780 (estimated_mean - true_value).abs() < 1.0,
781 "estimated = {estimated_mean}"
782 );
783 }
784
785 #[cfg(feature = "std")]
788 #[test]
789 fn test_rappor() {
790 let mut rappor = Rappor::default_params();
791
792 let encoded1 = rappor.privatize(12345);
793 let encoded2 = rappor.privatize(12345);
794
795 assert_eq!(encoded1.len(), 64);
798 assert_eq!(encoded2.len(), 64);
799 }
800
801 #[test]
802 fn test_xorshift_deterministic() {
803 let mut rng1 = XorShift64::new(42);
804 let mut rng2 = XorShift64::new(42);
805 assert_eq!(rng1.next_u64(), rng2.next_u64());
806 assert_eq!(rng1.next_u64(), rng2.next_u64());
807 }
808
809 #[test]
810 fn test_xorshift_zero_seed() {
811 let mut rng = XorShift64::new(0);
812 assert_ne!(rng.next_u64(), 0);
814 }
815
816 #[test]
817 fn test_xorshift_f64_range() {
818 let mut rng = XorShift64::new(42);
819 for _ in 0..100 {
820 let v = rng.next_f64();
821 assert!((0.0..1.0).contains(&v), "v = {v}");
822 }
823 }
824
825 #[test]
826 fn test_xorshift_f64_range_custom() {
827 let mut rng = XorShift64::new(42);
828 for _ in 0..100 {
829 let v = rng.next_f64_range(10.0, 20.0);
830 assert!((10.0..20.0).contains(&v), "v = {v}");
831 }
832 }
833
834 #[test]
835 fn test_xorshift_bool() {
836 let mut rng = XorShift64::new(42);
837 let mut trues = 0;
838 let n = 10000;
839 for _ in 0..n {
840 if rng.next_bool(0.5) {
841 trues += 1;
842 }
843 }
844 assert!(trues > 4000 && trues < 6000, "trues = {trues}");
846 }
847
848 #[cfg(feature = "std")]
851 #[test]
852 fn test_xorshift_default() {
853 let mut rng = XorShift64::default();
854 let v = rng.next_u64();
855 assert_ne!(v, 0);
856 }
857
858 #[test]
859 fn test_laplace_scale() {
860 let noise = LaplaceNoise::with_seed(2.0, 0.5, 42);
861 assert!((noise.scale() - 4.0).abs() < f64::EPSILON);
863 }
864
865 #[test]
866 fn test_laplace_privatize_int() {
867 let mut noise = LaplaceNoise::with_seed(1.0, 1.0, 42);
868 let result = noise.privatize_int(100);
869 assert!((result - 100).abs() < 20, "result = {result}");
871 }
872
873 #[test]
874 fn test_rr_privatize_bit() {
875 let mut rr = RandomizedResponse::with_probability(0.9, 42);
876 let bit = rr.privatize_bit(1);
877 assert!(bit == 0 || bit == 1);
878 }
879
880 #[test]
881 fn test_rr_p_true() {
882 let rr = RandomizedResponse::with_probability(0.8, 42);
883 assert!((rr.p_true() - 0.8).abs() < f64::EPSILON);
884 }
885
886 #[test]
887 fn test_rr_estimate_zero_n() {
888 let result = RandomizedResponse::estimate_proportion(0.8, 0, 0);
889 assert_eq!(result, 0.0);
890 }
891
892 #[test]
893 fn test_privacy_budget_exhausted() {
894 let mut budget = PrivacyBudget::new(1.0);
895 budget.try_spend(0.5);
896 budget.try_spend(0.5);
897 assert!(budget.is_exhausted());
898 }
899
900 #[test]
901 fn test_privacy_budget_remaining() {
902 let mut budget = PrivacyBudget::new(1.0);
903 budget.try_spend(0.3);
904 assert!((budget.remaining() - 0.7).abs() < 0.001);
905 }
906
907 #[test]
908 fn test_privacy_budget_reset() {
909 let mut budget = PrivacyBudget::new(1.0);
910 budget.try_spend(0.5);
911 budget.reset();
912 assert_eq!(budget.query_count(), 0);
913 assert!((budget.remaining() - 1.0).abs() < f64::EPSILON);
914 }
915
916 #[test]
917 fn test_private_aggregator_empty() {
918 let agg = PrivateAggregator::new(1.0);
919 assert_eq!(agg.estimate_mean(), 0.0);
920 assert_eq!(agg.count(), 0);
921 assert!(agg.standard_error().is_infinite());
922 }
923
924 #[test]
925 fn test_private_aggregator_reset() {
926 let mut agg = PrivateAggregator::new(1.0);
927 agg.add(10.0);
928 agg.add(20.0);
929 agg.reset();
930 assert_eq!(agg.count(), 0);
931 assert_eq!(agg.estimate_mean(), 0.0);
932 }
933
934 #[test]
935 fn test_private_aggregator_sum() {
936 let mut agg = PrivateAggregator::new(1.0);
937 agg.add(10.0);
938 agg.add(20.0);
939 assert!((agg.estimate_sum() - 30.0).abs() < f64::EPSILON);
940 }
941
942 #[cfg(feature = "std")]
945 #[test]
946 fn test_rappor_params() {
947 let rappor = Rappor::new(0.3, 0.7, 0.2);
948 let (f, p, q) = rappor.params();
949 assert!((f - 0.3).abs() < f64::EPSILON);
950 assert!((p - 0.7).abs() < f64::EPSILON);
951 assert!((q - 0.2).abs() < f64::EPSILON);
952 }
953}