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}