1use core::fmt;
4use rand::Rng;
5
6const SCALE: u128 = 1u128 << u64::BITS;
8
9const SCALED_EXPONENT: u64 = 1023 + 52 - 64;
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
14#[error(
15 "probability must be finite, within [0, 1], and exactly representable as a 64-bit threshold"
16)]
17pub struct InvalidProbability;
18
19#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
25#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
26pub struct Probability(u64);
27
28impl Probability {
29 pub const fn new(numerator: u64, denominator: u64) -> Option<Self> {
33 if denominator == 0 || numerator > denominator {
34 return None;
35 }
36
37 if numerator == denominator {
38 return Some(Self(u64::MAX));
39 }
40
41 let threshold = ((numerator as u128) << u64::BITS) / denominator as u128;
42
43 assert!(threshold < u64::MAX as u128);
46 Some(Self(threshold as u64))
47 }
48
49 pub const fn from_f64(value: f64) -> Option<Self> {
55 let bits = value.to_bits();
56 let magnitude = bits & (u64::MAX >> 1);
57
58 if magnitude == 0 {
59 return Some(Self(0));
60 }
61 if bits != magnitude || magnitude > 1.0f64.to_bits() {
62 return None;
63 }
64
65 let exponent = magnitude >> 52;
66 if exponent == 0 {
67 return None;
68 }
69 let significand = (1u64 << 52) | (magnitude & ((1u64 << 52) - 1));
70 if exponent < SCALED_EXPONENT {
71 let shift = (SCALED_EXPONENT - exponent) as u32;
72 if significand.trailing_zeros() < shift {
73 return None;
74 }
75 return Some(Self(significand >> shift));
76 }
77
78 let threshold = (significand as u128) << (exponent - SCALED_EXPONENT);
79 if threshold == SCALE {
80 return Some(Self(u64::MAX));
81 }
82
83 assert!(threshold < u64::MAX as u128);
85 Some(Self(threshold as u64))
86 }
87
88 pub const fn is_zero(self) -> bool {
90 self.0 == 0
91 }
92
93 pub const fn is_one(self) -> bool {
95 self.0 == u64::MAX
96 }
97
98 pub fn as_f64(self) -> f64 {
103 if self.is_one() {
104 return 1.0;
105 }
106
107 let value = self.0 as f64 / SCALE as f64;
108 if value == 1.0 {
109 f64::from_bits(1.0f64.to_bits() - 1)
110 } else {
111 value
112 }
113 }
114
115 pub fn sample<R: Rng + ?Sized>(self, rng: &mut R) -> bool {
117 match self.0 {
118 0 => false,
119 u64::MAX => true,
120 threshold => rng.next_u64() < threshold,
121 }
122 }
123}
124
125impl TryFrom<f64> for Probability {
126 type Error = InvalidProbability;
127
128 fn try_from(value: f64) -> Result<Self, Self::Error> {
129 Self::from_f64(value).ok_or(InvalidProbability)
130 }
131}
132
133impl fmt::Debug for Probability {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 fmt::Debug::fmt(&self.as_f64(), f)
136 }
137}
138
139#[cfg(not(any(
174 commonware_stability_GAMMA,
175 commonware_stability_DELTA,
176 commonware_stability_EPSILON,
177 commonware_stability_RESERVED
178)))] #[macro_export]
180macro_rules! probability {
181 ($value:literal) => {
182 const {
183 $crate::Probability::from_f64($value).expect(
184 "probability requires a value in [0, 1] exactly representable as a 64-bit threshold",
185 )
186 }
187 };
188 ($numerator:literal, $denominator:literal) => {
189 const {
190 $crate::Probability::new($numerator, $denominator)
191 .expect("probability requires a non-zero denominator and numerator <= denominator")
192 }
193 };
194 ($numerator:expr, $denominator:expr) => {
195 $crate::Probability::new($numerator, $denominator)
196 .expect("probability requires a non-zero denominator and numerator <= denominator")
197 };
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use core::convert::Infallible;
204 use rand::TryRng;
205
206 struct CountingRng {
207 value: u64,
208 calls: usize,
209 }
210
211 impl TryRng for CountingRng {
212 type Error = Infallible;
213
214 fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
215 self.calls += 1;
216 Ok(self.value as u32)
217 }
218
219 fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
220 self.calls += 1;
221 Ok(self.value)
222 }
223
224 fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
225 self.calls += 1;
226 dst.fill(0);
227 Ok(())
228 }
229 }
230
231 #[test]
232 fn construction() {
233 assert_eq!(Probability::new(0, u64::MAX), Some(probability!(0.0)));
234 assert_eq!(
235 Probability::new(u64::MAX, u64::MAX),
236 Some(probability!(1.0))
237 );
238 assert_eq!(probability!(1, 2), probability!(2, 4));
239 assert_eq!(probability!(1, 2).as_f64(), 0.5);
240 assert!(Probability::new(1, 0).is_none());
241 assert!(Probability::new(2, 1).is_none());
242 }
243
244 #[test]
245 fn f64_construction_preserves_clean_binary_value() {
246 const FROM_LITERAL: Probability = probability!(0.98);
247 const MINIMUM_INTERIOR: f64 = f64::from_bits(959u64 << 52);
248
249 assert_eq!(FROM_LITERAL.0, 18_077_809_192_235_360_256);
250 assert_eq!(FROM_LITERAL.as_f64(), 0.98);
251 assert_ne!(FROM_LITERAL, probability!(49, 50));
252 assert_eq!(probability!(0.5), probability!(1, 2));
253 assert_eq!(Probability::try_from(0.5), Ok(probability!(0.5)));
254 assert_eq!(Probability::from_f64(0.0), Some(probability!(0.0)));
255 assert_eq!(Probability::from_f64(-0.0), Some(probability!(0.0)));
256 assert_eq!(Probability::from_f64(1.0), Some(probability!(1.0)));
257 assert_eq!(
258 Probability::from_f64(MINIMUM_INTERIOR),
259 Some(Probability(1))
260 );
261
262 let below_one = f64::from_bits(1.0f64.to_bits() - 1);
263 assert_eq!(
264 Probability::from_f64(below_one).unwrap().as_f64(),
265 below_one
266 );
267 }
268
269 #[test]
270 fn f64_construction_rejects_lossy_or_invalid_values() {
271 const BELOW_MINIMUM: f64 = f64::from_bits(958u64 << 52);
272 const MINIMUM_INTERIOR_BITS: u64 = 959u64 << 52;
273
274 for value in [
275 BELOW_MINIMUM,
276 f64::from_bits(MINIMUM_INTERIOR_BITS + 1),
277 f64::from_bits(1),
278 -0.1,
279 1.1,
280 f64::from_bits(1.0f64.to_bits() + 1),
281 f64::NAN,
282 f64::from_bits(f64::NAN.to_bits() | (1u64 << 63)),
283 f64::INFINITY,
284 f64::NEG_INFINITY,
285 ] {
286 assert!(Probability::from_f64(value).is_none());
287 assert_eq!(Probability::try_from(value), Err(InvalidProbability));
288 }
289 }
290
291 #[test]
292 fn representation_matches_a_raw_rate() {
293 assert_eq!(core::mem::size_of::<Probability>(), size_of::<u64>());
294 }
295
296 #[test]
297 fn ratios_use_platform_independent_thresholds() {
298 assert_eq!(probability!(1, 3).0 as u128, SCALE / 3);
299 assert_eq!(probability!(2, 3).0 as u128, (2 * SCALE) / 3);
300
301 let below_one = Probability::new(u64::MAX - 1, u64::MAX).unwrap();
302 assert_eq!(below_one.0, u64::MAX - 1);
303 assert!(below_one.as_f64() < 1.0);
304 }
305
306 #[test]
307 fn sampling_uses_threshold_and_skips_endpoints() {
308 let mut rng = CountingRng { value: 0, calls: 0 };
309 assert!(!probability!(0.0).sample(&mut rng));
310 assert!(probability!(1.0).sample(&mut rng));
311 assert_eq!(rng.calls, 0);
312
313 rng.value = (1u64 << 63) - 1;
314 assert!(probability!(1, 2).sample(&mut rng));
315 assert_eq!(rng.calls, 1);
316
317 rng.value = 1u64 << 63;
318 assert!(!probability!(1, 2).sample(&mut rng));
319 assert_eq!(rng.calls, 2);
320 }
321
322 #[test]
323 #[should_panic(
324 expected = "probability requires a non-zero denominator and numerator <= denominator"
325 )]
326 fn expression_macro_rejects_invalid_probability() {
327 let numerator = 2;
328 let denominator = 1;
329 let _ = probability!(numerator, denominator);
330 }
331}