rs_stats/distributions/
exponential_distribution.rs

1//! # Exponential Distribution
2//!
3//! This module implements the exponential distribution, a continuous probability distribution
4//! that models the time between events in a Poisson point process.
5//!
6//! ## Key Characteristics
7//! - Continuous probability distribution
8//! - Memoryless property (future states depend only on the present, not the past)
9//! - Used to model waiting times and inter-arrival times
10//!
11//! ## Common Applications
12//! - Time between arrivals in a Poisson process
13//! - Lifetime analysis (e.g., how long until a component fails)
14//! - Queue theory and service times
15//! - Radioactive decay
16//!
17//! ## Mathematical Formulation
18//! The probability density function (PDF) is given by:
19//!
20//! f(x; λ) = λe^(-λx) for x ≥ 0
21//!
22//! where:
23//! - λ (lambda) is the rate parameter (λ > 0)
24//! - x is the random variable (time or space)
25//!
26//! The cumulative distribution function (CDF) is:
27//!
28//! F(x; λ) = 1 - e^(-λx) for x ≥ 0
29
30use rand::Rng;
31use rand::distributions::Distribution;
32use rand_distr::Exp;
33use serde::{Deserialize, Serialize};
34
35/// Configuration for the Exponential distribution.
36///
37/// # Fields
38/// * `lambda` - The rate parameter (must be positive)
39///
40/// # Examples
41/// ```
42/// use rs_stats::distributions::exponential_distribution::ExponentialConfig;
43///
44/// let config = ExponentialConfig { lambda: 2.0 };
45/// assert!(config.lambda > 0.0);
46/// ```
47#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
48pub struct ExponentialConfig {
49    /// The rate parameter.
50    pub lambda: f64,
51}
52
53impl ExponentialConfig {
54    /// Creates a new ExponentialConfig with validation
55    ///
56    /// # Arguments
57    /// * `lambda` - The rate parameter (must be positive)
58    ///
59    /// # Returns
60    /// `Some(ExponentialConfig)` if parameter is valid, `None` otherwise
61    pub fn new(lambda: f64) -> Option<Self> {
62        if lambda > 0.0 {
63            Some(Self { lambda })
64        } else {
65            None
66        }
67    }
68}
69
70/// Probability density function (PDF) for the Exponential distribution.
71///
72/// Calculates the probability density at point `x` for an exponential distribution
73/// with rate parameter `lambda`.
74///
75/// # Arguments
76/// * `x` - The point at which to evaluate the PDF (must be non-negative)
77/// * `lambda` - The rate parameter (must be positive)
78///
79/// # Returns
80/// The probability density at point `x`.
81///
82/// # Panics
83/// Panics if:
84/// - `x` is negative
85/// - `lambda` is not positive
86///
87/// # Examples
88/// ```
89/// use rs_stats::distributions::exponential_distribution::exponential_pdf;
90///
91/// // Calculate PDF at x = 1.0 with rate parameter lambda = 2.0
92/// let pdf = exponential_pdf(1.0, 2.0);
93/// assert!((pdf - 0.27067).abs() < 1e-5);
94/// ```
95pub fn exponential_pdf(x: f64, lambda: f64) -> f64 {
96    assert!(x >= 0.0, "x must be non-negative");
97    assert!(lambda > 0.0, "lambda must be positive");
98
99    if x == 0.0 {
100        lambda
101    } else {
102        lambda * (-lambda * x).exp()
103    }
104}
105
106/// Cumulative distribution function (CDF) for the Exponential distribution.
107///
108/// Calculates the probability of a random variable being less than or equal to `x`
109/// for an exponential distribution with rate parameter `lambda`.
110///
111/// # Arguments
112/// * `x` - The point at which to evaluate the CDF (must be non-negative)
113/// * `lambda` - The rate parameter (must be positive)
114///
115/// # Returns
116/// The cumulative probability at point `x`.
117///
118/// # Panics
119/// Panics if:
120/// - `x` is negative
121/// - `lambda` is not positive
122///
123/// # Examples
124/// ```
125/// use rs_stats::distributions::exponential_distribution::exponential_cdf;
126///
127/// // Calculate CDF at x = 1.0 with rate parameter lambda = 2.0
128/// let cdf = exponential_cdf(1.0, 2.0);
129/// assert!((cdf - 0.86466).abs() < 1e-5);
130/// ```
131pub fn exponential_cdf(x: f64, lambda: f64) -> f64 {
132    assert!(x >= 0.0, "x must be non-negative");
133    assert!(lambda > 0.0, "lambda must be positive");
134
135    1.0 - (-lambda * x).exp()
136}
137
138/// Inverse cumulative distribution function for the Exponential distribution.
139///
140/// Calculates the value of `x` for which the CDF equals the given probability `p`.
141///
142/// # Arguments
143/// * `p` - The probability (must be between 0 and 1)
144/// * `lambda` - The rate parameter (must be positive)
145///
146/// # Returns
147/// The value `x` such that P(X ≤ x) = p.
148///
149/// # Panics
150/// Panics if:
151/// - `p` is not between 0 and 1
152/// - `lambda` is not positive
153///
154/// # Examples
155/// ```
156/// use rs_stats::distributions::exponential_distribution::{exponential_inverse_cdf, exponential_cdf};
157///
158/// // Calculate inverse CDF for p = 0.5 with rate parameter lambda = 2.0
159/// let x = exponential_inverse_cdf(0.5, 2.0);
160///
161/// // Verify that CDF(x) is approximately p
162/// let p = exponential_cdf(x, 2.0);
163/// assert!((p - 0.5).abs() < 1e-10);
164/// ```
165pub fn exponential_inverse_cdf(p: f64, lambda: f64) -> f64 {
166    assert!((0.0..=1.0).contains(&p), "p must be between 0 and 1");
167    assert!(lambda > 0.0, "lambda must be positive");
168
169    -((1.0 - p).ln()) / lambda
170}
171
172/// Mean (expected value) of the Exponential distribution.
173///
174/// Calculates the mean of an exponential distribution with rate parameter `lambda`.
175///
176/// # Arguments
177/// * `lambda` - The rate parameter (must be positive)
178///
179/// # Returns
180/// The mean of the distribution.
181///
182/// # Panics
183/// Panics if `lambda` is not positive
184///
185/// # Examples
186/// ```
187/// use rs_stats::distributions::exponential_distribution::exponential_mean;
188///
189/// // Mean of exponential distribution with rate parameter lambda = 2.0
190/// let mean = exponential_mean(2.0);
191/// assert!((mean - 0.5).abs() < 1e-10);
192/// ```
193pub fn exponential_mean(lambda: f64) -> f64 {
194    assert!(lambda > 0.0, "lambda must be positive");
195
196    1.0 / lambda
197}
198
199/// Variance of the Exponential distribution.
200///
201/// Calculates the variance of an exponential distribution with rate parameter `lambda`.
202///
203/// # Arguments
204/// * `lambda` - The rate parameter (must be positive)
205///
206/// # Returns
207/// The variance of the distribution.
208///
209/// # Panics
210/// Panics if `lambda` is not positive
211///
212/// # Examples
213/// ```
214/// use rs_stats::distributions::exponential_distribution::exponential_variance;
215///
216/// // Variance of exponential distribution with rate parameter lambda = 2.0
217/// let variance = exponential_variance(2.0);
218/// assert!((variance - 0.25).abs() < 1e-10);
219/// ```
220pub fn exponential_variance(lambda: f64) -> f64 {
221    assert!(lambda > 0.0, "lambda must be positive");
222
223    1.0 / (lambda * lambda)
224}
225
226/// Generate a random sample from an Exponential distribution.
227///
228/// # Arguments
229/// * `lambda` - The rate parameter (must be positive)
230/// * `rng` - A random number generator
231///
232/// # Returns
233/// A random value from the exponential distribution.
234///
235/// # Panics
236/// Panics if `lambda` is not positive
237///
238/// # Examples
239/// ```
240/// use rs_stats::distributions::exponential_distribution::exponential_sample;
241/// use rand::thread_rng;
242///
243/// let mut rng = thread_rng();
244/// let sample = exponential_sample(2.0, &mut rng);
245/// assert!(sample >= 0.0); // Exponential distribution is always non-negative
246/// ```
247pub fn exponential_sample<R: Rng + ?Sized>(lambda: f64, rng: &mut R) -> f64 {
248    assert!(lambda > 0.0, "lambda must be positive");
249
250    let exp = Exp::new(lambda).unwrap();
251    exp.sample(rng)
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257    use rand::thread_rng;
258
259    const EPSILON: f64 = 1e-10;
260
261    #[test]
262    fn test_exponential_pdf() {
263        let lambda = 2.0;
264
265        // PDF at x = 0
266        let result = exponential_pdf(0.0, lambda);
267        assert_eq!(result, lambda);
268
269        // PDF at x = 1
270        let result = exponential_pdf(1.0, lambda);
271        let expected = lambda * (-lambda).exp();
272        assert!((result - expected).abs() < EPSILON);
273
274        // PDF at x = 0.5
275        let result = exponential_pdf(0.5, lambda);
276        let expected = lambda * (-lambda * 0.5).exp();
277        assert!((result - expected).abs() < EPSILON);
278    }
279
280    #[test]
281    fn test_exponential_cdf() {
282        let lambda = 2.0;
283
284        // CDF at x = 0
285        let result = exponential_cdf(0.0, lambda);
286        assert!((result - 0.0).abs() < EPSILON);
287
288        // CDF at x = 1
289        let result = exponential_cdf(1.0, lambda);
290        let expected = 1.0 - (-lambda).exp();
291        assert!((result - expected).abs() < EPSILON);
292
293        // CDF at x = 0.5
294        let result = exponential_cdf(0.5, lambda);
295        let expected = 1.0 - (-lambda * 0.5).exp();
296        assert!((result - expected).abs() < EPSILON);
297    }
298
299    #[test]
300    fn test_exponential_inverse_cdf() {
301        let lambda = 2.0;
302
303        // Test inverse CDF with various probabilities
304        let test_cases = vec![0.1, 0.25, 0.5, 0.75, 0.9];
305
306        for p in test_cases {
307            let x = exponential_inverse_cdf(p, lambda);
308            let cdf = exponential_cdf(x, lambda);
309            assert!(
310                (cdf - p).abs() < EPSILON,
311                "Inverse CDF failed for p = {}: got {}, expected {}",
312                p,
313                cdf,
314                p
315            );
316        }
317    }
318
319    #[test]
320    fn test_exponential_mean() {
321        let lambda = 2.0;
322        let result = exponential_mean(lambda);
323        let expected = 1.0 / lambda;
324        assert!((result - expected).abs() < EPSILON);
325    }
326
327    #[test]
328    fn test_exponential_variance() {
329        let lambda = 2.0;
330        let result = exponential_variance(lambda);
331        let expected = 1.0 / (lambda * lambda);
332        assert!((result - expected).abs() < EPSILON);
333    }
334
335    #[test]
336    fn test_exponential_sample() {
337        let lambda = 2.0;
338        let mut rng = thread_rng();
339
340        // Test that samples are non-negative
341        for _ in 0..100 {
342            let sample = exponential_sample(lambda, &mut rng);
343            assert!(sample >= 0.0);
344        }
345
346        // Test that mean of samples is close to theoretical mean
347        // This is a statistical test, so we allow some margin of error
348        let num_samples = 10000;
349        let mut sum = 0.0;
350
351        for _ in 0..num_samples {
352            sum += exponential_sample(lambda, &mut rng);
353        }
354
355        let sample_mean = sum / (num_samples as f64);
356        let theoretical_mean = exponential_mean(lambda);
357
358        // Allow a 10% margin of error for the mean
359        assert!(
360            (sample_mean - theoretical_mean).abs() < theoretical_mean * 0.1,
361            "Sample mean {} is too far from theoretical mean {}",
362            sample_mean,
363            theoretical_mean
364        );
365    }
366
367    #[test]
368    #[should_panic(expected = "lambda must be positive")]
369    fn test_exponential_pdf_invalid_lambda() {
370        exponential_pdf(1.0, -2.0);
371    }
372
373    #[test]
374    #[should_panic(expected = "x must be non-negative")]
375    fn test_exponential_pdf_invalid_x() {
376        exponential_pdf(-1.0, 2.0);
377    }
378
379    #[test]
380    fn test_exponential_config() {
381        // Valid config
382        let config = ExponentialConfig::new(2.0);
383        assert!(config.is_some());
384
385        // Invalid config
386        let config = ExponentialConfig::new(0.0);
387        assert!(config.is_none());
388
389        let config = ExponentialConfig::new(-1.0);
390        assert!(config.is_none());
391    }
392}