dashu_float/third_party/rand.rs
1//! Random floating-point number generation with the `rand` crate.
2//!
3//! There are two distributions for generating random floats. [Uniform01] generates floats
4//! between 0 and 1 (it also backs rand's `Standard` / `Open01` / `OpenClosed01`). [UniformFBig]
5//! generates floats in a given range and is the backend for rand's `SampleUniform` trait.
6//!
7//! The distributions and their sampling algorithms are defined here once, generic over the
8//! [`BitRng`](dashu_int::rand::BitRng) trait. Each rand version's `Distribution` /
9//! `UniformSampler` / `SampleUniform` impls live in the `rand_v08` / `rand_v09` / `rand_v010`
10//! modules (enable the matching feature); adapt that version's RNG with
11//! `dashu_int::rand::bridge_v08` / `bridge_v09` / `bridge_v010`. See those modules for usage
12//! examples.
13//!
14//! # Precision and rounding
15//!
16//! The precision of a float generated by different distributions is explained below:
17//! * [Uniform01] generates floats with the precision decided by the constructor.
18//! * The builtin rand distributions (`Standard` / `StandardUniform`, `Open01`, `OpenClosed01`)
19//! generate floats with the max precision such that the significand fits in a [DoubleWord].
20//! * [UniformFBig] (and therefore rand's `Uniform`) generates floats with the precision being the
21//! maximum between the interval boundaries.
22//!
23//! The rounding of the [FBig] type doesn't affect the number generation process.
24
25use core::marker::PhantomData;
26
27use crate::{
28 fbig::FBig,
29 repr::{Context, Repr, Word},
30 round::{mode, Round},
31};
32use dashu_base::EstimatedLog2;
33use dashu_int::{
34 rand::{BitRng, UniformBelow, UniformBits},
35 DoubleWord, UBig,
36};
37
38/// The back-end implementing `rand::distributions::uniform::UniformSampler` for [FBig]
39/// (and [DBig][crate::DBig]).
40pub struct UniformFBig<R: Round, const B: Word> {
41 pub(crate) sampler: Uniform01<B>,
42 pub(crate) scale: Repr<B>,
43 pub(crate) offset: Repr<B>,
44 /// Used to distinguish between uniform distributions with different rounding modes;
45 /// no actual effect on the sampling.
46 pub(crate) _marker: PhantomData<R>,
47}
48
49impl<R: Round, const B: Word> UniformFBig<R, B> {
50 /// Create a sampler over `[low, high)` at a given precision.
51 #[inline]
52 pub fn new(low: &FBig<R, B>, high: &FBig<R, B>, precision: usize) -> Self {
53 assert!(low <= high);
54 Self {
55 sampler: Uniform01::new(precision),
56 scale: (high - low).into_repr(),
57 offset: low.repr().clone(),
58 _marker: PhantomData,
59 }
60 }
61
62 /// Create a sampler over `[low, high]` at a given precision.
63 #[inline]
64 pub fn new_inclusive(low: &FBig<R, B>, high: &FBig<R, B>, precision: usize) -> Self {
65 assert!(low <= high);
66 Self {
67 sampler: Uniform01::new_closed(precision),
68 scale: (high - low).into_repr(),
69 offset: low.repr().clone(),
70 _marker: PhantomData,
71 }
72 }
73
74 /// Draw a random [FBig] from this sampler's range.
75 pub fn sample_fbig<BR: BitRng + ?Sized>(&self, rng: &mut BR) -> FBig<R, B> {
76 // After we have a sample in [0, 1), all the following operations are rounded down
77 // so that we can ensure we don't reach the right bound.
78 let unit: FBig<mode::Down, B> = self.sampler.sample01::<mode::Down, _>(rng);
79 let context = unit.context();
80 let scaled = context.mul(unit.repr(), &self.scale).value();
81 context
82 .add(scaled.repr(), &self.offset)
83 .value()
84 .with_rounding()
85 }
86}
87
88/// A uniform distribution between 0 and 1. It can be used to replace the `Standard`,
89/// `Open01`, `OpenClosed01` distributions from the `rand` crate when you want to customize the
90/// precision of the generated float number.
91pub struct Uniform01<const BASE: Word> {
92 pub(crate) precision: usize,
93 pub(crate) range: Option<UBig>, // BASE ^ precision (±1 if necessary)
94 pub(crate) include_zero: bool, // whether include the zero
95 pub(crate) include_one: bool, // whether include the one
96}
97
98impl<const B: Word> Uniform01<B> {
99 /// Create a uniform distribution in `[0, 1)` with a given precision.
100 #[inline]
101 pub fn new(precision: usize) -> Self {
102 let range = match B {
103 2 => None,
104 _ => Some(UBig::from_word(B).pow(precision)),
105 };
106 Self {
107 precision,
108 range,
109 include_zero: true,
110 include_one: false,
111 }
112 }
113
114 /// Create a uniform distribution in `[0, 1]` with a given precision.
115 #[inline]
116 pub fn new_closed(precision: usize) -> Self {
117 let range = Some(UBig::from_word(B).pow(precision) + UBig::ONE);
118 Self {
119 precision,
120 range,
121 include_zero: true,
122 include_one: true,
123 }
124 }
125
126 /// Create a uniform distribution in `(0, 1)` with a given precision.
127 #[inline]
128 pub fn new_open(precision: usize) -> Self {
129 let range = match B {
130 2 => None,
131 _ => Some(UBig::from_word(B).pow(precision) - UBig::ONE),
132 };
133 Self {
134 precision,
135 range,
136 include_zero: false,
137 include_one: false,
138 }
139 }
140
141 /// Create a uniform distribution in `(0, 1]` with a given precision.
142 #[inline]
143 pub fn new_open_closed(precision: usize) -> Self {
144 let range = match B {
145 2 => None,
146 _ => Some(UBig::from_word(B).pow(precision)),
147 };
148 Self {
149 precision,
150 range,
151 include_zero: false,
152 include_one: true,
153 }
154 }
155
156 /// Draw a random [FBig] in this distribution. Generic over the rounding mode `R` and the
157 /// [`BitRng`] driving the generation.
158 pub fn sample01<R: Round, BR: BitRng + ?Sized>(&self, rng: &mut BR) -> FBig<R, B> {
159 let repr = match (self.include_zero, self.include_one) {
160 (true, false) => {
161 // sample in [0, 1)
162 let signif: UBig = if B == 2 {
163 UniformBits::new(self.precision).sample_ubig(rng)
164 } else {
165 UniformBelow::new(self.range.as_ref().unwrap()).sample_ubig(rng)
166 };
167 Repr::<B>::new(signif.into(), -(self.precision as isize))
168 }
169 (true, true) => {
170 // sample in [0, 1]
171 let signif: UBig = UniformBelow::new(self.range.as_ref().unwrap()).sample_ubig(rng);
172 Repr::new(signif.into(), -(self.precision as isize))
173 }
174 (false, false) => {
175 // sample in (0, 1)
176 let signif = if B == 2 {
177 loop {
178 // simply reject zero
179 let n: UBig = UniformBits::new(self.precision).sample_ubig(rng);
180 if !n.is_zero() {
181 break n;
182 }
183 }
184 } else {
185 let n: UBig = UniformBelow::new(self.range.as_ref().unwrap()).sample_ubig(rng);
186 n + UBig::ONE
187 };
188 Repr::<B>::new(signif.into(), -(self.precision as isize))
189 }
190 (false, true) => {
191 // sample in (0, 1]
192 let signif: UBig = if B == 2 {
193 UniformBits::new(self.precision).sample_ubig(rng)
194 } else {
195 UniformBelow::new(self.range.as_ref().unwrap()).sample_ubig(rng)
196 };
197 Repr::<B>::new((signif + UBig::ONE).into(), -(self.precision as isize))
198 }
199 };
200
201 let context = Context::<mode::Down>::new(self.precision);
202 FBig::new(repr, context).with_rounding()
203 }
204}
205
206// When sampling with the builtin distributions, the precision is chosen such that the
207// significand fits in a double word and no allocation is required.
208#[inline]
209pub(crate) fn get_inline_precision<const B: Word>() -> usize {
210 (DoubleWord::BITS as f32 / B.log2_bounds().1) as _
211}