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
120    fn median(numbers: &mut [f64]) -> f64 {
121        sort(numbers);
122        let mid = numbers.len() / 2;
123        numbers[mid]
124    }
125
126    fn sort(numbers: &mut [f64]) {
127        numbers.sort_by(|a, b| a.partial_cmp(b).unwrap());
128    }
129
130    #[test]
131    fn test_cauchy_averages() {
132        // NOTE: given that the variance and mean are undefined,
133        // this test does not have any rigorous statistical meaning.
134        let cauchy = Cauchy::new(10.0, 5.0).unwrap();
135        let mut rng = crate::test::rng(123);
136        let mut numbers: [f64; 1000] = [0.0; 1000];
137        let mut sum = 0.0;
138        for number in &mut numbers[..] {
139            *number = cauchy.sample(&mut rng);
140            sum += *number;
141        }
142        let median = median(&mut numbers);
143        #[cfg(feature = "std")]
144        std::println!("Cauchy median: {}", median);
145        assert!((median - 10.0).abs() < 0.4); // not 100% certain, but probable enough
146        let mean = sum / 1000.0;
147        #[cfg(feature = "std")]
148        std::println!("Cauchy mean: {}", mean);
149        // for a Cauchy distribution the mean should not converge
150        assert!((mean - 10.0).abs() > 0.4); // not 100% certain, but probable enough
151    }
152
153    #[test]
154    #[should_panic]
155    fn test_cauchy_invalid_scale_zero() {
156        Cauchy::new(0.0, 0.0).unwrap();
157    }
158
159    #[test]
160    #[should_panic]
161    fn test_cauchy_invalid_scale_neg() {
162        Cauchy::new(0.0, -10.0).unwrap();
163    }
164
165    #[test]
166    fn value_stability() {
167        fn gen_samples<F: Float + FloatConst + fmt::Debug>(m: F, s: F, buf: &mut [F])
168        where
169            StandardUniform: Distribution<F>,
170        {
171            let distr = Cauchy::new(m, s).unwrap();
172            let mut rng = crate::test::rng(353);
173            for x in buf {
174                *x = rng.sample(distr);
175            }
176        }
177
178        let mut buf = [0.0; 4];
179        gen_samples(100f64, 10.0, &mut buf);
180        assert_eq!(
181            &buf,
182            &[
183                77.93369152808678,
184                90.1606912098641,
185                125.31516221323625,
186                86.10217834773925
187            ]
188        );
189
190        // Unfortunately this test is not fully portable due to reliance on the
191        // system's implementation of tanf (see doc on Cauchy struct).
192        let mut buf = [0.0; 4];
193        gen_samples(10f32, 7.0, &mut buf);
194        let expected = [15.023088, -5.446413, 3.7092876, 3.112482];
195        for (a, b) in buf.iter().zip(expected.iter()) {
196            assert_almost_eq!(*a, *b, 1e-5);
197        }
198    }
199
200    #[test]
201    fn cauchy_distributions_can_be_compared() {
202        assert_eq!(Cauchy::new(1.0, 2.0), Cauchy::new(1.0, 2.0));
203    }
204}