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/// 
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}