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 crate::error::{StatsError, StatsResult};
31use num_traits::ToPrimitive;
32use serde::{Deserialize, Serialize};
33
34/// Configuration for the Exponential distribution.
35///
36/// # Fields
37/// * `lambda` - The rate parameter (must be positive)
38///
39/// # Examples
40/// ```
41/// use rs_stats::distributions::exponential_distribution::ExponentialConfig;
42///
43/// let config = ExponentialConfig { lambda: 2.0 };
44/// assert!(config.lambda > 0.0);
45/// ```
46#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
47pub struct ExponentialConfig<T>
48where
49    T: ToPrimitive,
50{
51    /// The rate parameter.
52    pub lambda: T,
53}
54
55impl<T> ExponentialConfig<T>
56where
57    T: ToPrimitive,
58{
59    /// Creates a new ExponentialConfig with validation
60    ///
61    /// # Arguments
62    /// * `lambda` - The rate parameter (must be positive)
63    ///
64    /// # Returns
65    /// `Some(ExponentialConfig)` if parameter is valid, `None` otherwise
66    pub fn new(lambda: T) -> StatsResult<Self> {
67        let lambda_64 = lambda.to_f64().ok_or_else(|| StatsError::ConversionError {
68            message: "ExponentialConfig::new: Failed to convert lambda to f64".to_string(),
69        })?;
70
71        if lambda_64 > 0.0 {
72            Ok(Self { lambda })
73        } else {
74            Err(StatsError::InvalidInput {
75                message: "ExponentialConfig::new: lambda must be positive".to_string(),
76            })
77        }
78    }
79}
80
81/// Probability density function (PDF) for the Exponential distribution.
82///
83/// Calculates the probability density at point `x` for an exponential distribution
84/// with rate parameter `lambda`.
85///
86/// # Arguments
87/// * `x` - The point at which to evaluate the PDF (must be non-negative)
88/// * `lambda` - The rate parameter (must be positive)
89///
90/// # Returns
91/// The probability density at point `x`.
92///
93/// # Errors
94/// Returns an error if:
95/// - `x` is negative
96/// - `lambda` is not positive
97/// - Type conversion to f64 fails
98///
99/// # Examples
100/// ```
101/// use rs_stats::distributions::exponential_distribution::exponential_pdf;
102///
103/// // Calculate PDF at x = 1.0 with rate parameter lambda = 2.0
104/// let pdf = exponential_pdf(1.0, 2.0).unwrap();
105/// assert!((pdf - 0.27067).abs() < 1e-5);
106/// ```
107#[inline]
108pub fn exponential_pdf<T>(x: T, lambda: T) -> StatsResult<f64>
109where
110    T: ToPrimitive,
111{
112    let x_64 = x.to_f64().ok_or_else(|| StatsError::ConversionError {
113        message: "exponential_pdf: Failed to convert x to f64".to_string(),
114    })?;
115
116    if x_64 < 0.0 {
117        return Err(StatsError::InvalidInput {
118            message: "exponential_pdf: x must be non-negative".to_string(),
119        });
120    }
121
122    let lambda_64 = lambda.to_f64().ok_or_else(|| StatsError::ConversionError {
123        message: "exponential_pdf: Failed to convert lambda to f64".to_string(),
124    })?;
125
126    if lambda_64 <= 0.0 {
127        return Err(StatsError::InvalidInput {
128            message: "exponential_pdf: lambda must be positive".to_string(),
129        });
130    }
131
132    Ok(if x_64 == 0.0 {
133        lambda_64
134    } else {
135        lambda_64 * (-lambda_64 * x_64).exp()
136    })
137}
138
139/// Cumulative distribution function (CDF) for the Exponential distribution.
140///
141/// Calculates the probability of a random variable being less than or equal to `x`
142/// for an exponential distribution with rate parameter `lambda`.
143///
144/// # Arguments
145/// * `x` - The point at which to evaluate the CDF (must be non-negative)
146/// * `lambda` - The rate parameter (must be positive)
147///
148/// # Returns
149/// The cumulative probability at point `x`.
150///
151/// # Errors
152/// Returns an error if:
153/// - `x` is negative
154/// - `lambda` is not positive
155/// - Type conversion to f64 fails
156///
157/// # Examples
158/// ```
159/// use rs_stats::distributions::exponential_distribution::exponential_cdf;
160///
161/// // Calculate CDF at x = 1.0 with rate parameter lambda = 2.0
162/// let cdf = exponential_cdf(1.0, 2.0).unwrap();
163/// assert!((cdf - 0.86466).abs() < 1e-5);
164/// ```
165#[inline]
166pub fn exponential_cdf<T>(x: T, lambda: T) -> StatsResult<f64>
167where
168    T: ToPrimitive,
169{
170    let x_64 = x.to_f64().ok_or_else(|| StatsError::ConversionError {
171        message: "exponential_cdf: Failed to convert x to f64".to_string(),
172    })?;
173
174    if x_64 < 0.0 {
175        return Err(StatsError::InvalidInput {
176            message: "exponential_cdf: x must be non-negative".to_string(),
177        });
178    }
179
180    let lambda_64 = lambda.to_f64().ok_or_else(|| StatsError::ConversionError {
181        message: "exponential_cdf: Failed to convert lambda to f64".to_string(),
182    })?;
183
184    if lambda_64 <= 0.0 {
185        return Err(StatsError::InvalidInput {
186            message: "exponential_cdf: lambda must be positive".to_string(),
187        });
188    }
189    Ok(1.0 - (-lambda_64 * x_64).exp())
190}
191
192/// Inverse cumulative distribution function for the Exponential distribution.
193///
194/// Calculates the value of `x` for which the CDF equals the given probability `p`.
195///
196/// # Arguments
197/// * `p` - The probability (must be between 0 and 1)
198/// * `lambda` - The rate parameter (must be positive)
199///
200/// # Returns
201/// The value `x` such that P(X ≤ x) = p.
202///
203/// # Errors
204/// Returns an error if:
205/// - `p` is not between 0 and 1
206/// - `lambda` is not positive
207/// - Type conversion to f64 fails
208///
209/// # Examples
210/// ```
211/// use rs_stats::distributions::exponential_distribution::{exponential_inverse_cdf, exponential_cdf};
212///
213/// // Calculate inverse CDF for p = 0.5 with rate parameter lambda = 2.0
214/// let x = exponential_inverse_cdf(0.5, 2.0).unwrap();
215///
216/// // Verify that CDF(x) is approximately p
217/// let p = exponential_cdf(x, 2.0).unwrap();
218/// assert!((p - 0.5).abs() < 1e-10);
219/// ```
220#[inline]
221pub fn exponential_inverse_cdf<T>(p: T, lambda: T) -> StatsResult<f64>
222where
223    T: ToPrimitive,
224{
225    let p_64 = p.to_f64().ok_or_else(|| StatsError::ConversionError {
226        message: "exponential_inverse_cdf: Failed to convert p to f64".to_string(),
227    })?;
228
229    if !(0.0..=1.0).contains(&p_64) {
230        return Err(StatsError::InvalidInput {
231            message: "exponential_inverse_cdf: p must be between 0 and 1".to_string(),
232        });
233    }
234
235    let lambda_64 = lambda.to_f64().ok_or_else(|| StatsError::ConversionError {
236        message: "exponential_inverse_cdf: Failed to convert lambda to f64".to_string(),
237    })?;
238
239    if lambda_64 <= 0.0 {
240        return Err(StatsError::InvalidInput {
241            message: "exponential_inverse_cdf: lambda must be positive".to_string(),
242        });
243    }
244    Ok(-((1.0 - p_64).ln()) / lambda_64)
245}
246
247/// Mean (expected value) of the Exponential distribution.
248///
249/// Calculates the mean of an exponential distribution with rate parameter `lambda`.
250///
251/// # Arguments
252/// * `lambda` - The rate parameter (must be positive)
253///
254/// # Returns
255/// The mean of the distribution.
256///
257/// # Errors
258/// Returns an error if:
259/// - `lambda` is not positive
260/// - Type conversion to f64 fails
261///
262/// # Examples
263/// ```
264/// use rs_stats::distributions::exponential_distribution::exponential_mean;
265///
266/// // Mean of exponential distribution with rate parameter lambda = 2.0
267/// let mean = exponential_mean(2.0).unwrap();
268/// assert!((mean - 0.5).abs() < 1e-10);
269/// ```
270#[inline]
271pub fn exponential_mean<T>(lambda: T) -> StatsResult<f64>
272where
273    T: ToPrimitive,
274{
275    let lambda_64 = lambda.to_f64().ok_or_else(|| StatsError::ConversionError {
276        message: "exponential_mean: Failed to convert lambda to f64".to_string(),
277    })?;
278    if lambda_64 <= 0.0 {
279        return Err(StatsError::InvalidInput {
280            message: "exponential_mean: lambda must be positive".to_string(),
281        });
282    }
283
284    Ok(1.0 / lambda_64)
285}
286
287/// Variance of the Exponential distribution.
288///
289/// Calculates the variance of an exponential distribution with rate parameter `lambda`.
290///
291/// # Arguments
292/// * `lambda` - The rate parameter (must be positive)
293///
294/// # Returns
295/// The variance of the distribution.
296///
297/// # Errors
298/// Returns an error if:
299/// - `lambda` is not positive
300/// - Type conversion to f64 fails
301///
302/// # Examples
303/// ```
304/// use rs_stats::distributions::exponential_distribution::exponential_variance;
305///
306/// // Variance of exponential distribution with rate parameter lambda = 2.0
307/// let variance = exponential_variance(2.0).unwrap();
308/// assert!((variance - 0.25).abs() < 1e-10);
309/// ```
310#[inline]
311pub fn exponential_variance(lambda: f64) -> StatsResult<f64> {
312    if lambda <= 0.0 {
313        return Err(StatsError::InvalidInput {
314            message: "exponential_variance: lambda must be positive".to_string(),
315        });
316    }
317
318    Ok(1.0 / (lambda * lambda))
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    const EPSILON: f64 = 1e-10;
326
327    #[test]
328    fn test_exponential_pdf() {
329        let lambda = 2.0;
330
331        // PDF at x = 0
332        let result = exponential_pdf(0.0, lambda).unwrap();
333        assert_eq!(result, lambda);
334
335        // PDF at x = 1
336        let result = exponential_pdf(1.0, lambda).unwrap();
337        let expected = lambda * (-lambda).exp();
338        assert!((result - expected).abs() < EPSILON);
339
340        // PDF at x = 0.5
341        let result = exponential_pdf(0.5, lambda).unwrap();
342        let expected = lambda * (-lambda * 0.5).exp();
343        assert!((result - expected).abs() < EPSILON);
344    }
345
346    #[test]
347    fn test_exponential_cdf() {
348        let lambda = 2.0_f64;
349
350        // CDF at x = 0
351        let result = exponential_cdf(0.0, lambda).unwrap();
352        assert!((result - 0.0).abs() < EPSILON);
353
354        // CDF at x = 1
355        let result = exponential_cdf(1.0, lambda).unwrap();
356        let expected = 1.0 - (-lambda).exp();
357        assert!((result - expected).abs() < EPSILON);
358
359        // CDF at x = 0.5
360        let result = exponential_cdf(0.5, lambda).unwrap();
361        let expected = 1.0 - (-lambda * 0.5).exp();
362        assert!((result - expected).abs() < EPSILON);
363    }
364
365    #[test]
366    fn test_exponential_inverse_cdf() {
367        let lambda = 2.0_f64;
368
369        // Test inverse CDF with various probabilities
370        let test_cases = vec![0.1, 0.25, 0.5, 0.75, 0.9];
371
372        for p in test_cases {
373            let x = exponential_inverse_cdf(p, lambda).unwrap();
374            let cdf = exponential_cdf(x, lambda).unwrap();
375            assert!(
376                (cdf - p).abs() < EPSILON,
377                "Inverse CDF failed for p = {}: got {}, expected {}",
378                p,
379                cdf,
380                p
381            );
382        }
383    }
384
385    #[test]
386    fn test_exponential_mean() {
387        let lambda = 2.0;
388        let result = exponential_mean(lambda).unwrap();
389        let expected = 1.0 / lambda;
390        assert!((result - expected).abs() < EPSILON);
391    }
392
393    #[test]
394    fn test_exponential_variance() {
395        let lambda = 2.0;
396        let result = exponential_variance(lambda).unwrap();
397        let expected = 1.0 / (lambda * lambda);
398        assert!((result - expected).abs() < EPSILON);
399    }
400
401    #[test]
402    fn test_exponential_pdf_invalid_lambda() {
403        let result = exponential_pdf(1.0, -2.0);
404        assert!(result.is_err());
405        match result {
406            Err(StatsError::InvalidInput { message }) => {
407                assert!(message.contains("lambda must be positive"));
408            }
409            _ => panic!("Expected InvalidInput error"),
410        }
411    }
412
413    #[test]
414    fn test_exponential_pdf_invalid_x() {
415        let result = exponential_pdf(-1.0, 2.0);
416        assert!(result.is_err());
417        match result {
418            Err(StatsError::InvalidInput { message }) => {
419                assert!(message.contains("x must be non-negative"));
420            }
421            _ => panic!("Expected InvalidInput error"),
422        }
423    }
424
425    #[test]
426    fn test_exponential_config() {
427        // Valid config
428        let config = ExponentialConfig::new(2.0);
429        assert!(config.is_ok());
430
431        // Invalid config
432        let config = ExponentialConfig::new(0.0);
433        assert!(config.is_err());
434
435        let config = ExponentialConfig::new(-1.0);
436        assert!(config.is_err());
437    }
438
439    #[test]
440    fn test_exponential_inverse_cdf_p_negative() {
441        let result = exponential_inverse_cdf(-0.1, 2.0);
442        assert!(result.is_err());
443        assert!(matches!(
444            result.unwrap_err(),
445            StatsError::InvalidInput { .. }
446        ));
447    }
448
449    #[test]
450    fn test_exponential_inverse_cdf_p_greater_than_one() {
451        let result = exponential_inverse_cdf(1.5, 2.0);
452        assert!(result.is_err());
453        assert!(matches!(
454            result.unwrap_err(),
455            StatsError::InvalidInput { .. }
456        ));
457    }
458
459    #[test]
460    fn test_exponential_cdf_invalid_lambda() {
461        let result = exponential_cdf(1.0, -2.0);
462        assert!(result.is_err());
463        assert!(matches!(
464            result.unwrap_err(),
465            StatsError::InvalidInput { .. }
466        ));
467    }
468
469    #[test]
470    fn test_exponential_cdf_invalid_x() {
471        let result = exponential_cdf(-1.0, 2.0);
472        assert!(result.is_err());
473        assert!(matches!(
474            result.unwrap_err(),
475            StatsError::InvalidInput { .. }
476        ));
477    }
478
479    #[test]
480    fn test_exponential_mean_invalid_lambda() {
481        let result = exponential_mean(0.0);
482        assert!(result.is_err());
483        assert!(matches!(
484            result.unwrap_err(),
485            StatsError::InvalidInput { .. }
486        ));
487    }
488
489    #[test]
490    fn test_exponential_variance_invalid_lambda() {
491        let result = exponential_variance(0.0);
492        assert!(result.is_err());
493        assert!(matches!(
494            result.unwrap_err(),
495            StatsError::InvalidInput { .. }
496        ));
497    }
498
499    #[test]
500    fn test_exponential_pdf_x_positive() {
501        // Test the branch where x > 0 (not x == 0)
502        let result = exponential_pdf(0.5, 2.0).unwrap();
503        let lambda: f64 = 2.0;
504        let x: f64 = 0.5;
505        let expected = lambda * (-lambda * x).exp();
506        assert!((result - expected).abs() < EPSILON);
507    }
508
509    #[test]
510    fn test_exponential_inverse_cdf_p_zero() {
511        // When p = 0, inverse CDF should be 0 (or very close to 0)
512        let result = exponential_inverse_cdf(0.0, 2.0).unwrap();
513        assert_eq!(result, 0.0);
514    }
515
516    #[test]
517    fn test_exponential_inverse_cdf_p_one() {
518        // When p = 1, inverse CDF should approach infinity
519        // But due to numerical limits, we check it's very large
520        let result = exponential_inverse_cdf(1.0, 2.0).unwrap();
521        assert!(result.is_infinite() || result > 1e10);
522    }
523
524    #[test]
525    fn test_exponential_inverse_cdf_lambda_zero() {
526        let result = exponential_inverse_cdf(0.5, 0.0);
527        assert!(result.is_err());
528        assert!(matches!(
529            result.unwrap_err(),
530            StatsError::InvalidInput { .. }
531        ));
532    }
533
534    #[test]
535    fn test_exponential_inverse_cdf_lambda_negative() {
536        let result = exponential_inverse_cdf(0.5, -1.0);
537        assert!(result.is_err());
538        assert!(matches!(
539            result.unwrap_err(),
540            StatsError::InvalidInput { .. }
541        ));
542    }
543}