rand_distr/
triangular.rs

1// Copyright 2018 Developers of the Rand project.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8//! The triangular distribution.
9
10use num_traits::Float;
11use crate::{Distribution, Standard};
12use rand::Rng;
13use core::fmt;
14
15/// The triangular distribution.
16///
17/// A continuous probability distribution parameterised by a range, and a mode
18/// (most likely value) within that range.
19///
20/// The probability density function is triangular. For a similar distribution
21/// with a smooth PDF, see the [`Pert`] distribution.
22///
23/// # Example
24///
25/// ```rust
26/// use rand_distr::{Triangular, Distribution};
27///
28/// let d = Triangular::new(0., 5., 2.5).unwrap();
29/// let v = d.sample(&mut rand::thread_rng());
30/// println!("{} is from a triangular distribution", v);
31/// ```
32///
33/// [`Pert`]: crate::Pert
34#[derive(Clone, Copy, Debug)]
35#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
36pub struct Triangular<F>
37where F: Float, Standard: Distribution<F>
38{
39    min: F,
40    max: F,
41    mode: F,
42}
43
44/// Error type returned from [`Triangular::new`].
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum TriangularError {
47    /// `max < min` or `min` or `max` is NaN.
48    RangeTooSmall,
49    /// `mode < min` or `mode > max` or `mode` is NaN.
50    ModeRange,
51}
52
53impl fmt::Display for TriangularError {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        f.write_str(match self {
56            TriangularError::RangeTooSmall => {
57                "requirement min <= max is not met in triangular distribution"
58            }
59            TriangularError::ModeRange => "mode is outside [min, max] in triangular distribution",
60        })
61    }
62}
63
64#[cfg(feature = "std")]
65#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))]
66impl std::error::Error for TriangularError {}
67
68impl<F> Triangular<F>
69where F: Float, Standard: Distribution<F>
70{
71    /// Set up the Triangular distribution with defined `min`, `max` and `mode`.
72    #[inline]
73    pub fn new(min: F, max: F, mode: F) -> Result<Triangular<F>, TriangularError> {
74        if !(max >= min) {
75            return Err(TriangularError::RangeTooSmall);
76        }
77        if !(mode >= min && max >= mode) {
78            return Err(TriangularError::ModeRange);
79        }
80        Ok(Triangular { min, max, mode })
81    }
82}
83
84impl<F> Distribution<F> for Triangular<F>
85where F: Float, Standard: Distribution<F>
86{
87    #[inline]
88    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> F {
89        let f: F = rng.sample(Standard);
90        let diff_mode_min = self.mode - self.min;
91        let range = self.max - self.min;
92        let f_range = f * range;
93        if f_range < diff_mode_min {
94            self.min + (f_range * diff_mode_min).sqrt()
95        } else {
96            self.max - ((range - f_range) * (self.max - self.mode)).sqrt()
97        }
98    }
99}
100
101#[cfg(test)]
102mod test {
103    use super::*;
104    use rand::{rngs::mock, Rng};
105
106    #[test]
107    fn test_triangular() {
108        let mut half_rng = mock::StepRng::new(0x8000_0000_0000_0000, 0);
109        assert_eq!(half_rng.gen::<f64>(), 0.5);
110        for &(min, max, mode, median) in &[
111            (-1., 1., 0., 0.),
112            (1., 2., 1., 2. - 0.5f64.sqrt()),
113            (5., 25., 25., 5. + 200f64.sqrt()),
114            (1e-5, 1e5, 1e-3, 1e5 - 4999999949.5f64.sqrt()),
115            (0., 1., 0.9, 0.45f64.sqrt()),
116            (-4., -0.5, -2., -4.0 + 3.5f64.sqrt()),
117        ] {
118            #[cfg(feature = "std")]
119            std::println!("{} {} {} {}", min, max, mode, median);
120            let distr = Triangular::new(min, max, mode).unwrap();
121            // Test correct value at median:
122            assert_eq!(distr.sample(&mut half_rng), median);
123        }
124
125        for &(min, max, mode) in &[
126            (-1., 1., 2.),
127            (-1., 1., -2.),
128            (2., 1., 1.),
129        ] {
130            assert!(Triangular::new(min, max, mode).is_err());
131        }
132    }
133}