dashu_float/third_party/
rand.rs

1//! Random floating point number generation with the `rand` crate.
2//!
3//! There are two new distributions for generating random floats. The first one is [Uniform01],
4//! which supports generating floats between 0 and 1. This is also the underlying implementation
5//! of the builtin `rand` distributions [Standard], [Open01], [OpenClosed01]. The other one is
6//! [UniformFBig], which supports generating floats in a certain range. This is also the
7//! backend for the [SampleUniform] trait.
8//!
9//! # Examples
10//!
11//! ```
12//! use dashu_float::rand::Uniform01;
13//! # use rand_v08::{distributions::uniform::Uniform, thread_rng, Rng};
14//!
15//! type FBig = dashu_float::FBig;
16//!
17//! // generate FBigs in a [0, 1) or [0, 1] with a given precision
18//! let a: FBig = thread_rng().sample(Uniform01::new(10));
19//! let b: FBig = thread_rng().sample(Uniform01::new_closed(10));
20//! let c: FBig = thread_rng().gen(); // the default distribution generates in [0, 1)
21//! assert!(a >= FBig::ZERO && a < FBig::ONE);
22//! assert!(b >= FBig::ZERO && b <= FBig::ONE);
23//! assert!(c >= FBig::ZERO && c < FBig::ONE);
24//!
25//! // generate FBigs in a range
26//! let a = thread_rng().gen_range(FBig::from(3)..FBig::from(10));
27//! let b = thread_rng().sample(Uniform::new(FBig::from(-5), &a));
28//! assert!(a >= FBig::from(3) && a < FBig::from(10));
29//! assert!(b >= FBig::from(-5) && b < a);
30//! ```
31//!
32//! # Precision and Rounding
33//!
34//! The precision of a float generated by different distributions is explained below:
35//! * [Uniform01] will generate floats with the precision decided by the constructor.
36//! * [Standard], [Open01], [OpenClosed01] will generate floats with the max precision
37//!   such that the significand fits in a [DoubleWord].
38//! * [UniformFBig] (and therefore [Uniform][rand_v08::distributions::Uniform]) will generate floats
39//!   the precision being the maximum between the interval boundaries.
40//!
41//! The rounding of the [FBig] type doesn't affect the number generation process.
42
43use core::marker::PhantomData;
44
45use crate::{
46    fbig::FBig,
47    repr::{Context, Repr, Word},
48    round::{mode, Round},
49};
50
51use dashu_base::EstimatedLog2;
52use dashu_int::{
53    rand::{UniformBelow, UniformBits},
54    DoubleWord, UBig,
55};
56use rand_v08::{
57    distributions::{
58        uniform::{SampleBorrow, SampleUniform, UniformSampler},
59        Open01, OpenClosed01, Standard,
60    },
61    prelude::Distribution,
62    Rng,
63};
64
65/// The back-end implementing [UniformSampler] for [FBig] (and [DBig][crate::DBig]).
66///
67/// See the module ([rand][crate::rand]) level documentation for examples.
68pub struct UniformFBig<R: Round, const B: Word> {
69    sampler: Uniform01<B>,
70
71    scale: Repr<B>,
72    offset: Repr<B>,
73
74    /// This field is used to distinguish between uniform distributions
75    /// with different rounding modes, no actual effect on the sampling.
76    _marker: PhantomData<R>,
77}
78
79impl<R: Round, const B: Word> UniformFBig<R, B> {
80    /// Same as [UniformSampler::new] but with an additional argument to specify
81    /// the precision of the sampled [FBig].
82    #[inline]
83    pub fn new(low: &FBig<R, B>, high: &FBig<R, B>, precision: usize) -> Self {
84        assert!(low <= high);
85
86        Self {
87            sampler: Uniform01::new(precision),
88            scale: (high - low).into_repr(),
89            offset: low.repr().clone(),
90            _marker: PhantomData,
91        }
92    }
93
94    /// Same as [UniformSampler::new_inclusive] but with an additional argument to
95    /// specify the precision of the sampled [FBig].
96    #[inline]
97    pub fn new_inclusive(low: &FBig<R, B>, high: &FBig<R, B>, precision: usize) -> Self {
98        assert!(low <= high);
99
100        Self {
101            sampler: Uniform01::new_closed(precision),
102            scale: (high - low).into_repr(),
103            offset: low.repr().clone(),
104            _marker: PhantomData,
105        }
106    }
107}
108
109impl<R: Round, const B: Word> UniformSampler for UniformFBig<R, B> {
110    type X = FBig<R, B>;
111
112    #[inline]
113    fn new<B1, B2>(low: B1, high: B2) -> Self
114    where
115        B1: SampleBorrow<Self::X> + Sized,
116        B2: SampleBorrow<Self::X> + Sized,
117    {
118        let precision = low.borrow().precision().max(high.borrow().precision());
119        UniformFBig::new(low.borrow(), high.borrow(), precision)
120    }
121
122    #[inline]
123    fn new_inclusive<B1, B2>(low: B1, high: B2) -> Self
124    where
125        B1: SampleBorrow<Self::X> + Sized,
126        B2: SampleBorrow<Self::X> + Sized,
127    {
128        let precision = low.borrow().precision().max(high.borrow().precision());
129        UniformFBig::new_inclusive(low.borrow(), high.borrow(), precision)
130    }
131
132    #[inline]
133    fn sample<RNG: Rng + ?Sized>(&self, rng: &mut RNG) -> Self::X {
134        <Self as Distribution<FBig<R, B>>>::sample(self, rng)
135    }
136}
137
138impl<R: Round, const B: Word> Distribution<FBig<R, B>> for UniformFBig<R, B> {
139    fn sample<RNG: Rng + ?Sized>(&self, rng: &mut RNG) -> FBig<R, B> {
140        // After we have a sample in [0, 1), all the following operations are rounded down
141        // so that we can ensure we don't reach the right bound.
142        let unit: FBig<mode::Down, B> = self.sampler.sample(rng);
143        let context = unit.context();
144        let scaled = context.mul(unit.repr(), &self.scale).value();
145        context
146            .add(scaled.repr(), &self.offset)
147            .value()
148            .with_rounding()
149    }
150}
151
152impl<R: Round, const B: Word> SampleUniform for FBig<R, B> {
153    type Sampler = UniformFBig<R, B>;
154}
155
156// when sampling with the builtin distributions, the precision is choosen
157// such that the significand fits in a double word and no allocation is required.
158#[inline]
159fn get_inline_precision<const B: Word>() -> usize {
160    (DoubleWord::BITS as f32 / B.log2_bounds().1) as _
161}
162
163impl<R: Round, const B: Word> Distribution<FBig<R, B>> for Standard {
164    fn sample<RNG: Rng + ?Sized>(&self, rng: &mut RNG) -> FBig<R, B> {
165        Uniform01::<B>::new(get_inline_precision::<B>()).sample(rng)
166    }
167}
168
169impl<R: Round, const B: Word> Distribution<FBig<R, B>> for Open01 {
170    #[inline]
171    fn sample<RNG: Rng + ?Sized>(&self, rng: &mut RNG) -> FBig<R, B> {
172        Uniform01::<B>::new_open(get_inline_precision::<B>()).sample(rng)
173    }
174}
175
176impl<R: Round, const B: Word> Distribution<FBig<R, B>> for OpenClosed01 {
177    #[inline]
178    fn sample<RNG: Rng + ?Sized>(&self, rng: &mut RNG) -> FBig<R, B> {
179        Uniform01::<B>::new_open_closed(get_inline_precision::<B>()).sample(rng)
180    }
181}
182
183/// A uniform distribution between 0 and 1. It can be used to replace the [Standard], [Open01],
184/// [OpenClosed01] distributions from the `rand` crate when you want to customize the precision
185/// of the generated float number.
186pub struct Uniform01<const BASE: Word> {
187    pub(crate) precision: usize,
188    range: Option<UBig>, // BASE ^ precision (±1 if necessary)
189    include_zero: bool,  // whether include the zero
190    include_one: bool,   // whether include the one
191}
192
193impl<const B: Word> Uniform01<B> {
194    /// Create a uniform distribution in `[0, 1)` with a given precision.
195    #[inline]
196    pub fn new(precision: usize) -> Self {
197        let range = match B {
198            2 => None,
199            _ => Some(UBig::from_word(B).pow(precision)),
200        };
201        Self {
202            precision,
203            range,
204            include_zero: true,
205            include_one: false,
206        }
207    }
208
209    /// Create a uniform distribution in `[0, 1]` with a given precision.
210    #[inline]
211    pub fn new_closed(precision: usize) -> Self {
212        let range = Some(UBig::from_word(B).pow(precision) + UBig::ONE);
213        Self {
214            precision,
215            range,
216            include_zero: true,
217            include_one: true,
218        }
219    }
220
221    /// Create a uniform distribution in `(0, 1)` with a given precision.
222    #[inline]
223    pub fn new_open(precision: usize) -> Self {
224        let range = match B {
225            2 => None,
226            _ => Some(UBig::from_word(B).pow(precision) - UBig::ONE),
227        };
228        Self {
229            precision,
230            range,
231            include_zero: false,
232            include_one: false,
233        }
234    }
235
236    /// Create a uniform distribution in `(0, 1]` with a given precision.
237    #[inline]
238    pub fn new_open_closed(precision: usize) -> Self {
239        let range = match B {
240            2 => None,
241            _ => Some(UBig::from_word(B).pow(precision)),
242        };
243        Self {
244            precision,
245            range,
246            include_zero: false,
247            include_one: true,
248        }
249    }
250}
251
252impl<R: Round, const B: Word> Distribution<FBig<R, B>> for Uniform01<B> {
253    fn sample<RNG: Rng + ?Sized>(&self, rng: &mut RNG) -> FBig<R, B> {
254        let repr = match (self.include_zero, self.include_one) {
255            (true, false) => {
256                // sample in [0, 1)
257                let signif: UBig = if B == 2 {
258                    UniformBits::new(self.precision).sample(rng)
259                } else {
260                    UniformBelow::new(self.range.as_ref().unwrap()).sample(rng)
261                };
262                Repr::<B>::new(signif.into(), -(self.precision as isize))
263            }
264            (true, true) => {
265                // sample in [0, 1]
266                let signif: UBig = UniformBelow::new(self.range.as_ref().unwrap()).sample(rng);
267                Repr::new(signif.into(), -(self.precision as isize))
268            }
269            (false, false) => {
270                // sample in (0, 1)
271                let signif = if B == 2 {
272                    loop {
273                        // simply reject zero
274                        let n: UBig = UniformBits::new(self.precision).sample(rng);
275                        if !n.is_zero() {
276                            break n;
277                        }
278                    }
279                } else {
280                    let n: UBig = UniformBelow::new(self.range.as_ref().unwrap()).sample(rng);
281                    n + UBig::ONE
282                };
283                Repr::<B>::new(signif.into(), -(self.precision as isize))
284            }
285            (false, true) => {
286                // sample in (0, 1]
287                let signif: UBig = if B == 2 {
288                    UniformBits::new(self.precision).sample(rng)
289                } else {
290                    UniformBelow::new(self.range.as_ref().unwrap()).sample(rng)
291                };
292                Repr::<B>::new((signif + UBig::ONE).into(), -(self.precision as isize))
293            }
294        };
295
296        let context = Context::<mode::Down>::new(self.precision);
297        FBig::new(repr, context).with_rounding()
298    }
299}