Skip to main content

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.unwrap_fp(context.mul(unit.repr(), &self.scale));
81        context
82            .unwrap_fp(context.add(scaled.repr(), &self.offset))
83            .with_rounding()
84    }
85}
86
87/// A uniform distribution between 0 and 1. It can be used to replace the `Standard`,
88/// `Open01`, `OpenClosed01` distributions from the `rand` crate when you want to customize the
89/// precision of the generated float number.
90pub struct Uniform01<const BASE: Word> {
91    pub(crate) precision: usize,
92    pub(crate) range: Option<UBig>, // BASE ^ precision (±1 if necessary)
93    pub(crate) include_zero: bool,  // whether include the zero
94    pub(crate) include_one: bool,   // whether include the one
95}
96
97impl<const B: Word> Uniform01<B> {
98    /// Create a uniform distribution in `[0, 1)` with a given precision.
99    #[inline]
100    pub fn new(precision: usize) -> Self {
101        let range = match B {
102            2 => None,
103            _ => Some(UBig::from_word(B).pow(precision)),
104        };
105        Self {
106            precision,
107            range,
108            include_zero: true,
109            include_one: false,
110        }
111    }
112
113    /// Create a uniform distribution in `[0, 1]` with a given precision.
114    #[inline]
115    pub fn new_closed(precision: usize) -> Self {
116        let range = Some(UBig::from_word(B).pow(precision) + UBig::ONE);
117        Self {
118            precision,
119            range,
120            include_zero: true,
121            include_one: true,
122        }
123    }
124
125    /// Create a uniform distribution in `(0, 1)` with a given precision.
126    #[inline]
127    pub fn new_open(precision: usize) -> Self {
128        let range = match B {
129            2 => None,
130            _ => Some(UBig::from_word(B).pow(precision) - UBig::ONE),
131        };
132        Self {
133            precision,
134            range,
135            include_zero: false,
136            include_one: false,
137        }
138    }
139
140    /// Create a uniform distribution in `(0, 1]` with a given precision.
141    #[inline]
142    pub fn new_open_closed(precision: usize) -> Self {
143        let range = match B {
144            2 => None,
145            _ => Some(UBig::from_word(B).pow(precision)),
146        };
147        Self {
148            precision,
149            range,
150            include_zero: false,
151            include_one: true,
152        }
153    }
154
155    /// Draw a random [FBig] in this distribution. Generic over the rounding mode `R` and the
156    /// [`BitRng`] driving the generation.
157    pub fn sample01<R: Round, BR: BitRng + ?Sized>(&self, rng: &mut BR) -> FBig<R, B> {
158        let repr = match (self.include_zero, self.include_one) {
159            (true, false) => {
160                // sample in [0, 1)
161                let signif: UBig = if B == 2 {
162                    UniformBits::new(self.precision).sample_ubig(rng)
163                } else {
164                    UniformBelow::new(self.range.as_ref().unwrap()).sample_ubig(rng)
165                };
166                Repr::<B>::new(signif.into(), -(self.precision as isize))
167            }
168            (true, true) => {
169                // sample in [0, 1]
170                let signif: UBig = UniformBelow::new(self.range.as_ref().unwrap()).sample_ubig(rng);
171                Repr::new(signif.into(), -(self.precision as isize))
172            }
173            (false, false) => {
174                // sample in (0, 1)
175                let signif = if B == 2 {
176                    loop {
177                        // simply reject zero
178                        let n: UBig = UniformBits::new(self.precision).sample_ubig(rng);
179                        if !n.is_zero() {
180                            break n;
181                        }
182                    }
183                } else {
184                    let n: UBig = UniformBelow::new(self.range.as_ref().unwrap()).sample_ubig(rng);
185                    n + UBig::ONE
186                };
187                Repr::<B>::new(signif.into(), -(self.precision as isize))
188            }
189            (false, true) => {
190                // sample in (0, 1]
191                let signif: UBig = if B == 2 {
192                    UniformBits::new(self.precision).sample_ubig(rng)
193                } else {
194                    UniformBelow::new(self.range.as_ref().unwrap()).sample_ubig(rng)
195                };
196                Repr::<B>::new((signif + UBig::ONE).into(), -(self.precision as isize))
197            }
198        };
199
200        let context = Context::<mode::Down>::new(self.precision);
201        FBig::new(repr, context).with_rounding()
202    }
203}
204
205// When sampling with the builtin distributions, the precision is chosen such that the
206// significand fits in a double word and no allocation is required.
207#[inline]
208pub(crate) fn get_inline_precision<const B: Word>() -> usize {
209    (DoubleWord::BITS as f32 / B.log2_bounds().1) as _
210}