rand_distr/
cauchy.rs

1// Copyright 2018 Developers of the Rand project.
2// Copyright 2016-2017 The Rust Project Developers.
3//
4// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
5// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
7// option. This file may not be copied, modified, or distributed
8// except according to those terms.
9
10//! The Cauchy distribution `Cauchy(x₀, γ)`.
11
12use crate::{Distribution, StandardUniform};
13use core::fmt;
14use num_traits::{Float, FloatConst};
15use rand::Rng;
16
17/// The [Cauchy distribution](https://en.wikipedia.org/wiki/Cauchy_distribution) `Cauchy(x₀, γ)`.
18///
19/// The Cauchy distribution is a continuous probability distribution with
20/// parameters `x₀` (median) and `γ` (scale).
21/// It describes the distribution of the ratio of two independent
22/// normally distributed random variables with means `x₀` and scales `γ`.
23/// In other words, if `X` and `Y` are independent normally distributed
24/// random variables with means `x₀` and scales `γ`, respectively, then
25/// `X / Y` is `Cauchy(x₀, γ)` distributed.
26///
27/// # Density function
28///
29/// `f(x) = 1 / (π * γ * (1 + ((x - x₀) / γ)²))`
30///
31/// # Plot
32///
33/// The plot illustrates the Cauchy distribution with various values of `x₀` and `γ`.
34/// Note how the median parameter `x₀` shifts the distribution along the x-axis,
35/// and how the scale `γ` changes the density around the median.
36///
37/// The standard Cauchy distribution is the special case with `x₀ = 0` and `γ = 1`,
38/// which corresponds to the ratio of two [`StandardNormal`](crate::StandardNormal) distributions.
39///
40/// ![Cauchy distribution](https://raw.githubusercontent.com/rust-random/charts/main/charts/cauchy.svg)
41///
42/// # Example
43///
44/// ```
45/// use rand_distr::{Cauchy, Distribution};
46///
47/// let cau = Cauchy::new(2.0, 5.0).unwrap();
48/// let v = cau.sample(&mut rand::rng());
49/// println!("{} is from a Cauchy(2, 5) distribution", v);
50/// ```
51///
52/// # Notes
53///
54/// Note that at least for `f32`, results are not fully portable due to minor
55/// differences in the target system's *tan* implementation, `tanf`.
56#[derive(Clone, Copy, Debug, PartialEq)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58pub struct Cauchy<F>
59where
60    F: Float + FloatConst,
61    StandardUniform: Distribution<F>,
62{
63    median: F,
64    scale: F,
65}
66
67/// Error type returned from [`Cauchy::new`].
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub enum Error {
70    /// `scale <= 0` or `nan`.
71    ScaleTooSmall,
72}
73
74impl fmt::Display for Error {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        f.write_str(match self {
77            Error::ScaleTooSmall => "scale is not positive in Cauchy distribution",
78        })
79    }
80}
81
82#[cfg(feature = "std")]
83impl std::error::Error for Error {}
84
85impl<F> Cauchy<F>
86where
87    F: Float + FloatConst,
88    StandardUniform: Distribution<F>,
89{
90    /// Construct a new `Cauchy` with the given shape parameters
91    /// `median` the peak location and `scale` the scale factor.
92    pub fn new(median: F, scale: F) -> Result<Cauchy<F>, Error> {
93        if !(scale > F::zero()) {
94            return Err(Error::ScaleTooSmall);
95        }
96        Ok(Cauchy { median, scale })
97    }
98}
99
100impl<F> Distribution<F> for Cauchy<F>
101where
102    F: Float + FloatConst,
103    StandardUniform: Distribution<F>,
104{
105    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> F {
106        // sample from [0, 1)
107        let x = StandardUniform.sample(rng);
108        // get standard cauchy random number
109        // note that π/2 is not exactly representable, even if x=0.5 the result is finite
110        let comp_dev = (F::PI() * x).tan();
111        // shift and scale according to parameters
112        self.median + self.scale * comp_dev
113    }
114}
115
116#[cfg(test)]
117mod test {
118    use super::*;
119    use rand::RngExt;
120
121    fn median(numbers: &mut [f64]) -> f64 {
122        sort(numbers);
123        let mid = numbers.len() / 2;
124        numbers[mid]
125    }
126
127    fn sort(numbers: &mut [f64]) {
128        numbers.sort_by(|a, b| a.partial_cmp(b).unwrap());
129    }
130
131    #[test]
132    fn test_cauchy_averages() {
133        // NOTE: given that the variance and mean are undefined,
134        // this test does not have any rigorous statistical meaning.
135        let cauchy = Cauchy::new(10.0, 5.0).unwrap();
136        let mut rng = crate::test::rng(123);
137        let mut numbers: [f64; 1000] = [0.0; 1000];
138        let mut sum = 0.0;
139        for number in &mut numbers[..] {
140            *number = cauchy.sample(&mut rng);
141            sum += *number;
142        }
143        let median = median(&mut numbers);
144        #[cfg(feature = "std")]
145        std::println!("Cauchy median: {median}");
146        assert!((median - 10.0).abs() < 0.4); // not 100% certain, but probable enough
147        let mean = sum / 1000.0;
148        #[cfg(feature = "std")]
149        std::println!("Cauchy mean: {mean}");
150        // for a Cauchy distribution the mean should not converge
151        assert!((mean - 10.0).abs() > 0.4); // not 100% certain, but probable enough
152    }
153
154    #[test]
155    #[should_panic]
156    fn test_cauchy_invalid_scale_zero() {
157        Cauchy::new(0.0, 0.0).unwrap();
158    }
159
160    #[test]
161    #[should_panic]
162    fn test_cauchy_invalid_scale_neg() {
163        Cauchy::new(0.0, -10.0).unwrap();
164    }
165
166    #[test]
167    fn value_stability() {
168        fn gen_samples<F: Float + FloatConst + fmt::Debug>(m: F, s: F, buf: &mut [F])
169        where
170            StandardUniform: Distribution<F>,
171        {
172            let distr = Cauchy::new(m, s).unwrap();
173            let mut rng = crate::test::rng(353);
174            for x in buf {
175                *x = rng.sample(distr);
176            }
177        }
178
179        let mut buf = [0.0; 4];
180        gen_samples(100f64, 10.0, &mut buf);
181        assert_eq!(
182            &buf,
183            &[
184                77.93369152808678,
185                90.1606912098641,
186                125.31516221323625,
187                86.10217834773925
188            ]
189        );
190
191        // Unfortunately this test is not fully portable due to reliance on the
192        // system's implementation of tanf (see doc on Cauchy struct).
193        let mut buf = [0.0; 4];
194        gen_samples(10f32, 7.0, &mut buf);
195        let expected = [15.023088, -5.446413, 3.7092876, 3.112482];
196        for (a, b) in buf.iter().zip(expected.iter()) {
197            assert_almost_eq!(*a, *b, 1e-5);
198        }
199    }
200
201    #[test]
202    fn cauchy_distributions_can_be_compared() {
203        assert_eq!(Cauchy::new(1.0, 2.0), Cauchy::new(1.0, 2.0));
204    }
205}