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