Skip to main content

scirs2_stats/distributions/
cauchy.rs

1//! Cauchy distribution functions
2//!
3//! This module provides functionality for the Cauchy (Lorentz) distribution.
4
5use crate::error::{StatsError, StatsResult};
6use crate::sampling::SampleableDistribution;
7use crate::traits::{ContinuousDistribution, Distribution as ScirsDist};
8use scirs2_core::ndarray::Array1;
9use scirs2_core::numeric::{Float, NumCast};
10use scirs2_core::random::prelude::*;
11use scirs2_core::random::{Distribution, Uniform as RandUniform};
12
13/// Cauchy distribution structure
14///
15/// The Cauchy distribution, also known as the Lorentz distribution, is a continuous
16/// probability distribution that is the ratio of two independent normally distributed
17/// random variables. It is notable for not having defined moments (like mean or variance).
18pub struct Cauchy<F: Float> {
19    /// Location parameter
20    pub loc: F,
21    /// Scale parameter (gamma > 0)
22    pub scale: F,
23    /// Random number generator for uniform distribution
24    rand_distr: RandUniform<f64>,
25}
26
27impl<F: Float + NumCast + std::fmt::Display> Cauchy<F> {
28    /// Create a new Cauchy distribution with given parameters
29    ///
30    /// # Arguments
31    ///
32    /// * `loc` - Location parameter (median of the distribution)
33    /// * `scale` - Scale parameter (half-width at half-maximum) > 0
34    ///
35    /// # Returns
36    ///
37    /// * A new Cauchy distribution instance
38    ///
39    /// # Examples
40    ///
41    /// ```
42    /// use scirs2_stats::distributions::cauchy::Cauchy;
43    ///
44    /// let cauchy = Cauchy::new(0.0f64, 1.0).expect("Operation failed");
45    /// ```
46    pub fn new(loc: F, scale: F) -> StatsResult<Self> {
47        // Validate parameters
48        if scale <= F::zero() {
49            return Err(StatsError::DomainError(
50                "Scale parameter must be positive".to_string(),
51            ));
52        }
53
54        // Create RNG for uniform distribution in [0, 1)
55        let rand_distr = match RandUniform::new(0.0, 1.0) {
56            Ok(distr) => distr,
57            Err(_) => {
58                return Err(StatsError::ComputationError(
59                    "Failed to create uniform distribution for sampling".to_string(),
60                ))
61            }
62        };
63
64        Ok(Cauchy {
65            loc,
66            scale,
67            rand_distr,
68        })
69    }
70
71    /// Calculate the probability density function (PDF) at a given point
72    ///
73    /// # Arguments
74    ///
75    /// * `x` - The point at which to evaluate the PDF
76    ///
77    /// # Returns
78    ///
79    /// * The value of the PDF at the given point
80    ///
81    /// # Examples
82    ///
83    /// ```
84    /// use scirs2_stats::distributions::cauchy::Cauchy;
85    ///
86    /// let cauchy = Cauchy::new(0.0f64, 1.0).expect("Operation failed");
87    /// let pdf_at_zero = cauchy.pdf(0.0);
88    /// assert!((pdf_at_zero - 0.3183099).abs() < 1e-7);
89    /// ```
90    pub fn pdf(&self, x: F) -> F {
91        let pi = F::from(std::f64::consts::PI).expect("Failed to convert to float");
92        let one = F::one();
93
94        // PDF = 1 / (π * scale * (1 + ((x - loc) / scale)^2))
95        let z = (x - self.loc) / self.scale;
96        one / (pi * self.scale * (one + z * z))
97    }
98
99    /// Calculate the cumulative distribution function (CDF) at a given point
100    ///
101    /// # Arguments
102    ///
103    /// * `x` - The point at which to evaluate the CDF
104    ///
105    /// # Returns
106    ///
107    /// * The value of the CDF at the given point
108    ///
109    /// # Examples
110    ///
111    /// ```
112    /// use scirs2_stats::distributions::cauchy::Cauchy;
113    ///
114    /// let cauchy = Cauchy::new(0.0f64, 1.0).expect("Operation failed");
115    /// let cdf_at_zero = cauchy.cdf(0.0);
116    /// assert!((cdf_at_zero - 0.5).abs() < 1e-7);
117    /// ```
118    pub fn cdf(&self, x: F) -> F {
119        let pi = F::from(std::f64::consts::PI).expect("Failed to convert to float");
120        let half = F::from(0.5).expect("Failed to convert constant to float");
121
122        // CDF = 0.5 + (1/π) * arctan((x - loc) / scale)
123        let z = (x - self.loc) / self.scale;
124        half + z.atan() / pi
125    }
126
127    /// Inverse of the cumulative distribution function (quantile function)
128    ///
129    /// # Arguments
130    ///
131    /// * `p` - Probability value (between 0 and 1)
132    ///
133    /// # Returns
134    ///
135    /// * The value x such that CDF(x) = p
136    ///
137    /// # Examples
138    ///
139    /// ```
140    /// use scirs2_stats::distributions::cauchy::Cauchy;
141    ///
142    /// let cauchy = Cauchy::new(0.0f64, 1.0).expect("Operation failed");
143    /// let x = cauchy.ppf(0.75).expect("Operation failed");
144    /// assert!((x - 1.0).abs() < 1e-7);
145    /// ```
146    pub fn ppf(&self, p: F) -> StatsResult<F> {
147        if p < F::zero() || p > F::one() {
148            return Err(StatsError::DomainError(
149                "Probability must be between 0 and 1".to_string(),
150            ));
151        }
152
153        let pi = F::from(std::f64::consts::PI).expect("Failed to convert to float");
154        let half = F::from(0.5).expect("Failed to convert constant to float");
155
156        // Quantile = loc + scale * tan(π * (p - 0.5))
157        let tan_term = (pi * (p - half)).tan();
158        let quantile = self.loc + self.scale * tan_term;
159
160        Ok(quantile)
161    }
162
163    /// Generate random samples from the distribution
164    ///
165    /// # Arguments
166    ///
167    /// * `size` - Number of samples to generate
168    ///
169    /// # Returns
170    ///
171    /// * Vector of random samples
172    ///
173    /// # Examples
174    ///
175    /// ```
176    /// use scirs2_stats::distributions::cauchy::Cauchy;
177    ///
178    /// let cauchy = Cauchy::new(0.0f64, 1.0).expect("Operation failed");
179    /// let samples = cauchy.rvs_vec(10).expect("Operation failed");
180    /// assert_eq!(samples.len(), 10);
181    /// ```
182    pub fn rvs_vec(&self, size: usize) -> StatsResult<Vec<F>> {
183        let mut rng = thread_rng();
184        let mut samples = Vec::with_capacity(size);
185
186        // Generate samples using the inverse transform sampling method
187        for _ in 0..size {
188            // Generate uniform random number in (0, 1)
189            let u = self.rand_distr.sample(&mut rng);
190
191            // Avoid exactly 0.5 to prevent undefined tan(0)
192            if (u - 0.5).abs() < 1e-10 {
193                continue;
194            }
195
196            let u_f = F::from(u).expect("Failed to convert to float");
197
198            // Apply inverse CDF transform
199            let sample = match self.ppf(u_f) {
200                Ok(s) => s,
201                Err(_) => continue, // Skip invalid samples
202            };
203
204            samples.push(sample);
205        }
206
207        // Ensure we have exactly 'size' samples
208        while samples.len() < size {
209            // Generate uniform random number in (0, 1)
210            let u = self.rand_distr.sample(&mut rng);
211
212            // Avoid exactly 0.5 to prevent undefined tan(0)
213            if (u - 0.5).abs() < 1e-10 {
214                continue;
215            }
216
217            let u_f = F::from(u).expect("Failed to convert to float");
218
219            // Apply inverse CDF transform
220            let sample = match self.ppf(u_f) {
221                Ok(s) => s,
222                Err(_) => continue, // Skip invalid samples
223            };
224
225            samples.push(sample);
226        }
227
228        Ok(samples)
229    }
230
231    /// Generate random samples from the distribution
232    ///
233    /// # Arguments
234    ///
235    /// * `size` - Number of samples to generate
236    ///
237    /// # Returns
238    ///
239    /// * Array of random samples
240    ///
241    /// # Examples
242    ///
243    /// ```
244    /// use scirs2_stats::distributions::cauchy::Cauchy;
245    ///
246    /// let cauchy = Cauchy::new(0.0f64, 1.0).expect("Operation failed");
247    /// let samples = cauchy.rvs(10).expect("Operation failed");
248    /// assert_eq!(samples.len(), 10);
249    /// ```
250    pub fn rvs(&self, size: usize) -> StatsResult<Array1<F>> {
251        let samples_vec = self.rvs_vec(size)?;
252        Ok(Array1::from(samples_vec))
253    }
254
255    /// Calculate the median of the distribution
256    ///
257    /// # Returns
258    ///
259    /// * The median of the distribution
260    ///
261    /// # Examples
262    ///
263    /// ```
264    /// use scirs2_stats::distributions::cauchy::Cauchy;
265    ///
266    /// let cauchy = Cauchy::new(2.0f64, 1.0).expect("Operation failed");
267    /// let median = cauchy.median();
268    /// assert_eq!(median, 2.0);
269    /// ```
270    pub fn median(&self) -> F {
271        // Median = loc
272        self.loc
273    }
274
275    /// Calculate the mode of the distribution
276    ///
277    /// # Returns
278    ///
279    /// * The mode of the distribution
280    ///
281    /// # Examples
282    ///
283    /// ```
284    /// use scirs2_stats::distributions::cauchy::Cauchy;
285    ///
286    /// let cauchy = Cauchy::new(2.0f64, 1.0).expect("Operation failed");
287    /// let mode = cauchy.mode();
288    /// assert_eq!(mode, 2.0);
289    /// ```
290    pub fn mode(&self) -> F {
291        // Mode = loc
292        self.loc
293    }
294
295    /// Calculate the interquartile range (IQR) of the distribution
296    ///
297    /// # Returns
298    ///
299    /// * The interquartile range
300    ///
301    /// # Examples
302    ///
303    /// ```
304    /// use scirs2_stats::distributions::cauchy::Cauchy;
305    ///
306    /// let cauchy = Cauchy::new(0.0f64, 1.0).expect("Operation failed");
307    /// let iqr = cauchy.iqr();
308    /// assert!((iqr - 2.0).abs() < 1e-7);
309    /// ```
310    pub fn iqr(&self) -> F {
311        // IQR = 2 * scale
312        self.scale + self.scale
313    }
314
315    /// Calculate the entropy of the distribution
316    ///
317    /// # Returns
318    ///
319    /// * The entropy value
320    ///
321    /// # Examples
322    ///
323    /// ```
324    /// use scirs2_stats::distributions::cauchy::Cauchy;
325    ///
326    /// let cauchy = Cauchy::new(0.0f64, 1.0).expect("Operation failed");
327    /// let entropy = cauchy.entropy();
328    /// assert!((entropy - 2.5310242).abs() < 1e-7); // log(4π)
329    /// ```
330    pub fn entropy(&self) -> F {
331        let pi = F::from(std::f64::consts::PI).expect("Failed to convert to float");
332        let four = F::from(4.0).expect("Failed to convert constant to float");
333
334        // Entropy = log(4 * π * scale)
335        (four * pi * self.scale).ln()
336    }
337}
338
339/// Create a Cauchy distribution with the given parameters.
340///
341/// This is a convenience function to create a Cauchy distribution with
342/// the given location and scale parameters.
343///
344/// # Arguments
345///
346/// * `loc` - Location parameter (median of the distribution)
347/// * `scale` - Scale parameter (half-width at half-maximum) > 0
348///
349/// # Returns
350///
351/// * A Cauchy distribution object
352///
353/// # Examples
354///
355/// ```
356/// use scirs2_stats::distributions::cauchy;
357///
358/// let c = cauchy::cauchy(0.0f64, 1.0).expect("Operation failed");
359/// let pdf_at_zero = c.pdf(0.0);
360/// assert!((pdf_at_zero - 0.3183099).abs() < 1e-7);
361/// ```
362#[allow(dead_code)]
363pub fn cauchy<F>(loc: F, scale: F) -> StatsResult<Cauchy<F>>
364where
365    F: Float + NumCast + std::fmt::Display,
366{
367    Cauchy::new(loc, scale)
368}
369
370/// Implementation of SampleableDistribution for Cauchy
371impl<F: Float + NumCast + std::fmt::Display> SampleableDistribution<F> for Cauchy<F> {
372    fn rvs(&self, size: usize) -> StatsResult<Vec<F>> {
373        self.rvs_vec(size)
374    }
375}
376
377/// Implementation of Distribution trait for Cauchy
378///
379/// Note: Cauchy distribution doesn't have defined mean or variance,
380/// but we implement the Distribution trait with appropriate behavior.
381impl<F: Float + NumCast + std::fmt::Display> ScirsDist<F> for Cauchy<F> {
382    /// Returns NaN as the mean is undefined for Cauchy distribution
383    fn mean(&self) -> F {
384        F::nan()
385    }
386
387    /// Returns NaN as the variance is undefined for Cauchy distribution
388    fn var(&self) -> F {
389        F::nan()
390    }
391
392    /// Returns NaN as the standard deviation is undefined for Cauchy distribution
393    fn std(&self) -> F {
394        F::nan()
395    }
396
397    /// Generate random samples from the distribution
398    fn rvs(&self, size: usize) -> StatsResult<Array1<F>> {
399        self.rvs(size)
400    }
401
402    /// Calculate the entropy of the distribution
403    fn entropy(&self) -> F {
404        self.entropy()
405    }
406}
407
408/// Implementation of ContinuousDistribution trait for Cauchy
409impl<F: Float + NumCast + std::fmt::Display> ContinuousDistribution<F> for Cauchy<F> {
410    /// Calculate the probability density function (PDF) at a given point
411    fn pdf(&self, x: F) -> F {
412        self.pdf(x)
413    }
414
415    /// Calculate the cumulative distribution function (CDF) at a given point
416    fn cdf(&self, x: F) -> F {
417        self.cdf(x)
418    }
419
420    /// Calculate the inverse cumulative distribution function (quantile function)
421    fn ppf(&self, p: F) -> StatsResult<F> {
422        self.ppf(p)
423    }
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429    use approx::assert_relative_eq;
430
431    #[test]
432    fn test_cauchy_creation() {
433        // Standard Cauchy (loc=0, scale=1)
434        let cauchy = Cauchy::new(0.0, 1.0).expect("Operation failed");
435        assert_eq!(cauchy.loc, 0.0);
436        assert_eq!(cauchy.scale, 1.0);
437
438        // Custom Cauchy
439        let custom = Cauchy::new(-2.0, 0.5).expect("Operation failed");
440        assert_eq!(custom.loc, -2.0);
441        assert_eq!(custom.scale, 0.5);
442
443        // Error case: negative scale
444        assert!(Cauchy::<f64>::new(0.0, 0.0).is_err());
445        assert!(Cauchy::<f64>::new(0.0, -1.0).is_err());
446    }
447
448    #[test]
449    fn test_cauchy_pdf() {
450        // Standard Cauchy (loc=0, scale=1)
451        let cauchy = Cauchy::new(0.0, 1.0).expect("Operation failed");
452
453        // PDF at x = 0 should be 1/(π*1) = 1/π ≈ 0.3183099
454        let pdf_at_zero = cauchy.pdf(0.0);
455        assert_relative_eq!(pdf_at_zero, 0.3183099, epsilon = 1e-7);
456
457        // PDF at x = 1 should be 1/(π*1*(1+1)) = 1/(2π) ≈ 0.1591549
458        let pdf_at_one = cauchy.pdf(1.0);
459        assert_relative_eq!(pdf_at_one, 0.1591549, epsilon = 1e-7);
460
461        // PDF at x = -1 should same as x = 1 due to symmetry
462        let pdf_at_neg_one = cauchy.pdf(-1.0);
463        assert_relative_eq!(pdf_at_neg_one, pdf_at_one, epsilon = 1e-10);
464
465        // Custom Cauchy with loc=-2, scale=0.5
466        let custom = Cauchy::new(-2.0, 0.5).expect("Operation failed");
467
468        // PDF at x = -2 should be 1/(π*0.5) = 2/π ≈ 0.6366198
469        let pdf_at_loc = custom.pdf(-2.0);
470        assert_relative_eq!(pdf_at_loc, 0.6366198, epsilon = 1e-7);
471
472        // PDF at x = -1.5 should be 1/(π*0.5*(1+1)) = 1/π ≈ 0.3183099
473        let pdf_at_custom = custom.pdf(-1.5);
474        assert_relative_eq!(pdf_at_custom, 0.3183099, epsilon = 1e-7);
475    }
476
477    #[test]
478    fn test_cauchy_cdf() {
479        // Standard Cauchy (loc=0, scale=1)
480        let cauchy = Cauchy::new(0.0, 1.0).expect("Operation failed");
481
482        // CDF at x = 0 should be 0.5
483        let cdf_at_zero = cauchy.cdf(0.0);
484        assert_relative_eq!(cdf_at_zero, 0.5, epsilon = 1e-10);
485
486        // CDF at x = 1 should be 0.5 + (1/π)*arctan(1) = 0.5 + 1/4 = 0.75
487        let cdf_at_one = cauchy.cdf(1.0);
488        assert_relative_eq!(cdf_at_one, 0.75, epsilon = 1e-7);
489
490        // CDF at x = -1 should be 0.5 - (1/π)*arctan(1) = 0.5 - 1/4 = 0.25
491        let cdf_at_neg_one = cauchy.cdf(-1.0);
492        assert_relative_eq!(cdf_at_neg_one, 0.25, epsilon = 1e-7);
493
494        // Custom Cauchy with loc=-2, scale=0.5
495        let custom = Cauchy::new(-2.0, 0.5).expect("Operation failed");
496
497        // CDF at x = -2 should be 0.5
498        let cdf_at_loc = custom.cdf(-2.0);
499        assert_relative_eq!(cdf_at_loc, 0.5, epsilon = 1e-10);
500
501        // CDF at x = -1.5 should be 0.5 + (1/π)*arctan(1) = 0.75
502        let cdf_at_custom = custom.cdf(-1.5);
503        assert_relative_eq!(cdf_at_custom, 0.75, epsilon = 1e-7);
504    }
505
506    #[test]
507    fn test_cauchy_ppf() {
508        // Standard Cauchy (loc=0, scale=1)
509        let cauchy = Cauchy::new(0.0, 1.0).expect("Operation failed");
510
511        // PPF at p = 0.5 should be 0
512        let ppf_at_half = cauchy.ppf(0.5).expect("Operation failed");
513        assert_relative_eq!(ppf_at_half, 0.0, epsilon = 1e-10);
514
515        // PPF at p = 0.75 should be 1.0
516        let ppf_at_75 = cauchy.ppf(0.75).expect("Operation failed");
517        assert_relative_eq!(ppf_at_75, 1.0, epsilon = 1e-7);
518
519        // PPF at p = 0.25 should be -1.0
520        let ppf_at_25 = cauchy.ppf(0.25).expect("Operation failed");
521        assert_relative_eq!(ppf_at_25, -1.0, epsilon = 1e-7);
522
523        // Custom Cauchy with loc=-2, scale=0.5
524        let custom = Cauchy::new(-2.0, 0.5).expect("Operation failed");
525
526        // PPF at p = 0.5 should be -2.0
527        let ppf_at_half_custom = custom.ppf(0.5).expect("Operation failed");
528        assert_relative_eq!(ppf_at_half_custom, -2.0, epsilon = 1e-10);
529
530        // PPF at p = 0.75 should be -2.0 + 0.5*tan(π/4) = -2.0 + 0.5 = -1.5
531        let ppf_at_75_custom = custom.ppf(0.75).expect("Operation failed");
532        assert_relative_eq!(ppf_at_75_custom, -1.5, epsilon = 1e-7);
533
534        // Error cases
535        assert!(cauchy.ppf(-0.1).is_err());
536        assert!(cauchy.ppf(1.1).is_err());
537    }
538
539    #[test]
540    fn test_cauchy_properties() {
541        // Standard Cauchy (loc=0, scale=1)
542        let cauchy = Cauchy::new(0.0, 1.0).expect("Operation failed");
543
544        // Median = loc = 0
545        let median = cauchy.median();
546        assert_eq!(median, 0.0);
547
548        // Mode = loc = 0
549        let mode = cauchy.mode();
550        assert_eq!(mode, 0.0);
551
552        // IQR = 2 * scale = 2
553        let iqr = cauchy.iqr();
554        assert_eq!(iqr, 2.0);
555
556        // Entropy = log(4π) ≈ 2.5310242
557        let entropy = cauchy.entropy();
558        assert_relative_eq!(entropy, 2.5310242, epsilon = 1e-7);
559
560        // Custom Cauchy with loc=-2, scale=0.5
561        let custom = Cauchy::new(-2.0, 0.5).expect("Operation failed");
562
563        // Median = loc = -2
564        let median_custom = custom.median();
565        assert_eq!(median_custom, -2.0);
566
567        // Mode = loc = -2
568        let mode_custom = custom.mode();
569        assert_eq!(mode_custom, -2.0);
570
571        // IQR = 2 * scale = 1
572        let iqr_custom = custom.iqr();
573        assert_eq!(iqr_custom, 1.0);
574
575        // Entropy = log(4π*0.5) ≈ log(4π) - log(2) ≈ 2.5310242 - 0.6931472 ≈ 1.837877
576        let entropy_custom = custom.entropy();
577        assert_relative_eq!(entropy_custom, 1.837877, epsilon = 1e-6);
578    }
579
580    #[test]
581    fn test_cauchy_rvs() {
582        let cauchy = Cauchy::new(0.0, 1.0).expect("Operation failed");
583
584        // Generate samples
585        let samples_vec = cauchy.rvs_vec(100).expect("Operation failed");
586        let samples = cauchy.rvs(100).expect("Operation failed");
587
588        // Check the number of samples
589        assert_eq!(samples_vec.len(), 100);
590        assert_eq!(samples.len(), 100);
591
592        // Basic statistical checks are not very meaningful for Cauchy distribution
593        // since the mean and variance are undefined
594        // But we can check that the median is close to loc = 0
595
596        // Calculate sample median for vector samples
597        let mut sorted_samples = samples_vec.clone();
598        sorted_samples.sort_by(|a, b| a.partial_cmp(b).expect("Operation failed"));
599        let median = if sorted_samples.len() % 2 == 0 {
600            (sorted_samples[49] + sorted_samples[50]) / 2.0
601        } else {
602            sorted_samples[50]
603        };
604
605        // Median could be far from 0 due to extreme values, but generally should be within ±5
606        // This is a very loose test due to the heavy-tailed nature of the Cauchy
607        assert!(median.abs() < 5.0);
608    }
609
610    #[test]
611    fn test_cauchy_inverse_cdf() {
612        // Test that cdf(ppf(p)) == p and ppf(cdf(x)) == x
613        let cauchy = Cauchy::new(0.0, 1.0).expect("Operation failed");
614
615        // Test various probability values
616        let probabilities = [0.1, 0.25, 0.5, 0.75, 0.9];
617        for &p in &probabilities {
618            let x = cauchy.ppf(p).expect("Operation failed");
619            let p_back = cauchy.cdf(x);
620            assert_relative_eq!(p_back, p, epsilon = 1e-7);
621        }
622
623        // Test various x values
624        let x_values = [-3.0, -1.0, 0.0, 1.0, 3.0];
625        for &x in &x_values {
626            let p = cauchy.cdf(x);
627            let x_back = cauchy.ppf(p).expect("Operation failed");
628            assert_relative_eq!(x_back, x, epsilon = 1e-7);
629        }
630    }
631}